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


## はじめに: 関連(association)って何？

関連とはModel(データ)同士のつながりのことです。

単純な例で言うと、ブログの投稿(Post)とそれに対するコメント(Comment)は **関連** しています。
たとえば以下のような1件の投稿と2件のコメントを考えてみます。

````text
新しいMacBook Proを買いました！(投稿)
|-- わー、いいなあ！(コメント)
`-- うらやましすぎる！！(コメント)
````

上のデータを取得するRailsのコードはこんな感じになります。

````ruby
post.text #=> 新しいMacBook Proを買いました！
post.comments.size #=> 2
post.comments[0].text #=> わー、いいなあ！
post.comments[1].text #=> うらやましすぎる！！
````

コメントから投稿を参照することもできます。

````ruby
comment.text #=> わー、いいなあ！
comment.post.text #=> 新しいMacBook Proを買いました！
````

親子関係で言い換えると、投稿が **親** で、コメントが **子** です。
また、1件の投稿に対し、コメントは複数(0件以上)付けることができます。
これをModelの定義で表現するとこのようになります。

````ruby
class Post
  # 投稿は複数のコメントを持つ。投稿から見るとコメントは子
  has_many :comments
end

class Comment
  # コメントから見ると投稿は親
  belongs_to :post
end
````

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

![diagram.jpg](https://qiita-image-store.s3.amazonaws.com/0/7465/ddc1999e-fe6d-7e04-c38a-bbb221a4bf1b.jpeg "diagram.jpg")


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

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

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

````ruby
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`の形式で子のデータを作ると関連が設定できます。

````ruby
post = Post.create(text: "新しいMacBook Proを買いました！")
comment = post.comments.create(text: "わー、いいなあ！")

post.comments.size #=> 1
post.comments[0].text #=> わー、いいなあ！
````

`create`の代わりに`build`を使うこともできます。
この場合、データは保存されません。

````ruby
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`という形式で関連を設定することもできます。

````ruby
post = Post.create(text: "新しいMacBook Proを買いました！")
comment = Comment.new(text: "わー、いいなあ！")
post.comments << comment

post.comments.size #=> 1
post.comments[0].text #=> わー、いいなあ！
````

`<<`を使うと、データが同時に保存されてしまう点に注意してください。

````ruby
comment.persisted? #=> false
post.comments << comment # コメントが保存される
comment.persisted? #=> true
````

#### [備考] parent.children = children

同じような考え方で、`parent.children = children`という形式もあります。
配列を渡すので複数のコメントを同時に設定できますが、既存のコメントは削除されるのが特殊な点です。
使用する場合はこの副作用に十分注意してください。

````ruby
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`というように、「子に親を設定する」という形式もあります。

````ruby
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`の引数に親を渡す形式も考えられます。

````ruby
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を子に設定する方法もあります。

````ruby
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`を使うことができます。

````ruby
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

````ruby
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をじっくり読みましょう。

- http://guides.rubyonrails.org/association_basics.html

RSpecでRailsでテストする方法を学習するためにはこちらの電子書籍がオススメです！(宣伝)

- [Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)

![Everyday Rails - RSpecによるRailsテスト入門](https://s3.amazonaws.com/titlepages.leanpub.com/everydayrailsrspec-jp/large?1393556181)