我有一个表格可以将Parent
和许多Child
个对象保存在一起。在Child
对象初始化期间,它需要访问Grandparent
。这是模型的样子:
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
请记住,表单用于同时保存新父级和多个子级,但我尝试访问祖父对象中的信息。我读到inverse_of
选项应该已经完成了这个技巧,但遗憾的是child.grandparent
仍然是nil
。
这是控制器实际导致失败的部分:
@parent = @grandparent.parents.build(parent_params)
# prior to saving @parent
由于某种原因,父母不知道祖父母是谁。
更新
看起来我可以通过以下代码解决该错误:
@parent = Parent.new(parent_params.merge(grandparent: @grandparent))
但这似乎并不是很困难。对我来说。
更新2
根据要求,这是我的表格控制器。
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
以下是我的观点:
= 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
我可以在byebug
的{{1}}代码中放置after_initialize
,我可以看到未保存的Child
和{{1}并且可以使用:
Parent
答案 0 :(得分:2)
出现此问题的原因是在将父实例添加到(与之关联)祖父母实例之前初始化父实例。让我用以下示例向您说明:
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
从Rails控制台运行方法Grandparent.test
输出:
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
你可以从中看到,父母直到最后才被实际添加到祖父母。换句话说,在子初始化和自己的初始化结束之前,父母不知道祖父母。
如果我们修改每个puts
语句以包含grandparent.present?
,我们会得到以下输出:
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
所以你可以执行以下操作,首先自己初始化parent并在之后初始化child(ren):
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
以下是运行Grandparent.test
时输出的内容:
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
如您所见,父级初始化现在在调用子初始化之前运行并完成。
但明确地将grandparent: @grandparent
传递给params哈希可能是最简单的解决方案。
当您在传递给grandparent: @grandparent
的参数哈希中明确指定@grandparent.parents.build
时,祖父母从一开始就被初始化。可能是因为#initialize
方法运行后会立即处理所有属性。这是看起来像:
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
您甚至可以直接在控制器方法merge(grandparent: @grandparent)
中调用#parent_params
,如下所示:
def parent_params
params.require(:parent).permit(
:parent_attribute,
children_attributes: [
:some_attribute,
:other_attribute,
:id,
:_destroy
]
).merge(grandparent: @grandparent)
end
PS:对过长的回答表示道歉。
答案 1 :(得分:1)
我之前在类似于你的情景中遇到过这个问题。问题是Rails无法在新构建的grandparent
之前初始化parent
关系,以便为children
分配嵌套属性。这是一个快速修复:
# make sure the `grandparent` association is established on
# the `parent` _before_ assigning nested attributes
@parent = @grandparent.parents.build()
@parent.assign_attributes(parent_params)
如果此解决方案给您带来不好的感觉,您可以考虑转储accepts_nested_attributes_for
,转而提取一个明确的表单对象,您可以通过该对象获得更多控制权。 This post在第3节&#34; Extract Form Objects&#34;中给出了一个很好的例子,尽管确切的实现有点过时了。有关更新的实施,请参阅this question(尽管您可以使用Rails 5 attributes API代替Virtus。)
答案 2 :(得分:0)
是否必须是writerows
?
如果您可以使用after_initialize
,那么您应该能够在回调方法中访问before_create
。
您也可以使用grandparent
。这将允许您添加一些验证以确保正确设置了Child。此外,您可以在代码中的其他位置调用before_validation(:method, on: :create)
来设置子节点而不保存它。