在Rails中建立多对多关系


73

这是我要实现的目标的简化示例,对于Rails来说我还很陌生,并且努力使自己了解模型之间的关系。

我有两个模型,User模型和Category模型。用户可以与许多类别相关联。一个特定的类别可以出现在许多用户的类别列表中。如果删除了特定类别,则应在用户的类别列表中反映出来。

在此示例中:

我的Categories表包含五个类别:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| ID | 姓名|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1 | 体育|
| 2 | 新闻|
| 3 | 娱乐|
| 4 | 技术|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

我的Users表包含两个用户:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| ID | 姓名|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1 | 用户A |
| 2 | 用户B |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

UserA可以选择“体育和技术”作为他的类别

用户B可以选择新闻,体育和娱乐

运动类别已删除,UserA和UserB类别列表均反映了删除

我开玩笑地创建了一个UserCategories表,其中包含类别和用户的ID。通过这种方式,我可以查找类别名称,但是无法级联删除,因此整个解决方案似乎都是错误的。

我发现的使用belongs_to和has_many函数的示例似乎在讨论映射一对一关系。例如,对博客文章的评论。

  • 您如何使用内置的Rails功能表示这种多对多关系?
  • 使用Rails时,在两者之间使用单独的表格是否可行?

实际上,使用UserCategory模型并没有错,在大多数情况下甚至都可取。您只需要使用User.has_many :categories, through: :user_categories。也许可以找到一个更好的名字。
RocketR 2012年

1
对于其他人,这里有一个很好的主题话题:railscasts.com/episodes/47-two-many-to-many
Dofs 2013年

Answers:


167

你想要一个has_and_belongs_to_many关系。该指南很好地描述了它如何与图表和所有内容配合使用:

http://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association

您最终将得到如下结果:

# app/models/category.rb
class Category < ActiveRecord::Base
  has_and_belongs_to_many :users
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_and_belongs_to_many :categories
end

现在,您需要创建一个联接表供Rails使用。Rails不会自动为您执行此操作。这实际上是一个表,其中引用了类别和用户,并且没有主键。

像这样从CLI生成迁移:

bin/rails g migration CreateCategoriesUsersJoinTable

然后将其打开并进行编辑以匹配:

对于Rails 4.0.2+(包括Rails 5.2):

def change
  # This is enough; you don't need to worry about order
  create_join_table :categories, :users

  # If you want to add an index for faster querying through this join:
  create_join_table :categories, :users do |t|
    t.index :category_id
    t.index :user_id
  end
end

Rails <4.0.2:

def self.up
  # Model names in alphabetical order (e.g. a_b)
  create_table :categories_users, :id => false do |t|
    t.integer :category_id
    t.integer :user_id
  end

  add_index :categories_users, [:category_id, :user_id]
end

def self.down
  drop_table :categories_users
end

有了它,就可以运行迁移,然后可以将“类别”和“用户”与您习惯于使用的所有便捷访问器连接起来:

User.categories  #=> [<Category @name="Sports">, ...]
Category.users   #=> [<User @name="UserA">, ...]
User.categories.empty?

9
另一个提示:为表创建一个复合索引。更多内容stackoverflow.com/a/4381282/14540
kolrie

1
@kolrie大点;为了更全面,我在此处添加了示例。干杯!
coreyward

4
愚蠢的问题,这是否意味着有必要创建一个新模型?
tvieira 2014年

1
has_and_belongs_to_many不需要/创建模型。如果您需要直接处理该关系或需要向该关系添加属性,则应使用has_many:through选项。单击答案中的链接以获取详细信息。
Chong Yu

14
我注意到的另一件事是联接表的命名约定。必须按字母顺序排序。使用上面的示例,表名必须是Categories_users而不是users_categories。在这种情况下,“ u”之前的“ c”。
Chong Yu

10

最受欢迎的是“单音和物协会”,您可以这样做:

class Book < ApplicationRecord
  has_many :book_authors
  has_many :authors, through: :book_authors
end

# in between
class BookAuthor < ApplicationRecord
  belongs_to :book
  belongs_to :author
end

class Author < ApplicationRecord
  has_many :book_authors
  has_many :books, through: :book_authors
end

has_many:through关联通常用于与另一个模型建立多对多连接。这种关联表明,通过进行第三个模型,可以将声明模型与另一个模型的零个或多个实例匹配。例如,考虑一种医疗实践,患者会预约去看医生。参考:https : //guides.rubyonrails.org/association_basics.html#the-has-many-through-association


4

只是补充上述coreyward的回答:如果您已经有一个具有belongs_tohas_many关系的模型,并且想要has_and_belongs_to_many使用同一张表创建一个新的关系,则需要:

rails g migration CreateJoinTableUsersCategories users categories

然后,

rake db:migrate

之后,您将需要定义您的关系:

User.rb:

class Region < ApplicationRecord
  has_and_belongs_to_many :categories
end

Category.rb

class Facility < ApplicationRecord
  has_and_belongs_to_many :users
end

为了用旧数据填充新的联接表,您需要在控制台中:

User.all.find_each do |u|
  Category.where(user_id: u.id).find_each do |c|
    u.categories <<  c
  end
end

您可以保留“类别”和“用户”表中的一user_id列和一category_id列,也可以创建一个迁移来删除它。


0

如果您想添加有关该关系的其他数据,则has_many :things, through: :join_table可能是您想要的。不过,通常情况下,您不需要在联接关系上添加其他元数据(例如角色)has_and_belongs_to_many,绝对是最简单的方法(如本SO帖子的公认答案)。

但是,假设您正在构建一个论坛站点,其中有多个论坛,并且需要支持他们参与的每个论坛中担任不同角色的用户。加入自己:

class Forum
  has_many :forum_users
  has_many :users, through: :forum_users

  has_many :admin_forum_users, -> { administrating }, class_name: "ForumUser"
  has_many :viewer_forum_users, -> { viewing }, class_name: "ForumUser"

  has_many :admins, through: :admin_forum_users, source: :user
  has_many :viewers, through: :viewer_forum_users, source: :user
end

class User
  has_many :forum_users
  has_many :forums, through: :forum_users
end

class ForumUser
  belongs_to :user
  belongs_to :forum

  validates :role, inclusion: { in: ['admin', 'viewer'] }

  scope :administrating, -> { where(role: "admin") }
  scope :viewing, -> { where(role: "viewer") }
end

您的迁移看起来像这样

class AddForumUsers < ActiveRecord::Migration[6.0]
  create_table :forum_users do |t|
      t.references :forum
      t.references :user

      t.string :role

      t.timestamps
  end
end
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.