[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