在 Rails 中通过 has_many 连接表

Posted

技术标签:

【中文标题】在 Rails 中通过 has_many 连接表【英文标题】:Join table for has_many through in Rails 【发布时间】:2016-01-07 19:26:15 【问题描述】:

我是编程和 Rails 的新手,有些东西我不完全理解。 我正在创建一个应用程序

product has_many categories
category has_many products

如果我理解正确,我需要创建一个连接表 products_categories,它有一个product_id 和一个category_id。首先,我还需要这张桌子的模型吗?如果是的话,我想它看起来像这样:

class CategoryProduct < ActiveRecord::Base
   belongs_to :category
   belongs_to :product
end

以及 product.rb 中的其他模型:

 class Product < ActiveRecord::Base
  has_many :category_products
  has_many :categories, through: :category_product
  has_attached_file :picture,
    styles:  medium: "300x300>", thumb: "100x100>" 

  validates_attachment_content_type :picture,
    content_type: /\Aimage\/.*\z/
  validates :price,               presence:  message: "Merci d'indiquer le prix du produit" 
  validates :name,                presence:  message: "Merci d'indiquer le nom du produit" 
  validates :weight,              presence:  message: "Merci d'indiquer le poids du produit" 
  validates :description,         presence:  message: "Merci d'écrire une description du produit " 
end

在 category.rb 中

    class Category < ActiveRecord::Base
  has_many :category_products
  has_many :products,         through: :category_product
  validates :name,                presence:  message: "Merci d'indiquer le nom de la catégorie" 
end

现在假设我想创建一个产品,在创建它的同时,从类别列表中为该产品指定尽可能多的类别。

到目前为止,在我看来,这是我的 Product/new.html.slim :

  div class="container marged-top"
    div class= "col-xs-12 col-md-offset-3 col-md-5 bigmarge"
      div class="panel panel-default"
        div class= "panel-heading"
          h4 Création Produit
        div class= "panel-body"
          =simple_form_for @product, html:  multipart: true  do |t|
            = t.error_notification
            = t.input :name, label: 'Nom'
            = t.input :description, label: 'Description', required: true
            = t.input :price, label: 'Prix', required: true
            = t.input :weight, label: 'Poids', required: true
            = t.label :picture
            = t.file_field :picture
            = t.association :categories, as: :check_boxes
            = t.button :submit, value: "Valider",  class: "btn-success marge-bas"

这是我的 Product 实例的简单表单。我想我现在需要一个 CategoryProduct 的表格吗? 如果我希望用户能够在创建产品时向产品添加他想要的任意数量的类别,我该如何更改?

这是 category_product 表的迁移文件:

类 CreateTableCategoriesProducts <:migration>

  def change
    create_table :categories_products do |t|
      t.references :product, index: true
      t.references :category, index: true
    end
    add_foreign_key :categories_products, :categories
    add_foreign_key :categories_products, :products
  end
end

我用以下迁移文件重命名了之前的表:

class RenameTableCategoriesProducts < ActiveRecord::Migration
  def self.up
    rename_table :categories_products, :category_products
  end

 def self.down
    rename_table :category_products, :categories_products
 end
end

我在 product/new.html.slim 的 simple_form 上收到以下错误:

undefined method `klass' for nil:NilClass

代码在这里中断:

 = t.association :categories, as: :check_boxes

所以我猜我的联想还不是很正确

【问题讨论】:

【参考方案1】:

在 Rails 中,有两种方法可以建立多对多关系:

has_and_belongs_to_many

在没有干预模型的情况下建立多对多关系。

class Category < ActiveRecord::Base
  has_and_belongs_to_many :products
end

class Product < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

如果您知道不需要存储有关关系的任何其他数据或添加任何其他功能,这是一个不错的选择——这实际上非常罕见。它使用更少的内存,因为它不必为了执行product.category 而实例化额外的模型。

当使用has_and_belongs_to_many 时,约定是连接表以两个实体的复数形式命名。

Category + Product = products_categories

顺序似乎无关紧要。

has_many 通过:

正如您已经猜到的那样,使用中间模型。

class CategoryProduct < ActiveRecord::Base
  belongs_to :product
  belongs_to :category
end

class Category < ActiveRecord::Base
  has_many :category_products
  has_many :products, through: :category_products
end

class Product < ActiveRecord::Base
  has_many :category_products
  has_many :categories, through: :category_products
end

这里的优点是您可以在描述关系的连接表中存储和检索附加数据。例如,如果您想存储谁将产品添加到某个类别 - 或创建关系的时间。

为了让 Rails 能够正确找到 ProductCategory 类,has_many though 的命名约定是:

model 1(singular) + model 2(plural) 
Product + Category = category_products

这是由于 rails 基于表名推断模型类的方式。使用categories_products 将case rails 查找Category::CategoriesProduct,因为复数单词被解释为模块。然而,这实际上只是一个懒惰的命名约定,并且通常有一个名词可以更好地描述 A 和 B 之间的关系(例如Categorization)。

表单和控制器中的多对多。

正如 IvanSelivanov 已经提到的,SimpleForm 具有用于创建选择、复选框等的辅助方法。

https://github.com/plataformatec/simple_form#associations

但您可能希望使用 label_method 选项,而不是覆盖模型中的 .to_s 方法。

f.assocation :categories, as: :checkboxes, label_method: :name

覆盖.to_s 会使调试更加困难,并且在某些情况下会产生令人困惑的测试错误消息。

要将控制器中的参数列入白名单,您可以:

class ProductsController < ApplicationController
  def create
    @product = Product.new(product_params)
    if @product.save
      redirect_to @product
    else
      render :new
    end
  end

  def product_params
     params.require(:product)
           .permit(:name, :categories_ids, ...)
  end
end

【讨论】:

查看***.com/questions/4631630/renaming-table-in-rails 了解如何修复连接表的表名。 感谢您的回答,我知道我的 simple_form 产品出现错误。当遇到以下行时,代码会以 simple_form 中断:= t.association :categories,如::check_boxes。可能是因为我在迁移文件中命名表的方式::categories_products 应该是 category_products如果我关注你 NP,是的,我猜这就是问题所在。但是一个好的提示是比仅仅“得到一个错误”更具体一些 - 如果你包含错误消息,它会更容易提供帮助。 更改表名后没有事件我仍然在第 16 行的 simple_form 中得到 undefined method klass' for nil:NilClass` ` = t.association :categories, as: :check_boxes` 添加debug @product.categories会给你带来什么?【参考方案2】:

您还必须为每个模型添加 CategoryProduct:

class Product < ActiveRecord::Base
  has_many :category_products
  has_many :categories, through: :category_product

使用 gem simple form 非常简单。您所要做的就是添加:

t.association :categories

在产品表单中,并将:category_ids =&gt; [] 添加到产品控制器中允许的参数列表中

如果你更喜欢复选框而不是多选列表,你可以这样做

    t.association :categories, as: check_boxes

最后,要以人类可读的格式显示类别,您需要在类别模型中定义一个to_s 方法,即。 e.:

class Category < ActiveRecord::Base
  ...
  def to_s
    name
  end 
end

【讨论】:

我按照你说的做了,但是当我将 t.association :category 添加到我的 product/new.html.slim 表单中时;我收到以下错误:Association :category not found t.association :categories @max 现在我得到 nil:NilClass 的未定义方法 `klass' @IvanSelivanov 我也不明白为什么在你的联想中你写has_many :category_products,为什么不写:categories_products @Dave 这是因为我认为这是 CategoryProduct 的正确复数形式。您是否将has_and_belongs_to_many 更改为has_many?如果您发布相应的代码(错误消息中通常有一个行号),则更容易说出您的错误意味着什么

以上是关于在 Rails 中通过 has_many 连接表的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Ruby on Rails 中通过关联订购 has_many?

Rails 4,通过连接表在has_many中指定自定义外键?

Rails - 按连接表数据排序

Has_many 连接表,用于管理目的

Rails has_many :通过连接模型中的额外属性查找

Ruby-on-Rails:多个 has_many :通过可能吗?