Klucze obce a ActiveRecord

Mam pytanie. Kiedy mam dwa modele i jeden posiada klucz obcy do drugiego oznaczone poprzez belongs_to tak jak poniżej

class Employee
  has_many :equipments, dependent: :restrict_with_exception
end

class Equipment < ActiveRecord::Base
   belongs_to    :equipment_category
   belongs_to    :employee

   validates     :description, presence: true

   validates     :equipment_category_id, presence: true
   validates     :equipment_category, presence: true

   validates     :employee, presence: true
end

class EquipmentCategory < ActiveRecord::Base
   has_many :equipments, dependent: :restrict_with_exception
 end

I teraz interesuje mnie jak zrobić aby Equipment przy zapisie sprawdzał czy id w zmiennej equipment_category_id wskazuje na istniejący wpis. Jak zrobić aby przy usuwaniu elementu z EquipmentCategory nie pozwoliło mi na usunięcie dopóki istnieją wpisy w Equipment wskazujące na usuwany element.

Aby mieć pewność, że taki wpis istnieje:

validates :equipment_category, presence: true # walidacja istnienia obiektu, a nie tylko identyfikatora (ten mógł być wzięty z kapelusza),

Sprawa blokowania usuwania:

has_many :equipments, dependent: :restrict_with_exception

lub:

has_many :equipments, dependent: :restrict_with_error

Dodatkowo proponowałbym założenie w bazie ograniczeń klucza obcego (w Rails 4.2 jest to dostępne od ręki, w starszych wersjach warto skorzystać z foreignera).

@Jacku,

Czy ja dobrze rozumiem dokumentację, że jeżeli mamy zapis:

has_many :categories, dependent: :destroy

to później można też stosować takie konstrukcje:

  before_destroy :equipment_have_categories, prepend: true

  def equipment_have_categories
    if categories.any? 
      errors[:base] << "Cannot delete equipment while have categories "
      false
    end
  end

?

Owszem, choć ważny jest właśnie parametr: prepend: true. W przeciwnym razie kategorie byłyby usunięte jako pierwsze i nie byłoby co sprawdzać.

2 Likes

dzięki za rozwianie wątpliwości :slight_smile:

Super. To dość sporo tłumaczy. A co z sytuacją, kiedy klucz obcy może być NULL’em?

A możesz podać przypadek, gdy klucz obcy jest NULL?

Bo gdy definicja wygląda tylko tak

has_many :categories

bez tego:

dependent: :destroy

to mówi ona:
“W czasie usuwania rekordu tabeli nadrzędnej nie usuwaj wierszy skojarzonych definicją has_many (tabela podrzędna), a tylko wstaw do kolumn z referencją wartość NULL”

Zatem w tabeli powiązanej pozostaną tzw zombie/duchy, czyli wiersze bez odniesienia do tabeli nadrzędnej.

Czyli:
Tabela Ojciec
id name
1 Pan Jacek
2 Pan Krzysiek
3 Pan Robert

Tabela Dziecko
id ojciec_id name
1 1 Ania
2 1 Krysia
3 2 Janek
4 3 Basia

… Po usunięciu Ojca = “Pan Krzysiek” (id =2) będziesz miał

Tabela Dziecko
id ojciec_id name
1 1 Ania
2 1 Krysia
3 NULL Janek
4 3 Basia

ale jeżeli w definicji było:

has_many :categories, dependent: :destroy

to będziesz miał

Tabela Dziecko
id ojciec_id name
1 1 Ania
2 1 Krysia
4 3 Basia

Edit:
Nie chcę za bardzo tutaj rozwijać teorii, ale powinno się wystrzegać sytuacji, gdzie w bazie pozostają te ZOMBIE i to z kilku powodów.
Najważniejsze to takie:

  1. Gdy policzysz wszystkie dzieci (dzieci.count), to wyjdzie Ci, że jest ich 4, ale gdy policzysz dla każdego ojca w bazie ile on ma swoich dzieci, to wyjdzie, że 3. Widzisz niespójność?
  2. Usuwając rekordy z tabeli podrzędnej masz 100% pewność, że wyświetlając później dane na zasadzie
    @dziecko.ojciec.name, to zawsze będziesz miał jakieś “Pan Jacek”, “Pan Krzysiu” itd.
    Jeżeli jednak zrobisz to dla rekordu ZOMBIE, to będziesz miał NIL w kolumnie ojciec_id, co często zwróci Ci jakiś ERROR.

Mam taką sytuację, w której

W klasie Equipment employee_id może być NULL lub mieć klucz obcy, i chce sprawdzać czy jeśli nie jest NULL’em (przy tworzeniu/edycji podawana jest wartość) to czy obiekt o wskazanym id istnieje.

Kod klas będzie w 1 poście.

o ile dobrze rozumiem Twoje pytanie, to:

if @equipment_categories.equipment_id.nil?
  "Nawet nie szukam, bo jest NULL"
else
  if Equipment.find_by(id: @equipment_categories.equipment_id)
    "szukałem i znalazłem"
  else
    "szukałem i nie znalazłem"
  end
end

…chyba, że nie rozumiem o co pytasz.

No i zapis “wygładź”, bo w Railsach moda jest, by pisać:

"znalazłem" if Equipment.find_by(id: @equipment_categories.equipment_id)

Przeczytaj jeszcze o różnicy między find_by!() a find_by()

… ten wykrzyknik ma znaczenie :slight_smile:

tutaj link, który rzuca światło na to (wyszukanie z komunikatem błędu lub bez)
http://forum.rubyonrails.pl/t/find-by-i-404

Zrozumiałeś dobrze. Tylko skąd pomysł, że używam funkcji find :smile:

Chcę aby to model dbał o sytuację z kluczami obcymi tj. kiedy jest NULL nie robił walidacji tylko wsadził w bazę NULL’a a kiedy jest wartość sprawdził czy nadaje się to na klucz obcy i czy istnieje jakiś Employee z takim kluczem.

PS. Znam różnicę w metodach z i bez wykrzyknika :wink:

zatem napisz metodę validacji, która sprawdza co trzeba

Przyzwyczajenie z Django, że framework robi za mnie takie rzeczy :wink:

Nieprawda, domyślne zachowanie jest takie, że klucz obcy nie jest usuwany. Aby wstawić tam NULL-a, należy przekazać parametr dependent: :nullify

True

Winno być
“W czasie usuwania rekordu tabeli nadrzędnej nie usuwaj wierszy skojarzonych definicją has_many (tabela podrzędna). Stosując klauzulę dependent: :nullify wstaw do kolumny z referencją wartość NULL”

  • przyznam, że nie wiem, dlaczego opuściłem ten fragment.

:frowning: wstydzę się bardzo. :frowning:

Dobrze wiedzieć. Jest jakiś dokładny spis co i z czym mogę używać w Modelach RoR’a bez przekopywania się przez całą dokumentację (wszystkie te dependend: i :nullify w jednym krótkim spisie)?