[初心者向け] Railsで関連するデータ(親子関係)を保存する方法あれこれ

  • 254
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに: 関連(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

ついでにクラス図も載せておきましょう。

diagram.jpg

親子関係を保存する方法あれこれ

さて、データを取得するコードはわかりましたが、この親子関係を保存するときはどんなコードを書けば良いのでしょうか?

もしこんなコードを書いたとしても関連をうまく保存することはできません。

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)

さらに、このバリエーションとしてnewcreateの引数に親を渡す形式も考えられます。

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を設定する場合でもnewcreateを使うことができます。

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はこちらにあります。

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でテストする方法を学習するためにはこちらの電子書籍がオススメです!(宣伝)

Everyday Rails - RSpecによるRailsテスト入門