モデルクラス内でサービスクラスを利用する際にサービスクラスで発生したエラーをどうビューにまで持っていくのが一番いいのかなーと考えた
状況説明
下記の様なモデルがある時
class Person < ApplicationRecord
def complex_logic
Person::ComplexLogicService.call(self)
end
end
complex_logic は複雑で、外部APIなどにも接続する必要があるためその処理をサービスクラスPerson::ComplexLogicService
に外出ししている。
その Person::ComplexLogicService
で fail
することがあるとする。
class Person::ComplexLogicService
def self.call(person)
fail ArgumentError, "既に退会しているユーザーに複雑な処理は行えません" unless person.active?
end
end
Person#complex_logic
はビューからも呼ばれる処理なので、fail
しては困るのでエラー処理を加える。
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 で作成した。
# モデルの状態に関連しない 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
実際使用する場合はこんな感じ
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
を乱用する様なことになる可能性もあるので、ご利用は計画的に。