I have a form to save a Parent
and many Child
objects together. During the initialization of the Child
object, it needs access to the Grandparent
. Here's what the models looks like:
class Grandparent
has_many :parents, inverse_of: :grandparent
end
class Parent
belongs_to :grandparent, inverse_of: :parents
has_many :children, inverse_of: :parent
accepts_nested_attributes_for :children
end
class Child
belongs_to :parent
delegate :grandparent, to: :parent
# Test code
after_initialize do
raise 'NoParentError' unless parent.present?
raise 'NoGrandparentError' unless grandparent.present? # Errors here!
puts 'All good!'
end
end
Remember, the form is for saving a new parent and many children simultaneously, but I'm trying to access information in the grandparent object. I read that the inverse_of
options should have done the trick, but child.grandparent
is still nil
unfortunately.
Here's the piece of the controller that's actually causing the failure:
@parent = @grandparent.parents.build(parent_params)
# prior to saving @parent
For some reason the parent has no idea who the grandparent is.
Update
It looks like I can get past that error with this code:
@parent = Parent.new(parent_params.merge(grandparent: @grandparent))
But that doesn't seem very "railsy" to me.
Update 2
As requested, here is my controller for the form.
class ParentsController < ApplicationController
before_action :set_grandparent
def new
@parent = @grandparent.parents.new
@parent.children.build
end
def create
@parent = @grandparent.parents.build(parent_params)
if @parent.save
redirect_to @grandparent
else
render :new
end
end
private
def set_grandparent
@grandparent = Grandparent.find(params[:grandparent_id])
end
def parent_params
params.require(:parent).permit(:parent_attribute,
children_attributes: [:some_attribute, :other_attribute, :id, :_destroy]
end
end
Here's what my view looks like:
= simple_form_for [@grandparent, @parent] do |f|
= f.input :parent_attribute
= f.simple_fields_for :children do |child_form|
= child_form.input :some_attribute
= child_form.input :other_attribute
= f.submit
I am able to place a byebug
in the after_initialize
code of the Child
and I'm able to see the unsaved Parent
and the Child
and can access them with:
p = self.parent
=> Parent object
p.grandparent
=> nil
self.grandparent
=> nil
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.
I've hit this issue before in a scenario similar to yours. The problem is Rails fails to initialize the grandparent
relationship on the newly built parent
prior to assigning the nested attributes for the children
. Here's a quick fix:
# make sure the `grandparent` association is established on
# the `parent` _before_ assigning nested attributes
@parent = @grandparent.parents.build()
@parent.assign_attributes(parent_params)
If this solution gives you a bad feeling, you might consider dumping accepts_nested_attributes_for
in favor of extracting an explicit form object, over which you'll have more control. This post gives a good example in section 3 "Extract Form Objects", albeit the exact implementation is a bit outdated. See this question for a more up-to-date implementation (although instead of Virtus, you might use the Rails 5 attributes API).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With