[Boulder Ruby Group] rfc

ara.t.howard ara.t.howard at gmail.com
Wed Sep 19 19:16:25 EDT 2007



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){ returned = yield; nil }
             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 TransactionError, 'no transaction in effect' unless  
ActiveRecord::Transactions.are_active?

           name = nil
           finish = nil
           returned = nil
           begin
             name = begin_db_savepoint
             finish = catch(:savepoint){ returned = yield; nil }
             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 TransactionError, 'no transaction in effect' unless  
ActiveRecord::Transactions.are_active?
           ActiveRecord::Transactions.using :savepoint do
             transaction{ yield }
           end
         end

         def rollback! target = :transaction
           raise 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 TransactionError, 'no transaction in effect' unless  
ActiveRecord::Transactions.are_active?
           throw target, :commit
         end

         def commit! target = :transaction
           raise 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




a @ http://drawohara.com/
--
we can deny everything, except that we have the possibility of being  
better. simply reflect on that.
h.h. the 14th dalai lama





More information about the Bdrg-members mailing list