Ruby on Rails での多対多のアソシエーションの設定方法

Ruby on Rails での、多対多のアソシエーション設定をすると詰まることがあるので、手を動かしてまとめておきます。

では本編。

目次

実行環境

  • Rails 5.2.3

モデル

scaffold でモデル他作成

今回は User モデルと Group モデルがあり以下の条件を持ちます。

  • User は複数の Group に参加する。
  • Group は複数の User を抱えている。

という関係のアソシエーション(関連)を作ります。
2 つのモデルの生成のコマンドは以下の通り、アソシエーションをテストしたいだけなので、scaffold で作成する。

1
2
rails generate scaffold user name:string
rails generate scaffold group name:string

has_and_belogs_to_many でやってみる

モデル設定

User モデルと Group モデルにhas_and_belongs_to_many :[相手のテーブル名]を書き加え、それぞれ以下のようにします。

app/models/user.rb
1
2
3
class User < ApplicationRecord
has_and_belongs_to_many :groups
end
app/models/group.rb
1
2
3
class Group < ApplicationRecord
has_and_belongs_to_many :users
end

中間テーブル準備

has_and_belogs_to_many を使用するには、
user_id と group_id の 2 つのカラムのみ持つテーブルの作成が必要です。

以下のコマンドでマイグレーションファイルを作成します。

1
rails generate migration create_groups_users user:references group:references

作成されたマイグレーションファイルは以下の通りです。
groupsusers の部分は、単語同士(今回の場合 users と groups)をアルファベット順に「」でつなぐ必要があります。
(アルファベット順に_で繋ぐ必要がある点を見逃してしばらく悩み続けました。この点は注意が必要。)

db/migrate/[数字列]_create_groups_users.rb(書き換え前)
1
2
3
4
5
6
7
8
class CreateUsersGroups < ActiveRecord::Migration[5.2]
def change
create_table :groups_users do |t|
t.references :user
t.references :group
end
end
end

このままでは、id が作成されてしまうので、id 列を作成しないためのオプションが必要でした。
,id: falseを書き加え、以下のように書き換えます。

db/migrate/[数字列]_create_groups_users.rb(書き換え後)
1
2
3
4
5
6
7
8
class CreateUsersGroups < ActiveRecord::Migration[5.2]
def change
create_table :groups_users,id: false do |t|
t.references :user
t.references :group
end
end
end

以下コマンドでテーブル作成を実行。

1
rails db:migrate

テストデータ登録

db/seeds.rb を以下の通り編集します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ユーザー作成
user1 = User.create({name:"USER1"})
user2 = User.create({name:"USER2"})
user3 = User.create({name:"USER3"})

#グループ作成
group1 = Group.create({name:"GROUP1"})
group2 = Group.create({name:"GROUP2"})
group3 = Group.create({name:"GROUP3"})

#関連付けを追加
user1.groups << group1
user1.groups << group2
user1.save

user2.groups << group2
user2.groups << group3
user2.save

user3.groups << group3
user3.groups << group1
user3.save

以下コマンドで、db/seeds.rb に記述したでデータを DB に書き込みます。

1
rails db:seed

確認

rails コンソールで確認します。
以下を実行して、rails コンソールを起動します。

1
rails c

実行結果は以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
irb(main):001:0> user = User.find(1)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "USER1", created_at: "2019-11-06 13:48:16", updated_at: "2019-11-06 13:48:16">

irb(main):002:0> user.groups
Group Load (0.3ms) SELECT "groups".* FROM "groups" INNER JOIN "groups_users" ON "groups"."id" = "groups_users"."group_id" WHERE "groups_users"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [
#<Group id: 1, name: "GROUP1", created_at: "2019-11-06 13:48:16", updated_at: "2019-11-06 13:48:16">,
#<Group id: 2, name: "GROUP2", created_at: "2019-11-06 13:48:16", updated_at: "2019-11-06 13:48:16">
]>

irb(main):003:0> user.groups.first.name
Group Load (0.3ms) SELECT "groups".* FROM "groups" INNER JOIN "groups_users" ON "groups"."id" = "groups_users"."group_id" WHERE "groups_users"."user_id" = ? ORDER BY "groups"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> "GROUP1"

User から、複数参加している Group が取得できました。

以下のように Group から Ussr を取得できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
irb(main):001:0> group = Group.find(1)
Group Load (0.2ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Group id: 1, name: "GROUP1", created_at: "2019-11-06 13:48:16", updated_at: "2019-11-06 13:48:16">

irb(main):002:0> group.users
User Load (0.3ms) SELECT "users".* FROM "users" INNER JOIN "groups_users" ON "users"."id" = "groups_users"."user_id" WHERE "groups_users"."group_id" = ? LIMIT ? [["group_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [
#<User id: 1, name: "USER1", created_at: "2019-11-06 13:48:16", updated_at: "2019-11-06 13:48:16">,
#<User id: 3, name: "USER3", created_at: "2019-11-06 13:48:16", updated_at: "2019-11-06 13:48:16">
]>

irb(main):003:0> group.users.first.name
User Load (0.4ms) SELECT "users".* FROM "users" INNER JOIN "groups_users" ON "users"."id" = "groups_users"."user_id" WHERE "groups_users"."group_id" = ? ORDER BY "users"."id" ASC LIMIT ? [["group_id", 1], ["LIMIT", 1]]
=> "USER1"

User が所属している Group 一覧、Group に所属している User 一覧をそれぞれ取得できました。


has_many through でやってみる

今度は has_many through を使用して、やってみます。

中間テーブル準備

has_many through を用いて設定するときは、中間テーブルの作成を先に行うほうがわかりやすいと感じたので、こちらから行います。

has_many through 中間テーブルの名称に制限がありません。
今回は中間テーブル名称を Connections としてみたいと思ます。
中間テーブルを扱うモデルにもアソシエーション設定を記述する必要があるので、マイグレーションファイルを作成し、モデルを作成します。

1
2
rails generate model connection user:references group:references
rails db:migrate

モデル設定

User、Group と Connection の 3 つのモデルの設定を行います。

app/models/user.rb
1
2
3
4
class User < ApplicationRecord
has_many :connections
has_many :groups, through: :connections
end
app/models/group.rb
1
2
3
4
class Group < ApplicationRecord
has_many :connections
has_many :users, through: :connections
end
app/models/connection.rb
1
2
3
4
class Connection < ApplicationRecord
belongs_to :user
belongs_to :group
end

connection.rb の書き換えはありません。

テストデータ登録

has_and_belogs_to_many の確認をした時と同様に db/seeds.rb を以下の通り編集します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ユーザー作成
user1 = User.create({name:"USER1"})
user2 = User.create({name:"USER2"})
user3 = User.create({name:"USER3"})

#グループ作成
group1 = Group.create({name:"GROUP1"})
group2 = Group.create({name:"GROUP2"})
group3 = Group.create({name:"GROUP3"})

#関連付けを追加
user1.groups << group1
user1.groups << group2
user1.save

user2.groups << group2
user2.groups << group3
user2.save

user3.groups << group3
user3.groups << group1
user3.save

以下コマンドで、db/seeds.rb に記述したでデータを DB に書き込みます。

1
rails db:seed

確認

今回も rails コンソールで確認します。
以下を実行して、rails コンソールを起動します。

1
rails c

実行結果は以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
irb(main):001:0> user = User.find(1)
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "USER1", created_at: "2019-11-06 15:11:26", updated_at: "2019-11-06 15:11:26">

irb(main):002:0> user.groups
Group Load (0.3ms) SELECT "groups".* FROM "groups" INNER JOIN "connections" ON "groups"."id" = "connections"."group_id" WHERE "connections"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [
#<Group id: 1, name: "GROUP1", created_at: "2019-11-06 15:11:26", updated_at: "2019-11-06 15:11:26">,
#<Group id: 2, name: "GROUP2", created_at: "2019-11-06 15:11:26", updated_at: "2019-11-06 15:11:26">
]>

irb(main):003:0> user.groups.first.name
Group Load (0.3ms) SELECT "groups".* FROM "groups" INNER JOIN "connections" ON "groups"."id" = "connections"."group_id" WHERE "connections"."user_id" = ? ORDER BY "groups"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> "GROUP1"

User から Group の一覧が取得できました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
irb(main):001:0> group = Group.find(1)
Group Load (0.2ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<Group id: 1, name: "GROUP1", created_at: "2019-11-06 15:11:26", updated_at: "2019-11-06 15:11:26">

irb(main):002:0> group.users
User Load (0.3ms) SELECT "users".* FROM "users" INNER JOIN "connections" ON "users"."id" = "connections"."user_id" WHERE "connections"."group_id" = ? LIMIT ? [["group_id", 1], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [
#<User id: 1, name: "USER1", created_at: "2019-11-06 15:11:26", updated_at: "2019-11-06 15:11:26">,
#<User id: 3, name: "USER3", created_at: "2019-11-06 15:11:26", updated_at: "2019-11-06 15:11:26">
]>

irb(main):003:0> group.users.first.name
User Load (0.4ms) SELECT "users".* FROM "users" INNER JOIN "connections" ON "users"."id" = "connections"."user_id" WHERE "connections"."group_id" = ? ORDER BY "users"."id" ASC LIMIT ? [["group_id", 1], ["LIMIT", 1]]
=> "USER1"

逆に Group から User の一覧が取得できました。

has_many through が has_and_belogs_to_many と同様なことができることを確認できました。


拡張:has_many through でできて has_and_belogs_to_many ではできないこと

has_many through では可能ですが、has_and_belogs_to_many ではできないことがあります。
has_and_belogs_to_many では、カラムはそれぞれの外部キーのみ(今回は user_id と group_id)に制限されます。
has_many through は外部キー以外の別のカラムを持つことができます。

中間テーブルにカラムを追加して使ってみます。

カラムの追加

connections に name カラムを追加することにします。
以下のコマンドでマイグレーションファイルを作成して、マイグレーションします。

1
2
rails generate migration AddNameToConnections name:string
rails db:migrate

テストデータ登録

has_and_belogs_to_many の確認をした時と同様に db/seeds.rb を編集します。
connections に name カラムを追加したので、それぞれに名前を振っておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# ユーザー作成
user1 = User.create({name:"USER1"})
user2 = User.create({name:"USER2"})
user3 = User.create({name:"USER3"})

#グループ作成
group1 = Group.create({name:"GROUP1"})
group2 = Group.create({name:"GROUP2"})
group3 = Group.create({name:"GROUP3"})

#関連付けを追加
## user1への関連付け
user1.groups << group1

connect1 = Connection.find_by({user_id:user1.id,group_id:group1.id})
connect1.name="CONNECT1"
connect1.save

user1.groups << group2

connect2 = Connection.find_by({user_id:user1.id,group_id:group2.id})
connect2.name="CONNECT2"
connect2.save

user1.save


## user2への関連付け
user2.groups << group2

connect3 = Connection.find_by({user_id:user2.id,group_id:group2.id})
connect3.name="CONNECT3"
connect3.save

user2.groups << group3

connect4 = Connection.find_by({user_id:user2.id,group_id:group3.id})
connect4.name="CONNECT4"
connect4.save

user2.save

## user3への関連付け
user3.groups << group3

connect5 = Connection.find_by({user_id:user3.id,group_id:group3.id})
connect5.name="CONNECT5"
connect5.save

user3.groups << group1

connect6 = Connection.find_by({user_id:user3.id,group_id:group1.id})
connect6.name="CONNECT6"
connect6.save

user3.save

connect1 = Connection.find_by({user_id:user1.id,group_id:group1.id})の引き当て方は正しい自信が無いです。
現在、考えられる引き当て方はこれでした。

確認

今回も rails コンソールで確認します。
以下を実行して、rails コンソールを起動します。

1
rails c

実行結果は以下の通り。

1
2
3
4
5
6
7
8
9
10
11
12
irb(main):001:0> user = User.find(1)
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "USER1", created_at: "2019-11-06 16:01:15", updated_at: "2019-11-06 16:01:15">

irb(main):002:0> user.connections.first.name
Connection Load (0.2ms) SELECT "connections".* FROM "connections" WHERE "connections"."user_id" = ? ORDER BY "connections"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> "CONNECT1"

irb(main):003:0> user.connections.first.group.name
Connection Load (0.2ms) SELECT "connections".* FROM "connections" WHERE "connections"."user_id" = ? ORDER BY "connections"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
Group Load (0.1ms) SELECT "groups".* FROM "groups" WHERE "groups"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> "GROUP1"

User から、Connection の name と Group の name を取得できました。
今回の場合、CONNECT1という名前の connection で User と Group がつながっているといえます。


今回は、has_and_belogs_to_many と has_many through の扱い方を確認しました。

今回のようなアソシエーションは、具体的にはどんなところで使うのかと考えていました。
よくあるものは今回みたいな「所属」の構造だと感じますが、
Twitter のフォローの構造とかもこういったアソシエーションで表現しているのもしれないと考えていました。

ではでは。