元WEBディレクターのプログラミング奮闘日記

元WEBディレクター、今はなんちゃってエンジニア。思っていること、ハマったところ、なんでも書いていきます。

railsでネストされた小モデルに対してバリデーションしようと思った時に苦しんだお話

掲題のようなことをやろうとして大いにハマったので、解決方法を書いておく(ちなみに、やりたいことは実現できたというだけなので、こうすると楽だよ、的なご指摘あればコメントしていただけると大変嬉しいです。)

まず今回やりたかったこと(機能的には投稿の一時保存的なものをつくろうとしていました)

accepts_nested_attributes_for :fugafugaのようにネストされた小モデルのバリデーションをしたい

・あるアクション(僕の場合は一時保存アクション)から呼ばれた場合のみ、presence: trueだけ無視したい(その他にもlengthのチェックなどをしている)

1. on: :hogehoge の逆が簡単にできない

特定のアクションの場合のみバリデーションをしたいときは on: :hogehoge のようにvalidation_cotextをonオプションで指定してやってやるのが便利なんだけど、今回は特定のアクション以外の場合という条件分岐をしたかったので結構手こずった。

一時保存以外のアクションの場合(今回はcreateとupdate)にonオプションをつけても実現できるのだが、

with_options on: [:create, :update] do |hogehoge|
  hogehoge.validates :fuga, presence: true
end

上記のようにしてしまうと、今後このモデルを扱うアクションが増えた時なんかにメンテナンスがしににくなりそう(というか本質的にやりたいことではないのでなんか気持ち悪い)なので、なんとか一時保存のアクションでなければという条件分岐を作ってやりたかった。

そしたらちょうど同じことで悩んでいる方の記事を発見

http://labs.timedia.co.jp/2014/07/rails-validation-not-on-context.html

こちらにならい、ブロックをunlessに渡してやることにより一旦この問題は解決

with_options unless: -> { [:draft].include?(validation_context) } do |offer|
  offer.validates :position, presence: true
end

こんな感じ

2. ネストされた小モデルにvalidation_contextが渡らない

親モデルは上の方法でうまくいったのだが、accepts_nested_attributes_for :fugafugaのようにネストされた小モデルに対してはうまく行かなかった。 いろいろ調べてみると、どうやらvalidation_contextがうまく渡っていないご様子。一時保存のアクションからmodel.saveをしても親モデルには渡っているが、ネストされた小モデルのvalidation_contextにはupdate or createが渡ってしまっていた。

どのようにcontextを渡しているのかを調べてみると、以下のようにcontextがnilの場合は、新規レコードかどうかを見てcreateかupdateをつっこんでいるらしい。

#activerecord/lib/active_record/validations.rb

def valid?(context = nil)
  context ||= (new_record? ? :create : :update)
  output = super(context)
  errors.empty? && output
end

親モデルに対してcontextを指定してあげてもaccepts_nested_attributes_forでネストした子モデルにはnilが渡っているっぽい。

結構いろいろ試したんだけど、どうも子モデルのバリデーションにcontextを渡すことが出来ず。

以下のように親モデルの中で無理やりバリデーションさせることで対応した。

with_options unless: -> { [:draft].include?(validation_context) } do |offer|
  offer.validates :position, presence: true
  # 追加部分
  offer.validates :fugafuga_name, presence: true
end

private 
def fugafuga_name
  fugafuga.name
end

うーん、一応やりたいことは実現できたのだが、、、

そもそもRailsが用意していないことをやろうとしている時点で、何かがおかしいですかね、、、