123456789_123456789_123456789_123456789_123456789_

Module: ActiveRecord::Locking::Pessimistic

Relationships & Source Files
Extension / Inclusion / Inheritance Descendants
Included In:
Defined in: activerecord/lib/active_record/locking/pessimistic.rb

Overview

Pessimistic Locking

Pessimistic provides support for row-level locking using SELECT … FOR UPDATE and other lock types.

Chain ActiveRecord::Base#find to QueryMethods#lock to obtain an exclusive lock on the selected rows:

# select * from accounts where id=1 for update
Account.lock.find(1)

Call lock('some locking clause') to use a database-specific locking clause of your own such as ‘LOCK IN SHARE MODE’ or ‘FOR UPDATE NOWAIT’. Example:

Account.transaction do
  # select * from accounts where name = 'shugo' limit 1 for update nowait
  shugo = Account.lock("FOR UPDATE NOWAIT").find_by(name: "shugo")
  yuko = Account.lock("FOR UPDATE NOWAIT").find_by(name: "yuko")
  shugo.balance -= 100
  shugo.save!
  yuko.balance += 100
  yuko.save!
end

You can also use ActiveRecord::Base#lock! method to lock one record by id. This may be better if you don’t need to lock every row. Example:

Account.transaction do
  # select * from accounts where ...
  accounts = Account.where(...)
  account1 = accounts.detect { || ... }
  account2 = accounts.detect { || ... }
  # select * from accounts where id=? for update
  account1.lock!
  account2.lock!
  account1.balance -= 100
  account1.save!
  account2.balance += 100
  account2.save!
end

You can start a transaction and acquire the lock in one go by calling #with_lock with a block. The block is called from within a transaction, the object is already locked. Example:

 = Account.first
.with_lock do
  # This block is called within a transaction,
  # account is already locked.
  .balance -= 100
  .save!
end

Database-specific information on row locking:

MySQL

dev.mysql.com/doc/refman/en/innodb-locking-reads.html

PostgreSQL

www.postgresql.org/docs/current/interactive/sql-select.html#SQL-FOR-UPDATE-SHARE

Instance Method Summary

Instance Method Details

#lock!(lock = true)

Obtain a row lock on this record. Reloads the record to obtain the requested lock. Pass an SQL locking clause to append the end of the SELECT statement or pass true for “FOR UPDATE” (the default, an exclusive row lock). Returns the locked record.

[ GitHub ]

  
# File 'activerecord/lib/active_record/locking/pessimistic.rb', line 69

def lock!(lock = true)
  if persisted?
    if has_changes_to_save?
      raise(<<-MSG.squish)
        Locking a record with unpersisted changes is not supported. Use
        `save` to persist the changes, or `reload` to discard them
        explicitly.
        Changed attributes: #{changed.map(&:inspect).join(', ')}.
      MSG
    end

    reload(lock: lock)
  end
  self
end

#with_lock(*args)

Wraps the passed block in a transaction, reloading the object with a lock before yielding. You can pass the SQL locking clause as an optional argument (see #lock!).

You can also pass options like requires_new:, isolation:, and joinable: to the wrapping transaction (see ConnectionAdapters::DatabaseStatements#transaction).

[ GitHub ]

  
# File 'activerecord/lib/active_record/locking/pessimistic.rb', line 92

def with_lock(*args)
  transaction_opts = args.extract_options!
  lock = args.present? ? args.first : true
  transaction(**transaction_opts) do
    lock!(lock)
    yield
  end
end