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.3.1"

Class Method Summary collapse

Class Method Details

.after_commit(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
# File 'lib/after_commit_everywhere.rb', line 41

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

.after_rollback(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:



93
94
95
96
97
98
99
100
# File 'lib/after_commit_everywhere.rb', line 93

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

.before_commit(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



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

def before_commit(
  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(
    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:



147
148
149
150
151
152
153
# File 'lib/after_commit_everywhere.rb', line 147

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)


126
127
128
129
130
131
132
133
# File 'lib/after_commit_everywhere.rb', line 126

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(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)


103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/after_commit_everywhere.rb', line 103

def register_callback(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)
  connection.add_transaction_record(wrap)
end