I have this class in rails
class ServiceClass < ApplicationService
attr_reader :resource
def initialize(resource)
@resource = resource
end
def call
ActiveRecord::Base.transaction do
record = Record.new(
resource: resource,
code: SecureRandom.hex(8),
created_at: Time.current
)
unless record.save
return Outcome.new(success: false, errors: record.errors.full_messages)
end
increment_count
Outcome.new(success: true, result: record)
rescue ActiveRecord::Rollback => e
Outcome.new(success: false, errors: [ e.message ])
end
end
private
def increment_count
begin
resource.increment!(:occupied_count)
rescue ActiveRecord::ActiveRecordError => e
raise ActiveRecord::Rollback, e.message
end
end
end
I want to make the whole transaction fail when record.save
or increment_count
fails, meaning that no record will be saved and the increment will not be applied. However, I failed to do so. In this case, for example, when increment_count fails, the record is already saved.
Is there a clean Rails way to handle this? The alternative would be to have a reset method that deletes the record and/or reverts the count manually, but that seems off.
Please let me know your thoughts. Thanks!
A rollback is triggered when any error is raised in the transaction
block. For most errors, the error is then propagated further. For ActiveRecord::Rollback
specifically, the rollback is still made, but the error is automatically caught (rescued) and not propagated.
In your case you are rescuing the error yourself, meaning that the transaction never sees the error and a rollback is not triggered.
Just let it fail.
As for how to specifically handle it in your code, I suggest using save!
instead of save
and rescue the error outside the transaction
block. Then you can return whatever you need after the transaction has already been rolled back.
class ServiceClass < ApplicationService
attr_reader :resource
def initialize(resource)
@resource = resource
end
def call
ActiveRecord::Base.transaction do
record = Record.new(
resource: resource,
code: SecureRandom.hex(8),
created_at: Time.current
)
record.save!
resource.increment!(:occupied_count)
end
Outcome.new(success: true, result: record)
rescue ActiveRecord::ActiveRecordError => e
Outcome.new(success: false, errors: [ e.message ])
end
end