Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

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

概要

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


  • 2018/05/09: 初版公開
  • 2021/12/02: 更新

概要

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`を使ってバリデーションをモデルから分離する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。