Reputation: 444
I have problem with storing data into Postgres using update_all
.
To explain the problem, we have 2 classes, Meter
and Readings
. Every meter has many readings. Meter has attributes unit
, like energy unit kWh, MWh, ..., and multiplier
, number that multiplies readings state to get final value.
When user wants to update Meter
params (unit
, multiplier
), we use Interactors
to firstly update Readings
states and then save Meter
itself.
All these operations happen in single transaction, so if one fails, then all fail.
But we came in situation, when meter is saved and readings are not updated or vice versa.
I checked, that when meter does not save correctly, it causes context.fail!
. Readings
uses update_all
without any check of success, but I read, that update_all
goes directly to the DB and when it fails on constraints, it fails with an exception.
I did not find any way, how to replicate it.
// update readings
class Meters::ChangeUnit
// includes
def call
coefficient = 1.0
coefficient *= unit_change if context.meter.energy_unit_changed?
coefficient *= multiplier_change if context.meter.multiplier_changed?
return if coefficient == 1.0
// this probably fails:
context.meter.readings.update_all "state = state * #{coefficient}"
end
// ...
end
// save meter
class Meters::Save
include Meters::BaseInteractor
def call
context.fail! meter_errors: context.meter.errors unless context.meter.save
end
end
My idea is to use something like this into Meters::ChangeUnit call
:
// ...
cnt = context.meter.readings.count
updated = context.meter.readings.update_all "state = state * #{coefficient}"
unless cnt == updated
context.fail! updated_meter_readings: "#{updated}/#{cnt}"
end
// ...
but I have no idea, how to prove it.
EDIT1:
// usage in cotroller
context = UpdateMeter.call(meter: @meter, bonds_definition: params[:meters_ids])
// UpdateMeter
class UpdateMeter
include Interactor::Organizer
organize Meters::Update, ProcessAfterCommitQueue
end
// Meters::Update
class Meters::Update
include Interactor::Organizer
include Interactor::InTransaction
organize Meters::ValidateActive,
// ...
Meters::ChangeUnit,
// ...
Meters::Save,
// ...
end
// Interactor::InTransaction
module Interactor::InTransaction
extend ActiveSupport::Concern
included do
around do |interactor|
ActiveRecord::Base.transaction { interactor.call }
end
end
end
Upvotes: 0
Views: 709
Reputation: 444
The problem was all about threads. We found out, that this problem occurred just several times within last few years. So it was really edge case.
There are two operations. One is mentioned meter params update by user, and the other is automatic import of readings from actual devices (physical meters). When import starts, it selects the unit and multiplier (action 1), modifies readings and saves them into the DB (action 2). But those 2 actions are not in the same transaction. So if user saved meter between those 2 actions, we would save wrong data, coz of updated unit or multiplier.
We resolved that using meter.reload
in transaction with readings.save
. We compare meter before reload and after reload, and if it has changed, we have to recalculate readings.
Upvotes: 0
Reputation: 2401
(comment can't format, so here it is in an answer)
I don't see a transaction. Without unwinding your code, I don't understand why this isn't just
Meter.transaction do
context.meter.save
interactor.call
end
Upvotes: 1