読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

Rails 4でモデルのバリデーションまとめ

Rails初級 Rails Model

Railsではバリデーション(Validation)という仕組みがあります。
フォームなどでユーザーからの入力値をDBに保存する前にその値が正しいものかモデル層で(システムとして許可している値か)を検証する仕組みです。

バリデーションの基本的な流れ、バリデーションの定義とバリデーションのテスト方法、バリデーションのスキップなどのバリデーションの基本についてまとめました。

動作確認

目次


1. Railsでのバリデーションの流れ

Railsでのバリデーションの流れをステップバイステップで説明します。

1. validatesを使い、モデルクラスに検証する条件を定義する。

# app/models/product.rb
class Product < ActiveRecord::Base
  # 商品名は必須
  validates :title, presence: true
  # 値段は数値で0以上
  validates :price, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end


2. コントローラーなどでモデルを保存/更新する。

# app/controllers/products_controlle.rb
class ProductsController < ApplicationController
  def create
    # product_paramsにユーザーの入力値がハッシュ形式で入っている
    @product = Product.new(product_params)

    # 商品クラスのバリデーション定義に照らし合わせ、
    # - バリデーションエラーが発生しなかった場合、DBにレコードが保存され、show画面に遷移する
    # - バリデーションエラーが発生した場合、DBにレコードは保存されず、new画面を再び表示する
    if @product.save
      redirect_to @product, notice: '商品を作成しました'
    else
      render :new
    end
  end
end


3. バリデーションエラー時には、errorsにエラーメッセージが設定されるので、それを表示する

# app/views/products/_form.html.erb
<%= form_for(@product) do |f| %>
  <!-- バリデーションエラー時のみ、@product.errors に値があるので、エラーメッセージを表示する -->
  <% if @product.errors.any? %>
    <div id="error_explanation">
      <h2><%= @product.errors.count %>件のエラーが発生しました。</h2>

      <ul>
      <% @product.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  ...
<% end %>


2. バリデーションを定義する

バリデーションの定義方法を説明します。
また、RSpecshoulda-matchersを使ったテスト方法も一緒に記載しています。



2.1. 存在チェック(presence)

presenceオプションにより、指定した属性が空でないことを検証する。

# 定義
validates :name, presence: true # 値が存在すれば検証成功

# テスト
it { is_expected.to validate_presence_of(:name) }

boolean型のカラムの場合は、次のようにinclusionを使います。(テストはinclusionの項目を参照)

# boolean型のカラムの場合の存在チェック
validates :completed, inclusion: { in: [true, false] }


2.2. 一意性(ユニーク制約)のチェック(uniqueness)

uniquenessオプションにより、指定した属性がユニークであることを検証する。

# 定義
validates email, uniqueness: true # 値がユニークであれば検証成功

# テスト
it { is_expected.to validate_uniqueness_of(:email) }


注意点として、サーバー高負荷時などにユーザが2度登録ボタンを押してしまい、モデルでユニーク制約を記載していたのに同じ値がDBに登録されてしまうという問題が発生してしまいます。
そのため、モデルのバリデーションだけではなく、DBでもユニーク制約をつけておくべきです。

# マイグレーションファイルの作成
rails g migration add_index_to_users_email

# マイグレーションファイル
# db/migrate/YYYYMNDDHHMMSS_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end

# マイグレート
rake db:migarte


複数カラムでの一意性を検証したい場合には、scopeパラメータを指定します。
もちろん、DBとしても複数カラムで一意性制約を追加してください。
以下の例では、ユーザー単位に投稿(Article)のタイトルを一意にする検証を追加しています。

# 定義
class Article < ActiveRecord::Base
  belongs_to :user
  validates :title, uniqueness: { scope: [:user_id] }
end

# テスト
it { is_expected.to validate_uniqueness_of(:title).scoped_to(:user_id) }

# マイグレーション
add_index :articles, [:title, :user_id], unique: true


メールアドレスの場合は、小文字に変換して一意性を保証すべきです。

# app/models/user.rb
class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
end

2.3. 長さのチェック(length)

lengthオプションにより、指定した値の長さを検証する。
マルチバイト文字であっても1文字としてカウントします。

# 定義
class Person < ActiveRecord::Base
  validates :name,     length: { minimum: 2 }        # 値が「2文字以上」であれば有効
  validates :bio,      length: { maximum: 500 }      # 値が「500文字以下」であらば有効
  validates :password, length: { in: 6..20 }         # 値が「6文字以上20文字以下」であれば有効
  validates :registration_number, length: { is: 6 }  # 値が「6文字のみ」有効
end

# テスト
it { is_expected.to ensure_length_of(:name).is_at_least(2) }
it { is_expected.to ensure_length_of(:bio).is_at_most(500) }
it { is_expected.to ensure_length_of(:password).is_at_least(6).is_at_most(20) }
it { is_expected.to ensure_length_of(:registration_number).is_equal_to(6) }

2.4. フォーマットのチェック(format)

formatオプションにより、指定した値のフォーマットを検証する。
正規表現を検証するためにRubularで、テストしたい正規表現とサンプル文字列を入力することで正規表現にマッチするか調べられます。

# 定義
VALID_EMAIL_REGEX =  /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i #メールアドレスフォーマットの検証(完璧な正規表現ではない)
validates :email, format: { with: VALID_EMAIL_REGEX }
# 特定の値が入ってほしくない場合は without オプションを利用
# validates :email, format: { without: <入ってほしくない値の正規表現> }

# テスト
it "valid emails" do
  valid_emails = %w(
      user@foo.COM
      A_US-ER@f.b.org
      frst.lst@foo.jp
      a+b@baz.cn
    )
  is_expected.to allow_value(*valid_emails).for(:email)
end
it "invalid emails" do
  invalid_emails = %w(
      user@foo,com
      user_at_foo.org
      example.user@foo.
      foo@bar_baz.com
      foo@bar+baz.com
      foo@bar..com
    )
  is_expected.not_to allow_value(*invalid_emails).for(:email)
end

2.5. 数値の値チェック(numericality)

numericalityオプションにより、指定した値(数値)を検証する。

# 定義
class Player < ActiveRecord::Base
  validates :points, numericality: true # 数値か小数点のみ有効
  validates :games_played, numericality: { only_integer: true } # 数値のみ有効
end

# テスト
  it { is_expected.to validate_numericality_of(:points) }
  it { is_expected.to validate_numericality_of(:games_played).only_integer }


only_integer: trueをしたいした場合、数値の値自体の検証をすることができます。

  • greater_than
  • greater_than_or_equal_to
  • equal_to
  • less_than
  • less_than_or_equal_to
  • odd (奇数)
  • even (偶数)
# 定義
# 数値であり、0以上の場合有効
validates :games_played, numericality: {
            only_integer: true, greater_than_or_equal_to: 0
          }

# テスト
it { is_expected.to validate_numericality_of(:games_played)
                      .only_integer.is_greater_than_or_equal_to(0) }

2.6. コレクションに含まれていることを検証(inclusion)

inclusionオプションにより、inで指定したコレクションに含まれていることを検証する。

# 定義
validates :size, inclusion: { in: %w(small medium large) }

# テスト
it { is_expected.to ensure_inclusion_of(:size).in_array(%w(small medium large)) }

2.7. コレクションに含まれていないことを検証(exclusion)

exclusionオプションにより、inで指定したコレクションに含まれていないことを検証する。

# 定義
validates :subdomain, exclusion: { in: %w(www us ca jp) }

# テスト
it { is_expected.to ensure_exclusion_of(:subdomain).in_array(%w(www us ca jp)) }


3. バリデーションをスキップする

次のメソッドはバリデーションを実行し、成功した場合のみ、DBに保存/更新します。

  • create
  • create!
  • save
  • save!
  • update
  • update!


下記のメソッド群、もしくは、上記のメソッドvalidate: falseを引数に追加することでバリデーションをスキップできます。

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • touch
  • update_all
  • update_attribute
  • update_column
  • update_columns
  • update_counters
# バリデーションがスキップされる
save(validate: false)


4. 条件付きバリデーションを定義する

ある条件のときにだけバリデーションを実施したい場合に、条件付きバリデーションが使えます。
条件付きバリデーションを使うためには、validatesメソッドifunlessオプションを指定して下さい。

class Order < ActiveRecord::Base
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end


5. バリデーションがvalidかinvalidか確認する

valid?invalid?メソッドを使うことでバリデーションが「有効」か「無効」かをboolean型で取得することができます。
このメソッドをコールした後に、バリデーションが失敗した場合、errors内にエラーメッセージが設定されます。

# バリデーション定義
# app/models/article.rb
class Article < ActiveRecord::Base
  validates :title, presence: true
end

# 利用
## valid? メソッド(true)
Article.new(title: "タイトル").valid?
# => true

## valid? メソッド(false)
article = Article.new(title: nil)
article.valid?
# => false (errorsに値がエラーメッセージが設定される)
article.errors.full_messages
# => ["Title can't be blank"]

## invalid? メソッド(false)
Article.new(title: "タイトル").invalid?
# => false

## invalid? メソッド(true)
article = Article.new(title: nil)
article.invalid?
# => true (errorsに値がエラーメッセージが設定される)
article.errors.full_messages
# => ["Title can't be blank"]


6. 独自のカスタムバリデーションを定義する

標準のバリデーションでは足りない場合に、独自のカスタムバリデーションを作成することができます。

カスタムValidate

validateメソッドを使うことで「独自のカスタムバリデーション」を作成できます。
validateメソッドには「メソッド名のシンボル」を渡し、そのメソッド内でバリデーションを実装します。
バリデーションを実施するメソッド内では、errorsに値を設定することにより、バリデーションエラーになったということをActiveRecordに知らせます。

# app/models/invoice.rb
class Invoice < ActiveRecord::Base
  validate :expiration_date_cannot_be_in_the_past,
    :discount_cannot_be_greater_than_total_value

  def expiration_date_cannot_be_in_the_past
    if expiration_date.present? && expiration_date < Date.today
      errors.add(:expiration_date, "can't be in the past")
    end
  end

  def discount_cannot_be_greater_than_total_value
    if discount > total_value
      errors.add(:discount, "can't be greater than total value")
    end
  end
end

カスタムValidator

ActiveModel::Validatorを拡張することにより「カスタムValidator」を作成できます。
この継承したクラスは、validateメソッドを実装しなければいけません。
複数のモデルで同じカスタムバリデーションを使いたいときに使うのが良いです。

# カスタムValidatorを作成
class MyValidator < ActiveModel::Validator
  def validate(record)
    unless record.name.starts_with? 'X'
      record.errors[:name] << 'Need a name starting with X please!'
    end
  end
end

# app/models/person.rb
class Person
  # カスタムValidatorを追加
  include ActiveModel::Validations
  validates_with MyValidator
end

以上です。

参考文献