Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ensuring a transaction within a method with ActiveRecord

I have a situation in which I would like a method to work within a transaction, but only if a transaction has not already been started. Here's a contrived example to distill what I'm talking about:

class ConductBusinessLogic
  def initialize(params)
    @params = params
  end

  def process!
    ActiveRecord::Base.transaction do
      ModelA.create_multiple(params[:model_a])
      ModelB.create_multiple(params[:model_a])
    end
  end
end

class ModelA < ActiveRecord::Base
  def self.create_multiple(params)
    # I'd like the below to be more like "ensure_transaction"
    ActiveRecord::Base.transaction do
      params.each { |p| create(p) }
    end
  end
end

class ModelB < ActiveRecord::Base
  def self.create_multiple(params)
    # Again, a transaction here is only necessary if one has not already been started
    ActiveRecord::Base.transaction do
      params.each { |p| create(p) }
    end
  end
end

Basically, I don't want these to act as nested transactions. I want the .create_multiple methods to only start transactions if they are not already called within a transaction, such as through ConductBusinessLogic#process!. If the model methods are called by themselves, they should start their own transaction, but if they are already being called inside a transaction, as through ConductBusinessLogic#process!, they should not nest a sub-transaction.

I don't know of a way in which Rails provides this out of the box. If I run the above code as-is and a rollback is triggered by one of the model methods, the whole transaction will still go through because the sub-transaction swallows the ActiveRecord::Rollback exception. If I use the requires_new option on the sub-transactions, savepoints will be used to simulate nested transactions, and only that sub-transaction will actually be rolled back. The behavior I would like would be something to the effect of ActiveRecord::Base.ensure_transaction, such that a new transaction is started only if there isn't already an outer transaction, so that any sub-transaction can trigger a rollback on the entire outer transaction. This would allow these methods to be transactional on their own, but defer to a parent transaction if there is one.

Is there a built-in way to achieve this behavior, and if not, is there a gem or patch that will work?

like image 463
Chris Vincent Avatar asked Sep 29 '13 09:09

Chris Vincent


People also ask

How does ActiveRecord transaction work?

Transactions in ActiveRecordEvery database operation that happens inside that block will be sent to the database as a transaction. If any kind of unhandled error happens inside the block, the transaction will be aborted, and no changes will be made to the DB.

What is database transactions and how it is represented in rails?

Rails transactions are a way to ensure that a set of database operations will only occur if all of them succeed. Otherwise they will rollback to the previous state of data.


1 Answers

How about just adding a create_multiple_without_transaction method to your ModelA and ModelB classes? which would look something like this:

class ConductBusinessLogic
  def initialize(params)
    @params = params
  end

  def process!
    ActiveRecord::Base.transaction do
      ModelA.create_multiple_without_transaction(params[:model_a])
      ModelB.create_multiple_without_transaction(params[:model_a])
    end
  end
end

class ModelA < ActiveRecord::Base
  def self.create_multiple(params)
    # I'd like the below to be more like "ensure_transaction"
    ActiveRecord::Base.transaction do
      self.create_multiple_without_transaction(params)
    end
  end

  def self.create_multiple_without_transaction(params)
    params.each { |p| create(p) }
  end
end

then your regular create_multiple would work as before, but for cases were you don't need a transaction you would just call create_multiple_without_transaction

like image 71
Wolfgang Avatar answered Oct 06 '22 19:10

Wolfgang