Rails tips: 知らないと損する4つのバリデーションレベル(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: 知らないと損する4つのバリデーションレベル(翻訳)

考えるまでもないことですが、アプリがユーザー入力を受け取ったらバリデーションが必要になります。Ruby on Railsアプリでバリデーションといえば真っ先に思い当たるのがモデルのバリデーションです。しかしそれ以外のレベルのバリデーションについてはどうでしょう。モデルのバリデーションがあれば完璧なソリューションになるのでしょうか?今回はRailsアプリの4つのレベルのバリデーションを簡単にご紹介しつつ、それぞれのメリットとデメリットについて説明したいと思います。お題として、Userモデルのemailカラムを使います。

1. モデルレベルのバリデーション

Railsアプリでよく見られるアプローチです。emailUserのレコードに確実に存在するようにするために、以下のバリデーションを定義できます。

class User < ActiveRecord::Base
  validates :email, presence: true
end

このデータ保護方法は間違っていませんが、これだけではメールが空のUserレコードをまだ作成できてしまう点を肝に銘じてください。User#saveUser#save!を呼んでも無効なレコードはデータベースに保存されませんが、以下のメソッドを呼べば保存されてしまうのです。

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • touch
  • update_all
  • update_attribute
  • update_column
  • update_columns
  • update_counters

これらのメソッドを用いる場合、特にupdate_で始まるメソッドについては注意が必要です。では逆に、emailカラムのバリデーションが不要な場合はどうすればよいでしょうか?その場合は:ifオプションを渡すか、コントローラレベルのバリデーションを検討しましょう。

2. コントローラレベルのバリデーション

上述したように、特定の場合に限ってバリデーションを行いたいことがあります。:ifオプションや:unlessオプションを渡してもよいのですが、バリデーションルールが複雑になって読みづらくなったりテストがしにくくなるかもしれません。そこでコントローラレベルのバリデーションが選択肢として浮かび上がってきます。これを正しく行うには、Form Objectパターンの利用をおすすめします。ただし、コントローラレベルのバリデーションはモデルレベルのバリデーションに比べてメンテの難易度がぐっと上がります。Form Objectパターンは、モデルに多数のバリデーションがあり、ときどき必須にしたいバリデーションやときどきオプションにしたいバリデーションがあるような非常に大規模なアプリでとても有用です。

訳注: Form Objectについては以下の記事やForm Objectタグなどもどうぞ。

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)

3. データベースレベルのバリデーション

データベースレベルのバリデーションは最も安全性が高く、これをかいくぐって無効な値を保存することはできません。よく使われるのはpresenceuniquenessで、どちらもUserモデルのemailカラムにうってつけです。以下のようにマイグレーションを作成して追加します。

class AddValidationOnUserEmail < ActiveRecord::Migration
  def change
    change_column :users, :email, :string, null: false
    add_index :users, :email, unique: true
  end
end

rake db:migrateを実行すればバリデーションをテストできます。モデルのバリデーションを呼び出さないupdate_columnメソッドを使ってみましょう。メールアドレスのないユーザーを試しに保存してみます。

user = User.find(user_id)
user.update_column(:email, nil) # => raises ActiveRecord::StatementInvalid: Mysql2::Error: Column 'email' cannot be null

ちゃんとエラーがraiseされました。今度はメールアドレスが重複しているユーザーを保存してみます。

user = User.find(user_id)
user_2 = User.find(user_2_id)

user.update_column(:email, user_2.email)
# => raises ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry

2つのバリデーションは種類が異なります。presenceバリデーションはカラム定義に渡せばよいのですが、uniquenessバリデーションの場合はデータベースに正しいインデックスを作成しなければなりません。このエラーは、無効なデータを保存しようとしたときにもraiseされます。

user = User.find(user_id)
user.update_column(:created_at, "string")
# => raises ActiveRecord::StatementInvalid: Mysql2::Error: Incorrect datetime value

ご覧いただいたように、パフォーマンス上の意味だけではなく、セキュリティのためにも、カラムを慎重に定義して正しくインデックスを作成することが重要です。

4. フロントエンドのバリデーション

最も安全性の低いバリデーションです。ブラウザでJavaScriptをオフにしたり、コードを使ってリクエストを直接送信したり、Postmanなどのブラウザ拡張を使ったりすれば、このバリデーションをバイパスできます。

データの保護はどんな場合であっても、バックエンド側のバリデーションで最初に行うべきです。しかしフロントエンドのバリデーションは、ユーザーエクスペリエンス向上には最も適しています。私は、フォーム送信を待たずにアプリがその場でフォームのエラーを表示してくれるのが好きです。

新着記事を見逃したくない方はTwitterをフォローしてください。もちろん「hello」だけでも構いません!

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Rails tips: カスタムバリデータクラスを作る(翻訳)

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ