LoginSignup
23
27

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-02-11

はじめに

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

サンプルアプリケーションの概要

1対多のデータを保管するフォームを作成する。
フォームの作成には simple_formnested_form を利用している。

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

モデル

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

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

ビュー

ビューのコードは以下の通り。
simple_nested_form_forlink_to_removelink_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

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

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

エラー時の表示例

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

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

まとめ

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

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

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

参考サイト

23
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
27