Więcej na rubyonrails.pl: Start | Pobierz | Wdrożenie | Kod (en) | Screencasty | Dokumentacja | Ekosystem | Forum | IRC

Walidacje modułu Active Record i Callbacks

Ten przewodnik nauczy Cię w jaki sposób podłączyć się w cykl życia obiektów modułu Active Record. Dowiesz się jak walidować stan obiektów przed wysłaniem ich do bazy danych, oraz jak wykonać niektóre operację w różnych momentach cyklu życia obiektu.

Po przeczytaniu tego przewodnika i wypróbowaniu przedstawionych koncepcji, mamy nadzieję, że będziesz mógł:

1 Cykl życia obiektu

Podczas normalnego użytkowania aplikacji Railsowej obiekty mogą być tworzone, aktualizowane oraz niszczone. Moduł Active Record oferuje zakotwiczenia do poszczególnych cyklów życia obiektu tak abyś mógł kontrolować aplikację i jej dane.

Walidacje zapewniają, że tylko poprawne dane są przechowywane w bazie danych. Callbacks i observers pozwalają wywołać logikę przed lub po zmianach stanu obiektu.

2 Przegląd walidacji

Zanim się zagłębisz w szczegóły walidacji Railsów, powinieneś zrozumieć jak wpisują się one w szerszy obraz.

2.1 Po co używać walidacji?

Walidacje są stosowane w celu zapewnienia, że tylko prawidłowe dane są zapisywane w bazie danych. Na przykład, ważne dla twojej aplikacji może być upewnienie się, że każdy użytkownik poda prawidłowy adres e-mail i adres pocztowy.

Istnieje kilka sposobów sprawdzenia danych zanim zostaną zapisane w bazie danych, są to: własne ograniczenia bazy danych, walidacje po stronie klienta, walidacje z poziomu kontrolera oraz walidacje z poziomu modelu.

  • Ograniczenia bazy danych i/lub przechowywane procedury sprawiają, że mechanizmy walidacji są zależne od bazy danych i mogą utrudniać testowanie i utrzymanie bazy. Jeśli jednak baza danych jest wykorzystywana przez inne aplikacje, dobrym pomysłem może być używanie ograniczeń na poziomie bazy danych. Dodatkowo, walidacje na poziomie bazy danych mogą bezpiecznie obsługiwać pewne rzeczy (takie jak wyjątkowość w często używanych tabelach), które mogą być trudne do wykonywania w inny sposób.
  • Walidacja po stronie klienta może być przydatna, ale ogólnie jest niewiarygodna jeśli jest stosowana sama. Jeżeli są one realizowane z wykorzystaniem Java Scriptu, mogą być pominięte, jeśli użytkownik ma wyłączoną obsługę Java Script w swojej przeglądarce. Jednak w połączeniu z innymi technikami, walidacja po stronie klienta może być wygodnym sposobem do zapewnienia użytkownikom natychmiastowej odpowiedzi podczas przeglądania twojej strony.
  • Walidacja na poziomie kontrolera może być kusząca w użyciu, ale często staje się niewygodna i trudna do testowania i utrzymania. Jeśli to możliwe, dobrym pomysłem jest utrzymanie jak “najchudszych” kontrolerów, co zwiększy przyjemność pracowania w dłuższej perspektywie.
  • Walidacja na poziomie modelu jest najlepszym sposobem zapewnienia, że tylko prawidłowe dane są zapisywane w bazie danych. Są one agnostycznymi bazami danych, które nie mogą być pomijane przez użytkowników końcowych i są wygodne do testowania i utrzymania. Railsy sprawiają, że są one łatwe w użyciu, oferując wbudowane helpery dla powszechnych potrzeb i pozwalają również na tworzenie własnych metod walidacji.

2.2 Kiedy zachodzi walidacja?

Istnieją dwa rodzaje obiektów modułu Active Record: te które odpowiadają oraz te, które nie odpowiadają rzędowi wewnątrz twojej bazy danych. Podczas tworzenia nowego obiektu, na przykład za pomocą nowej metody, obiekt nie należy jeszcze do nowej bazy danych. Kiedy zrobisz save na obiekcie, to zostanie on zapisany w odpowiedniej tabeli bazy danych. Moduł Active Record używa metody instancji new_record? w celu ustalenia czy obiekt jest już w bazie danych, czy nie. Rozważmy następującą, prostą klasę modułu Active Record:

class Person < ActiveRecord::Base end

Możemy zobaczyć jak to działa patrząc na wynik w konsoli:

>> p = Person.new(:name => "John Doe") => #<Person id: nil, name: "John Doe", created_at: nil, :updated_at: nil> >> p.new_record? => true >> p.save => true >> p.new_record? => false

Tworzenie i zapisywanie nowego rekordu wyśle operacje SQL INSERT do bazy danych. Aktualizacja istniejącego rekordu wyśle operacje SQL UPDATE . Walidacje zwykle są uruchamiane przed wysłaniem tych poleceń do bazy danych. Jeśli któraś z walidacji się nie powiedzie, obiekt zostanie oznaczony jako nieprawidłowy i moduł Active Record nie wykona operacji INSERT lub UPDATE. Pomaga to uniknąć przechowywania nieprawidłowych obiektów w bazie danych. Możesz uruchomić różne walidacje kiedy obiekt jest stworzony, zapisany lub zaktualizowany.

Istnieje wiele sposobów, aby zmienić stan obiektów w bazie danych. Niektóre sposoby spowodują walidację, ale niektóre nie. Oznacza to, że możliwe jest zapisanie obiektu w bazie danych w nieprawidłowym stanie, jeśli nie jesteś ostrożny. Następujące metody spowodują walidację i zapiszą obiekt do bazy danych, tylko jeśli jest on poprawny:

  • create
  • create!
  • save
  • save!
  • update
  • update_attributes
  • update_attributes!

Wersje z wykrzyknikiem (e.g.save!) zgłaszają zastrzeżenia jeśli rekord jest niepoprawny. Wersje bez wykrzykników The non-bang versions don’t: save and update_attributes return false, create and update just return the object/s.

2.3 Pomijanie walidacji

Następujące metody pominą walidację i zapiszą obiekt w bazie danych niezależnie od ich poprawności. Należy je stosować z ostrożnością.

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • update_all
  • update_attribute
  • update_counters

Należy pamiętać, że save ma również możliwość pomijania walidacji jeśli opuścimy false jako argument. Tą technikę należy stosować z ostrożnością.

  • save(false)

2.4 valid? i invalid?

Do sprawdzenia czy obiekt jest prawidłowy, Railsy używają metody valid?. Możesz również użyć tej metody na własną rękę. valid? powoduje walidację i zwraca prawdę jeśli żadne błędy nie były dodane do obiektu, lub fałsz jeśli były dodane jakieś błędy.

class Person < ActiveRecord::Base validates_presence_of :name end Person.create(:name => "John Doe").valid? # => true Person.create(:name => nil).valid? # => false

Kiedy moduł Active Record wykonuje walidację, można objąć wszystkie znalezione błędy przykładową komendą errors. Z definicji obiekt jest poprawny (valid) jeśli ten zbiór jest pusty po przeprowadzeniu na nim walidacji.

Zauważ że obiekt ponaglony komendą new nie powiadomi o błędach nawet w przypadku gdy technicznie rzecz ujmując będzie niepoprawny, ponieważ walidacje nie przebiegają podczas używania komendy new.

class Person < ActiveRecord: :Base validates_presence_of :name end >> p = Person.new => #<Person id: nil, name: nil> >> p.errors => #<ActiveRecord: :Errors..., @errors={}> >> p.valid? => false >> p.errors => #ActiveRecord: :Errors..., @errors={"name"=>["can't be blank"]}> >> p = Person.create => #<Person id: nil, name: nil> >> p.errors => #<ActiveRecord: :Errors..., @errors={"name"=>["can't be blank"]}> >> p.save => false >> p.save! => ActiveRecord: :RecordInvalid: Validation failed: Name can't be blank >> Person.create! => ActiveRecord: :RecordInvalid: Validation failed: Name can't be blank

invalid? jest po prostu odwrotnością valid?. Komenda invalid? włącza walidacje i powraca prawdziwa jeśli jakiekolwiek błędy zostały dodane do obiektu, a powraca fałszywa jeśli jest inaczej.

2.5 errors.invalid?

Aby sprawdzić czy jakiś konkretny atrybut obiektu jest poprawny, można użyć metody errors.invalid?. Ta metoda jest użyteczna jedynie PO przeprowadzeniu walidacji, ponieważ sprawdza ona jedynie zbiory błędów a nie włącza procesu walidacji. Jest to inny sposób od omówionego wcześniej ActiveRecord: :Base#invalid? , ponieważ nie potwierdza walidacji obiektu jako całości, a sprawdza jedynie czy znaleziono błędy w pojedynczych atrybutach obiektu.

class Person < ActiveRecord: :Base validates_presence_of :name end >> Person.new.errors.invalid?(:name) # => false >> Person.create.errors.invalid?(:name) # => true

Dokładniej problem walidacji błędów zostanie omówiony w części Praca z Walidacjami Błędów. Na razie wrócimy do wbudowanych pomocy walidacji które są domyślne dla Railsów.

3 Helpery walidacji

Moduł Active Record oferuje wiele zdefiniowanych helperów walidacji których można użyć bezpośrednio w twoich definicjach klasy(class definitions). Helpery te działają według wspólnych zasad walidacji. Za każdym razem kiedy walidacja zawodzi, wiadomość błędu jest dodawana do zbioru errors obiektu, a sama wiadomość jest powiązana z polem podlegającym walidacji.

Każdy helper akceptuje arbitralny numer imion atrybutu, tak że w jednej linii kodu można dodać tą samą walidację do kilku atrybutów.

Wszystkie one akceptują opcje :on oraz :message , które definiują kiedy walidacja powinna zostać uruchomiona i jaka wiadomość powinna być dodana do zbioru errors jeśli zakończy się niepowodzeniem. Opcja :on przyjmuje jedną z wartości :save (domyślna), :create, lub :update. Dla każdego helpera walidacji jest domyślna wiadomość błędu. Wiadomości te są używane kiedy opcja :message nie jest sprecyzowana. Spójrzmy na każdą z dostępnych opcji helperów.

3.1 validates_acceptance_of

Walidacje które w polu interfejsu użytkownika były sprawdzone kiedy forma została wysłana. Typowy przypadek użycia tej komendy to kiedy użytkownik potrzebuje wyrazić zgodę na warunki aplikacji usługi, potwierdzić przeczytania jakiegoś tekstu, lub jakieś innej podobnej czynności. Ta walidacja jest specyficzna dla aplikacji sieciowych i jej “akceptacja” nie musi zostać zapisana gdziekolwiek w bazie danych (jeśli nie ma na nią miejsca, pomoc po prostu stworzy wirtualny atrybut).

class Person < ActiveRecord: :Base validates_acceptance_of :terms_of_service end

Domyślna wiadomość błędu dla komendy validates_acceptance_of to “musi zostać zaakceptowane” (“must be accepted”).

validates_acceptance_of może otrzymać opcję :accept, która opisuje wartość która zostanie przyjęta jako akceptacja. Domyślnie jest to “1”, ale może to zostać zmienione.

class Person < ActiveRecord: :Base validates_acceptance_of :terms_of_service, :accept => 'yes' end

3.2 validates_associated

Z tego helpera należy skorzystać gdy twój model ma powiązania z innymi modelami które również muszą zostać zwalidowane. Gdy spróbujesz zachować swój obiekt, komenda valid? zostanie uruchomiona dla każdego z powiązanych obiektów.

class Library < ActiveRecord: :Base has_many :books validates_associated :books end

Ta walidacja współpracuje z wszystkimi typami powiązań.

Nie używaj validates_associated przy obu końcach powiązania, każde wtedy zamknie się w nieskończoną pętlę.

Domyślna wiadomość błędu dla validates_associated to “niepoprawny” (“is invalid”). Zauważ że każdy z powiązanych obiektów będzie zawierał swój własny zbiór errors; błędy nie gromadzą się w danym modelu.

3.3 validates_confirmation_of

Z tego helpera powinno się korzystać gdy dwa pola tekstowe powinny wypełniać ta sama zawartość. Na przykład, dla potwierdzenia hasła lub adresu mailowego. Ta walidacja tworzy wirtualny atrybut którego nazwa to nazwa pola które ma być potwierdzone załącznikiem “_potwierdzenie” (“_confirmation”).

class Person < ActiveRecord: :Base validates_confirmation_of :email end

W twoim widoku szablon z którego korzystasz może wyglądać tak:

<%= text_field :person, email %> <%= text_field :person, email_confirmation %>

To potwierdzenie stosuje się jedynie gdy email_confirmation to nie nil. Aby żądać potwierdzenia, upewnij się aby dodać sprawdzenie obecności dla atrybutu potwierdzenia (validates_presence_of omówimy dokładniej później):

class Person < ActiveRecord: :Base validates_confirmation_of :email validates_presence_of :email_confirmation end

Domyślna wiadomość błędu dla validates_confirmation_of to “nie ma potwierdzenia” (“doesn’t match confirmation”).

3.4 validates_exclusion_of

Ten helper waliduje czy wartości atrybutów są zawarte w danym zestawie. Właściwie tym zestawem może być każdy przeliczalny obiekt.

class Account < ActiveRecord: :Base validates_exclusion_of :subdomain, :in => %w(www), :message => "Subdomain {{value}} is reserved." end

Helper validates_exclusion_of ma opcję :in która otrzymuje zestaw wartości który nie będzie zaakceptowany dla walidowanego atrybutu. Opcja :in jest inaczej nazywana :within której można używać w ten sam sposób. Ten przykład wykorzystuje opcję :message aby pokazać jak można w nim zawrzeć wartość atrybutu.

Domyślna wiadomość błędu dla validates_exclusion_of to “lista nie zawiera” (“is not included in the list”).

3.5 validates_format_of

Ten helper waliduje czy wartości atrybutów poprzez testowanie pasują do danej regularnej linii kodu która jest sprecyzowana poprzez użycie opcji :with

class Product < ActiveRecord: :Base validates_format_of :legacy_code, :with => /\A[a-zA-Z]+\Z/, :message => "Only letters allowed" end

Domyślna wiadomość błędu dla validates_format_of to “nieprawidłowe” (“is invalid”).

3.6 validates_incusion_of

Ten helper waliduje czy wartości atrybutów są zawarte w danym zestawie. Zestawem tym może być jakikolwiek przeliczalny obiekt.

class Coffee < ActiveRecord: :Base validates_inclusion_of :size, :in => %w (small medium large), :message => "{{value}} is not a valid size" end

Helper validates_inclusion_of posiada opcję :in która otrzymuje zestaw wartości które będą akceptowane. Opcja :in jest też inaczej nazywana :within, której można używać w ten sam sposób. Poprzedni przykład korzysta z opcji :message aby pokazać jak można zawrzeć wartość w atrybucie.

Domyślna wiadomość błędu dla validates_inclusion_of to “nie jest na liście” (“is not included in the list”).

3.7 validates_length_of

Ten helper waliduje długość wartości atrybutów. Przewiduje mnogość opcji, tak że można sprecyzować długość ograniczeń na wiele sposobów.

class Person < ActiveRecord: :Base validates_length_of :name, :minimum => 2 validates_length_of :bio, :maximum => 500 validates_length_of :password, :in => 6 . . 20 validates_length_of :registration_number, :is => 6 end

Możliwe opcje ograniczenia długości to:

  • :minimum – atrybut nie może być krótszy niż sprecyzowana długość.
  • :maximum – atrybut nie może być dłuższy niż sprecyzowana długość.
  • :in (lub :within) – wartość atrybutu musi się zawierać w danym interwale. Wartość dla tej opcji może mieć dużą rozpiętość.
  • :is – wartość atrybutu musi być równa dla danej wartości.

Domyślną wiadomością błędu zależy od typu dla jakiej przeprowadzana jest walidacja długości. Można tym wiadomościom nadać cechy osobiste używając :wrong_length, :too_long lub :too_short oraz %{count} jako numer zastępczy zgodny z wykorzystywaną długością ograniczenia. Wciąż można używać opcji :message dla specyfikacji wiadomości błędu.

class Person < ActiveRecord: :Base validates_length_of :bio, :maximum => 1000, :too_long => "%{count} characters is the maximum allowed" end

Ten helper liczy cyfry domyślnie, ale można podzielić wartość w inny sposób używając opcji :tokenizer :

class Essay < ActiveRecord: :Base validates_length_of :content, :minimum => 300, :maximum => 400, :tokenizer => lambda { |str| str.scan(/\w+/\) }, :too_short => "must have at least {{count}} words", :too_long => "must have at most {{count}} words" end

Helper validates_size_of jest równoznaczny helperowi validates_length_of

3.8 validates_numericality_of

Ten helper waliduje czy atrybuty mają po jednej wartości numerycznej. Domyślnie porówna znaki opcjonalne podążające za całkowitymi lub rzeczywistymi liczbami. Aby sprecyzować że tylko liczby całkowite są dozwolone należy ustawić :only_integer na true.

Gdy ustawić :only_integer na true, to wtedy użyjemy:

/\A[+-]?\d+\Z/

jako stałego wyrażenia dla walidacji wartości atrybutu. W innym wypadku, wartość zostanie zmieniona na liczbę używając Float.

Zauważyć należy że stałe wyrażenie powyżej pozwala na trailing nowego znaku.

class Player < ActiveRecord: :Base validates_numericality_of :points validates_numericality_of :games_played, :only_integer => true end

Poza :only_integer, helper validates_umericality_of akceptuje również następujące opcje do dodania ograniczeń dla akceptowanych wartości:

  • :greater_than – określa że wartość musi być większa niż dostarczona wartość. Domyślna wiadomość błędu dla tej opcji to must be greater than %{count} (musi być większa niż %{count}).
  • :greater_than_or_equal_to – określa że wartość musi być większa lub równa dostarczonej liczby. Domyślna wiadomość błędu dla tej opcji to must be greater than or equal to %{count} (musi być większa lub równa %{liczba}).
  • :equal_to – określa że wartość musi być równa dostarczonej liczbie. Domyślna wiadomość błędu dla tej opcji to must be equal to %{count} (musi być równa %{liczba}).
  • :less_than – określa ze wartość musi być mniejsza niż dostarczona liczba. Domyślna wiadomość błędu tej opcji to must be less than %{count} (musi być mniejsza niż %{liczba}).
  • :less_than_or_equal_to – określa ze wartość musi być mniejsza lub równa dostarczonej liczbie. Domyślna wiadomość błędu tej opcji to must be less or equal to %{count} (musi być mniejsza lub równa %{liczba}).
  • :odd – określa że liczba musi być nieparzysta jeśli ustawiona opcja jest na true. Domyślna wiadomość błędu tej opcji to must be odd (musi być nieparzysta).
  • :even – określa że liczba musi być parzysta jeśli ustawiona opcja jest na true. Domyślna wiadomość błędu tej opcji to must be even (musi być parzysta).

Domyślna wiadomość błędu dla validates_numericality_of to is not a number (nie jest liczbą).

3.9 validates_presence_of

Ten helper waliduje czy określone atrybuty są puste. Używa on metody blank? aby sprawdzić czy wartość to nil lub pusty łańcuch znaków albo łańcucha znaków który składa się z pustej przestrzeni.

class Person < ActiveRecord: :Base validates_presence_of :name, :login, :email end

Chcąc być pewnym że związek istnieje, trzeba sprawdzić czy obcy klucz użyty do stworzenia związku jest obecny, a nie tylko obiekt związku.

class LineItem < ActiveRecord: :Base belongs_to :order validates_presence_of :order_id end

Kiedy false.blank? jest true, a chcemy zwalidować obecność typu logicznego powinniśmy użyć validates_presence_of :field_name, :in => [true, false].

Domyślna wiadomość błędu dla validates_presence_of to can’t be empty (nie może być pusty).

3.10 validates_uniqueness_of

Ten helper waliduje czy atrybut obiektu jest unikatowy tuż przed zapisaniem obiektu. To nie wymusza unikatowości w bazie danych, może się wiec zdarzyć że w dwóch różnych połączeniach tworzą się dwa zapisy o tej samej wartości która miała być wyjątkowa. Aby tego uniknąć, trzeba stworzyć unikatowy wskaźnik w bazie danych.

class Account < ActiveRecord: :Base validates_uniqueness_of :email end

Ta walidacja zachodzi poprzez zainicjowanie zapytania SQL w strukturze modelu, szukając istniejącego zapisu z tą samą wartością co atrybut.

Istnieje opcja :scope której można użyć aby określić inne atrybuty wykorzystywane do ograniczenia liczby unikatów:

class Holiday < ActiveRecord: :Base validates_uniqueness_of :name, :scope => :year, :message => "should happen once per year" end

Jest również opcja :case_sensitive której można użyć aby określić czy ograniczenie unikatowości będzie case sensitive lub nie. Domyślnie opcja jest na true.

class Person < ActiveRecord: :Base validates_uniqueness_of :name, :case_sensitive => false end

Zauważ że niektóre bazy danych są tak skonfigurowane aby przeprowadzać również przeszukiwania case-insensitive .

Domyślna wiadomość błędu dla validates_uniqueness_of to has already been taken (już jest zajęty/-a).

3.11 validates_each

Ten helper waliduje atrybuty przez blok. Nie ma zdefiniowanej funkcji walidacji. Trzeba ją stworzyć używając bloków, i każdy zawarty atrybut w validates_each zostanie przetestowany. W tym przypadku, nie chcemy nazwisk i imion zaczynających się małymi literami.

class Person < ActiveRecord: :Base validates_each :name, :surname do |model, attr, value| model.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/ end end

Blok otrzymuje model, nazwę atrybutu i jego wartość. Można zrobić wszystko aby poszukać poprawnych danych wewnątrz bloku. Jeśli walidacja zawiedzie, można dodać wiadomość błędu do modelu, czyniąc go niepoprawnym.

4 Wspólne opcje walidacji

Są pewne wspólne opcje których mogą używać wszystkie helpery walidacji. Wszystkie są tu omówione prócz :if oraz :unless, które omówione są później w rozdziale Walidacja warunkowa.

4.1 :allow_nil

Opcja :allow_nil opuszcza walidację gdy wartość która jest walidowana to nil. Używanie opcji :allow_nil wraz z validates_presence_of pozwala na walidację nil, ale każda wartość blank? będzie odrzucona.

class Coffee < ActiveRecord: :Base validates_inclusion_of :size, :in => %w (small medium large), :message => "{{value}} is not a valid size", :allow_nil => true end

4.2 :allow_blank

:allow_blank jest opcją podobną do :allow_nil. Ta opcja pozwoli na walidację jeśli wartość atrybutu to blank?, podobnie jak nil lub pusty łańcuch znaków.

class Topic < ActiveRecord: :Base validates_length_of :title, :is => 5, :allow_blank => true end Topic.create("title" => "").valid # => true Topic.create("title" => nil).valid # => true

4.3 :message

Jak już wcześniej pokazano, opcja :message pozwala na sprecyzowanie wiadomości która zostanie dodana do zbioru errors kiedy walidacja zawiedzie. Kiedy ta opcja nie jest używana, moduł Active Record użyje domyślnej wiadomości błędu dla każdego helpera walidacji.

4.4 :on

Opcja :on pozwala na precyzyjne określenie kiedy walidacja powinna nastąpić. Domyślnie zachowanie wbudowanych helperów walidacji jest uruchomienie tuż przed zapisaniem (w obu przypadkach kiedy tworzy się nowy zapis jak i aktualizuje zapis). Chcąc to zmienić, można użyć opcji :on => :create aby uruchomić walidację tylko w momencie tworzenia nowego zapisu lub opcji :on => :update aby uruchomić walidację w momencie aktualizacji już istniejącego zapisu.

class Person < ActiveRecord: :Base # it will be possible to update email with a duplicated value validates_uniqueness_of :email, :on => :create # it will be possible to create the record with a non-numerical age validates_numericality_of :age, :on => :update # the default (validates on both create and update) validates_presence_of :name, :on => :save end

5 Walidacja Warunkowa

Czasami dobrze jest przeprowadzić walidację obiektu nawet gdy dane twierdzenie jest spełnione. Można to zrobić używając opcji :if oraz :unless, które mogą przyjąć symbol, łańcuch znaków lub Proc. Można użyć :if kiedy chcemy sprecyzować kiedy walidacja powinna nastąpić. Precyzując kiedy walidacja nie powinna następować używamy opcji :unless.

5.1 Użycie symbolu w opcjach :if oraz :unless

Można powiązać opcje :if oraz :unless z symbolem odpowiadającym nazwie metody która zostanie wymieniona tuż przed walidacją. Ta opcja jest najczęściej używana.

class Order < ActiveRecord: :Base validates_presence_of :card_number, :if => :paid_with_card? def paid_with_card? payment_type == "card" end end

5.2 Użycie łańcucha znaków w opcjach :if oraz :unless

Można również użyć łańcucha znaków który zostanie oceniony korzystając z eval i potrzebuje zawierać kod Ruby. Tę opcję powinno się używać kiedy łańcuch znaków reprezentuje bardzo krótki warunek.

class Person < ActiveRecord: :Base validates_prasence_of :surname, :if => "name.nil?" end

5.3 Używanie Proc w opcjach :if oraz :unless

Na koniec można powiązać opcje :if oraz :unless z wymienionym obiektem Proc. Używając obiektu Proc można napisać warunek w linii kodu zamiast korzystać z osobnej metody. Ta opcja jest idealna dla jedno liniowych kodów.

class Account < ActiveRecord: :Base validates_confirmation_of :password, :unless => Proc.new { |a| a.password.blank? } end

6 Tworzenie Standardowych Metod Walidacji

Kiedy wbudowane helpery walidacji już nie są wystarczające, można stworzyć własne metody walidacji.

Po prostu należy stworzyć metody które weryfikują stan modelu i dodają wiadomości do zbioru errors kiedy są nieprawidłowe. Następnie należy zarejestrować te metody używając któregoś z podanych metod klasy (class methods) validate, validate_on_create lub validate_on_update, wpisując znaki w nazwy metod walidacji.

Można wpisać więcej niż jeden znak dla każdej z metod klasy i oczekiwać że walidacje zostaną przeprowadzone w tej samej kolejności jak zostały wpisane.

class Invoice < ActiveRecord: :Base validate :expiration_date_cannot_be_in_the_past, :discount_cannot_be_greater_than_total_value def expiration_date_cannot_be_in_the_past errors.add(:expiration_date, "can't be in the past") if !expiration_date.blank? and expiration_date < Date.today end def doscount_cannot_be_greater_than_total_value errors.add(:discount, "can't be greater than total value") if discount > total_value end end

Można nawet stworzyć swoje własne helpery walidacji i używać ich w kilku różnych modelach. Tu jest przykład gdzie stworzyliśmy niestandardowy helper walidacji aby zwalidować format pola które przedstawia adres mailowy:

ActiveRecord: :Base.class_eval do def self.validates_as_radio(attr_name, n, options={}) validates_inclusion_of attr_name, {:in => 1 . . n}.merge(options) end end

Po prostu otwórz ponownie ActiveRecord: :Base i w ten sposób zdefiniuj metodę klasy. Normalnie tę linię kodu umieścilibyśmy gdzieś w config/initializers. tego helpera można użyć w następujący sposób:

class Movie < ActiveRecord: :Base validates_as_radio :rating, 5 end

7 Praca z Błędami Walidacji

Dodatkowo dla metod valid? i invalid? omówionych wcześniej, Railsy przewidują wiele metod pracy ze zbiorami errors i badania walidacji obiektów.

Tu następuje lista najczęściej używanych metod. Proszę się odnosić do dokumentacji ActiveRecord::Errors dla pełnej listy dostępnych metod.

7.1 errors.add_to_base

Metoda add_to_base pozwala na dodawanie wiadomości błędów które odnoszą się do obiektu jako całości, w przeciwieństwie do odnoszenia się do poszczególnych atrybutów. Tej metody można użyć do stwierdzenia że obiekt jest nieprawidłowy, niezależnie do wartości jego atrybutów. add_to_base po prostu otrzymuje łańcuch znaków i używa go jako wiadomości błędu.

class Person < ActiveRecord: :Base def a_method_used_for_validation_purposes errors.add_to_base("This person is invalid because . . . ") end end

7.2 errors.add

Metoda add pozwala na ręczne dodawanie wiadomości które odnoszą się do konkretnych atrybutów. Można użyć opcji =full_messages aby zobaczyć wiadomości w takiej formie w jakiej ukazałyby się użytkownikowi. Te konkretne wiadomości poprzedzają nazwy atrybutów. Opcja add+ otrzymuje nazwę atrybutu do którego chcemy dodać wiadomość, i samą wiadomość.

class Person < ActiveRecord: :Base def a_method_used_for_validation_purposes errors.add(:name, "cannot contain the characters !@#%*()_-+=") end end person = Person.create(:name => "!@#") person.errors.on(:name) # => "cannot contain the characters !@#%*()_-+=" person.errors.full_messages # => ["Name cannot contain the characters !@#%*()_-+="]

7.3 errors.on

Metoda on jest używana gdy chce się sprawdzić wiadomość błędu dla konkretnego atrybutu. Zwraca to różne rodzaje obiektów w zależności od stanu zbioru errors dla danego atrybutu. Gdy nie ma żadnych wiadomości błędu dla danego atrybutu opcja on powraca z nil. Jeśli jest tylko jedna wiadomość błędu on powraca z łańcuchem znaków w wiadomości. Kiedy errors posiada dwie lub więcej wiadomości dal atrybutu on powraca z szykiem łańcucha znaków, dla każdego z jedną wiadomością błędu.

class Person < ActiveRecord: :Base validates_presence_of :name validates_length_of :name, :minimum => 3 end person = Person.new(:name => "John Doe") person.valid? # => true person.errors.on(:name) # => nil person = Person.new(:name => "JD") person.valid? # => false person.errors.on(:name) # => "is too short (minimum is 3 characters)" person = Person.new person.valid? # => false person.errors.on(:name) # => ["can't be blank", "is too short (minimum is 3 characters)"]

7.4 errors.clear

Metoda clear używana jest gdy świadomie chce się wyczyścić zbiór errors. Włączenie errors.clear dla nieprawidłowego obiektu nie uczyni go poprawnym: zbiór errors będzie po prostu pusty, i następnym razem przy komendzie valid? lub jakiejkolwiek innej która spróbuje zapisać obiekt w bazie danych sprawi ponowne uruchomienie walidacji. Jeśli jakaś walidacja zawiedzie, zbiór errors będzie znów zapełniony.

class Person < ActiveRecord: :Base validates_presence_of :name validates_length_of :name, :minimum => 3 end person = Person.new person.valid? # => false person.errors.on(:name) # => ["can't be blank", "is too short (minimum is 3 characters)"] person.errors.clear person.errors.empty? # => true p.save # => false p.errors.on(:name) # => ["can't be blank", "is too short (minimum is 3 characters)"]

7.5 errors.size

Metoda size powraca z całkowitym numerem wiadomości błędów dla danego obiektu.

class Person < ActiveRecord: :Base validates_presence_of :name validates_length_of :name, :minimum => 3 validates_presence_of :email end person = Person.new person.valid? # => false person.errors.size # => 3 person = Person.new(:name => "Andrea", :email => "andrea@example.com") person.valid? # => true person.errors.size # => 0

8 Wyświetlanie błędów walidacji w podglądzie

Railsy oferują wbudowane helpery do wyświetlania powiadomień o błędach twojego modelu w widoku szablonów.

8.1 error_messages i error_messages_for

Podczas tworzenia formularza z aplikacją pomocniczą form_for możesz użyć metody error_messages podczas generowania formularza, aby wyświetlić wszystkie komunikaty błędów dla obecnej postaci modelu.

class Product < ActiveRecord::Base validates_presence_of :description, :value validates_numericality_of :value, :allow_nil => true end
<% form_for(@product) do |f| %> <%= f.error_messages %> <p> <%= f.label :description %><br /> <%= f.text_field :description %> </p> <p> <%= f.label :value %><br /> <%= f.text_field :value %> </p> <p> <%= f.submit "Create" %> </p> <% end %>

Aby zrozumieć sposób działania: jeśli wyślesz formularz z pustymi polami, zazwyczaj wrócisz do niego i niezależnie od tego jakie są style, brakujące pola zostaną oznaczone domyślnie:

Error messages

Do wyświetlania komunikatów błędów modelu możesz użyć również metody error_messages_for. Jest bardzo podobna do poprzedniego przykładu i daje dokładnie taki sam rezultat.

<%= error_messages_for :product %>

Wyświetlany dla każdego komunikatu błędu tekst będzie się zawsze składał z wypisanej wielkimi literami nazwy atrybutu, w którym występuje błąd, oraz właściwej treści komunikatu.

Obydwa helpery, zarówno form.error_messages jak i error_messages_for, zawierają opcję umożliwiającą przystosowywanie bloku div, w którym wyświetlane są komunikaty o błędach, zmianę jego nagłówka, wiadomości wyświetlającej się pod nagłówkiem oraz tagu używanego do elementu zawierającego nagłówek.

<%= f.error_messages :header_message => "Invalid product!", :message => "You'll need to fix the following fields:", :header_tag => :h3 %>

Skutkuje to pojawieniem się następujących treści:

Customized error messages

Jeżeli przypiszesz nil do którejś z tych pozycji, pozbawi to element div indywidualnego charakteru danej pozycji.

8.2 Personalizowanie komunikatów błędów za pomocą CSS

Selektorami personalizowania komunikatów błędów są:

  • .fieldWithErrors – styl dla pól i linii z błędami
  • #errorExplanation – styl dla elementów div zawierających komunikaty błędów
  • #errorExplanation h2 – styl dla nagłówka elementu div
  • #errorExplanation p – styl akapitu zawierającego komunikat błędu, znajdującego się zaraz pod nagłówkiem elementu div
  • #errorExplanation ul li – styl listy pozycji z wyszczególnionymi komunikatami błędów

Rusztowanie generuje na przykład public/stylesheets/scaffold.css, który określa ten “czerwony” styl, który widziałeś powyżej.

Nazwa klasy oraz id może być zmieniona za pomocą opcji :class oraz :id, co jest obsługiwane przez obydwie metody.

8.3 Personalizowanie komunikatów błędów za pomocą HTML

Domyślnie pola formularza z błedami są wyświetlane wewnątrz bloku div o klasie CSS fieldWithErrors. Tak czy inaczej, można to obejść.

Sposób, w jaki traktowane są pola formularza z błędami, jest określony przez ActionView::Base.field_error_proc. Jest to Proc, który otrzymuje dwa parametry:

  • łańcuch znaków z tagiem HTML
  • instancję ActionView::Helpers::InstanceTag

Oto prosty przykład gdzie zmieniamy zachowanie Railsów tak, aby zawsze wyświetlać komunikaty błędów przed każdym polem formularza z błędem. Komunikaty błędów będą zawarte w bloku span o klasie CSS validation-error. Nie będzie bloku div zawierającego element input, więc pozbywamy sie czerwonego obramowania dookoła pola tekstowego. Możesz użyc klasy CSS validation-error, aby wystylizować je jak chcesz.

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| if instance.error_message.kind_of?(Array) %(#{html_tag}<span class="validation-error">&nbsp; #{instance.error_message.join(',')}</span>) else %(#{html_tag}<span class="validation-error">&nbsp; #{instance.error_message}</span>) end end

Skutkuje to pojawieniem się czegoś podobnego do następującej treści:

Validation error messages

9 Przegląd callbacków

Callbacki są metodami przywoływanymi w pewnych momentach cyklu życia obiektu. Dzięki callbackom możliwe jest napisanie kodu, który zadziała, kiedykolwiek obiekt modułu Active Record zostanie utworzony, zachowany, zaktualizowany, usunięty, zwalidowany lub załadowany z bazy danych.

9.1 Rejestracja callbacków

Aby używać dostępnych callbacków, musisz je najpierw zarejestrować. Możesz to zrobić implementując je jako zwykłe metody, a potem użyć metody makrostylowych klas, aby zarejestrować je jako callbacki.

class User < ActiveRecord::Base validates_presence_of :login, :email before_validation :ensure_login_has_a_value protected def ensure_login_has_a_value if login.nil? self.login = email unless email.blank? end end end

Metody makrostylowych klas mogą również otrzymywać bloki. Rozważ używanie tych stylów, jeśli kod wewnątrz twojego bloku jest tak krótki, że mieści się w jednej linii.

class User < ActiveRecord::Base validates_presence_of :login, :email before_create {|user| user.name = user.login.capitalize if user.name.blank?} end

Deklarowanie callbacków w strefie chronionej lub prywatnej uważa się za dobrą praktykę. Jeśli zostanie to zrobione w strefie publicznej, mogą one zostać wywołane spoza modelu i naruszają zasadę jego enkapsulacji.

10 Dostępne callbacki

Oto lista dostępnych callbacków modułu Active Records, wypisanych w tej samej kolejności, w której są wywoływane podczas poszczególnych operacji:

10.1 Tworzenie obiektu

  • before_validation
  • before_validation_on_create
  • after_validation
  • after_validation_on_create
  • before_save
  • before_create
  • INSERT OPERATION
  • after_create
  • after_save

10.2 Aktualizowanie obiektu

  • before_validation
  • before_validation_on_update
  • after_validation
  • after_validation_on_update
  • before_save
  • before_update
  • UPDATE OPERATION
  • after_update
  • after_save

10.3 Usuwanie obiektu

  • before_destroy
  • DELETE OPERATION
  • after_destroy

after_save działa i podczas tworzenia i aktualizowania, ale zawsze po bardziej specyficznych callbackach after_create i after_update, nie ważne w jakiej kolejności wykonywane były makrowywołania.

10.4 after_initialize i after_find

Callback after_initialize jest wywoływany kiedykolwiek zostaje utworzony egzemplarz obiektu modułu Active Records. Jest to użyteczne do uniknięcia pominięcia metody initialize modułu Active Record.

Callback after_find jest uruchamiany kiedykolwiek rekord zostaje załadowany z bazy danych. after_find jest wywoływana przed after_initialize, jeżeli zdefiniowane są obydwie z tych metod.

Callbacki after_initialize i after_find są trochę inne niż pozostałe. Nie mają odpowiedników before_*, a jedynym sposobem na ich rejestrację jest zdefiniowanie ich jako zwykłych metod. Jeżeli spróbujesz zarejestrować je jako metody klasy makrostylowej, zostaną one po prostu pominięte. Takie zachowanie spowodowane jest kwestiami związanymi z wydajnością, odkąd after_initialize i after_find są wywoływane przy każdym odnalezieniu rekordu w bazie, spowalniając znacząco zapytania.

class User < ActiveRecord::Base def after_initialize puts "You have initialized an object!" end def after_find puts "You have found an object!" end end >> User.new You have initialized an object! => #<User id: nil> >> User.first You have found an object! You have initialized an object! => #<User id: 1>

11 Uruchamianie callbacków

Następujące metody uruchamiają callbacki:

  • create
  • create!
  • decrement!
  • destroy
  • destroy_all
  • increment!
  • save
  • save!
  • save(false)
  • toggle!
  • update
  • update_attribute
  • update_attributes
  • update_attributes!
  • valid?

Callback after_find jest dodatkowo uruchamiany przez następujące metody wyszukiwania:

  • all
  • first
  • find
  • find_all_by_attribute
  • find_by_attribute
  • find_by_attribute!
  • last

Calback after_initialize jest uruchmiany zawsze, kiedy zostaje zainicjowana nowa klasa lub obiekt.

12 Pomijanie callbacków

Podobnie jak walidację można pominąć callbacki. Tak czy inaczej takie metody powinny być używane ostrożnie, ponieważ logika aplikacji może być przez nie utrzymywana. Omijanie ich bez zrozumienia może potencjalnie skutkować nieprawidłościami w danych.

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • find_by_sql
  • increment
  • increment_counter
  • toggle
  • update_all
  • update_counters

13 Zatrzymywanie wykonania

Kiedy zaczynasz rejestrację nowych callbacków dla swojego modelu są one ustawiane w kolejce wykonania. Ta kolejka zawiera wszystkie walidacje modelu, zarejestrowane callbacki oraz operacje na bazie danych.

Łańcuch callbacków zachowuje się jak transakcja. Jeśli któryś z callbacków zwróci false lub wyjątek, łańcuch transakcji zostaje zatrzymany i emitowany jest ROLLBACK. Transakcja może byż zakończona dopiero po zgłoszeniu wyjątku.

Zgłoszenie przypadkowego wyjątku może załamać kod oczekujący metody save i jej podobnych a nie przerwania w ten sposób. Wyjątek ActiveRecord::Rollback jest przemyślany precyzyjnie w ten sposób, by zakomunikować modułowi Active Record, że wykonany zostaje rollback. Jest on wewnętrznie wyłapywany, ale nie zgłaszany ponownie.

14 Relacyjne callbacki

Callbacki działaja za pomocą relacji modelu i mogą być przez nie definiowane. Spójrzmy na następujący przykład: użytkownik ma wiele postów. W naszym przykładzie posty danego użytkownika powinny być usuwane, jeżeli usunięty zostanie użytkownik. Tak więc dodajemy callback after_destroy do modelu Users na drodze jego relacji z modelem Post.

class User < ActiveRecord::Base has_many :posts, :dependent => :destroy end class Post < ActiveRecord::Base after_destroy :log_destroy_action def log_destroy_action puts 'Post destroyed' end end >> user = User.first => #<User id: 1> >> user.posts.create! => #<Post id: 1, user_id: 1> >> user.destroy Post destroyed => #<User id: 1>

15 Callbacki warunkowe

Tak jak przy walidacjach, możemy skonstruować callbacki warunkowe, tak by były wykonywane tylko kiedy zostanie spełniony dany warunek. Możesz to zrobić używając opcji :if oraz :unless, które mogą pobierać symbol, łańcuch znaków lub Proc. Możesz użyć opcji :if, kiedy chcesz zaznaczyć, kiedy dany callback powinien zostać wykonany. Aby zaznaczyć, że callback nie powinien zostać wykonany w danej sytuacji, możesz użyć opcji :unless.

15.1 Używanie :if oraz :unless z symbolami

Możesz używać opcji :if i :unless z symbolem właściwym dla nazwy metody wywoływanej zaraz przed callbackiem. Jeżeli metoda ta zwróci false, callback nie zostanie wykonany. Sposób ten jest najbardziej rozpowszechniony. Używanie tej formy rejestracji callbacków umożliwia również zarejestrownie kilku innych metod, które powinny zostać poddane sprawdzeniu, czy callback może zostać wykonany.

class Order < ActiveRecord::Base before_save :normalize_card_number, :if => :paid_with_card? end

15.2 Używanie if oraz unless z łańcuchami znaków

Możesz użyć rownież łańcucha znaków, który będzie przeliczany za pomocą eval i będzie zawierał poprawny kod Ruby. Powinieneś używać tej opcji tylko wtedy, gdy łańcuch znaków reprezentuje naprawdę krótki warunek.

class Order < ActiveRecord::Base before_save :normalize_card_number, :if => "paid_with_card?" end

15.3 Używanie :if oraz :unless z Proc

Możliwe jest także skojarzenie :if oraz :unless z obiektem Proc. Ta opcja najlepiej się dopasowuje przy pisaniu kraótkich metod walidacji, zazwyczaj jedno liniowych.

class Order < ActiveRecord::Base before_save :normalize_card_number, :if => Proc.new { |order| order.paid_with_card? } end

15.4 Warunki wielokrotne dla callbacków

Podczas pisania warunkowych callbacków jest możliwe łączenie obu opcji – :if oraz :unless w tej samej deklaracji callbacka.

class Comment < ActiveRecord::Base after_create :send_email_to_author, :if => :author_wants_emails?, :unless => Proc.new { |comment| comment.post.ignore_comments? } end

16 Klasy callbacków

Czasami metody callbackowe, które napisałeś, są na tyle użyteczne, że mogą zostać wykorzystane w innych modelach. Moduł Active Records umożliwia tworzenie klas, które enkapsulują metody callbackowe, więc ponowne ich użycie jest bardzo proste.

Oto przykład, w którym tworzymy klasę zawierającą callback after_destroy dla modelu PictureFile:

class PictureFileCallbacks def after_destroy(picture_file) File.delete(picture_file.filepath) if File.exists?(picture_file.filepath) end end

Jeśli jest to zdeklarowane wewnątrz klasy, metoda callbackowa będzie otrzymywała obiekt modelu jako parametr. Możemy jej teraz użyć w następujący sposób:

class PictureFile < ActiveRecord::Base after_destroy PictureFileCallbacks.new end

Zauważ, że musimy stworzyć egzemplarz obiektu PictureFileCallbacks odkąd zadeklarowaliśmy nasze callbacki jako metody instancyjne. Czasami wydaje się być bardziej sensownym zadeklarować je jako metody klasowe.

class PictureFileCallbacks def self.after_destroy(picture_file) File.delete(picture_file.filepath) if File.exists?(picture_file.filepath) end end

Jeżeli klasy callbackowe zostaną zadeklarowane w ten sposób, nie będzie koniczne tworzenie egzemplarza obiektu PictureFileCallbacks.

class PictureFile < ActiveRecord::Base after_destroy PictureFileCallbacks end

Możesz stworzyć tyle callbacków ile chcesz w ramach swoich klas callbackowych.

17 Obserwatorzy

Obserwatorzy są podobni do callbacków, ale są pewne ważne różnice. Gdziekolwiek callbacki mogą zaśmiecać kodem, który nie jest bezpośrednio związany z celem modelu, obserwatorzy umożliwiają dodanie takiej funkcjonalności poza modelem. Na przykład, uzasadnionym wydaje się, aby model User nie zawierał kodu wysyłającego e-maile potwierdzające rejestrację. Kiedykolwiek używasz callbacków o kodzie nie związanym bezpośrednio z modelem, możesz rozważyć stworzenie zamiast tego obserwatora.

17.1 Tworzenie obserwatorów

Wyobraź sobie model User, za pomocą którego chcemy, aby wysyłane były e-maile potwierdzające rejestrację nowego użytkownika. Ponieważ wysyłanie maili nie jest bezpośrednio powiązane z celem obiektu, możemy stworzyć obserwatora zawierającego tą funkcjonalność.

class UserObserver < ActiveRecord::Observer def after_create(model) # code to send confirmation email... end end

Tak jak w klasach callbackowych, metody obserwatorów otrzymują obserwowaną metodę jako parametr.

17.2 Rejestrowanie obserwatorów

Obserwatorzy są zazwyczaj umiejscowieni w lokalizacji app/models a zarejestrowani w pliku config/environment.rb. Na przykład UserObserver, którym zajmowaliśmy się powyżej, zostanie zachowany jako app/models/user_observer.rb a zarejestrowany w config/environment.rb w następujący sposób:

# Activate observers that should always be running config.active_record.observers = :user_observer

Jak zwykle, ustawienia w config/environments mają pierwszeństwo nad tymi w config/environment.rb. Więc jeśli wolisz, aby obserwator nie działał na wszytskich środowiskach, możesz go zdefiniować w specjalnie dla niego poświęconym środowisku.

17.3 Udostępnianie obserwatorów

Domyślnie Railsy pozbawiają członu “Observer” nazwę obserwatora aby odszukać model, który ma on obserwować. Tak czy inaczej, obserwatorzy mogą być wykorzystywani do obserowawania więcej niż jednego modelu i możliwe jest ręczne uściślenie, których modeli mają dotyczyć.

class MailerObserver < ActiveRecord::Observer observe :registration, :user def after_create(model) # code to send confirmation email... end end

W tym przykładzie metoda after_create będzie wywoływana zawsze, kiedy stworzony zostanie Registration lub User. Zauważ, że MailerObserver musi być zarejestrowany w config/environment.rb, aby mógł działać.

# Activate observers that should always be running config.active_record.observers = :mailer_observer

18 Changelog

Lighthouse ticket

  • March 7, 2009: Callbacks revision by Trevor Turk
  • February 10, 2009: Observers revision by Trevor Turk
  • February 5, 2009: Initial revision by Trevor Turk
  • January 9, 2009: Initial version by Cássio Marques