Ruby on Rails での、多対多のアソシエーション設定をすると詰まることがあるので、手を動かしてまとめておきます。
では本編。
目次
実行環境
モデル 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
以下コマンドでテーブル作成を実行。
テストデータ登録 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 に書き込みます。
確認 rails コンソールで確認します。 以下を実行して、rails コンソールを起動します。
実行結果は以下の通り。
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 に書き込みます。
確認 今回も rails コンソールで確認します。 以下を実行して、rails コンソールを起動します。
実行結果は以下の通り。
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.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.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.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 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 のフォローの構造とかもこういったアソシエーションで表現しているのもしれないと考えていました。
ではでは。