This problem occurs because the instance of Parent is initialized before it gets added to (associated with) the instance of Grandparent. Let me illustrate that to you with the following example:
class Grandparent < ApplicationRecord
# before_add and after_add are two callbacks specific to associations
# See: http://guides.rubyonrails.org/association_basics.html#association-callbacks
has_many :parents, inverse_of: :grandparent,
before_add: :run_before_add, after_add: :run_after_add
# We will use this to test in what sequence callbacks/initializers are fired
def self.test
@grandparent = Grandparent.first
# Excuse the poor test parameters -- I set up a bare Rails project and
# did not define any columns, so created_at and updated_at was all I
# had to work with
parent_params =
{
created_at: 'now',
children_attributes: [{created_at: 'test'}]
}
# Let's trigger the chain of initializations/callbacks
puts 'Running initialization callback test:'
@grandparent.parents.build(parent_params)
end
# Runs before parent object is added to this instance's #parents
def run_before_add(parent)
puts "before adding parent to grandparent"
end
# Runs after parent object is added to this instance's #parents
def run_after_add(parent)
puts 'after adding parent to grandparent'
end
end
class Parent < ApplicationRecord
belongs_to :grandparent, inverse_of: :parents
has_many :children, inverse_of: :parent,
before_add: :run_before_add, after_add: :run_after_add
accepts_nested_attributes_for :children
def initialize(attributes)
puts 'parent initializing'
super(attributes)
end
after_initialize do
puts 'after parent initialization'
end
# Runs before child object is added to this instance's #children
def run_before_add(child)
puts 'before adding child'
end
# Runs after child object is added to this instance's #children
def run_after_add(child)
puts 'after adding child'
end
end
class Child < ApplicationRecord
# whether it's the line below or
# belongs_to :parent, inverse_of: :children
# makes no difference in this case -- I tested :)
belongs_to :parent
delegate :grandparent, to: :parent
def initialize(attributes)
puts 'child initializing'
super(attributes)
end
after_initialize do
puts 'after child initialization'
end
end
Running the method Grandparent.test
from Rails console outputs this:
Running initialization callback test:
parent initializing
child initializing
after child initialization
before adding child
after adding child
after parent initialization
before adding parent to grandparent
after adding parent to grandparent
What you can see from this is that the parent is not actually added to grandparent until the very end. In other words, parent does not know about grandparent until the child initialization and its own initialization are over.
If we modify each puts
statement to include grandparent.present?
, we get the following output:
Running initialization callback test:
parent initializing: n/a
child initializing: n/a
after child initialization: false
before adding child: false
after adding child: false
after parent initialization: true
before adding parent to grandparent: true
after adding parent to grandparent: true
So you could do the following to initialize parent by itself first and initialize child(ren) afterwards:
class Parent < ApplicationRecord
# ...
def initialize(attributes)
# Initialize parent but don't initialize children just yet
super attributes.except(:children_attributes)
# Parent initialized. At this point grandparent is accessible!
# puts grandparent.present? # true!
# Now initialize children. MUST use self
self.children_attributes = attributes[:children_attributes]
end
# ...
end
Here is what that outputs when running Grandparent.test
like:
Running initialization callback test:
before parent initializing: n/a
after parent initialization: true
child initializing: n/a
after child initialization: true
before adding child: true
after adding child: true
before adding parent to grandparent: true
after adding parent to grandparent: true
As you can see, parent initialization now runs and completes before invoking child initialization.
But explicitly passing grandparent: @grandparent
into the params hash may be the easiest solution.
When you explicitly specify grandparent: @grandparent
in the params hash you pass to @grandparent.parents.build
, grandparent is initialized from the very beginning. Probably because all attributes are processed as soon as the #initialize
method runs. Here is what that looks like:
Running initialization callback test:
parent initializing: n/a
child initializing: n/a
after child initialization: true
before adding child: true
after adding child: true
after parent initialization: true
before adding parent to grandparent: true
after adding parent to grandparent: true
You could even call merge(grandparent: @grandparent)
directly in your controller method #parent_params
, like so:
def parent_params
params.require(:parent).permit(
:parent_attribute,
children_attributes: [
:some_attribute,
:other_attribute,
:id,
:_destroy
]
).merge(grandparent: @grandparent)
end
PS: Apologies for the overly long answer.