Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳)

概要 原著者の許諾を得て翻訳・公開いたします。 英語記事: The Case Against Exotic Usage of :before_validate Callbacks 原文公開日: 2017/10/29 著者: Karol Galanciak サイト: BookingSync なお、原文のbefore_validateは訳文でbefore_validationに修正しました。 Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳) ActiveRecordのコールバックが多くのプロジェクトで乱用され、もっとよい方法で簡単に回避できるユースケースであっても誤った理由で使われているのは今に始まったことではありません。実行される処理と関係のない、かなり逸脱した理由で多用される、特殊なコールバックが1つあります。それがbefore_validationコールバックです。 データ整形 データ整形、特に文字列のストリップは、アプリの主要な部分を占めることが多い処理です。たとえば、スペースで問題が生じないように URLをストリップすることを考えてみましょう。どのようなアプローチが考えられるでしょうか。 1つの方法は、before_validationを使うことです。特にデータ形式のバリデーションを行っている場合です。 # app/models/my_model.rb class MyModel before_validation :strip_url private def strip_url self.url = url.to_s.strip end end これで処理は完了します。しかしテストはどうすればよいでしょうか。そのモデルでvalid?メソッドを呼び、URLがストリップされたかどうかをチェックする必要があるのでしょうか?これはいかにも変ですし、次のspecを見れば違和感がもっとよくわかるでしょう。 # spec/models/my_model_spec.rb require “rails_helper” RSpec.describe MyModel, type: :model do it “バリデーション前にURLをストリップする” do model = MyModel.new(url: ” http://rubyonrails.org”) model.valid? expect(model.url).to eq “http://rubyonrails.org” end end このコードがTDDの結果であるとはちょっと考えられません。では他に方法はあるでしょうか。 次のように、単に専用の属性ライターメソッド(#url=)を書く方法ならどうでしょう。 # app/models/my_model.rb class MyModel def url=(val) super(val.to_s.strip) end end この機能に対応するspecとして次が考えられます。 # spec/models/my_model_spec.rb require “rails_helper” RSpec.describe MyModel, type: :model do it “strips URL” do model = MyModel.new(url: ” http://rubyonrails.org”) expect(model.url).to eq “http://rubyonrails.org” end end この実装とspecならどちらもずっとシンプルになりますし、ずっと自然です。データ整形はバリデーションと何の関係もないので、このようなユースケースを扱うためにバリデーションがらみのコールバックを使う必要はありません。 属性やリレーションシップを代入する もうひとつのよくあるシナリオは、属性やリレーションシップの代入です。たとえば、contentを1つ持つコメントと、current_userになる著者(author)を作成し、かつパフォーマンス上の理由から何らかのdenormalization(非正規化)を行って、current_userが属するコメントにgroupを直接代入したいとします。以下はbefore_validationコールバックを少し使った例です。 Comment.create!( content: content, author: current_user, ) # app/models/my_model.rb class MyModel before_validation :assign_group private def assign_group self.group = author.group if author end end これも先のデータ整形のユースケースとかなり似ています。この機能のテストを書くためにvalid?を呼ぶ必要があるのでしょうか?バリデーションは、属性やリレーションシップの代入とは何の関係もないのですから、必要性はさほど感じられません。これは、次のようにもっとシンプルで明示的な方法で扱えます。 Comment.create!( content: content, author: current_user, group: current_user.group, ) マジックを使わない単なる代入なので、テストも理解も簡単です。 まとめ before_validationコールバックが考えうる限り最善の選択になることがもしかするとあるかもしれません(私はそのような状況に出会ったことがありませんが)。しかし私は、データ整形や属性/関連付けの代入は、before_validationコールバックに適していないとある程度確信しています。 関連記事 Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳) 3年以上かけて培ったRails開発のコツ集大成(翻訳)