I have a situation where I would like access to an associated grandparent before the parent object is saved. I can think of several hacks, but I'm searching for a clean way to accomplish this. Take the following code as an illustration of my problem:
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
I'm using Rails 3.2.15. I understand what is happening here, and I understand why empl.department_id is nil; though I wish Rails held a direct reference to the prospective association prior to calling save, such that the last line could be delegated through the unsaved department object. Is there a clean work around?
UPDATE: I've tried this in Rails 4 as well, here is a console session:
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: Here is a test project on github.
Please look into the :inverse_of
option for belongs_to
and has_many
. This option will handle two-way assignments when building and fetching associated records in different cases.
From Bi-directional associations for ActiveRecord::Associations::ClassMethods
in the docs:
Specifying the :inverse_of
option on associations lets you tell Active Record about inverse relationships and it will optimise object loading.
I don't love this solution, but this seems to solve the problem:
empl = dept.employees.build { |e| e.association(:department).target = dept}
It turns out you can pass a block to build, and ActiveRecord will yield to the block with the newly created record. Who knows what ActiveRecord weirdness such will bring. I'm leaving the question open for now to see if there are better solutions.
After some console experimentation--you can just say employee.department.company
, even if department is not yet saved. department_id
may be nil, but the department
association is there.
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: so, this didn't work on a different machine with another clean Rails 4 app. However, it still works on my laptop...also in a clean Rails 4 app. Let's try and figure out what's different!
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]
Which leads us here:
def define_readers
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(*args)
association(:#{name}).reader(*args)
end
CODE
end
No surprises really, this defines a method called :department
def department *args
association(:department).reader(*args)
end
That call to reader
just returns us the @target of the association if it is present, or tries to read it if it has an id at hand. In my case the @target
is set to department d
. To discover the point at which @target is set, we can intercept target=
in ActiveRecord::Associations::Association
:
class ActiveRecord::Associations::Association
alias :_target= :target=
def target= t
puts "#{caller} set the target!"
_target = t
end
end
Now when we call d.employees.build
we get this...
"/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
is checking invertible_for?(record)
, (where record
is our new Employee instance.) This just calls reflection.inverse_of
, and this has to return a truthy value in order for target to be set.
def inverse_of
return unless inverse_name
@inverse_of ||= klass.reflect_on_association inverse_name
end
So let's try that out...
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">
That's non-nil, so @target will be set in my association when I call d.employee.build
, so I can call e.department
, and so on. So why is it non-nil here, but nil for you (and over on my other machine?) If I call Employee.reflections
, I get the following:
> 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">}
This is the product of the belongs_to
method--it's got to be there if you look. So why (in your case) doesn't set_inverse_instance
find it?