概要
Railsにおけるデータベースのテーブル間のアソシエーションについて、基本を整理する。
以下のようなテーブルを例にする。Railsが生成するcreated_atとupdated_atは省略している。

users、posts、imagesのレコードはそれぞれUser、Post、ImageモデルのインスタンスPostはUserに、ImageはPostに属するUserは0以上複数のPostを持つPostは0以上複数のImageを持つ
アソシエーションの意義
単純にテーブル操作する場合
普通に3つのテーブルだけがある場合に、特定idのユーザーの投稿記事を作成し、その記事が2つの画像を持たせるには以下の様になるだろう。
|
1 2 3 4 |
user = User.find(1) post = Post.create(user_id: 1) Image.create(post_id: post.id, file_name: "image1") Image.create(post_id: post.id, file_name: "image2") |
このときに生成されるデータは以下の様になる。
|
1 2 3 4 |
<User id: 1, name: "user1", created_at: ...> <Post id: 1, user_id: 1, created_at: ...> <Image id: 1, post_id: 1, file_name: "image1", created_at:...> <Image id: 2, post_id: 1, file_name: "image2", created_at:...> |
ここで投稿記事Post(id=1)をそれらに付随する2つの画像データとともに削除する。それには2つのImageを削除してから元のPostを削除する。
|
1 2 3 4 5 |
Image.where(post_id: post.id).each do |image| image.delete end post.delete |
このように個別のテーブルを操作する場合、データの生成・登録や削除の際に、
- 関連するテーブルのデータのidを常に意識し
- データ間の関連を念頭に置きながら個々のデータを操作する
必要がある。
アソシエーション導入後
テーブル間のアソシエーションを導入した場合、関連付けられたデータの操作が直感的に、またシンプルになる。
データの生成
データの生成は以下のとおりで、親となるデータのidを意識しなくても、具体の親オブジェクトを指定して生成すれば関連付けられたデータが生成される。
|
1 2 3 4 5 |
user = User.find(1) user.posts.create post = user.posts.create post.images.create(file_name: "image1") post.images.create(file_name: "image2") |
上の例では、まずpostsをcreateした後に、そのインスタンスに従属するようにimagesをcreateしている。
このようなデータベースへの書き込みを、さらにまとめることができる。
|
1 2 3 4 5 6 |
user = User.find(1) post = user.posts.build post.images.build(file_name: "image1") post.images.build(file_name: "image2") post.save |
最後にsaveするまではデータベースには保存されずidもnilのままだが、オブジェクト間の関係は保たれている。最後に保存するときに、postと2つのImagesのインスタンスが同時に保存される。
データの取得
データの取得は、親モデル.子モデルのコレクションで取得し、親モデルは単数形、子モデルは複数形。
|
1 2 3 4 5 6 7 8 9 10 11 |
# 1つだけ取り出す user.posts[0] # 全て取り出す user.posts.each do |post| # 1つだけ取り出す post.images[0] # 全部取り出す post.images.each do |image| # imageに関する処理 end end |
データの削除
そして投稿記事を画像ごと削除する場合には、以下の様にたった1行で済む
|
1 |
post.destroy |
さらに、外部キー制約をかけているので、以下のように親のデータだけを削除しようとするとエラーとなる。
|
1 |
post.delete # ActiveRecord::StatementInvalid (Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails ...) |
アソシエーション設定の流れ
仕様
冒頭の3つのテーブルのうち、PostとImageの2つを取り出して考える。以下の様に若干変更。
Postはcomment要素を持つImage→PostImageに変更Userとの関係は考えない
2つのモデルの関係は以下のとおりとする。
PostImageはPostに属するPostは0以上複数のPostImageを持つPostの削除に伴って、これに属するPostImageのデータも削除される
モデルの生成~外部キー指定
以下のコマンドで2つのモデルを生成する。
|
1 2 |
$ rails generate model post comment:text $ rails generate model post_image post:references file_name:string |
PostImageがPostに従属するよう、post:referencesを指定している。生成するモデルが属するモデル(親のモデル)をAとするとA:referencesと指定する。
Postモデルについては枠組みのみ生成されている。
app/models/post.rb
|
1 2 |
class Post < ApplicationRecord end |
PostImageモデルについては、references指定に伴って、Railsによりbelongs_toの1行が追加されている。
app/models/post_image.rb
|
1 2 3 |
class PostImage < ApplicationRecord belongs_to :post end |
モデルのアソシエーションの定義
has_many
Postが複数のPostImageを持つことをRailsに伝えるため、Postモデルにhas_manyメソッドを追記する。
dependent
親のデータを削除するときにこのデータも一括して削除するためには、dependent: :destroyを指定。
|
1 2 3 |
class Post < ApplicationRecord has_many :post_images, dependent: :destroy end |
注意点
belongs_to :parentはメソッドの引数が単数形has_many :childrenはメソッドの引数が複数形dependent: :destroyは引数のハッシュで値がシンボル
マイグレーションファイル
Postsモデル
コマンドでの指定に従って、comment要素が設定されている。
|
1 2 3 4 5 6 7 8 9 |
class CreatePosts < ActiveRecord::Migration[5.1] def change create_table :posts do |t| t.text :comment t.timestamps end end end |
PostImageモデル
コマンドで指定したfile_nameのほか、Railsによって以下の外部キーの指定が生成されている。
t.references :user, foreign_key: true
|
1 2 3 4 5 6 7 8 9 10 |
class CreatePostImages < ActiveRecord::Migration[5.1] def change create_table :post_images do |t| t.references :post, foreign_key: true t.string :file_name t.timestamps end end end |
マイグレーション実行~テーブル
マイグレーション実行の結果、posts、imagesの3つのテーブルが生成される。
|
1 2 3 4 5 6 7 8 9 10 |
mysql> show tables; +-------------------------------+ | Tables_in_testapp_development | +-------------------------------+ | ar_internal_metadata | | post_images | | posts | | schema_migrations | +-------------------------------+ 4 rows in set (0.00 sec) |
postsテーブル
デフォルトのid、created_at、updated_atのほか、不通に指定したcomment要素が含まれる。
|
1 2 3 4 5 6 7 8 9 10 |
mysql> DESCRIBE posts; +------------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | comment | text | YES | | NULL | | | created_at | datetime | NO | | NULL | | | updated_at | datetime | NO | | NULL | | +------------+------------+------+-----+---------+----------------+ 4 rows in set (0.00 sec) |
post_imagesテーブル
デフォルトの要素のほか、Railsコマンドで指定した2つの要素が含まれる。
post: referencesの指定に対して、postsテーブルのidを参照する外部キーpost_id- 普通に指定した
file_name
|
1 2 3 4 5 6 7 8 9 10 11 |
mysql> DESCRIBE post_images; +------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +------------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | post_id | bigint(20) | YES | MUL | NULL | | | file_name | varchar(255) | YES | | NULL | | | created_at | datetime | NO | | NULL | | | updated_at | datetime | NO | | NULL | | +------------+--------------+------+-----+---------+----------------+ 5 rows in set (0.00 sec) |
アソシエーション設定の確認
rails consoleによる確認
rails consoleでデータベース操作の効果を確認する。
|
1 2 3 4 |
$ rails console Running via Spring preloader in process 1557 ^[[6~Loading development environment (Rails 5.1.7) irb(main):001:0> |
データの生成と保存
2つのPostImageオブジェクトを持つPostオブジェクトを1つ生成し、まとめて保存する。表示は見やすくするために再構成している。
saveより前では3つのオブジェクトともidがnilとなっているが、オブジェクト間の関連は保持されている。
|
1 2 3 4 5 6 7 8 9 10 11 |
irb(main):001:0> post = Post.new(comment: "こんにちは") irb(main):002:0> post.post_images.build(file_name: "image1") irb(main):003:0> post.post_images.build(file_name: "image2") => #<Post id: nil, comment: "こんにちは", created_at: nil, updated_at: nil> => #<PostImage id: nil, post_id: nil, file_name: "image1", created_at: nil, updated_at: nil> => #<PostImage id: nil, post_id: nil, file_name: "image2", created_at: nil, updated_at: nil> # この段階ではDBには登録されていない irb(main):004:0> post.save # これでpostと従属する2つのpost_imagesの2つのレコードがDBに登録される |
postsテーブルとPostImagesテーブルへの登録内容を確認。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
mysql> SELECT id, comment FROM posts; +----+-----------------+ | id | comment | +----+-----------------+ | 1 | こんにちは | +----+-----------------+ 1 row in set (0.00 sec) mysql> SELECT id, post_id, file_name FROM post_images; +----+---------+-----------+ | id | post_id | file_name | +----+---------+-----------+ | 1 | 1 | image1 | | 2 | 1 | image2 | +----+---------+-----------+ 2 rows in set (0.00 sec) |
データの取得
postsに従属する複数のPostImageデータをループで取り出す例。
この操作はデータベース登録(save)の前でも可能。
|
1 2 3 4 5 6 |
post.post_images.each do |image| puts image.file_name end # image1 # image2 |
deleteに対する制約エラー
PostImageデータを持っている状態でpost.deleteしようとすると外部キー制約エラー
|
1 2 3 4 5 6 |
irb(main):007:0> post = Post.find(1) irb(main):009:0> post.delete SQL (10.8ms) DELETE FROM `posts` WHERE `posts`.`id` = 1 Traceback (most recent call last): 1: from (irb):9 ActiveRecord::StatementInvalid (Mysql2::Error: Cannot delete or update a parent row: a foreign key constraint fails (`testapp_development`.`post_images`, CONSTRAINT `fk_rails_3ee54b46c4` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`)): DELETE FROM `posts` WHERE `posts`.`id` = 1) |
destroyによる一括削除
post.destroyだと従属する2つのImageも削除される
|
1 2 3 4 5 6 7 |
irb(main):005:0> post.destroy (0.2ms) BEGIN PostImage Load (0.4ms) SELECT `post_images`.* FROM `post_images` WHERE `post_images`.`post_id` = 1 SQL (0.4ms) DELETE FROM `post_images` WHERE `post_images`.`id` = 1 SQL (0.2ms) DELETE FROM `post_images` WHERE `post_images`.`id` = 2 SQL (0.2ms) DELETE FROM `posts` WHERE `posts`.`id` = 1 (11.0ms) COMMIT |
1つの実行文で、当該Postデータとそれに関連付けられたPostImageデータも削除される。