2013-10-23 8 views
5

Mam sytuację, w której chciałbym uzyskać dostęp do powiązanego dziadka przed zapisaniem obiektu nadrzędnego. Mogę wymyślić kilka hacków, ale szukam czystej drogi, aby to osiągnąć. Poniższy kod ilustruje mój problem:Jak uzyskać dostęp do powiązania między dziadkami ActiveRecord za pośrednictwem powiązania nadrzędnego, które nie zostało jeszcze zapisane?

class Company < ActiveRecord::Base 
    has_many :departments 
    has_many :custom_fields 
    has_many :employees, :through => :departments 
end 
class Department < ActiveRecord::Base 
    belongs_to :company 
    has_many :employees 
end 
class Employee < ActiveRecord::Base 
    belongs_to :department 
    delegate :company, :to => :department 
end 

company = Company.find(1)   # => <Company id: 1> 
dept = company.departments.build # => <Department id: nil, company_id: 1> 
empl = dept.employees.build   # => <Employee id: nil, department_id: nil> 

empl.company # => Employee#company delegated to department.company, but department is nil 

Używam Rails 3.2.15. Rozumiem, co się tutaj dzieje i rozumiem, dlaczego empl.department_id ma wartość nil; Chciałem jednak, aby Railsy odwoływały się do przyszłego skojarzenia przed wywołaniem zapisu, tak aby ostatnia linia mogła być delegowana przez niezapisany obiekt działu. Czy jest czysta praca?

UPDATE: Próbowałem to w Rails 4, jak również, o to sesja konsoli:

2.0.0-p247 :001 > company = Company.find(1) 
    Company Load (1.5ms) SELECT "companies".* FROM "companies" WHERE "companies"."id" = ? LIMIT 1 [["id", 1]] 
=> #<Company id: 1, name: nil, created_at: "2013-10-24 03:36:11", updated_at: "2013-10-24 03:36:11"> 
2.0.0-p247 :002 > dept = company.departments.build 
=> #<Department id: nil, name: nil, company_id: 1, created_at: nil, updated_at: nil> 
2.0.0-p247 :003 > empl = dept.employees.build 
=> #<Employee id: nil, name: nil, department_id: nil, created_at: nil, updated_at: nil> 
2.0.0-p247 :004 > empl.company 
RuntimeError: Employee#company delegated to department.company, but department is nil: #<Employee id: nil, name: nil, department_id: nil, created_at: nil, updated_at: nil> 
2.0.0-p247 :005 > empl.department 
=> nil 

UPDATE 2: Oto test project on github.

+0

Jak bardzo dziwne. Muszę to wypróbować później. Wczoraj wieczorem działało dość wesoło. Sprawdź, czy 'empl.association (: department) == Employee.new.association (: department)' ... – struthersneil

+0

Wynikiem tego wyrażenia jest "false". Widzę, że istnieje zmienna instancji '@ target' na skojarzeniu, która zawsze wydaje się być' nil' (nawet dla zapisanych rekordów). Nie wiem, jaki powinien być cel "@ target", ale miałoby to dla mnie sens, gdyby zawierało ono odnośnik, którego pragnę. –

Odpowiedz

5

Proszę spojrzeć na opcję :inverse_of dla belongs_to i has_many. Ta opcja obsłuży dwustronne przypisania podczas budowania i pobierania powiązanych rekordów w różnych przypadkach.

Od Bi-directional associations dla ActiveRecord::Associations::ClassMethods w docs:

Określanie opcji :inverse_of o stowarzyszeniach pozwala powiedzieć Active Record o odwrotnych relacji i będzie optymalizować obiektu załadunek.

+1

'inverse_of' jest świetny dla niestandardowych sprawdzania poprawności, takich jak' ActiveModel :: EachValidator' na zagnieżdżonej formie, używając 'accepts_nested_attributes_for', gdy potrzebujesz dostępu do rodzica relacji' has_many' z elementu potomnego, ale żaden z rodziców ani potomnych nie jest zapisywany. bez niego, parametr child.parent' będzie równy 'nil' –

0

Po kilku eksperymentach z konsolami - możesz po prostu powiedzieć employee.department.company, nawet jeśli dział nie jest jeszcze zapisany. department_id może być nieobecny, ale istnieje tam skojarzenie department.

2.0.0-p195 :041 > c = Company.create 
    (0.4ms) begin transaction 
    SQL (0.9ms) INSERT INTO "companies" DEFAULT VALUES 
    (486.4ms) commit transaction 
=> #<Company id: 4, department: nil, custom_fields: nil> 
2.0.0-p195 :042 > d = c.departments.build 
=> #<Department id: nil, company_id: 4, employee_id: nil> 
2.0.0-p195 :043 > e = d.employees.build 
=> #<Employee id: nil, department_id: nil> 
2.0.0-p195 :044 > e.department === d 
=> true 
2.0.0-p195 :045 > e.department.company === c 
=> true 

Edit: tak, to nie działa na innym komputerze z inną czystą Rails 4 aplikacji. Jednak nadal działa na moim laptopie ... również w czystej aplikacji Rails 4. Spróbujmy dowiedzieć się, co jest innego!

e.method(:department) 
=> #<Method: Employee(Employee::GeneratedFeatureMethods)#department> 

e.method(:department).source_location 
=> ["/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord- 
    4.0.0/lib/active_record/associations/builder/association.rb", 69] 

Która prowadzi nas tutaj:

Żadnych niespodzianek Naprawdę, ten definiuje metodę zwaną :department

def department *args 
    association(:department).reader(*args) 
end 

to wezwanie do reader prostu zwraca nam @target stowarzyszenia, jeżeli jest obecny lub próbuje go odczytać, jeśli ma identyfikator pod ręką. W moim przypadku @target jest ustawiony na dział d. Aby dowiedzieć się, w którym momencie @target jest ustawiony, możemy przechwytywać target= w ActiveRecord::Associations::Association:

class ActiveRecord::Associations::Association 
    alias :_target= :target= 
    def target= t 
    puts "#{caller} set the target!" 
    _target = t 
    end 
end 

Teraz kiedy nazywamy d.employees.build możemy uzyskać to ...

"/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord-4.0.0/lib/active_record/associations/association.rb:112:in `set_inverse_instance'", 
"/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord-4.0.0/lib/active_record/associations/collection_association.rb:376:in `add_to_target'", 
"/home/neil/.rvm/gems/ruby-2.0.0-p195/gems/activerecord-4.0.0/lib/active_record/associations/collection_association.rb:114:in `build'" 

set_inverse_instance sprawdza invertible_for?(record) (gdzie record to nasza nowa instancja Employee.) To po prostu wywołuje reflection.inverse_of, a to musi zwrócić wartość truey, aby ustawić cel.

def inverse_of 
    return unless inverse_name 

    @inverse_of ||= klass.reflect_on_association inverse_name 
end 

Więc spróbujmy, że obecnie ...

2.0.0-p195 :055 > Employee.reflect_on_association :department 
=> #<ActiveRecord::Reflection::AssociationReflection:0xa881788 @macro=:belongs_to, @name=:department, @scope=nil, @options={}, @active_record=Employee(id: integer, department_id: integer), @plural_name="departments", @collection=false, @class_name="Department", @foreign_key="department_id"> 

To non-zero, więc @target być ustawione w moim związku, kiedy zadzwonić d.employee.build, więc mogę zadzwonić e.department, i tak dalej . Dlaczego więc nie jest tu zero, ale zero dla ciebie (i na drugiej mojej maszynie?) Jeśli zadzwonię Employee.reflections, pojawia się następujący:

> Employee.reflections 
=> {:department=>#<ActiveRecord::Reflection::AssociationReflection:0x9a04598 @macro=:belongs_to, @name=:department, @scope=nil, @options={}, @active_record=Employee(id: integer, department_id: integer), @plural_name="departments", @collection=false, @class_name="Department", @foreign_key="department_id">} 

Jest to produkt sposobu belongs_to - to musi być tam, jeśli spojrzeć. Więc dlaczego (w twoim przypadku) nie ma go pod numerem set_inverse_instance?

+0

Więc, zmieniłem tę odpowiedź po wypróbowaniu tego :) – struthersneil

+0

Hmmm. Nie dla mnie. Z jaką wersją Railsów próbowałeś? –

+0

To było z Rails 4. To powiedziawszy, nie ma o tym żadnej wzmianki w "zauważalnych zmianach" dla ActiveRecord w Railsach 4, więc powinno być dostępne w 3. Nie mam środowiska Rails 3 do ręki lub ja dać mu szansę. Spójrz na to, co otrzymasz z employee.association (: department) i sprawdź, czy referencja tam jest pochowana. – struthersneil

0

Nie kocham tego rozwiązania, ale to wydaje się rozwiązać problem:

empl = dept.employees.build { |e| e.association(:department).target = dept} 

Okazuje się, można przekazać blok budować, a ActiveRecord przyniesie do bloku z nowo utworzonego rekordu . Kto wie, co przyniesie takie dziwactwo ActiveRecord. Pozostawiam pytanie otwarte, aby sprawdzić, czy istnieją lepsze rozwiązania.

Powiązane problemy