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