Module: AfterCommitEverywhere

Defined in:
lib/after_commit_everywhere.rb,
lib/after_commit_everywhere/wrap.rb,
lib/after_commit_everywhere/version.rb

Overview

Module allowing to use ActiveRecord transactional callbacks outside of ActiveRecord models, literally everywhere in your application.

Include it to your classes (e.g. your base service object class or whatever)

Defined Under Namespace

Classes: NotInTransaction, Wrap

Constant Summary collapse

RAISE =

Causes before_commit and after_commit to raise an exception when called outside a transaction.

:raise
EXECUTE =

Causes before_commit and after_commit to execute the given callback immediately when called outside a transaction.

:execute
WARN_AND_EXECUTE =

Causes before_commit and after_commit to log a warning before calling the given callback immediately when called outside a transaction.

:warn_and_execute
VERSION =
"1.6.0"

Class Method Summary collapse

Class Method Details

.after_commit(prepend: false, connection: nil, without_tx: EXECUTE, &callback) ⇒ Object

Runs callback after successful commit of outermost transaction for database connection.

Parameters:

  • connection (ActiveRecord::ConnectionAdapters::AbstractAdapter) (defaults to: nil)

    Database connection to operate in. Defaults to ActiveRecord::Base.connection

  • without_tx (Symbol) (defaults to: EXECUTE)

    Determines the behavior of this function when called without an open transaction.

    Must be one of: RAISE, EXECUTE, or WARN_AND_EXECUTE.

  • callback (#call)

    Callback to be executed

Returns:

  • void



41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/after_commit_everywhere.rb', line 41

def after_commit(
  prepend: false,
  connection: nil,
  without_tx: EXECUTE,
  &callback
)
  register_callback(
    prepend: prepend,
    connection: connection,
    name: __method__,
    callback: callback,
    without_tx: without_tx,
  )
end

.after_rollback(prepend: false, connection: nil, &callback) ⇒ Object

Runs callback after rolling back of transaction or savepoint (if declared in nested transaction) for database connection.

Caveat: do not raise ActivRecord::Rollback in nested transaction block! See api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions

Parameters:

  • connection (ActiveRecord::ConnectionAdapters::AbstractAdapter) (defaults to: nil)

    Database connection to operate in. Defaults to ActiveRecord::Base.connection

  • callback (#call)

    Callback to be executed

Returns:

  • void

Raises:



97
98
99
100
101
102
103
104
105
# File 'lib/after_commit_everywhere.rb', line 97

def after_rollback(prepend: false, connection: nil, &callback)
  register_callback(
    prepend: prepend,
    connection: connection,
    name: __method__,
    callback: callback,
    without_tx: RAISE,
  )
end

.before_commit(prepend: false, connection: nil, without_tx: WARN_AND_EXECUTE, &callback) ⇒ Object

Runs callback before committing of outermost transaction for connection.

Available only since Ruby on Rails 5.0. See github.com/rails/rails/pull/18936

Parameters:

  • connection (ActiveRecord::ConnectionAdapters::AbstractAdapter) (defaults to: nil)

    Database connection to operate in. Defaults to ActiveRecord::Base.connection

  • without_tx (Symbol) (defaults to: WARN_AND_EXECUTE)

    Determines the behavior of this function when called without an open transaction.

    Must be one of: RAISE, EXECUTE, or WARN_AND_EXECUTE.

  • callback (#call)

    Callback to be executed

Returns:

  • void



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/after_commit_everywhere.rb', line 68

def before_commit(
  prepend: false,
  connection: nil,
  without_tx: WARN_AND_EXECUTE,
  &callback
)
  if ActiveRecord::VERSION::MAJOR < 5
    raise NotImplementedError, "#{__method__} works only with Rails 5.0+"
  end

  register_callback(
    prepend: prepend,
    connection: connection,
    name: __method__,
    callback: callback,
    without_tx: without_tx,
  )
end

.in_transaction(connection = default_connection, requires_new: false, **new_tx_options) ⇒ Object

Makes sure the provided block runs in a transaction. If we are not currently in a transaction, a new transaction is started.

It mimics the ActiveRecord’s transaction method’s API and actually uses it under the hood.

However, the main difference is that it doesn’t swallow ActiveRecord::Rollback exception in case when there is no transaction open.

Parameters:

  • connection (ActiveRecord::ConnectionAdapters::AbstractAdapter) (defaults to: default_connection)

    Database connection to operate in. Defaults to ActiveRecord::Base.connection

  • requires_new (Boolean) (defaults to: false)

    Forces creation of new subtransaction (savepoint) even if transaction is already opened.

  • new_tx_options (Hash<Symbol, void>)

    Options to be passed to connection.transaction on new transaction creation

Returns:

  • void

See Also:



161
162
163
164
165
166
167
# File 'lib/after_commit_everywhere.rb', line 161

def in_transaction(connection = default_connection, requires_new: false, **new_tx_options)
  if in_transaction?(connection) && !requires_new
    yield
  else
    connection.transaction(requires_new: requires_new, **new_tx_options) { yield }
  end
end

.in_transaction?(connection = nil) ⇒ Boolean

Helper method to determine whether we’re currently in transaction or not

Returns:

  • (Boolean)


140
141
142
143
144
145
146
147
# File 'lib/after_commit_everywhere.rb', line 140

def in_transaction?(connection = nil)
  # Don't establish new connection if not connected: we apparently not in transaction
  return false unless connection || ActiveRecord::Base.connection_pool.active_connection?

  connection ||= default_connection
  # service transactions (tests and database_cleaner) are not joinable
  connection.transaction_open? && connection.current_transaction.joinable?
end

.register_callback(prepend:, connection: nil, name:, without_tx:, callback:) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Raises:

  • (ArgumentError)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/after_commit_everywhere.rb', line 108

def register_callback(prepend:, connection: nil, name:, without_tx:, callback:)
  raise ArgumentError, "Provide callback to #{name}" unless callback

  unless in_transaction?(connection)
    case without_tx
    when WARN_AND_EXECUTE
      warn "#{name}: No transaction open. Executing callback immediately."
      return callback.call
    when EXECUTE
      return callback.call
    when RAISE
      raise NotInTransaction, "#{name} is useless outside transaction"
    else
      raise ArgumentError, "Invalid \"without_tx\": \"#{without_tx}\""
    end
  end

  connection ||= default_connection
  wrap = Wrap.new(connection: connection, "#{name}": callback)

  if prepend
    # Hacking ActiveRecord's transaction internals to prepend our callback
    # See https://github.com/rails/rails/blob/f0d433bb46ac233ec7fd7fae48f458978908d905/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L148-L156
    records = connection.current_transaction.instance_variable_get(:@records)
    records = connection.current_transaction.instance_variable_set(:@records, []) if records.nil?
    records.unshift(wrap)
  else
    connection.add_transaction_record(wrap)
  end
end