概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: 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
コールバックに適していないとある程度確信しています。