DataMapperでOptimisticLock(楽観ロック)を利用する
dm-core-1.2.0の話です。
DataMapperでOptimisticLock(楽観ロック)を実現するプラグインはいくつか検索でヒットするのですが、古すぎて利用できなかったり、UPDATE文のWHERE句に条件が入るのではなく、プログラム側で判断しているためアトミック性がなかったりします。 ということでモンキーパッチです。*1
モンキーパッチ
module DataMapper class StaleObjectError < PersistenceError def initialize(query = nil) @query = query end def message if @query "Attempted to update a stale object: #{@query.model.name}" else "Attempted to update a stale object" end end end module Adapters # dm-do-adapter-1.2.0/lib/dm-do-adapter/adapter.rb DataObjectsAdapter.class_eval do DEFAULT_LOCKING_COLUMN = 'lock_version' alias_method :original_update, :update def update(attributes, collection) query = collection.query locking_property = query.model.properties(name)[locking_column.to_sym] if locking_property.blank? or collection.length != 1 return original_update(attributes, collection) end properties = [] bind_values = [] # make the order of the properties consistent query.model.properties(name).each do |property| next unless attributes.key?(property) properties << property bind_values << attributes[property] end # Add a condition to confirm whether a locking column is the same version. query = query.dup.update(locking_column.to_sym => collection.first.__send__(locking_column.to_sym)) properties << locking_property next_locking_column_value = collection.first.__send__(locking_column.to_sym) + 1 bind_values << next_locking_column_value statement, conditions_bind_values = update_statement(properties, query) bind_values.concat(conditions_bind_values) affected_rows = with_connection do |connection| connection.create_command(statement).execute_non_query(*bind_values) end.affected_rows unless affected_rows == 1 raise DataMapper::StaleObjectError.new(query) end collection.first.__send__("#{locking_column}=".to_sym, next_locking_column_value) affected_rows end def locking_column=(value) @original_locking_column = @locking_column if defined?(@locking_column) @locking_column = value.to_s end def locking_column reset_locking_column unless defined?(@locking_column) @locking_column end def reset_locking_column self.locking_column = DEFAULT_LOCKING_COLUMN end end end end
使い方
モデルにlock_versionというカラムを追加します。
class User include DataMapper::Resource property :id, Serial property :name, String property :lock_version, Integer, default: 0 end
モデルのlock_versionとDBのlock_versionの値と異なるとDataMapper::StaleObjectErrorが発生します。
a = User.create(name: 'troter') # INSERT INTO `users` (`name`, `lock_version`) VALUES ('troter', 0) # => #<User @id=1 @name="troter" @lock_version=0> a.name = 'TROTER' a.save # UPDATE `users` SET `name` = 'TROTER', `lock_version` = 1 WHERE (`id` = 1 AND `lock_version` = 0) a # => #<User @id=1 @name="TROTER" @lock_version=1> a.name = 'troter' a.lock_version = 0 a.save # UPDATE `users` SET `name` = 'troter', `lock_version` = 0, `lock_version` = 1 WHERE (`id` = 1 AND `lock_version` = 0) # DataMapper::StaleObjectError: DataMapper::StaleObjectError # from /home/troter/sandbox/datamapper-sandbox/project_name/config/initializers/data_mapper.rb:143:in `update' from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/repository.rb:180:in `update' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource/persistence_state/dirty.rb:54:in `update_resource' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource/persistence_state/dirty.rb:22:in `commit' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:956:in `_persist' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:987:in `block in update_with_hooks' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:984:in `catch' # from /home/troter/.rbenv/versions/1.9.3-p392/lib/ruby/gems/1.9.1/gems/dm-core-1.2.0/lib/dm-core/resource.rb:984:in `update_with_hooks' # ...省略
これで、安心して楽観ロックが利用できます。
*1:誰かgemにして。。