概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Ruby on Rails - four levels of the data validation you should be aware of
- 原文公開日: 2018/04/06
- 著者: Paweł Dąbrowsk
Rails tips: 知らないと損する4つのバリデーションレベル(翻訳)
考えるまでもないことですが、アプリがユーザー入力を受け取ったらバリデーションが必要になります。Ruby on Railsアプリでバリデーションといえば真っ先に思い当たるのがモデルのバリデーションです。しかしそれ以外のレベルのバリデーションについてはどうでしょう。モデルのバリデーションがあれば完璧なソリューションになるのでしょうか?今回はRailsアプリの4つのレベルのバリデーションを簡単にご紹介しつつ、それぞれのメリットとデメリットについて説明したいと思います。お題として、User
モデルのemail
カラムを使います。
1. モデルレベルのバリデーション
Railsアプリでよく見られるアプローチです。email
がUser
のレコードに確実に存在するようにするために、以下のバリデーションを定義できます。
class User < ActiveRecord::Base
validates :email, presence: true
end
このデータ保護方法は間違っていませんが、これだけではメールが空のUser
レコードをまだ作成できてしまう点を肝に銘じてください。User#save
やUser#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タグなどもどうぞ。
3. データベースレベルのバリデーション
データベースレベルのバリデーションは最も安全性が高く、これをかいくぐって無効な値を保存することはできません。よく使われるのはpresence
やuniqueness
で、どちらも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』をどうぞお役立てください。