-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add pg_xmin_optimistic_locking plugin for optimistic locking for all …
…models without database changes This implements similar support to the optimistic_locking plugin, but without the need for database changes to add a lock version column. Instead, it uses the xmin system column, which is automatically updated every time the row is updated. This allows you to load the plugin into a base class and have all models under the base class support optimistic locking. Internally, this splits off an optimistic_locking_base internal plugin from optimistic_locking, makes optimistic_locking and mssql_optimistic_locking use it, and uses it as the basis for the new pg_xmin_optimistic_locking plugin.
- Loading branch information
1 parent
d5f9d11
commit c280ff5
Showing
8 changed files
with
410 additions
and
80 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# frozen-string-literal: true | ||
|
||
module Sequel | ||
module Plugins | ||
# Base for other optimistic locking plugins | ||
module OptimisticLockingBase | ||
# Load the instance_filters plugin into the model. | ||
def self.apply(model) | ||
model.plugin :instance_filters | ||
end | ||
|
||
module ClassMethods | ||
# The column holding the version of the lock | ||
attr_accessor :lock_column | ||
|
||
Plugins.inherited_instance_variables(self, :@lock_column=>nil) | ||
end | ||
|
||
module InstanceMethods | ||
# Add the lock column instance filter to the object before destroying it. | ||
def before_destroy | ||
lock_column_instance_filter | ||
super | ||
end | ||
|
||
# Add the lock column instance filter to the object before updating it. | ||
def before_update | ||
lock_column_instance_filter | ||
super | ||
end | ||
|
||
private | ||
|
||
# Add the lock column instance filter to the object. | ||
def lock_column_instance_filter | ||
instance_filter(model.lock_column=>lock_column_instance_filter_value) | ||
end | ||
|
||
# Use the current value of the lock column | ||
def lock_column_instance_filter_value | ||
public_send(model.lock_column) | ||
end | ||
|
||
# Clear the instance filters when refreshing, so that attempting to | ||
# refresh after a failed save removes the previous lock column filter | ||
# (the new one will be added before updating). | ||
def _refresh(ds) | ||
clear_instance_filters | ||
super | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
# frozen-string-literal: true | ||
|
||
module Sequel | ||
module Plugins | ||
# This plugin implements optimistic locking mechanism on PostgreSQL based | ||
# on the xmin of the row. The xmin system column is automatically set to | ||
# the current transaction id whenever the row is inserted or updated: | ||
# | ||
# class Person < Sequel::Model | ||
# plugin :pg_xmin_optimistic_locking | ||
# end | ||
# p1 = Person[1] | ||
# p2 = Person[1] | ||
# p1.update(name: 'Jim') # works | ||
# p2.update(name: 'Bob') # raises Sequel::NoExistingObject | ||
# | ||
# The advantage of pg_xmin_optimistic_locking plugin compared to the | ||
# regular optimistic_locking plugin as that it does not require any | ||
# additional columns setup on the model. This allows it to be loaded | ||
# in the base model and have all subclasses automatically use | ||
# optimistic locking. The disadvantage is that testing can be | ||
# more difficult if you are modifying the underlying row between | ||
# when a model is retrieved and when it is saved. | ||
# | ||
# This plugin may not work with the class_table_inheritance plugin. | ||
# | ||
# This plugin relies on the instance_filters plugin. | ||
module PgXminOptimisticLocking | ||
WILDCARD = LiteralString.new('*').freeze | ||
|
||
# Define the xmin column accessor | ||
def self.apply(model) | ||
model.instance_exec do | ||
plugin(:optimistic_locking_base) | ||
@lock_column = :xmin | ||
def_column_accessor(:xmin) | ||
end | ||
end | ||
|
||
# Update the dataset to append the xmin column if it is usable | ||
# and there is a dataset for the model. | ||
def self.configure(model) | ||
model.instance_exec do | ||
set_dataset(@dataset) if @dataset | ||
end | ||
end | ||
|
||
module ClassMethods | ||
private | ||
|
||
# Ensure the dataset selects the xmin column if doing so | ||
def convert_input_dataset(ds) | ||
append_xmin_column_if_usable(super) | ||
end | ||
|
||
# If the xmin column is not already selected, and selecting it does not | ||
# raise an error, append it to the selections. | ||
def append_xmin_column_if_usable(ds) | ||
select = ds.opts[:select] | ||
|
||
unless select && select.include?(:xmin) | ||
xmin_ds = ds.select_append(:xmin) | ||
begin | ||
columns = xmin_ds.columns! | ||
rescue Sequel::DatabaseConnectionError, Sequel::DatabaseDisconnectError | ||
raise | ||
rescue Sequel::DatabaseError | ||
# ignore, could be view, subquery, table returning function, etc. | ||
else | ||
ds = xmin_ds if columns.include?(:xmin) | ||
end | ||
end | ||
|
||
ds | ||
end | ||
end | ||
|
||
module InstanceMethods | ||
private | ||
|
||
# Only set the lock column instance filter if there is an xmin value. | ||
def lock_column_instance_filter | ||
super if @values[:xmin] | ||
end | ||
|
||
# Include xmin value when inserting initial row | ||
def _insert_dataset | ||
super.returning(WILDCARD, :xmin) | ||
end | ||
|
||
# Remove the xmin from the columns to update. | ||
# PostgreSQL automatically updates the xmin value, and it cannot be assigned. | ||
def _save_update_all_columns_hash | ||
v = super | ||
v.delete(:xmin) | ||
v | ||
end | ||
|
||
# Add an RETURNING clause to fetch the updated xmin when updating the row. | ||
def _update_without_checking(columns) | ||
ds = _update_dataset | ||
rows = ds.clone(ds.send(:default_server_opts, :sql=>ds.returning(:xmin).update_sql(columns))).all | ||
values[:xmin] = rows.first[:xmin] unless rows.empty? | ||
rows.length | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.