Transaction & Lock

Transaction

Transaction mechanism provides block-like syntax:

Jennifer::Adapter.default_adapter.transaction do |tx|
  Contact.create({:name => "Chose", :age => 20})
end

If any error was raised in block transaction will be rollbacked. To rollback transaction raise DB::Rollback exception.

Transaction lock connection for current fiber avoiding grepping new one from pool.

Lock

Row level

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

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

# select * from contacts where id=1 for update
Contact.all.lock.find(1)

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

Contact.transaction do
  # select * from contacts where ...
  contacts = Contact.where { _age > 15 }.to_a
  contact = contacts.find { |c| c.name =~ /John/ }.not_nil!
  contact.lock!
  contact.age += 10
  contact.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:

contact = Contact.first!
contact.with_lock do
  # This block is called within a transaction,
  # record is already locked.
  contact.age += 100
  contact.save!
end

Table level

To lock table use Jennifer::Adapter#with_table_lock method

Jennifer::Adapter.default_adapter("table_name") do
  # some operations here
end

Or performing directly on model class:

Contact.with_table_lock do
  # some operations here
end

But only postgres adapter supports the real table LOCK statement - mysql one just wraps a call to the transaction. This is caused by performing queries with prepared statements and mysql doesn’t allow lock table via it.

Database-specific information on row locking:

Optimistic locking

Jennifer.cr supports optimistic locking by using an Int32 or Int64 column acting as a lock. Each update to the record increments the lock column and the locking facilities ensure that records instantiated twice will let the last one saved raise a StaleObjectError if the first was also updated. Example:

p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.first_name = "should fail"
p2.save # Raises an Jennifer::StaleObjectError

StaleObjectError could also be raised when objects are destroyed.

p1 = Person.find(1)
p2 = Person.find(1)

p1.first_name = "Michael"
p1.save

p2.destroy # Raises an Jennifer::StaleObjectError

NOTE: Only #save and #destroy check for stale data. #update_columns and #delete bypass all checks.

To add optimistic locking to a model use macro method with_optimistic_lock in model class definition. By default, it uses column lock_version : Int32 as lock. The column used as lock must be type Int32 or Int64 and the default value set to 0. You can use a different column name as lock by passing the column name to the method.

class MyModel < Jennifer::Model::Base
  with_optimistic_lock

  mapping(
    id: {type: Int64, primary: true},
    lock_version: {type: Int32, default: 0},
  )
end

Or use a custom column name for the locking column:

class MyModel < Jennifer::Model::Base
  with_optimistic_lock :custom_lock

  mapping(
    id: {type: Int64, primary: true},
    custom_lock: {type: Int64, default: 0},
  )
end