访问 Rails 中未保存的 parent 和 child 的 grandparent
Access grandparent of unsaved parent and child in Rails
我有一个表格可以将 Parent
和许多 Child
object 保存在一起。在 Child
object 的初始化期间,它需要访问 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
请记住,该表格用于同时保存一个新的 parent 和许多 children,但我正在尝试访问 grandparent object 中的信息。我读到 inverse_of
选项应该可以解决问题,但不幸的是 child.grandparent
仍然是 nil
。
这是实际导致故障的控制器部分:
@parent = @grandparent.parents.build(parent_params)
# prior to saving @parent
出于某种原因,parent 不知道谁是 grandparent。
更新
看来我可以使用此代码克服该错误:
@parent = Parent.new(parent_params.merge(grandparent: @grandparent))
但这对我来说似乎不是很 "railsy"。
更新 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
我能够在 Child
的 after_initialize
代码中放置一个 byebug
并且我能够看到未保存的 Parent
和 Child
并且可以通过以下方式访问它们:
p = self.parent
=> Parent object
p.grandparent
=> nil
self.grandparent
=> nil
出现此问题是因为 Parent 的实例在添加到(关联)Grandparent 的实例之前被初始化。让我用以下示例向您说明这一点:
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
从这里可以看出,parent 直到最后才真正添加到 grandparent 中。换句话说,parent直到child初始化和它自己的初始化结束后才知道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
如您所见,parent 初始化现在运行并在调用 child 初始化之前完成。
但显式将 grandparent: @grandparent
传递到参数散列中可能是最简单的解决方案。
当您在传递给 @grandparent.parents.build
的参数哈希中明确指定 grandparent: @grandparent
时,grandparent 从一开始就被初始化。可能是因为 #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
您甚至可以在控制器方法 #parent_params
中直接调用 merge(grandparent: @grandparent)
,如下所示:
def parent_params
params.require(:parent).permit(
:parent_attribute,
children_attributes: [
:some_attribute,
:other_attribute,
:id,
:_destroy
]
).merge(grandparent: @grandparent)
end
PS: 很抱歉回答太长。
一定要after_initialize
吗?
如果你可以使用 before_create
代替,那么你应该可以在回调方法中访问 grandparent
。
您也可以使用 before_validation(:method, on: :create)
。这将允许您添加一些验证以确保 Child 设置正确。此外,您可以在代码的其他地方调用 child.valid?
来设置一个子对象而不保存它。
我以前在与您类似的情况下遇到过这个问题。问题是 Rails 无法在新建的 parent
上初始化 grandparent
关系 在 为 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 gives a good example in section 3 "Extract Form Objects", albeit the exact implementation is a bit outdated. See for a more up-to-date implementation (although instead of Virtus, you might use the Rails 5 attributes API).
我有一个表格可以将 Parent
和许多 Child
object 保存在一起。在 Child
object 的初始化期间,它需要访问 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
请记住,该表格用于同时保存一个新的 parent 和许多 children,但我正在尝试访问 grandparent object 中的信息。我读到 inverse_of
选项应该可以解决问题,但不幸的是 child.grandparent
仍然是 nil
。
这是实际导致故障的控制器部分:
@parent = @grandparent.parents.build(parent_params)
# prior to saving @parent
出于某种原因,parent 不知道谁是 grandparent。
更新
看来我可以使用此代码克服该错误:
@parent = Parent.new(parent_params.merge(grandparent: @grandparent))
但这对我来说似乎不是很 "railsy"。
更新 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
我能够在 Child
的 after_initialize
代码中放置一个 byebug
并且我能够看到未保存的 Parent
和 Child
并且可以通过以下方式访问它们:
p = self.parent
=> Parent object
p.grandparent
=> nil
self.grandparent
=> nil
出现此问题是因为 Parent 的实例在添加到(关联)Grandparent 的实例之前被初始化。让我用以下示例向您说明这一点:
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
从这里可以看出,parent 直到最后才真正添加到 grandparent 中。换句话说,parent直到child初始化和它自己的初始化结束后才知道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
如您所见,parent 初始化现在运行并在调用 child 初始化之前完成。
但显式将 grandparent: @grandparent
传递到参数散列中可能是最简单的解决方案。
当您在传递给 @grandparent.parents.build
的参数哈希中明确指定 grandparent: @grandparent
时,grandparent 从一开始就被初始化。可能是因为 #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
您甚至可以在控制器方法 #parent_params
中直接调用 merge(grandparent: @grandparent)
,如下所示:
def parent_params
params.require(:parent).permit(
:parent_attribute,
children_attributes: [
:some_attribute,
:other_attribute,
:id,
:_destroy
]
).merge(grandparent: @grandparent)
end
PS: 很抱歉回答太长。
一定要after_initialize
吗?
如果你可以使用 before_create
代替,那么你应该可以在回调方法中访问 grandparent
。
您也可以使用 before_validation(:method, on: :create)
。这将允许您添加一些验证以确保 Child 设置正确。此外,您可以在代码的其他地方调用 child.valid?
来设置一个子对象而不保存它。
我以前在与您类似的情况下遇到过这个问题。问题是 Rails 无法在新建的 parent
上初始化 grandparent
关系 在 为 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 gives a good example in section 3 "Extract Form Objects", albeit the exact implementation is a bit outdated. See