Nested Transactions in Postgres with DataMapper
Hacks to get nested transactions support for Postgres in DataMapper. Not extensively tested, more a proof of concept. It re-opens the existing Transaction class to add a check for whether we need a nested transaction or not, and adds a new NestedTransaction transaction primitive that issues savepoint commands rather than begin/commit.
I put this code in a Rails initializer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# Hacks to get nested transactions in Postgres # Not extensively tested, more a proof of concept # # It re-opens the existing Transaction class to add a check for whether # we need a nested transaction or not, and adds a new NestedTransaction # transaction primitive that issues savepoint commands rather than begin/commit. module DataMapper module Resource def transaction(&block) self.class.transaction(&block) end end class Transaction # Overridden to allow nested transactions def connect_adapter(adapter) if @transaction_primitives.key?(adapter) raise "Already a primitive for adapter #{adapter}" end primitive = if adapter.current_transaction adapter.nested_transaction_primitive else adapter.transaction_primitive end @transaction_primitives[adapter] = validate_primitive(primitive) end end module NestedTransactions def nested_transaction_primitive DataObjects::NestedTransaction.create_for_uri(normalized_uri, current_connection) end end class NestedTransactionConfig < Rails::Railtie config.after_initialize do repository.adapter.extend(DataMapper::NestedTransactions) end end end module DataObjects class NestedTransaction < Transaction # The host name. Note, this relies on the host name being configured # and resolvable using DNS HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost" @@counter = 0 # The connection object for this transaction - must have already had # a transaction begun on it attr_reader :connection # A unique ID for this transaction attr_reader :id def self.create_for_uri(uri, connection) uri = uri.is_a?(String) ? URI::parse(uri) : uri DataObjects::NestedTransaction.new(uri, connection) end # # Creates a NestedTransaction bound to an existing connection # def initialize(uri, connection) @connection = connection @id = Digest::SHA256.hexdigest( "#{HOST}:#{$$}:#{Time.now.to_f}:nested:#{@@counter += 1}") end def close end def begin run %{SAVEPOINT "#{@id}"} end def commit run %{RELEASE SAVEPOINT "#{@id}"} end def rollback run %{ROLLBACK TO SAVEPOINT "#{@id}"} end private def run(cmd) connection.create_command(cmd).execute_non_query end end end |
I wrote code similar to this with hassox while at NZX, big ups to those guys. I’m working on a proper patch, but haven’t quite figured out the internals enough. If you know how DataMapper works, please check out and comment on this sample patch for three dm gems.
October 09, 2010 at 7:30 AM
Good stuff. Do you think it would be hard to incorporate this code in some shape into Postgres data_objects driver or DataMapper adapter?
October 10, 2010 at 4:20 AM
Marcin, see the patch I link to in the last sentence - I think that is what is required, though I'm not 100% sure. Trying to get feedback on it.
March 03, 2011 at 6:52 PM
Just did this for dm-1.0.2 in a Rails3/mysql project with multiple adapters (code for that not shown). Xavier's NestedTransaction class is now natively in data_objects as SavePoint, so that part of the patch isn't needed anymore, but Transaction has a state check, so in connect_adapter I forced the state back to :none.
module DataMapper class Transaction module DataObjectsAdapter def nested_transaction_primitive DataObjects::SavePoint.create_for_uri(normalized_uri, current_connection) end end # Override to use savepoints for nested transactions def connect_adapter(adapter) if @transaction_primitives.key?(adapter) raise "Already a primitive for adapter #{adapter}" end primitive = if adapter.current_transaction self.state = :none adapter.nested_transaction_primitive else adapter.transaction_primitive end @transaction_primitives[adapter] = validate_primitive(primitive) end end end