【问题标题】:accepts_nested_attributes_for with belongs_to polymorphic接受_nested_attributes_for 和belongs_to 多态
【发布时间】:2010-10-19 13:48:57
【问题描述】:

我想与accepts_nested_attributes_for 建立多态关系。代码如下:

class Contact <ActiveRecord::Base
  has_many :jobs, :as=>:client
end

class Job <ActiveRecord::Base
  belongs_to :client, :polymorphic=>:true
  accepts_nested_attributes_for :client
end

当我尝试访问Job.create(..., :client_attributes=&gt;{...} 时,给了我NameError: uninitialized constant Job::Client

【问题讨论】:

标签: ruby-on-rails ruby polymorphic-associations nested-attributes


【解决方案1】:

我也遇到了“ArgumentError:无法建立关联模型名称。您是否尝试建立多态一对一关联?”的问题

我为这类问题找到了更好的解决方案。您可以使用本机方法。让我们看看 Rails3 内部的 nested_attributes 实现:

elsif !reject_new_record?(association_name, attributes)
  method = "build_#{association_name}"
  if respond_to?(method)
    send(method, attributes.except(*UNASSIGNABLE_KEYS))
  else
    raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
  end
end

那么实际上我们需要在这里做什么?只是在我们的模型中创建 build_#{association_name}。我在底部做了完全可行的例子:

class Job <ActiveRecord::Base
  CLIENT_TYPES = %w(Contact)

  attr_accessible :client_type, :client_attributes

  belongs_to :client, :polymorphic => :true

  accepts_nested_attributes_for :client

  protected

  def build_client(params, assignment_options)
    raise "Unknown client_type: #{client_type}" unless CLIENT_TYPES.include?(client_type)
    self.client = client_type.constantize.new(params)
  end
end

【讨论】:

  • 我成功地使用了这个build_xyz 技巧。我认为您缺少从参数中获取contact_type 的行(然后您需要将其删除到发送到新的参数中)
  • 我可能遗漏了一些东西,但我相信在最后一个方法中使用的词 contact 实际上应该改为 client...此外,我遇到了两个参数的问题在调用 build 时传递给此方法而不是一个。第二个参数是一个空白哈希。
  • 感谢@JackCA!固定代码和最新的 Rails 3.2 代码已更改,因为在第二个参数中现在传递分配选项:github.com/rails/rails/blob/master/activerecord/lib/…
  • 在 Rails 4.1 中,我不得不将“def build_client(params, assignment_options)”改回“def build_client(params)”。
  • 不得不从protected 中删除build_client,因为它没有看到方法。
【解决方案2】:

终于让它与 Rails 4.x 一起工作。这是基于 Dmitry/ScotterC 的回答,所以对他们 +1。

步骤 1. 首先,这是具有多态关联的完整模型:

# app/models/polymorph.rb
class Polymorph < ActiveRecord::Base
  belongs_to :associable, polymorphic: true

  accepts_nested_attributes_for :associable

  def build_associable(params)
    self.associable = associable_type.constantize.new(params)
  end
end

# For the sake of example:
# app/models/chicken.rb
class Chicken < ActiveRecord::Base
  has_many: :polymorphs, as: :associable
end

是的,这并不是什么新鲜事。但是您可能想知道,polymorph_type 来自哪里,它的值是如何设置的?它是基础数据库记录的一部分,因为多态关联将&lt;association_name&gt;_id&lt;association_name&gt;_type 列添加到表中。就目前而言,当build_associable 执行时,_type 的值为nil

步骤 2. 传入并接受子类型

让您的表单视图将child_type 连同典型的表单数据一起发送,并且您的控制器必须在其强大的参数检查中允许它。

# app/views/polymorph/_form.html.erb
<%= form_for(@polymorph) do |form| %>
  # Pass in the child_type - This one has been turned into a chicken!
  <%= form.hidden_field(:polymorph_type, value: 'Chicken' %>
  ...
  # Form values for Chicken
  <%= form.fields_for(:chicken) do |chicken_form| %>
    <%= chicken_form.text_field(:hunger_level) %>
    <%= chicken_form.text_field(:poop_level) %>
    ...etc...
  <% end %>
<% end %>

# app/controllers/polymorph_controllers.erb
...
private
  def polymorph_params
    params.require(:polymorph).permit(:id, :polymorph_id, :polymorph_type)
  end

当然,您的视图将需要处理“可关联”的不同类型的模型,但这展示了一个。

希望这可以帮助那里的人。 (为什么还需要多态鸡?)

【讨论】:

  • 另一件事:如果更新/补丁请求修改了 child_type,您需要格外小心以维护数据库完整性。一种解决方案是添加代码来阻止或忽略在controller#update 中为现有对象 修改polymorph_type 的请求。我也只在record_new? 为真时添加 polymorph_type 隐藏字段。
  • 很好的答案。我只想补充一点,出于安全原因,您可能不想在用户可以作为字符串传入的任意类上调用.new。您可以将它们列入白名单,或者您可以想出另一个类方法名称,如 .build_as_associable,只有相关类才能实现。
【解决方案3】:

上面的答案很好,但不适用于显示的设置。它启发了我,我能够创建一个可行的解决方案:

用于创建和更新

class Job <ActiveRecord::Base
  belongs_to :client, :polymorphic=>:true
  attr_accessible :client_attributes
  accepts_nested_attributes_for :client

  def attributes=(attributes = {})
    self.client_type = attributes[:client_type]
    super
  end

  def client_attributes=(attributes)
    some_client = self.client_type.constantize.find_or_initilize_by_id(self.client_id)
    some_client.attributes = attributes
    self.client = some_client
  end
end

【讨论】:

  • 这对于下面的 Dmitry / ScotterC 的解决方案来说不是最理想的,因为 Rails 预见到了这种情况并允许您正确地覆盖行为。
  • @MarkP 但是在更新已创建的同一记录时,由于覆盖了client_attributes,它会创建新记录而不是更新它。我们有什么解决办法吗?
【解决方案4】:

刚刚发现 rails 不支持这种行为,所以我想出了以下解决方法:

class Job <ActiveRecord::Base
  belongs_to :client, :polymorphic=>:true, :autosave=>true
  accepts_nested_attributes_for :client

  def attributes=(attributes = {})
    self.client_type = attributes[:client_type]
    super
  end

  def client_attributes=(attributes)
    self.client = type.constantize.find_or_initialize_by_id(attributes.delete(:client_id)) if client_type.valid?
  end
end

这让我可以像这样设置我的表单:

<%= f.select :client_type %>
<%= f.fields_for :client do |client|%>
  <%= client.text_field :name %>
<% end %>

不是确切的解决方案,但想法很重要。

【讨论】:

  • 这对于下面的 Dmitry / ScotterC 的解决方案来说不是最理想的,因为 Rails 预见到了这种情况并允许您正确地覆盖行为。
  • 在此示例中,您正在评估用户输入。考虑改用constantize
猜你喜欢
  • 1970-01-01
  • 2023-04-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-04
  • 1970-01-01
  • 2013-12-24
相关资源
最近更新 更多