開発を進める中で、Active Record で1対多の関連先レコードを差分更新したい場面があります。私個人の話ですが、そのような要件があった際に自前で差分のみを抽出し、関連先の属性に指定するような実装を行っていました。
ある時、関連先の属性に直接指定することで、Active Record が差分更新してくれることを同僚に教えてもらいました。とても便利に思った反面、Active Record の内部でどのように差分更新されているかについて気になったので、調べてみました。
ここまで、has_many 関連を持つ属性にオブジェクトを含む配列を渡すことで、Active Record が対応するテーブルに対して差分更新を行ってくれることを紹介してきました。
続いて、Active Record が自動的に行う差分更新の仕組みについて、内部のコードを追いながら見ていきたいと思います。
イメージしやすいように、登場するクラスやメソッドを簡単にまとめてみました。
has_manyクラスマクロの呼び出し
前述した例を改めて。
class Author < ApplicationRecord
has_many :books
end
# Replace this collection with +other_array+. This will perform a diff# and delete/add only records that have changed.defreplace(other_array)
other_array.each { |val| raise_on_type_mismatch!(val) }
original_target = skip_strict_loading { load_target }.dup
if owner.new_record?
replace_records(other_array, original_target)
else
replace_common_records_in_memory(other_array, original_target)
if other_array != original_target
transaction { replace_records(other_array, original_target) }
else
other_array
endendend
defreplace_on_target(record, skip_callbacks, replace:, inversing: false)
if replace && (!record.new_record? || @replaced_or_added_targets.include?(record))
index = @target.index(record)
end
...
if index
target[index] = record
elsif@_was_loaded || !loaded?
@association_ids = nil
target << record
endend
defreplace_records(new_target, original_target)
delete(difference(target, new_target))
unless concat(difference(new_target, target))
@target = original_target
raiseRecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
"new records could not be saved."end
target
end
今回、has_manyな関連先の更新時に Active Record が差分更新を行う挙動について見てきました。
この機能を知る前に自前で実装していた処理が、Active Record 内にすでに組み込まれており、同様の要件であれば Active Record の機能を素直に使用する方が、コードをシンプルに保てることが分かりました。
事前にドキュメントをしっかり読んだり、直感的に使用していれば、そもそも自前で差分更新のロジックを書く必要はなかったかもしれません。
ただ、Active Record がどのように差分更新を行うのかを深掘りできたことで、今後自信を持って活用できるようになる良い機会になりました。
GraphQL errors were originally designed to represent exceptional events and client-related issues, not necessarily expected product or business errors that need to be relayed to the end-user.
The general philosophy at play is that Errors are considered exceptional. Your user data should never be represented as an Error. If your users can do something that needs to provide negative guidance, then you should represent that kind of information in GraphQL as Data not as an Error. Errors should always represent either developer errors or exceptional circumstances (e.g. the database was offline).
typeSignUpPayload{userErrors: [UserError!]!account: Account}typeUserError{# The error messagemessage: String!# Indicates which field cause the error, if any## Field is an array that acts as a path to the error# Example:## ["accounts", "1", "email"]#field: [String!]# An optional error code for clients to match on.code: UserErrorCode}
defcached_find(record)
policy_cache.fetch(user: user, record: record) do
klass = yield policy_finder(record)
...
model = pundit_model(record)
begin
klass.new(user, model)
rescueArgumentErrorraiseInvalidConstructorError, "Invalid #<#{klass}> constructor is called"endendend
classHomeController < ApplicationControllerdefindex# Does some stuff ...
logger.info "Someone visited the site!"# Does some more stuff ...endend
it "logs a message"do
visit root_path
expect(page).to have_content "Welcome to my site!"
expect(Rails.logger).to receive(:info).with("Someone visited the site!")
end
context 'when it blows up'do
let(:capture_logger) { SemanticLogger::Test::CaptureLogEvents.new } # ①
it 'should should log the error'do
allow_any_instance_of(MyThing).to receive(:logger).and_return(capture_logger) # ②MyThing.new('asdf').do_something!
# ③
expect(capture_logger.events.last.message).to include('Here is a message')
expect(capture_logger.events.last.level).to eq(:error)
endend
moduleActiveJobmoduleLoggingextendActiveSupport::Concern
included do### Accepts a logger conforming to the interface of Log4r or the default# Ruby +Logger+ class. You can retrieve this logger by calling +logger+ on# either an Active Job job class or an Active Job job instance.
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
...
end
...
endend