在 Rails 中创建多对多关系

这是一个简化的例子,说明了我正在努力实现的目标。我对 Rails 相对较新,正在努力理解模型之间的关系。

我有两种型号,User型和 Category型。用户可以与许多类别关联。对于许多用户,特定类别可以出现在类别列表中。如果删除了特定的类别,那么这应该反映在用户的类别列表中。

在这个例子中:

我的 Categories表包含五个类别:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| ID | Name                       |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1  | Sports                     |
| 2  | News                       |
| 3  | Entertainment              |
| 4  | Technology                 |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

我的 Users表包含两个用户:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| ID | Name                       |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| 1  | UserA                      |
| 2  | UserB                      |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

用户可以选择体育和技术作为他的类别

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

体育类别被删除,UserA 和 UserB 类别列表都反映了该删除

我曾经尝试过创建一个 UserCategories表,其中包含类别和用户的 id。这种方法是有效的,我可以查找类别名称,但是我不能得到一个级联删除操作,整个解决方案似乎是错误的。

我发现的使用 properties _ to 和 has _ many 函数的示例似乎讨论了映射一对一关系。例如,在博客帖子上的评论。

  • 如何使用内置的 Rails 功能来表示这种多对多的关系?
  • 在使用 Rails 时,在两者之间使用一个单独的表是可行的解决方案吗?
95801 次浏览

You want a has_and_belongs_to_many relationship. The guide does a great job of describing how this works with charts and everything:

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

You will end up with something like this:

# 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

Now you need to create a join table for Rails to use. Rails will not do this automatically for you. This is effectively a table with a reference to each of Categories and Users, and no primary key.

Generate a migration from the CLI like this:

bin/rails g migration CreateCategoriesUsersJoinTable

Then open it up and edit it to match:

For Rails 4.0.2+ (including 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

With that in place, run your migrations and you can connect Categories and Users with all of the convenient accessors you're used to:

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

Just complementing coreyward's answer above: If you already have a model that has a belongs_to, has_many relation and you want to create a new relation has_and_belongs_to_many using the same table you will need to:

rails g migration CreateJoinTableUsersCategories users categories

Then,

rake db:migrate

After that, you will need to define your relations:

User.rb:

class Region < ApplicationRecord
has_and_belongs_to_many :categories
end

Category.rb

class Facility < ApplicationRecord
has_and_belongs_to_many :users
end

In order to populate the new join table with the old data, you will need to in your console:

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

You can either leave the user_id column and category_id column from the Category and User tables or create a migration to delete it.

The most popular is 'Mono-transitive Association', you can do this:

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

A has_many :through association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model. For example, consider a medical practice where patients make appointments to see physicians. Ref.: https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

If you want to add additional data on the relationship, the has_many :things, through: :join_table may be what you're looking for. Often times, though, you won't need to additional metadata (like a role) on a join relationship, in which case has_and_belongs_to_many is definitely the simplest way to go (as in the accepted answer for this SO post).

However, let's say, you're building a forum site where you have several forums and need to support users holding different roles within each forum they participate in. It might be useful to allow for tracking how a user is related to a forum on the join itself:

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

And your migration would look something like this

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

Side note: here's how you can add relationships to records once you've set up the relationship.

category1.users << user1
user2.categories << category2

Here's how to delete them

category1.users.delete(user1)
user2.categories.delete(category2)