nested_form で子レコード間の関連を検証する


# はじめに
1対多で関連するデータを nested_form で登録する際、子レコード間の関連を検証したいときがある。
この記事ではそうしたユースケースに対応可能なバリデーションの実装例を紹介する。

# サンプルアプリケーションの概要
1対多のデータを保管するフォームを作成する。
フォームの作成には [simple_form](https://github.com/plataformatec/simple_form) と [nested_form](https://github.com/ryanb/nested_form) を利用している。

**参考： nested_form について**
1対多のフォームを作成するときに便利なgem。
子要素を動的に追加、削除ができるビューのヘルパーメソッドが利用が可能になる。
nested_form の詳細は下記ページを参照。

- [simple_formで動的にフォーム要素を追加・削除する](http://qiita.com/kimihito_/items/cc986ddf5cf3e4275bc6)
- [Rails4で　１対多、多対多関連しているときのフォームを作るなら、Nested Form Gemが便利
](http://qiita.com/regonn/items/cdbda32900a15721d59b)


## モデル

モデルのコードは以下の通り。（nested_form関連の細かい設定は省略している）

```rb
class Project < ActiveRecord::Base
  has_many :tasks
end

class Task < ActiveRecord::Base
  belongs_to :project
end
```

ProjectとTaskにはそれぞれ以下のような属性がある。

Project

- name (プロジェクト名)
- published (公開フラグ。公開 or 非公開)

Task

- name (タスク名)
- priority (優先度。最優先 or 高い or 低い)

## フォーム
ブラウザで表示したフォームは以下の通り。
nested_form を使っているので、フォーム内でタスクの追加や削除が行える。

![qiita_01.png](https://qiita-image-store.s3.amazonaws.com/0/41362/7f3e7f4f-dc76-2317-f5d7-52091f96f120.png "qiita_01.png")

## ビュー
ビューのコードは以下の通り。
`simple_nested_form_for` や `link_to_remove`、`link_to_add` メソッド等が nested_form でのメソッド。

```projects/_form.html.haml
= simple_nested_form_for(@project) do |f|
  - if @project.errors.any?
    #error_explanation
      %h2
        = pluralize(@project.errors.count, "error")
        prohibited this project from being saved:
      %ul
        - @project.errors.full_messages.each do |message|
          %li= message
  .field
    = f.input :name
    = f.input :published
  .field
    %h2 タスク
    = f.fields_for :tasks do |tf|
      = tf.input :name
      = tf.input :priority, collection: Task.priority.options, include_blank: false
      = tf.link_to_remove 'タスク削除'
      %br
      %br
  .field
    = f.link_to_add 'タスク追加', :tasks
  .actions
    = f.submit '登録'

```

# バリデーション例

ここでは次のようなバリデーションを実装する。

1. `Task` は1つ以上登録が必要 (子レコード全体でチェックするケース)
2. 最優先の `Task` は1つだけ (子レコード同士でチェックするケース)
3. `Project` が非公開の場合は `Task`に 最優先は設定してはいけない (親と子レコード複合でチェックするケース)

今回、このバリデーションを次のように実装した。

```models/project.rb
class Project < ActiveRecord::Base
  has_many :tasks, dependent: :destroy, inverse_of: :project
  accepts_nested_attributes_for :tasks, allow_destroy: true

  validates :name, presence: true

  default_value_for :published, true

  # 1. タスクが1つも登録されていない
  validate :require_any_task
  def require_any_task
    errors.add(:base, :no_task) if tasks.blank?
  end

  # 2. 優先度が最優先は1つだけ
  validate :only_one_top_priority
  def only_one_top_priority
    top_priority_count = tasks.select { |task| task.priority.top? }.count
    errors.add(:base, :only_one_top_priority_for_project) if top_priority_count > 1
  end

  # 3. 非公開の場合はタスクに最優先は設定してはいけない
  validate :not_available_top_priority_if_private
  def not_available_top_priority_if_private
    if published.blank? && tasks.any? { |task| task.priority.top? }
      errors.add(:base, :not_available_top_priority_if_private)
    end
  end
end
```

子レコードのチェック時はまだデータの保管自体はされていない(配列にデータが入っているだけの状態)。
そのため、 `where` や `count` 等のSQLを使う方法では判定できないことに注意する。

なお、バリデーション部分のコードはあくまでRailsの機能を使っているだけであり、 nested_form とは無関係である。

## エラー時の表示例

バリデーションエラーが発生すると次のように表示される。

![qiita_02.png](https://qiita-image-store.s3.amazonaws.com/0/41362/f5a483a3-9bf4-449c-494f-bae71e649260.png "qiita_02.png")


# エラー表示を子レコードの指定フィールドにしたい場合

上の表示例だと、画面の上部にしかメッセージが表示されないため、フォームのどこでエラーが起きているのかわかりづらい。
そこで各フィールドにもエラーを表示できるようにした。

```models/project.rb
  # 指定のフィールドにエラーを付与したい
  validate :only_one_top_priority
  def only_one_top_priority
    top_priority_count = tasks.inject(0) do |count, task|
      count += 1 if task.priority.top?
      task.errors.add(:priority, :only_one_top_priority) if count > 1
      count
    end

    # 親レコードにエラーを設定しないと保管されてしまうことに注意
    errors.add(:base, :only_one_top_priority_for_project) if top_priority_count > 1
  end
```

上のコードのポイントは次の通り。

- 子レコード毎にチェックしてエラーを設定する。
- このとき、親レコードにエラーを設定しないとバリデーションがパスして保存されてしまうので注意する。

## エラー時の表示例

問題が発生したタスク（フィールド）にエラーが表示されている。

![qiita_03.png](https://qiita-image-store.s3.amazonaws.com/0/41362/400b328a-83c9-84f1-9968-e97458305f67.png "qiita_03.png")

# まとめ

今回作成したサンプルアプリケーションのポイントは以下の通り。

- 親モデル内で子モデルのデータを全てチェックできるので、柔軟にバリデーションが実装できる。
- 子レコードをチェックする際は `where` や `count` 等のSQLを使う方法では判定できない。（レコードがまだ保存されていないため）
- 子レコードだけがバリデーションエラーになった場合は、親レコードにもエラーを設定する必要がある。

また、今回のサンプルアプリケーションでは採用しなかったが、可読性や保守性の観点からバリデーション部分を Concern に切り分けるのも良い。

## 参考サイト

- [複数の子レコードを作成・更新する. accepts_nested_attributes_for](http://qiita.com/hmuronaka/items/818c421dc632e3efb7a6)
- [ネストされたモデル（親子関係を持つモデル）でStrongParameterを利用する](http://qiita.com/hmuronaka/items/580b977834d4b3010454)