はじめに: 関連(association)って何?
関連とはModel(データ)同士のつながりのことです。
単純な例で言うと、ブログの投稿(Post)とそれに対するコメント(Comment)は 関連 しています。
たとえば以下のような1件の投稿と2件のコメントを考えてみます。
新しいMacBook Proを買いました!(投稿)
|-- わー、いいなあ!(コメント)
`-- うらやましすぎる!!(コメント)
上のデータを取得するRailsのコードはこんな感じになります。
post.text #=> 新しいMacBook Proを買いました!
post.comments.size #=> 2
post.comments[0].text #=> わー、いいなあ!
post.comments[1].text #=> うらやましすぎる!!
コメントから投稿を参照することもできます。
comment.text #=> わー、いいなあ!
comment.post.text #=> 新しいMacBook Proを買いました!
親子関係で言い換えると、投稿が 親 で、コメントが 子 です。
また、1件の投稿に対し、コメントは複数(0件以上)付けることができます。
これをModelの定義で表現するとこのようになります。
class Post
# 投稿は複数のコメントを持つ。投稿から見るとコメントは子
has_many :comments
end
class Comment
# コメントから見ると投稿は親
belongs_to :post
end
ついでにクラス図も載せておきましょう。
親子関係を保存する方法あれこれ
さて、データを取得するコードはわかりましたが、この親子関係を保存するときはどんなコードを書けば良いのでしょうか?
もしこんなコードを書いたとしても関連をうまく保存することはできません。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment = Comment.create(text: "わー、いいなあ!")
# 投稿にコメントが1件も付いていない
post.comments.size #=> 0
親子関係を保存する方法はいくつかバリエーションがあるので、それを今から紹介していきます。
1. parent.children.create / parent.children.build
parent.children.create
の形式で子のデータを作ると関連が設定できます。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment = post.comments.create(text: "わー、いいなあ!")
post.comments.size #=> 1
post.comments[0].text #=> わー、いいなあ!
create
の代わりにbuild
を使うこともできます。
この場合、データは保存されません。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment = post.comments.build(text: "わー、いいなあ!")
post.comments.size #=> 1
post.comments[0].text #=> わー、いいなあ!
# buildを使ったのでコメントは保存されていない
comment.persisted? #=> false
2. parent.children << child
parent.children << child
という形式で関連を設定することもできます。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment = Comment.new(text: "わー、いいなあ!")
post.comments << comment
post.comments.size #=> 1
post.comments[0].text #=> わー、いいなあ!
<<
を使うと、データが同時に保存されてしまう点に注意してください。
comment.persisted? #=> false
post.comments << comment # コメントが保存される
comment.persisted? #=> true
[備考] parent.children = children
同じような考え方で、parent.children = children
という形式もあります。
配列を渡すので複数のコメントを同時に設定できますが、既存のコメントは削除されるのが特殊な点です。
使用する場合はこの副作用に十分注意してください。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment_old = Comment.new(text: "やった、コメント1番乗り!!")
post.comments << comment_old
post.comments.size #=> 1
post.comments[0].text #=> やった、コメント1番乗り!!
comment_1 = Comment.new(text: "わー、いいなあ!")
comment_2 = Comment.new(text: "うらやましすぎる!!")
# 新しい関連が保存されるのと同時に既存のコメントは削除される
post.comments = [comment_1, comment_2]
post.comments.size #=> 2
post.comments[0].text #=> わー、いいなあ!
post.comments[1].text #=> うらやましすぎる!!
comment_1.persisted? #=> true
comment_2.persisted? #=> true
comment_old.destroyed? #=> true
3. child.parent = parent
child.parent = parent
というように、「子に親を設定する」という形式もあります。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment = Comment.new(text: "わー、いいなあ!")
comment.post = post
comment.save # 関連を確定するためには子のsaveが必要
post.comments.size #=> 1
post.comments[0].text #=> わー、いいなあ!
Child.new(parent: parent) / Child.create(parent: parent)
さらに、このバリエーションとしてnew
やcreate
の引数に親を渡す形式も考えられます。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment_1 = Comment.new(text: "わー、いいなあ!", post: post)
comment_1.save
comment_2 = Comment.create(text: "うらやましすぎる!!", post: post)
post.comments.size #=> 2
post.comments[0].text #=> わー、いいなあ!
post.comments[1].text #=> うらやましすぎる!!
4. child.parent_id = parent_id
オブジェクトの代わりに親のidを子に設定する方法もあります。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment = Comment.new(text: "わー、いいなあ!")
comment.post_id = post.id
comment.save # 関連を確定するためには子のsaveが必要
post.comments.size #=> 1
post.comments[0].text #=> わー、いいなあ!
Child.new(parent_id: parent_id) / Child.create(parent_id: parent_id)
idを設定する場合でもnew
やcreate
を使うことができます。
post = Post.create(text: "新しいMacBook Proを買いました!")
comment_1 = Comment.new(text: "わー、いいなあ!", post_id: post.id)
comment_1.save
comment_2 = Comment.create(text: "うらやましすぎる!!", post_id: post.id)
post.comments.size #=> 2
post.comments[0].text #=> わー、いいなあ!
post.comments[1].text #=> うらやましすぎる!!
親子関係を保存する主な方法はこんな感じです。
上の説明をRSpecで表現する
最後に、上の説明をRSpecで表現してみましょう。
これは自分の説明が間違っていないか実際に動かして確認する目的で作成しました。
動作確認に使ったRailsアプリケーションと実際のSpecはこちらにあります。
- https://github.com/JunichiIto/association-sandbox
- https://github.com/JunichiIto/association-sandbox/blob/master/spec/models/comment_spec.rb
require 'spec_helper'
describe Comment do
let!(:post) { Post.create(text: "新しいMacBook Proを買いました!") }
context "not associated" do
specify do
Comment.new(text: "わー、いいなあ!")
expect(post.comments).to be_empty
end
end
describe "parent.children.create" do
specify do
comment = post.comments.create(text: "わー、いいなあ!")
expect(post.comments).to eq [comment]
expect(comment).to be_persisted
end
end
describe "parent.children.build" do
specify do
comment = post.comments.build(text: "わー、いいなあ!")
expect(post.comments).to eq [comment]
expect(comment).to_not be_persisted
end
end
describe "parent.children << child" do
specify do
comment = Comment.new(text: "わー、いいなあ!")
expect(comment).to_not be_persisted
post.comments << comment
expect(post.comments).to eq [comment]
expect(comment).to be_persisted
end
end
describe "parent.children = children" do
specify do
comment_old = Comment.new(text: "やった、コメント1番乗り!!")
post.comments << comment_old
expect(post.comments).to eq [comment_old]
expect(comment_old).to be_persisted
comment_1 = Comment.new(text: "わー、いいなあ!")
comment_2 = Comment.new(text: "うらやましすぎる!!")
post.comments = [comment_1, comment_2]
expect(post.comments).to eq [comment_1, comment_2]
expect(comment_1).to be_persisted
expect(comment_2).to be_persisted
expect(comment_old).to be_destroyed
end
end
describe "child.parent = parent" do
specify do
comment = Comment.new(text: "わー、いいなあ!")
comment.post = post
comment.save
expect(post.comments).to eq [comment]
end
end
describe "Child.new(parent: parent) / Child.create(parent: parent)" do
specify do
comment_1 = Comment.new(text: "わー、いいなあ!", post: post)
comment_1.save
comment_2 = Comment.create(text: "うらやましすぎる!!", post: post)
expect(post.comments).to eq [comment_1, comment_2]
end
end
describe "child.parent_id = parent_id" do
specify do
comment = Comment.new(text: "わー、いいなあ!")
comment.post_id = post.id
comment.save
expect(post.comments).to eq [comment]
end
end
describe "Child.new(parent_id: parent_id) / Child.create(parent_id: parent_id)" do
specify do
comment_1 = Comment.new(text: "わー、いいなあ!", post_id: post.id)
comment_1.save
comment_2 = Comment.create(text: "うらやましすぎる!!", post_id: post.id)
expect(post.comments).to eq [comment_1, comment_2]
end
end
end
役に立つ情報源
関連についてもっと深く知りたい場合はRails Guidesをじっくり読みましょう。
RSpecでRailsでテストする方法を学習するためにはこちらの電子書籍がオススメです!(宣伝)