– layout: post author: den title: 2モデル間の1:多関連 tagline: 参照の関連付け方と保存のされ方 - Rails Tips description: 2モデル間に1:多(one to many)関連をつくり、Rails console で関連を確認します。 date: 2015-04-09 categories : [Programming, Ruby, Rails] tags : [model, relation, 関連, Rails console] —

Railsのモデル間の関連を具体的に作成します。関連には、2つのモデル間の関連以外に一つのモデルのインスタンス間(テーブルのデータ間)の関連、いわゆる自己関連というものもあります。

ここでは、2つのモデル間に1対多の関連を作りRails consoleで動作を確認します。

1対1関連については2モデル間の1:1関連を参照してください。

1対多関連については2モデル間の多:多関連を参照してください。

自己関連については自己関連(自己結合)を参照してください。

Model

今回、登場するモデルは、 Roomモデル と Meetingモデル の2つです。 テーブルでいえば、roomsテーブル と meetingsテーブル です。

2モデル間の1対多関連

部屋(会議室やパーティ会場のようなところ)で集まり(会合や会議、パーティなど)を開きます。その一つの部屋で時間割を組んで、会合やパーティを開いたり別の時間に会議をしたりできます。 一つの部屋(room)で複数の会合(meetings)を開くことができます。このような関係を1対多の関連というのでした。

したがって、

となります。

RoomモデルとMeetingモデル

Roomモデル と Meetingモデル の関係を以下のものとして作成します。

references を使ってフィールドを作ると、indexもつけてくれます。 Railsアプリケーションを作ったディレクトリで

$ rails g model meeting name:string room:references
$ rake db:migrate

を実行しモデルを作成します。もし、(Userモデルや)Roomモデルの作成がまだでしたら(必要に応じて)次のように作ります。

$ rails g model user name:string password:string
$ rails g model room name:string user:references
$ rake db:migrate

次に各モデルに以下を追加記述します。ここで、参照相手が入るroom_idフィールドを持つ(room:referencesの指定で作成されました)Meetingモデルの方に belongs_to を記述します。反対のRoomモデルの側には「1対多」関連なので has_many を記述します。

class Room < ActiveRecord::Base
   has_many :meetings
   ...
end

関連付けられる参照先が「1 対多」の「多」なので :meetings のように関連付ける相手のモデル名の複数形を属性名として参照することができます。参照結果は、複数、すなわちActiveRecordの配列になります。

class Meeting < ActiveRecord::Base
  belongs_to :room
  ...
end

関連付けられる参照先が「1 対多」の「1」なので単数形 :room が参照に使われる属性名となります。

クラス図ではこのようになります。

Railsのデフォルトではなく参照の属性名を変えたい時には、次のように各モデルに記述をします。

class Room < ActiveRecord::Base
  has_many :my_meetings, :class_name => "Meeting"
end

属性名を my_meetings にしました。

class Meeting < ActiveRecord::Base
  belongs_to :meeting_room, :class_name => "Room", :foreign_key=>"room_id"
end

こちらの属性名を meeting_room にしました。Railsのデフォルト、お約束に従っていると :class_name や :foreign_key を省略することができるわけです(:class_name :through :sourceを参照)。

Rails console で確認

Rails console で確認してみます。

1対多関連の確認

手順

  1. meeting1(サンプル会議1)、meeting2(サンプル会議2)、meeting3(サンプル会議3)をみなroom1(第一会議室)に関連付けます。
  2. room1.my_meetings で3つの会議が参照できることを確認します。
  3. meeting1.meeting_room で会議室が参照できることを確認します。

Rails console に打ち込むコマンドは次のものです。

  1. room1 = Room.create(name: ‘第一会議室’)
  2. meeting1 = Meeting.create(name: ‘サンプル会議1’, meeting_room: room1)
  3. meeting2 = Meeting.create(name: ‘サンプル会議2’, meeting_room: room1)
  4. meeting3 = Meeting.create(name: ‘サンプル会議3’, meeting_room: room1)
  5. meeting2.meeting_room
  6. room1.my_meetings
  7. room1.my_meetings[1]

やってみましょう。

$ rails console --sandbox
     ...

room1 = Room.create(name: '第一会議室')

  SQL (0.4ms)  INSERT INTO "rooms" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-04-11 01:53:45.054166"], ["name", "第一会議室"], ["updated_at", "2015-04-11 01:53:45.054166"]]

+----+------------+---------+-------------------------+-------------------------+
| id | name       | user_id | created_at              | updated_at              |
+----+------------+---------+-------------------------+-------------------------+
| 1  | 第一会議室 |         | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
+----+------------+---------+-------------------------+-------------------------+
1 row in set
meeting1 = Meeting.create(name: 'サンプル会議1', meeting_room: room1)

  SQL (0.2ms)  INSERT INTO "meetings" ("created_at", "name", "room_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-04-11 01:53:45.131371"], ["name", "サンプル会議1"], ["room_id", 1], ["updated_at", "2015-04-11 01:53:45.131371"]]

+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 1  | サンプル会議1 | 1       | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
+----+----------------+---------+-------------------------+-------------------------+
1 row in set
meeting2 = Meeting.create(name: 'サンプル会議2', meeting_room: room1)

  SQL (0.1ms)  INSERT INTO "meetings" ("created_at", "name", "room_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-04-11 01:53:45.137300"], ["name", "サンプル会議2"], ["room_id", 1], ["updated_at", "2015-04-11 01:53:45.137300"]]

+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 2  | サンプル会議2 | 1       | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
+----+----------------+---------+-------------------------+-------------------------+
1 row in set
meeting3 = Meeting.create(name: 'サンプル会議3', meeting_room: room1)

  SQL (0.1ms)  INSERT INTO "meetings" ("created_at", "name", "room_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-04-11 01:53:45.141753"], ["name", "サンプル会議3"], ["room_id", 1], ["updated_at", "2015-04-11 01:53:45.141753"]]

+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 3  | サンプル会議3 | 1       | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
+----+----------------+---------+-------------------------+-------------------------+
1 row in set

meeting2.meeting_room
+----+------------+---------+-------------------------+-------------------------+
| id | name       | user_id | created_at              | updated_at              |
+----+------------+---------+-------------------------+-------------------------+
| 1  | 第一会議室 |         | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
+----+------------+---------+-------------------------+-------------------------+
1 row in set

room1.my_meetings
  Meeting Load (0.2ms)  SELECT "meetings".* FROM "meetings"  WHERE "meetings"."room_id" = ?  [["room_id", 1]]
+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 1  | サンプル会議1 | 1       | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
| 2  | サンプル会議2 | 1       | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
| 3  | サンプル会議3 | 1       | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
+----+----------------+---------+-------------------------+-------------------------+
3 rows in set

room1.my_meetings[1]
+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 2  | サンプル会議2 | 1       | 2015-04-11 01:53:45 UTC | 2015-04-11 01:53:45 UTC |
+----+----------------+---------+-------------------------+-------------------------+
1 row in set
 

参照の関連付け方と保存のされ方

参照の関連付け方と保存のされ方について、以下の4つの場合を試してみようと思います。インスタンスを new で作りそれらのうち一つだけを save します。

  1. Meeting.newにRoom.newをセットしRoomを保存
  2. Meeting.newにRoom.newをセットしMeetingを保存
  3. Room.newにMeeting.newをセットしRoomを保存
  4. Room.newにMeeting.newをセットしMeetingを保存

さて、RoomとMeetingの関連はすべての場合で正しく保存されるのでしょうか。確認手順を整理し実際に確認してみようと思います。

ケース 1

Meeting.newにRoom.newをセットしRoomを保存

打ち込むコマンドです。

  1. room1 = Room.new(name: ‘第一会議室’)
  2. meeting1 = Meeting.new(name: ‘サンプル会議1’)
  3. meeting1.meeting_room = room1
  4. room1.save

以下、本筋と無関係なところは省略します。まずは下準備をします。

room1 = Room.new(name: '第一会議室')
+----+------------+---------+------------+------------+
| id | name       | user_id | created_at | updated_at |
+----+------------+---------+------------+------------+
|    | 第一会議室 |         |            |            |
+----+------------+---------+------------+------------+

meeting1 = Meeting.new(name: 'サンプル会議1')
+----+----------------+---------+------------+------------+
| id | name           | room_id | created_at | updated_at |
+----+----------------+---------+------------+------------+
|    | サンプル会議1 |         |            |            |
+----+----------------+---------+------------+------------+

では、結果は

meeting1.meeting_room = room1
+----+------------+---------+------------+------------+
| id | name       | user_id | created_at | updated_at |
+----+------------+---------+------------+------------+
|    | 第一会議室 |         |            |            |
+----+------------+---------+------------+------------+

room1.save

  SQL (0.2ms)  INSERT INTO "rooms" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-04-10 11:54:23.747917"], ["name", "第一会議室"], ["updated_at", "2015-04-10 11:54:23.747917"]]

Meeting.all
  Meeting Load (0.1ms)  SELECT "meetings".* FROM "meetings"
#<ActiveRecord::Relation []>

Room.all
  Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
+----+------------+---------+-------------------------+-------------------------+
| id | name       | user_id | created_at              | updated_at              |
+----+------------+---------+-------------------------+-------------------------+
| 1  | 第一会議室 |         | 2015-04-10 11:54:23 UTC | 2015-04-10 11:54:23 UTC |
+----+------------+---------+-------------------------+-------------------------+

予想通りというか、room1は登録されていますが、meeting1は、登録されていません。

ケース 2

Meeting.newにRoom.newをセットしMeetingを保存

打ち込むコマンドです。

  1. room1 = Room.new(name: ‘第一会議室’)
  2. meeting1 = Meeting.new(name: ‘サンプル会議1’)
  3. meeting1.meeting_room = room1
  4. meeting1.save

始めはケース1と同じなので省略します。先ほど同様無用な個所も省略します。 結果です。

meeting1.meeting_room = room1
+----+------------+---------+------------+------------+
| id | name       | user_id | created_at | updated_at |
+----+------------+---------+------------+------------+
|    | 第一会議室 |         |            |            |
+----+------------+---------+------------+------------+

meeting1.save

  SQL (0.2ms)  INSERT INTO "rooms" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-04-10 12:08:14.336024"], ["name", "第一会議室"], ["updated_at", "2015-04-10 12:08:14.336024"]]
  SQL (0.2ms)  INSERT INTO "meetings" ("created_at", "name", "room_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-04-10 12:08:14.339695"], ["name", "サンプル会議1"], ["room_id", 1], ["updated_at", "2015-04-10 12:08:14.339695"]]

Meeting.all
  Meeting Load (0.1ms)  SELECT "meetings".* FROM "meetings"
+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 1  | サンプル会議1 | 1       | 2015-04-10 12:08:14 UTC | 2015-04-10 12:08:14 UTC |
+----+----------------+---------+-------------------------+-------------------------+

Room.all
  Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
+----+------------+---------+-------------------------+-------------------------+
| id | name       | user_id | created_at              | updated_at              |
+----+------------+---------+-------------------------+-------------------------+
| 1  | 第一会議室 |         | 2015-04-10 12:08:14 UTC | 2015-04-10 12:08:14 UTC |
+----+------------+---------+-------------------------+-------------------------+

今度は、 meeting1.save としただけで room1 も登録されました。これは、meetingsテーブルに room_id があることから、 room1 をさきに登録しなければ room_id に値をセットできないためにRailsが room1 をさきに登録しました。

ケース 3

Room.newにMeeting.newをセットしRoomを保存

今度は、 room1.my_meetings « meeting1 のようにroom1の方にmeeting1をセットします。

打ち込むコマンドです。

  1. room1 = Room.new(name: ‘第一会議室’)
  2. meeting1 = Meeting.new(name: ‘サンプル会議1’)
  3. room1.my_meetings « meeting1
  4. room1.save

結果です。

room1.my_meetings << meeting1
+----+----------------+---------+------------+------------+
| id | name           | room_id | created_at | updated_at |
+----+----------------+---------+------------+------------+
|    | サンプル会議1 |         |            |            |
+----+----------------+---------+------------+------------+

room1.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.1ms)  INSERT INTO "rooms" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-04-10 12:12:50.547477"], ["name", "第一会議室"], ["updated_at", "2015-04-10 12:12:50.547477"]]
  SQL (0.1ms)  INSERT INTO "meetings" ("created_at", "name", "room_id", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2015-04-10 12:12:50.551027"], ["name", "サンプル会議1"], ["room_id", 1], ["updated_at", "2015-04-10 12:12:50.551027"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
true
Meeting.all
  Meeting Load (0.0ms)  SELECT "meetings".* FROM "meetings"
+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 1  | サンプル会議1 | 1       | 2015-04-10 12:12:50 UTC | 2015-04-10 12:12:50 UTC |
+----+----------------+---------+-------------------------+-------------------------+
1 row in set
Room.all
  Room Load (0.1ms)  SELECT "rooms".* FROM "rooms"
+----+------------+---------+-------------------------+-------------------------+
| id | name       | user_id | created_at              | updated_at              |
+----+------------+---------+-------------------------+-------------------------+
| 1  | 第一会議室 |         | 2015-04-10 12:12:50 UTC | 2015-04-10 12:12:50 UTC |
+----+------------+---------+-------------------------+-------------------------+

両方とも登録されました。room1が参照するようになっているmeeting1が存在しないのでRailsがmeeting1も登録しました。

ケース 4

Room.newにMeeting.newをセットしMeetingを保存

先ほどと1.2.3.は同じで最後にmeeting1の登録を行います。

  1. room1 = Room.new(name: ‘第一会議室’)
  2. meeting1 = Meeting.new(name: ‘サンプル会議1’)
  3. room1.my_meetings « meeting1
  4. meeting1.save
room1.my_meetings << meeting1
+----+----------------+---------+------------+------------+
| id | name           | room_id | created_at | updated_at |
+----+----------------+---------+------------+------------+
|    | サンプル会議1 |         |            |            |
+----+----------------+---------+------------+------------+

meeting1.save

  SQL (0.2ms)  INSERT INTO "meetings" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2015-04-10 12:21:20.651311"], ["name", "サンプル会議1"], ["updated_at", "2015-04-10 12:21:20.651311"]]

Meeting.all
  Meeting Load (0.1ms)  SELECT "meetings".* FROM "meetings"
+----+----------------+---------+-------------------------+-------------------------+
| id | name           | room_id | created_at              | updated_at              |
+----+----------------+---------+-------------------------+-------------------------+
| 1  | サンプル会議1 |         | 2015-04-10 12:21:20 UTC | 2015-04-10 12:21:20 UTC |
+----+----------------+---------+-------------------------+-------------------------+

今度はmeeting1だけが登録されました。meeting1には参照するroom1の情報がないため登録されません。

まとめ

以上の結果を表でまとめておきます。

Room.my_meetings « meeting1
Meeting
Room
Meeting.meeting_room = room1
Room.save insert ROOM
insert MEETING
insert ROOM
 
Meeting.save
insert MEETING
insert ROOM
insert MEETING

つまり、関連付けをセットしたモデル(参照相手を知っている)側を save すると両方のデータが登録されるということがわかりました。

やっぱりRailsはすばらしい。