Railsでvalidationを行う際のwith_optionsの使い方

こんにちは!kossyです!



さて、今回はRailsでvalidationを行う際のwith_optionsの使い方について、ブログに残してみたいと思います!



環境

Ruby 2.6.6
Rails 6.0.3.4
MacOS catalina



Documentを読んで使い方を学ぶ

まずはドキュメントを読んでみましょう。


5.3 条件付きバリデーションをグループ化する


1つの条件を複数のバリデーションで共用できると便利なことがあります。これはwith_optionsを使うことで簡単に実現できます。

class User < ApplicationRecord
  with_options if: :is_admin? do |admin|
    admin.validates :password, length: { minimum: 10 }
    admin.validates :email, presence: true
  end
end

with_optionsブロックの内側にあるすべてのバリデーションには、if: :is_admin?という条件が渡されます。

Userがadminの場合にのみ適用したいvalidationを行う例が示されています。


次はこのドキュメントを読みます。

with_options(options, &block) public

An elegant way to factor duplication out of options passed to a series of method calls. Each method called in the block, with the block variable as the receiver, will have its options merged with the default options hash provided. Each method called on the block variable must take an options hash as its final argument.

一連のメソッド呼び出しに渡されるオプションから重複を除外するための洗練された方法。
ブロック変数をレシーバーとしてブロック内で呼び出される各メソッドのオプションは、提供されているデフォルトのオプションハッシュとマージされます。ブロック変数で呼び出される各メソッドは、最後の引数としてオプションハッシュを取る必要があります。

with_optionsの使い方の例として、dependent: :destroyをまとめて定義する方法が記載されていました。

class Account < ActiveRecord::Base
  with_options dependent: :destroy do |assoc|
    assoc.has_many :customers
    assoc.has_many :products
    assoc.has_many :invoices
    assoc.has_many :expenses
  end
end

上記のようにwith_optionsの引数にdependent: :destroy とブロックを渡すことで、
まとめて定義することができるようです。

以下のように記述した場合でも上記の例と同じ効果を得られるみたいです。

class Account < ActiveRecord::Base
  with_options dependent: :destroy do
    has_many :customers
    has_many :products
    has_many :invoices
    has_many :expenses
  end
end

また、with_optionsにifとブロックを渡して定義する記述方法もあります。

class Post < ActiveRecord::Base
  with_options if: :persisted?, length: { minimum: 50 } do
    validates :content, if: -> { content.present? }
  end
end

この場合は、インスタンスがDBに保存済みかどうかを判定して、保存済みだった場合に適用されるバリデーションになっています。

with_optionsを使う場合の注意点

ドキュメントにこのようなNOTEがありました。

NOTE: You cannot call class methods implicitly inside of with_options. You can access these methods using the class name instead:

class Phone < ActiveRecord::Base
  enum phone_number_type: [home: 0, office: 1, mobile: 2]

  with_options presence: true do
    validates :phone_number_type, inclusion: { in: Phone.phone_number_types.keys }
  end
end

なるほど、with_options内だとselfを参照できないから、明示的にPhone.phone_number_types.keysを呼び出さないといけないと注意を促しているんですね。

知らないとハマりそうです。

binding.pryしながら実行してみた

試しに以下のように、インスタンス自身を参照できるのか試してみます。

  with_options presence: true do |_self|
    binding.pry
    validates :condition, presence: true
  end
pry $ _self
=> #<ActiveSupport::OptionMerger:0x2b00cbb1492c>

インスタンスそのものが返ってくると予想していましたが、
ActiveSupport::OptionMergerクラスが返ってきました。

apidock.com
joker1007.hatenablog.com


次は_selfを渡さずに試してみます。

  with_options presence: true do
    binding.pry
    validates :condition, presence: true
  end
pry $ self
=> #<ActiveSupport::OptionMerger:0x2b00cbb1492c>

うーん、やはりwith_optionsメソッドのブロック内だと、selfはActiveSupport::OptionMergerになるみたいですね。



勉強になりました。