在 Rails 上创建 many-to-many 记录

Creating a many-to-many record on Rails

我有一个简单的任务列表应用程序,上面有用户和列表,用户由 Devise 管理,可以创建任务列表,以及其他用户或他们自己创建的收藏夹列表。用户和列表之间的所有权关系很容易建立,但我无法建立用户喜欢列表的关系。我设想它毕竟是一个 many-to-many 关系,一个用户可以收藏许多列表,一个列表可以被许多用户收藏,这种关系发生在另一个已经存在的 one-to-many 列表所有权关系之上用户让我犹豫了一下,这是否是一个好的做法,但我还是继续尝试。

目前我有两种模型,一种用于用户,一种用于列表,我尝试通过 运行ning rails g migration CreateJoinTableFavorites users lists 为收藏夹创建迁移,结果如下迁移

class CreateJoinTableFavorites < ActiveRecord::Migration[5.2]
  def change
    create_join_table :users, :lists do |t|
      t.index [:user_id, :list_id] <-- I uncommented this line
      # t.index [:list_id, :user_id]
      t.timestamps <-- I added this line
    end
  end
end

我认为这会创建一个名为 "Favorites" 的 table,它会自动 link 用户和列表,但它创建了一个名为 "lists_users" 的 table .现在我不知道下一步该怎么做。我读到我需要为此连接创建一个模型 table,但我不知道该怎么做。我 运行 执行什么命令? rails g model Favoritesrails g model ListsUsers?我是否还要通知我要添加的字段,例如 rails g model Favorites user_id:integer list_id:integer,或者是否有其他更好的方法,例如 rails g model Favorites user:references list:references?这里的最佳做法是什么

除此之外,我在我的 list#show 视图中添加了一个按钮,供用户单击以将该列表添加到他们的收藏夹,但在路由它时遇到了一些麻烦。我所做的是创建一个这样的按钮:

<%= button_to 'Add to favorites', add_favorites_path({list_id: @list.id}), method: :post %>

以及一条新路线:

post 'add_favorites', to: 'lists#add_favorites'

虽然我设法访问了该操作中的列表 ID 和用户 ID,但现在我不知道如何继续在我的 lists_users 中创建 "favorite" 数据库条目 table。为了说明,我将在此处粘贴我的 "add_favorite" 操作

def add_favorites
    user_id = current_user.id
    list_id = params[:list_id]

    #TODO: create the relation in lists_items table
end

我知道如果没有连接模型 table,我无法让它工作,但即使我有那个模型,我也没有太多运气来寻找要做什么在控制器中创建该关系。不管怎样,我的模型如下:

class List < ApplicationRecord
    belongs_to :user
    has_many :users, through: :lists_users
end
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :lists
  has_many :lists, through: :lists_users
end

总而言之,我知道我缺少连接 table 的模型,并且想要 step-by-step 关于如何创建它、给它起什么名字等,以及如何在我的控制器中继续我的操作以创建一个新的收藏条目

您可以尝试以下操作:

class User
  has_and_belongs_to_many :lists
end

class List
  has_and_belongs_to_many :users
end

class CreateUsersAndLists
  def change
    create_table :users do |t|
      # Code
    end

    create_table :lists do |t|
      # Code
    end

    create_table :users_lists id: false do |t|
      t.belongs_to :user, index: true
      t.belongs_to :list, index: true
      t.boolean :is_favourite
    end
  end
end

有两种方法可以在 Rails 中创建 many-to-many 关系。你所做的似乎将两者混为一谈,我怀疑这是你问题的根源。

简而言之,这两种方法是:

1) has_many :other_models, through: :relation

2) has_and_belongs_to_many :other_models

主要区别在于 "has_many through" 方法期望连接 table 是一个单独的模型,如果需要可以独立于此关系进行处理,而 "has_and_belongs_to_many" 方法不需要join table有对应的模型。在后一种情况下,您将无法 独立处理连接table。 (顺便说一句,这使得连接 table 上的时间戳无用。)

您应该使用哪种方法取决于您的用例。 docs 很好地总结了标准:

The simplest rule of thumb is that you should set up a has_many :through relationship if you need to work with the relationship model as an independent entity. If you don't need to do anything with the relationship model, it may be simpler to set up a has_and_belongs_to_many relationship (though you'll need to remember to create the joining table in the database). (emphasis added)


现在回答您的问题:当您使用 create_join_table 时,您将其视为正在为 has_and_belongs_to_many 关系进行设置。 create_join_table 将创建一个名为 "#{table1}_#{table2}" 的 table,其 ID 指向那些 table。它也按字母顺序排列它们,这就是为什么你得到 "lists_users" 而不是 "users_lists" 的原因。如果您计划使用 has_and_belongs_to_many,这实际上是 rails 加入 table 的标准命名约定,通常不应重命名。

如果您真的想使用 has_and_belongs_to_many,请使用 create_join_table 保持迁移并在您的模型中执行以下操作:

# user.rb
class User
  has_and_belongs_to_many :lists
end

# list.rb
class List
  has_and_belongs_to_many :users
end

瞧。不需要 Favorite 模型,rails 足够聪明,可以通过 table 自行处理关系。虽然更容易一些,但缺点是,如上所述,您将无法将连接 table 作为独立模型处理。 (同样,连接 table 上的时间戳在这种情况下没有用,因为 Rails 不会设置它们。)

编辑: 由于您不能直接触摸 lists_users,您可以通过设置用户的列表关系或设置用户关系来创建关系在列表上,像这样:

def add_favorites
  list = List.find(params[:list_id])
  current_user.lists << list # creates the corresponding entry in lists_users

  # Don't forget to test how this works when the current_user has already favorited a list!
  # If you want to prevent that from happening, try
  # current_user.lists << list unless current_user.lists.include?(list)

  # Alternatively you can do the assignment in reverse:
  # list.users << current_user

  # Again, because the join table is not an independent model, Rails won't be able to do much to other columns on lists_users out of the box.
  # This includes timestamps
end

另一方面,如果您想使用 "has_many through",请不要使用 create_join_table。如果你使用 has_many through,连接 table 应该被认为几乎是一个完全独立的模型,它恰好有两个外键并将其他两个模型绑定在一起 many-to-many 关系。在这种情况下,您可以执行以下操作:

# migration
class CreateFavorites < ActiveRecord::Migration[5.2]
  def change
    create_table :favorites do |t|
      t.references :list
      t.references :user
      t.timestamps
    end
  end
end

# user.rb
class User
  has_many :favorites
  has_many :lists, through: :favorites
end

# list.rb
class List
  has_many :favorites
  has_many :users, through: :favorites
end

# favorite.rb
class Favorite
  belongs_to :list
  belongs_to :user
end

# controller
def add_favorites
  # You actually have a Favorite model in this case, while you don't in the other. The Favorite model can be more or less independent of the List and User, and can be given other attributes like timestamps.
  # It's the rails methods like `save`, `create`, and `update` that set timestamps, so this will track those for you as any other model.
  Favorite.create(list_id: params[:list_id], user: current_user)
end

您可能需要考虑使用哪种方法。同样,这实际上取决于您的用例和上述标准。就我个人而言,当我不确定时,我更喜欢 "has_many through" 方法,因为它为您提供了更多的工作工具并且通常更灵活。