ruby-on-railsrubyrollback

How do I perform a rollback in multiple models?


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!


Solution

  • 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.

    Relevant docs

    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