Cześć w dzisiejszym odcinku chciałbym przedstawić wam temat walidacji. A konkretnie pokazać wam jak dodać walidacje do modelu. Walidacja ma na celu sprawdzenie stanu obiektu zanim trafi on do bazy. Ruby on Rails używa domyślnie Active Record Validations. Po za nimi istnieje kilka innych rozwiązań na walidacje, możemy też oczywiście zrobić coś własnego.
def foo(arg)
if arg.is_a?(String)
return arg
else
raise StandardError.new "Is not a string"
end
end
Bądźmy szczerzy, nikt nie będzie pisał czegoś takiego dla każdej metody w kodzie
Jak już wspomniałem, railsy używają Active Record Validations i właśnie temu rozwiązaniu chcę poświęcić dzisiejszy odcinek.
Dlaczego używać walidacji?
Walidacje służą do weryfikacji czy w bazie danych są zapisywane tylko prawidłowe dane. Przykładowo podczas rejestracji istotnym jest by użytkownik zawsze podał email i hasło. Albo post zawierał tytuł i treść. Walidacje na poziomie modelu to dobry sposób na zapewnienie, że w bazie danych są zapisywane tylko prawidłowe dane. Są niezależne od bazy danych, oraz są wygodne w testowaniu i utrzymaniu.
popatrzmy na przykład
jeżeli zechcemy dodać nowy post, ale nie uzupełnimy żadnych danych, post zapisze nam się z pustymi polami.
Jeżeli dodamy walidacje np
class Post
validates :title, presence: true
validates :body, presence: true
end
to teraz nie da się zapisać posta bez uzupełniania tych pól
Rodzaje walidacji
Active Record oferuje bardzo dużo gotowych typów walidacji. Możemy dodać wiele typów walidacji i będą one działać wspólnie. Za każdym razem, gdy walidacja się nie powiedzie, do tablicy z błędami obiektu dodawany jest błąd.
Tak jak wcześniejszy przykład z postem
post = Post.create
post.errors
zwróci nam
#<ActiveModel::Errors:0x00007ff04f3d2718 @base=#<Post id: nil, title: nil, body: nil, created_at: nil, updated_at: nil, slug: nil>, @errors=[#<ActiveModel::Error attribute=title, type=blank, options={}>, #<ActiveModel::Error attribute=body, type=blank, options={}>]>
Przyjrzyjmy się niektórym z rodzajów walidacji
validates_associated
Tego rodzaju walidacji powinniśmy używać gdy nasz model jest powiązany z innym modelem. Np w przypadku posta, może on wymagać posiadania kategorii. Bez niej nie powinno dojść do zapisu
class Post
has_many :categories
validates_associated :categories
end
confirmation
Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól powinno zawierać taką samą treść jak inne. Np email, lub hasło podczas rejestracji wtedy nazwa takiego pola powinna się nazywać *_confirmation
Moim zdaniem tego typu walidacja powinna odbywać się na etapie formularza więc przejdę dalej
class Person < ApplicationRecord
validates :email, confirmation: true
end
exclusion
Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól nie powinno zawierać jakieś treści. Np. Tworząc subdomeny nie chcemy by ktoś podał www
lub innych używanych przez nas treści wtedy możemy dodać tego typu walidacje.
class Account < ApplicationRecord
validates :subdomain, exclusion: { in: %w(www us ca jp),
message: "%{value} is reserved." }
end
inclusion
Przeciwieństwo poprzedniego przykładu. Jednocześnie częściej używany. Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól musi zawierać jakieś treści. Np rozmiary produktów, płeć, województwo, coś co występuje w kilku wersjach ale nie ma sensu trzymać tego w osobnych tabelach.
class Coffee < ApplicationRecord
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }
end
format
Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól jak sama nazwa wskazuje powinno mieć jakiś format. Np nie zawierać cyfr, zaczynać się z wielkiej litery. Itp. Walidacja do tego celu używa wyrażeń regularnych, często nazywanych regexem
class Product < ApplicationRecord
validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
message: "only allows letters" }
end
length
Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól powinno mieć jakiś limit znaków. Np minimalna treść posta, czy komentarza. W drugą stronę przeważnie ogranicza nas limit bazy danych. Ale gdybyśmy chcieli mieć limit jak na twiterze oczywiście możemy to zrobić . Limit może być minimalny, maksymalny, można połączyć oba czyli od do, ale też można podać jakąś konkretną wartość
class Person < ApplicationRecord
validates :name, length: { minimum: 2 }
validates :bio, length: { maximum: 500 }
validates :password, length: { in: 6..20 }
validates :registration_number, length: { is: 6 }
end
numericality
Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól powinno zawierać same liczby. Taką walidacje można rozszerzyć o dodatkowe warunki np only_integer
będzie sprawdzać czy pole zawiera tylko liczby całkowite. Ale tych warunków jest dużo więcej
class Player < ApplicationRecord
validates :points, numericality: true
validates :games_played, numericality: { only_integer: true }
validates :games_played, numericality: { greater_than: 1 }
validates :games_played, numericality: { greater_than_or_equal_to: 1 }
validates :games_played, numericality: { equal_to: 1 }
validates :games_played, numericality: { less_than: 2 }
validates :games_played, numericality: { less_than_or_equal_to: 2 }
validates :games_played, numericality: { other_than: 0 }
validates :games_played, numericality: { odd: true }
validates :games_played, numericality: { even: true }
end
presence
Najpopularniejsza chyba walidacja. Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól jest wymagane. Tak jak w przykładzie, nie powinniśmy móc utworzyć obiektu bez nazwy, loginu i emaila
class Person < ApplicationRecord
validates :name, :login, :email, presence: true
end
uniqueness
Tego rodzaju walidacji powinniśmy używać gdy jedno z naszych pól powinno być unikalne. Np emaile użytkowników. Ale też można łączyć to w grupy
class Account < ApplicationRecord
validates :email, uniqueness: true
end
np nazwa musi być unikalna ale tylko w danym roku więc name: foo, year: 2000
oraz name: foo, year:2001
powinno przejść walidacje
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
validates_with
w tym przypadku możemy użyć oddzielnej klasy do walidacji. Musicie pamiętać, by zwracać tam error bo domyślnie go nie ma. Jeżeli tej samej walidacji chcecie użyć w kilku modelach takie rozwiązanie ma sens, w innym wypadku można zrobić osobną metodę wewnątrz modelu
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
record.errors.add :base, "This person is evil"
end
end
end
class Person < ApplicationRecord
validates_with GoodnessValidator
end
Dodatkowe opcje walidacji
allow_nil
pomija walidacje, jeżeli pole jest nilem
class Coffee < ApplicationRecord
validates :size, inclusion: { in: %w(small medium large),
message: "%{value} is not a valid size" }, allow_nil: true
end
allow_blank
używa helpera .blank?
w porównaniu do nila pozwala na zapisanie pustego stringa
class Topic < ApplicationRecord
validates :title, length: { is: 5 }, allow_blank: true
end
message
Wszystkie wcześniej wymienione walidacje mają domyślnie swoje zdefiniowane wiadomości w przypadku errora. jeżeli dodamy parametr message
możemy stworzyć własną wiadomość
class Person < ApplicationRecord
validates :name, presence: { message: "must be given please" }
validates :age, numericality: { message: "%{value} seems wrong" }
validates :username,
uniqueness: {
message: ->(object, data) do
"Hey #{object.name}, #{data[:value]} is already taken."
end
}
end
on
bardzo fajny parametr, możemy ustawić osobne walidacje podczas tworzenia a osobne podczas aktualizacji obiektu
podczas tworzenia sprawdza, email czy jest unikalny ale podczas aktualizacji pozwoli na duplikaty, ale podczas tworzenia można podać dowolny wiek, jednak przy aktualizacji musi być liczbą. Walidacja pola name, będzie się zawsze odbywać
class Person < ApplicationRecord
validates :email, uniqueness: true, on: :create
validates :age, numericality: true, on: :update
validates :name, presence: true
end
if/unless
możemy dodać if
do naszej walidacji i odpalać ją tylko gdy jakiś warunek jest spełniony. Tutaj dla przykładu sprawdza numer karty tylko gdy wybrana płatność to płatność kartą
class Order < ApplicationRecord
validates :card_number, presence: true, if: :paid_with_card?
def paid_with_card?
payment_type == "card"
end
end
takie warunki można też grupować
np user admin musi mieć inne warunki niż zwykły user
class User < ApplicationRecord
with_options if: :is_admin? do |admin|
admin.validates :password, length: { minimum: 10 }
admin.validates :email, presence: true
end
end
własna metoda
wcześniej pokazywałem walidacje przy użyciu osobnej klasy. Ale można to też zrobić metodą wewnątrz modelu
class Invoice < ApplicationRecord
validate :active_customer
def active_customer
errors.add(:customer_id, "is not active") unless customer.active?
end
end
Pominięcie walidacji
czasami zdarza się, że musimy zignorować walidjacje. Z jednej strony skoro z jakiegoś powodu ją dodaliśmy to najprawdopodobniej nie powinniśmy tego robić. No ale różne są przypadki.
w takiej sytuacji podczas zapisywania wystarczy dodać argument validate: false
np.
post = Post.new(body: "bar")
post.save(validate: false)
w tym przypadku zapisze nam post bez tytułu
Errory
W idealnym świecie walidacje zawsze przechodzą i w ogóle nie powinniśmy się przejmować errorami. Ale potem przychodzą użytkownicy i pojawiają się błędy których nawet się nie spodziewaliśmy. O takich opowiem innym razem. Gdy już nasza walidacja zwróci error, musimy ją jakoś obsłużyć.
Jeżeli railsów używamy jako API to w zwrotce do klienta po prostu zwracamy błędy, oraz odpowiedni kod. Jeżeli widoki są po stronie railsów to trzeba te errory wyświetlic.
w przypadku API może to wyglądać tak
def create
post = Post.create(title: prams[:title], body: params[:body])
if post.success
render json: { status: "Ok" }
else
render json: { status: "Error", message: post.errors }, status: :bad_request
end
end
w przypadku tradycyjnych widoków musimy dodać obsługę błędów. Oczywiście możemy to dowolnie ostylować
<% if @article.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
<ul>
<% @article.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
To by było na tyle. Mam nadzieje, że temat walidacji jest teraz dla was bardziej zrozumiały. Tak jak wspomniałem po za active recordem można inaczej zrobić walidacje, np przy pomocy dry-validation który przy bardziej skomplikowanych aplikacjach będzie lepszym rozwiązaniem. Ale w sporej ilości przypadków to co oferuje active record jest wystarczające. Gdybyście mieli jakieś pytania zachęcam do zostawienia komentarza. Dziękuje za oglądanie i życzę miłego kodowania.