【问题标题】:Access grandparent of unsaved parent and child in Rails在 Rails 中访问未保存的父母和孩子的祖父母
【发布时间】:2018-01-04 01:57:08
【问题描述】:

我有一个表单可以将 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

我可以在Childafter_initialize 代码中放置一个byebug,我可以看到未保存的ParentChild,并且可以通过以下方式访问它们:

p = self.parent
=> Parent object

p.grandparent
=> nil

self.grandparent
=> nil

【问题讨论】:

  • 什么是Event
  • 为什么在初始化Child 时实际上需要祖父母?你在哪里使用它?
  • 对不起,Event 是我的实际模型。我试图通过更改所有名称来简化情况。
  • @JagdeepSingh 好问题。基本上祖父母有一个has_many 关联。孩子需要遍历这些记录并创建一些其他记录。如果您需要更多详细信息,我可以稍后更新我的问题。
  • 也许parent.grandparent.present? 可以在没有授权的情况下工作?

标签: ruby-on-rails associations nested-forms


【解决方案1】:

出现此问题的原因是 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

从中可以看出,直到最后,父级才真正添加到祖父级。换句话说,直到孩子初始化和自己的初始化结束,父母才知道祖父母。

如果我们修改每个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 显式传递到参数哈希中可能是最简单的解决方案。

当您在传递给@grandparent.parents.build 的参数散列中明确指定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:抱歉回答过长。

【讨论】:

    【解决方案2】:

    我之前在与您类似的场景中遇到过这个问题。问题是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 在第 3 节“提取表单对象”中给出了一个很好的例子,尽管确切的实现有点过时了。请参阅 this question 以获得更新的实现(尽管您可以使用 Rails 5 attributes API 来代替 Virtus)。

    【讨论】:

      【解决方案3】:

      必须是after_initialize吗?

      如果你可以使用before_create,那么你应该可以在回调方法中访问grandparent

      您也可以使用before_validation(:method, on: :create)。这将允许您添加一些验证以确保正确设置子项。此外,您可以在代码中的其他位置调用 child.valid? 来设置 Child 而无需保存它。

      【讨论】:

        猜你喜欢
        • 2015-08-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多