Reputation: 3945
Sometime one in a long series of events within a controller action fails. For example, a credit card is processed but then an ActiveRecord query times out. Is there any way to make those calls reversible?
E.g. with this controller action:
def process_order
cart = Cart.new(params[:cart])
load_order
response = credit_card.charge
if response
submit_order
order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
order.receipt.pdf.generate
order.receipt.save
render :action => 'finished'
else
order.transaction = response
@message = order.transaction.message
order.transaction.save
render :action => 'charge_failed'
end
end
I would like to be able to put a block around it like so:
def process_order
transaction
cart = Cart.new(params[:cart])
load_order
response = credit_card.charge
if response
submit_order
order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
order.receipt.pdf.generate
order.receipt.save
render :action => 'finished'
else
order.transaction = response
@message = order.transaction.message
order.transaction.save
render :action => 'charge_failed'
end
rollback
credit_card.cancel_charge
...
end
end
This is just a contrived example and I'm not really sure how it would work in practice. What typically happens is we get an exception like ActiveRecord::StatementInvalid: : execution expired
for the line with submit_order
and then we have to go and manually run the rest of the lines that should have run.
Upvotes: 1
Views: 1487
Reputation: 2439
I'm a bit late but I think you should use save!
instead of save
. save just returns false if something fails within your model but save! raises an exception and your ActiveRecord::Base.transaction do
block rolls back your changes correctly...
For example:
def process_order
ActiveRecord::Base.transaction do
begin
cart = Cart.new(params[:cart])
load_order
response = credit_card.charge
if response
submit_order
order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
order.receipt.pdf.generate
order.receipt.save!
render :action => 'finished'
else
order.transaction = response
@message = order.transaction.message
order.transaction.save!
render :action => 'charge_failed'
end
rescue
# Exception raised ... ROLLBACK
raise ActiveRecord::Rollback
end
end
Upvotes: 1
Reputation: 20912
Here's a generic solution.
class Transactable
def initialize(&block)
raise LocalJumpError unless block_given?
@block = block
end
def on_rollback(&block)
raise LocalJumpError unless block_given?
@rollback = block
self
end
def call
@block.call
end
def rollback
@rollback.call if @rollback
end
end
class Transaction
def initialize(tasks)
tasks = Array(tasks)
tasks.each do |t|
Transactable === t or raise TypeError
end
@tasks = tasks
end
def run
finished_tasks = []
begin
@tasks.each do |t|
t.call
finished_tasks << t
end
rescue => err
finished_tasks.each do |t|
t.rollback
end
raise err
end
end
end
if __FILE__ == $0
Transaction.new([
Transactable.new { puts "1: call" }.on_rollback { puts "1: rollback" },
Transactable.new { puts "2: call" }.on_rollback { puts "2: rollback" },
Transactable.new { puts "3: call"; raise "fail!" }.on_rollback { puts "3: rollback" },
]).run
end
Note that it doesn't:
Upvotes: 3
Reputation: 30434
Just wrap it in
cart.transaction do
# ...
end
to use transactions. For details see http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
Upvotes: 1