unless defined? SaneTransactions <<-README NAME sane_transactions SYNOPSIS the standard activerecord transactions are not sane. consider the following ActiveRecord::Base.connection.transaction do Model.create! raise end in this case the Model creation is NOT rolled back. the reason can be summarized thusly: ActiveRecord never starts a transaction when one is open, but commiting a transaction always occurs. this means that, no matter how hard you try, one errant save|destroy|create from deep in the bowels of a plugin will unravel your carefully laid plans - throwing data integrity out the window, along with part of your salary. for the most part people simply ignore this fact in rails, and live with the possibility of inconsistent data after a failed complex controller action. sane_transactions address this issue, and more, in a small, furry, rails munging way. USAGE * POLS transactions require 'sane_transactions' ActiveRecord::Base.connection.transaction do Model.create! raise end # Model creation is rolled back * nested transactions work, only the top level transaction executes sql require 'sane_transactions' ActiveRecord::Base.connection.transaction do Model.create! ActiveRecord::Base.connection.transaction do ActiveRecord::Base.connection.transaction do ActiveRecord::Base.connection.transaction do raise end end end end # Model creation is rolled back * shortcuts are given, db code is important so certain methods are available at the top level require 'sane_transactions' transaction do Model.create! raise end # ahhhhhh * throw/catch based switches enable you to bail out and commit or rollback a transaction early and from any depth require 'sane_transactions' transaction do Model.create! rollback! end # Model creation is rolled back * a commit switch is provided too require 'sane_transactions' transaction do a = Model.create! commit! b = Model.create! end # a IS created, b IS NOT * savepoints are implemented, and can be individually rolled back require 'sane_transactions' transaction do savepoint do a = Model.create! rollback! :savepoint end b = Model.create! end # a IS NOT created, b IS INSTALL - put this file into RAILS_ROOT/lib - add "require 'sane_transactions'" to RAILS_ROOT/environment.rb DISCLAIMER this is pre-pre-pre-pre-alpha code AUTHOR ara.t.howard [[at]] gmail [[dot]] com README class Object def transaction *argv, &block ActiveRecord::Base.connection.transaction *argv, &block end def transaction? *argv, &block ActiveRecord::Base.connection.transaction? *argv, &block end def savepoint *argv, &block ActiveRecord::Base.connection.savepoint *argv, &block end def rollback! *argv, &block ActiveRecord::Base.connection.rollback! *argv, &block end def commit! *argv, &block ActiveRecord::Base.connection.commit! *argv, &block end end class Module def thread_safe_attribute name, options = {} options.to_options! prefix = options[:prefix] prefix = "#{ prefix.name.underscore }_" if Module === prefix key = "#{ prefix }#{ name }".inspect code = <<-code def #{ name } *argv, &block if argv.empty? Thread.current[#{ key }] else if block value = Thread.current[#{ key }] begin Thread.current[#{ key }] = argv.first block.call ensure Thread.current[#{ key }] = value end else Thread.current[#{ key }] = argv.first end end end def #{ name }= value #{ name } value end def #{ name }? #{ name } end code module_eval code end end module ActiveRecord module Transactions class Savepoint < ::String class << self thread_safe_attribute 'initial', :prefix => ActiveRecord::Transactions::Savepoint end def initialize initial = self.class.initial reset! initial end def next succ! end def reset! initial = self.class.initial replace initial end initial "rails_savepoint_0" end class << self thread_safe_attribute 'using', :prefix => ActiveRecord::Transactions thread_safe_attribute 'are_active', :prefix => ActiveRecord::Transactions thread_safe_attribute 'savepoint', :prefix => ActiveRecord::Transactions end using :transaction are_active false savepoint Savepoint.new end module ConnectionAdapters module DatabaseStatements def transaction *argv, &block method = "transaction_using_#{ ActiveRecord::Transactions.using }" send method, *argv, &block end def transaction? ActiveRecord::Transactions.are_active? end def transaction_using_transaction *ignored return yield if ActiveRecord::Transactions.are_active? finish = nil returned = nil begin ActiveRecord::Transactions.are_active true ActiveRecord::Transactions.savepoint.reset! begin_db_transaction finish = catch(:transaction) do returned = yield ActiveRecord::Base.connection nil end returned ensure ActiveRecord::Transactions.are_active false if finish finish == :rollback ? rollback_db_transaction : commit_db_transaction else $! ? rollback_db_transaction : commit_db_transaction end end end def transaction_using_savepoint *ignored raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless ActiveRecord::Transactions.are_active? name = nil finish = nil returned = nil begin name = begin_db_savepoint finish = catch(:savepoint) do returned = yield ActiveRecord::Base.connection nil end returned ensure if finish rollback_db_savepoint name if finish == :rollback and name else rollback_db_savepoint name if $! and name end end end def savepoint raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless ActiveRecord::Transactions.are_active? ActiveRecord::Transactions.using :savepoint do transaction{ yield } end end def rollback! target = :transaction raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless ActiveRecord::Transactions.are_active? throw target, :rollback end def rollback_savepoint! rollback! :savepoint end def commit! target = :transaction raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless ActiveRecord::Transactions.are_active? throw target, :commit end def commit! target = :transaction raise ActiveRecord::Transactions::TransactionError, 'no transaction in effect' unless ActiveRecord::Transactions.are_active? throw target, :commit end def begin_db_savepoint name = ActiveRecord::Transactions.savepoint.next execute "SAVEPOINT #{ name }" name end def rollback_db_savepoint name execute "ROLLBACK TO SAVEPOINT #{ name }" name end def release_db_savepoint name execute "RELEASE SAVEPOINT #{ name }" name end end end end SaneTransactions = 42 end