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.