1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ActiveRecord で Validation 実行前にエラーを記録できるモジュールを作った

Posted at

モデルクラス内でサービスクラスを利用する際にサービスクラスで発生したエラーをどうビューにまで持っていくのが一番いいのかなーと考えた

状況説明

下記の様なモデルがある時

app/models/person.rb
class Person < ApplicationRecord
  def complex_logic
    Person::ComplexLogicService.call(self)
  end
end

complex_logic は複雑で、外部APIなどにも接続する必要があるためその処理をサービスクラスPerson::ComplexLogicService に外出ししている。

その Person::ComplexLogicServicefail することがあるとする。

app/services/person/complex_logic_service.rb
class Person::ComplexLogicService
  def self.call(person)
    fail ArgumentError, "既に退会しているユーザーに複雑な処理は行えません" unless person.active?
  end
end

Person#complex_logic はビューからも呼ばれる処理なので、fail しては困るのでエラー処理を加える。

app/models/person.rb
class Person < ApplicationRecord
  def complex_logic
    Person::ComplexLogicService.call(self)
  rescue ArgumentError => err
    errors.add(:base, err.message)
  end
end

たが、このままではコントローラークラスで#saveをした時に、Validation が実行されて#errorsの内容がリセットされてしまうので、エラーにならない。

そもそもValidationはモデルの状態変化に応じて#valid?,#invalid?出来る様になっているため、#errorsは直前の#valid?の結果しか保持しない仕様。

解決策

と言うことで、エラー内容を一旦別のインスタンス変数に格納し、Validation 実行時にそのエラー内容を#errors.addするモジュールを Concern で作成した。

app/models/concerns/static_error.rb
# モデルの状態に関連しない Validation Error エラーを発生させるために使用します。
#
# @note エラー内容はデータベースには保存されないため、{#reload} などを行うと消えます。
# @note include すると下記のメソッド、インスタンス変数が追加されます
#   - @static_errors
#   - add_static_error
#   - clear_static_error
#   - check_static_errors (private)
#
# @example
#   class Model < ApplicationRecord
#     include StaticError
#   end
#   model = Model.new
#   model.add_static_error(:base, "このモデルは保存できません")
#   model.valid? #=> false
#   model.errors.messages #=> {:base=>["このモデルは保存できません"]}
module StaticError
  extend ActiveSupport::Concern

  included do
    validate :check_static_errors
  end

  # @overload add_static_error(attribute, message = :invalid, options = {})
  #   @param attribute [Symbol]
  #   @param message [Symbol]
  #   @param options [Hash]
  def add_static_error(*args)
    @static_errors = [] if @static_errors.nil?
    @static_errors << args

    true
  end

  # {#add_static_error} で追加されたエラー内容を削除します
  def clear_static_error
    @static_errors = nil
  end

  private

  def check_static_errors
    @static_errors&.each do |error|
      errors.add(*error)
    end
  end
end

実際使用する場合はこんな感じ

app/models/person.rb
class Person < ApplicationRecord
  include StaticError

  def complex_logic
    Person::ComplexLogicService.call(self)
  rescue ArgumentError => err
    add_static_error(:base, err.message)
  end
end

これで #valid?を何度実行してもfalseが返り、save! した場合には ActiveRecord::RecordInvalid が発生するので、ビュー以外のバッチ処理などで実行した場合もちゃんとエラーになる。

ただ、通常の Validation と動きが変わるので、乱用するとエラーが解消できず困って#clear_static_error を乱用する様なことになる可能性もあるので、ご利用は計画的に。

参考文献

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?