Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Railsのカスタムバリデーションコンテキストで、さまざまな検証状態をスマートに処理する

(この記事は数ヶ月前に私のメーリングリストへお送りしたものです。気に入っていただけたら、ぜひ登録して続きもお読みください!)

先日、自分のアプリのひとつで奇妙な状況に遭遇しました。

たとえば Article モデルがあり、記事はまず「下書き」として作成されるとします。下書きは軽量であるべきです。本文どころかタイトルすらなくても、すぐに作成・編集できるようにしたいはずです。

しかし、記事を公開する段階になると、追加のバリデーションが必要になります。タイトルや本文など、必要な項目がすべて揃っていることを確認しなければなりません。

if 文を並べて自分でチェックを実装することも可能ですが、そうすると Rails が提供してくれる便利なフォーム処理の恩恵を受けられなくなります。Rails が得意とすることは Rails に任せるほうが、後々の悩みを大幅に減らせると私は考えています。それでも、この用途にバリデーションを活用する方法はあるのでしょうか?

ifunless では不十分?

バリデーションには :if:unless パラメータが用意されていますが、必ずしもきれいに書けるとは限りません。

app/models/article.rb
class Article < ActiveRecord::Base
  # 下書きモードでのバリデーション
  validates_presence_of :author_id 

  # 公開時のみバリデーション
  validates_presence_of :body, unless: lambda { |o| o.draft? }
  
  ...
end

そもそも、これは私が望む「記事の公開」のやり方ではありません。 記事の公開は、記事が置かれた状態ではなく、ひとつのアクションとして捉えたいのです。属性を設定して条件分岐で確認するのは、正しいアプローチとは言えません。

あまり知られていない、ドキュメントも少ない Rails の機能

この問題こそ、Rails の「バリデーションコンテキスト」がぴったりはまるケースです。私は DHH が書いた gist を通じてカスタムバリデーションコンテキストの存在を初めて知りました。しかし、それが何なのか、どう使うのかについての分かりやすい解説を見つけるのは容易ではありませんでした。

カスタムコンテキストに関する公式ドキュメントは非常に薄く、API ドキュメント内で言及されている箇所はごくわずかです。しかもバリデーション自体の API ドキュメントにはカスタムコンテキストへの言及すらありません。名前を知らなければ、ドキュメントを探しようもない——これは本当に厄介な問題です。

最終的に、良い概説記事を見つけました:https://blog.arkency.com/2014/04/mastering-rails-validations-contexts/ 。一読の価値があります。では、冒頭の例に適用するとどうなるか見てみましょう。

app/models/article.rb
class Article < ActiveRecord::Base
  # 下書きモードでのバリデーション
  validates_presence_of :author_id
 
  # 公開時のみバリデーション
  validates_presence_of :body, on: :publish
  ...
end
app/controllers/article_controller.rb
class ArticleController < ApplicationController
  respond_to :html

  def create
    @article = Article.create(article_params)
    respond_with @article
  end

  def publish
    @article = Article.find(params[:id])
    @article.attributes = article_params
    @article.save(context: :publish)
    respond_with @article
  end

  private
  def article_params
    params.
      require(:article).
      permit(:body, :title, :author_id)
  end
end

なかなかシンプルですね。唯一ひっかかるのは、publish アクションで update ではなく save を使う必要がある点です。これは update がバリデーションコンテキストを引数として受け取れないためです。(プルリクエストのチャンスかもしれませんね。)

保存にとどまらない活用法

カスタムバリデーションコンテキストは、レコードを状況に応じて保存するときだけに使えるものではありません。コンテキストは valid? とも組み合わせられます。

@article.valid?(:publish)

つまり、「このオブジェクトはこの性質を満たしているか? 満たしていないなら、その理由は何か?」という問いに答えたいあらゆる場面で、バリデーションコンテキストを活用できるのです。

DHH は valid?(:context) へ委譲する、-able? で終わる名前のメソッドを作っています。この書き方、とても美しいと思います。

app/models/article.rb
class Article
  validate :has_been_published, on: :view
  validate :is_not_spam, on: :view

  def viewable?
    valid? :view
  end

  private

  def has_been_published
    if published_at.future?
      errors.add(:published_at, "is in the future")
    end
  end

  def is_not_spam
    if is_spam?(body)
      errors.add(:body, "has been detected as spam")
    end
  end
end

こうしておけば、チェックしたときに得られるのは単なる true / false ではなく、より詳しい情報です。

unless @article.viewable?
  # @article.errors に詳細が格納されている
end

その記事は閲覧可能か? もし不可能なら、その理由は?——そんな問いに即座に答えられるわけです。

ちょうどいい軽さ

同様のバリデーション挙動を実現する方法は他にもあります。独自のバリデーションを持つカスタム ActiveModel サービスオブジェクトを構築してもいいし、バリデーションに :if:unless を付けても構いません。しかし Rails の多くの機能と同じように、カスタムバリデーションコンテキストは可読性と柔軟性の間の絶妙な妥協点となる選択肢なのです。


  1. Rackアプリケーションで受信メールを処理する「Incoming!」の使い方

    クライアントから突然電話が… クライアントは、自分のアプリで受信メールを処理したいと言っています。まるでBasecampのように。 「問題ないよ」と思うでしょう。多くのサービスがPOSTリクエスト経由でメールをアプリに送信してくれるのだから、簡単なはずです。本当に? 実は、そう甘くありません。 各サービスは独自のフォーマットを採用しています。パース済みのメールを送ってくるものもあれば、生データを送るものもあります。認証署名付きのサービスもあれば、そうでないものもあります。 この面倒な工程を丸ごとスキップして、標準的(よく知られた)なRubyオブジェクトとして直接扱えたら素敵だと思いま

  2. rack-mini-profilerとフレームグラフでRailsアプリのボトルネックを可視化する方法

    あなたのRailsアプリは遅くなっていませんか? 本来シンプルに表示されるはずのビューの読み込みに数秒かかるなら、原因を掘り下げて調査すべきサインです。 原因としては、データベースへの呼び出しが多すぎたり、処理の遅いメソッドがあったり、あるいは誰かがコードに仕込んだまま忘れられてしまった無駄なループだったりします。 アプリの遅さの原因を突き止めるためのツールは数多く存在します。以前このブログでもrbtraceについて紹介しましたし、New Relicのrpm gemもアプリの高速化に役立ってくれました。 しかし、私がパフォーマンス問題の調査に最も愛用しているツールは、それ以上のことができるので