概要
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
データも削除される。