Jedna z tych rzeczy którą prędzej czy później się spotyka przy zaawansowanych projektach i na którą aktualnie nie ma łatwego rozwiazania to walidacja wielu obiektów które są ze sobą powiązane w jakiś sposób. Czy macie jakieś dobre przykłady dla tego typu case’a ? Jak sobie z tym radzicie ? Co możecie polecić ?
Inna rzecz. Walidacje na destroy. Obiekt może być usunięty tylko pod jakimiś warunkami. Jak rozwiązujecie taki problem ?
To raczej kwestia autoryzacji a nie walidacji. Zwykle w naszych projektach obiekt ma metodę typu “deletable_by?” (destroyable_by?), która sprawdza czy użytkownik może usunąć dany obiekt.
Co do pierwszego pytania - niebardzo wiem czego konkretnie dotyczy. ActiveRecord domyślnie waliduje wszystkie modele podrzędne (asocjacje has_many, has_one, itp.). Walidator “Associated” umożliwia też walidowanie asocjacji z drugiej strony (belongs_to), trzeba tylko uważać, żeby się nie zapętlić :-).
Walidacja destroy: najłatwiej nadpisać metodę “destroy” i zwrócić z niej false w przypadku gdy rekord nie może być usunięty. Przynajmniej tak zakłada defaultowy responder w Rails 3.
Jeśli chodzi o sprawdzenie warunku przy usuwaniu, wystarczy przekazać metodę do callbacku before_destroy - jeśli metoda zwróci false, obiekt nie zostanie usunięty
[code=ruby]class User < AR
before_destroy :destroyable?
def destroyable?
User.count != 1
end
end[/code]
Można też zrobić callbackiem after_destroy -zwraca false-zmiany są cofnięte. Jest to o tyle dobre, że jeśli obiekt usuwany jest w jakichś relacjach, to w after_destroy możesz sobie łatwo sprawdzić dla nich warunki.
A jak później pozbyć się takiego obiektu bez modyfikowania kodu (oczywiście wraz z obiektami powiązanymi i wyzwoleniem pozostałych callbacków)?
Właśnie dlatego od jakiegoś czasu nie wrzucam takich reguł do modeli. To kontroler ma zapytać model, czy można go usunąć (@user.destroy if @user.destroyable?), ale jak się uprę, to model zawsze pozwoli na usunięcie siebie. Czyli nie do końca głupi kontroler i inteligentny, choć uległy model.
Wolałbym coś w stylu destroy(:validate => false) jak przy save. Nie chodzi o samą niemożność usunięcia obiektu ale też by mieć jakieś komunikaty odnośnie tego dlaczego nie można go usunąć, najchętniej w jakiś prosty sposób zaprzągłbym walidacje do tego celu.
No jest jeszcze metoda Model.delete(id)
Generalnie jeżeli chodzi o walidację powiązanych projektów to jest validates_associated z tego co pamiętam.
Jeżeli chodzi o niszczenie pod pewnymi warunkami, to albo walidacja :on => :destroy albo before filter. Generalnie standardowo zasada jest taka że kontroler nie powinien próbować niszczyć obiektów które nie powinien (zgodnie z tym co powiedziano wcześniej), a jeżeli próbuje powinien zostać rzucony exception.
A dlaczego walidacje przy aktualizacji obiektu mamy mieć po stronie modelu i kontroler po prostu wywołuje i albo się uda albo nie a przy usuwaniu to już kontroler nie powienien jak nie może i musi się pytac czy może łaskawie ten model usunąć ? Fajny pomysł z walidacją “on destroy” ale czy ona się w ogóle wykona ? mniemam, że destroy nie wywołuje pod spodem valid? ale może coś w stylu:
class Model
validate :costam, :on => :destroy
def destroy
return false if invalid?(:destroy)
super
end
end
IMO to zależy jakie są te warunki o sprawdzaniu których mówisz.
Jeśli usunięcie danego obiektu spowoduje naruszenie spójności danych, to powinno być to regulowane przez before_destroy lub podobny mechanizm.
Jeśli natomiast chcesz zapobiec usunięcia obiektu po logika aplikacji na to nie pozwala, to lepiej użyć autoryzacji.
Dla przykładu:
Jeśli masz powiązane obiekty User i Profile i użytkownik nie jest poprawny bez profilu to powinieneś zabronić usuwania tego modelu przez before_destroy, walidacje lub podobne.
Jeśli natomiast masz model Article i powiązany z nim Attachment i logika Twojej aplikacji mówi, że użytkownik nie powinien móc usuwać załączników z istniejących artykułów, to jest to kwesta autoryzacji.
Rule of thumb: Jeśli akcja, której chcesz zabronić powinna być możliwa do wykonania przez administratora lub z konsoli—użyj walidacji. Jeśli nawet w takiej sytuacji usunięcie obiektu nie powinno być możliwe, before_destroy et al. są lepszym rozwiązaniem.
Jest jeszcze ciekawy problem, które się pojawia przy takim nie do końca powiązaniu obiektów.
Mamy np zbiór jakiś obiektów, które posiadają pewną wartość i suma tych wartości musi być mniejsza niz 20. Przykład od czapy ale tak by pokazać case.
Gdzie zapisać tą walidację? Nie jest to właściwość poszczególnego każdego obiektu ale ich zbióru. Zakładając, że istnieje obiekt Zbiór, który ma wiele tych obiektów które zawiera można by to zapisać w nim. Zakładam wtedy, że podpowiadacie użycie validates_associated :set po stronie elementu zbioru tak ?
Tylko czy wtedy edytując 2 obiekty jednocześnie uda mi się osiągnać dobry efekt ?
Jak wtedy usywasz User i Profile jednocześnie ?
Skoro przy usuwaniu Profile pójdzie before_destroy.
Dodajesz jakiś wirtualny atrybut który jest ustawiany przy usuwaniu Profile kiedy usuwany jest User i ten wirtualny atrybut powoduje brak odpalenia tego sprawdzającego before_destroy ?
Moje aktualne rozwiązanie wygląda tak:
[code] # By default validations should not run
on :destroy context
def self.validate(*args, &block)
options = args.extract_options!
unless options.key?(:on)
options = options.dup
options[:if] = Array.wrap(options[:if])
options[:if] << “validation_context != :destroy”
end
args << options
super
end
Add validation on destroy
def destroy
return false if invalid?(:destroy)
super
end[/code]
i pozwala odpalać walidacje na przy usuwaniu: w stylu “nie mozna usunąć klienta, jęsli wystawiono mu już conajmniej jedną fakturę”.
Wydaje mi się to o tyle lepsze rozwiązanie, że poprzez :errors mogę zwrócić informacje dlaczego to usunięcie się nie powiodło co przy before_destroy nie byłoby już takie proste.