Ruby on Rails 嵌套属性

发布日期:2026-06-25 05:59:06   来源 : 杭州电子商务研究院    浏览量 :22
杭州电子商务研究院 发布日期:2026-06-25 05:59:06  
22

介绍

嵌套属性是一种功能,可让您通过其关联的父级保存记录的属性。在此示例中,我们将考虑以下场景:

我们正在创建一个包含大量产品的在线商店。每个产品可以有零个或多个变体。变体就是字面意思;它们代表同一产品的不同变体,例如颜色不同。两者都有名称和价格。每个产品还将与一个图像记录相关联,其中包含一个网址、alt 和一个标题。

在本教程的后面,我们将改进这些模型:

      class Product < ActiveRecord::Base
  has_many :variants
  has_one :image
  # Attributes: name:string, price:float
end
    
      class Variant < ActiveRecord::Base
  belongs_to :product
  # Attributes: name:string, price:float
end
    
      class Image < ActiveRecord::Base
  belongs_to :product
  # Attributes: url:string, alt:string caption:string
end
    

一对一关联

嵌套属性最简单的例子是一对一关联。要为产品模型添加嵌套属性支持,只需添加以下行:

      class Product < ActiveRecord::Base
  has_many :variants
  accepts_nested_attributes_for :image
end
    

这到底是做什么的?它将把已保存的属性从产品模型代理到图像模型。在产品表单中,您需要添加用于图像关联的其他字段。您可以使用 fields_for助手来完成此操作。

      = form_for @product do |f|

  // Product attributes
  .form-group
    = f.label :name
    = f.text_field :name  
  .form-group
    = f.label :price
    = f.text_field :price

  // Image attributes
  = f.fields_for :image do |f|
    = f.label :url
    = f.text_field :url

    = f.label :alt
    = f.text_field :alt

    = f.label :caption
    = f.text_field :caption

  = f.submit
    

现在,剩下的唯一部分就是修改控制器以接受这些新属性。嵌套属性背后的整个想法是,您不必在控制器中添加额外的代码来处理此输入和关联,但您确实需要允许这些属性到达模型,而强参数默认会阻止这种情况。因此,您需要将以下内容添加到ProductsController中的product_params方法中

      def product_params
  params.require(:product).permit(
    :name, :price,
    image_attributes: [ :id, :url, :alt, :caption ]
  )
end
    

瞧!现在您可以从同一表单内联编辑产品模型的图像关联。现在让我们看看如何通过多对多关系构建相同的行为。

多对多关联

产品变体非常简单(只有两个字段),因此没有必要创建单独的页面来编辑它们。相反,我们希望从同一个产品表单中内联编辑它们以及产品属性。由于每个产品可以有多种变体,这意味着我们必须处理多个项目。我们还需要添加新变体并删除旧变体。让我们逐一解决这些问题。

显示多个关联

fields_for方法为每个关联记录生成一个块,因此我们不需要更改任何内容 - 但因为我们需要重复使用此表单(为了通过 JavaScript 自动添加新字段),所以我们需要将其移动到单独的文件中。我们将创建一个名为_variant_fields.slim的新部分,其中仅包含变体字段,如下所示:

      = f.label :name
= f.text_field :name

= f.label :price
= f.text_field :price
    

回到产品表单,为了呈现字段,我们只需利用fields_for为每个关联产生一个块的事实,并将表单帮助对象传递给部分。

      = f.fields_for :variants do |f|
  = render 'variant_fields', f: f
    

添加新关联

为了添加新的关联,我们需要创建一些添加新字段的 JavaScript。我喜欢创建一个链接,当单击该链接时,它将添加一个新的字段元组。如下所示:

      = link_to_add_fields 'Add Product Variant', f, :variants
    

这是我编写的一个有用的辅助方法,它将创建一个带有data-form-prepend属性的链接,其中包含_variant_fields.slim部分的全部内容。这里的想法是,当您单击它时,您将使用一些简单的可重复使用的 JavaScript 将这些字段附加到表单的末尾。

实际的帮助程序看起来相当复杂和混乱,但请耐心等待——我保证它和大多数代码一样简单。它只处理参数,关键逻辑位于最后七行。您可以将此代码放在 application_helper.rb

      def link_to_add_fields(name = nil, f = nil, association = nil, options = nil, html_options = nil, &block)
  # If a block is provided there is no name attribute and the arguments are
  # shifted with one position to the left. This re-assigns those values.
  f, association, options, html_options = name, f, association, options if block_given?

  options = {} if options.nil?
  html_options = {} if html_options.nil?

  if options.include? :locals
    locals = options[:locals]
  else
    locals = { }
  end

  if options.include? :partial
    partial = options[:partial]
  else
    partial = association.to_s.singularize + '_fields'
  end

  # Render the form fields from a file with the association name provided
  new_object = f.object.class.reflect_on_association(association).klass.new
  fields = f.fields_for(association, new_object, child_index: 'new_record') do |builder|
    render(partial, locals.merge!( f: builder))
  end

  # The rendered fields are sent with the link within the data-form-prepend attr
  html_options['data-form-prepend'] = raw CGI::escapeHTML( fields )
  html_options['href'] = '#'

  content_tag(:a, name, html_options, &block)
end
    

在 JavaScript 方面,我使用 jQuery 查找每个将 name 属性设置为new_record的元素,并将其替换为时间戳。这解决了添加多条新记录时的问题;两条记录将具有相同的 id ( new_record )。

      $("[data-form-prepend]").click(function(e) {
  var obj = $($(this).attr("data-form-prepend"));
  obj.find("input, select, textarea").each(function() {
    $(this).attr("name", function() {
      return $(this)
        .attr("name")
        .replace("new_record", new Date().getTime());
    });
  });
  obj.insertBefore(this);
  return false;
});
    

删除关联

幸运的是,accepts_nested_attributes_for有一些删除关联的巧妙功能。如果我们将allow_destroy: true参数传递给accepts_nested_attributes_for ,它将销毁包含_destroy键的属性中的任何成员

      accepts_nested_attributes_for :variants, allow_destroy: true
    

在视图中,这可以通过一个简单的复选框来实现。所以我在_variant_fields.slim中添加了一个复选框:

      = f.check_box :_destroy
= f.label :delete
    

对模型和控制器的修改

再次,与以前一样, ProductsController中添加的只是product_params方法,现在还应该包括variants_attributes

      def product_params
  params.require(:product).permit(
    :name, :price,
    image_attributes: [ :id, :url, :alt, :caption ],
    variants_attributes: [ :id, :name, :price, :_destroy ]
  )
end
    

产品模型只需添加以下内容即可为变体关联启用嵌套属性:

      accepts_nested_attributes_for :variants, reject_if: :all_blank, allow_destroy: true
    

注意reject_if :all_blank选项。这意味着任何属性全为空白(不包括_destroy的值)的记录都将被拒绝。reject_if还支持传递一个Proc,可用于一些额外的验证,它还会检查是否拒绝/包含关联。

还有两个有用的选项。第一个是limit选项,它指定将处理的最大记录数。第二个选项是update_only,它适用于一对一关联,并且具有相当有趣的行为。如果将其设置为 true,它将仅更新关联记录的属性。如果在更改时设置为 false,它不会触及旧记录,但会使用新属性创建一个新记录。默认情况下,它是 false,并且会创建一个新记录,除非记录包含id属性,这正是我们在product_params中包含id属性的原因,用于一对一图像关联。另一种解决方案是定义嵌套属性,如下所示:

      accepts_nested_attributes_for :image, update_only: true
    

关于作者

Itay Grudev 是一名学生,目前正在英国阿伯丁大学攻读计算机科学和物理学学位</p

以上内容来自杭州电子商务研究院推送
关注
关于我们
热门推荐
合作伙伴
免责声明:本站部分资讯来源于网络,如有侵权请及时联系客服,我们将尽快处理
Copyright © 2025-2027 ToB产业网址导航 公安备案 浙公网安备33010602013138号 浙ICP备16025413号-9
支持 反馈 关注 数据