今回は Transaction 機能について.
- 公式サイト
- 環境
- dm-core-0.9.11
モデル定義
class Account include DataMapper::Resource property :id, Serial property :name, String, :nullable => false property :amount, Integer, :default => 0 # ↑本来なら BigDecimal validates_with_method :amount, :method => :check_amount def check_amount if 0 <= self.amount then true else [false, "can't specify the value of under 0 to :amount."] end end def withdraw(value) add_to_amount(-value) end def deposit(value) add_to_amount(+value) end class ValidationError < StandardError def initialize(message) super end end private def add_to_amount(value) self.amount += value # もっと普通に例外投げられなかったけか??? raise ValidationError.new(self.errors[:amount]) if not self.save end end
transaction を利用しない場合
以下のコードでは transaction が保証されない.
krdlab = Account.first_or_create(:name => 'krdlab', :amount => 100) hatena = Account.first_or_create(:name => 'hatena', :amount => 1000) # なんだか作為的な順序だが気にしない def transfer(from, to, value) to.deposit(value) from.withdraw(value) end transfer(krdlab, hatena, 200)
レコードの値は以下の通り.
mysql> select * from accounts; +----+--------+--------+ | id | name | amount | +----+--------+--------+ | 1 | krdlab | 100 | ← 初期値: 100 | 2 | hatena | 1200 | ← 初期値: 1000 +----+--------+--------+
最初の deposit は成功するので 1000 + 200 =1200 となる.
2番目の withdraw は save 時の validation で引っかかるため,変更されない.
ちなみに,モデルの値は変更されたままになる.
#<Account id=1 name="krdlab" amount=-100> #<Account id=2 name="hatena" amount=1200>
transaction を利用する場合
このままでは非常にやっかいであるため transaction 機能を利用する.
以下の 3 通りの方法がある (分類としては 2 通りだが).
# Account のメソッドとして定義 class Account def self.transfer(from, to, value) transaction do |tr| to.deposit(value) from.withdraw(value) end end end # Account のメソッドにしない方法 def transfer(from, to, value) tr = DataMapper::Transaction.new(from, to) # ↓ エラーの時は rollback される tr.commit do to.deposit(value) from.withdraw(value) end end # 一番面倒な方法 (Transaction#commit の中身そのまま) def transfer(from, to, value) tr = DataMapper::Transaction.new(from, to) begin tr.begin begin DataMapper.repository(:default).adapter.push_transaction(tr) # 実処理 to.deposit(value) from.withdraw(value) ensure DataMapper.repository(:default).adapter.pop_transaction end tr.commit rescue => e tr.rollback raise e end end
エラーになってもテーブルの値は維持される.
+----+--------+--------+ | id | name | amount | +----+--------+--------+ | 1 | krdlab | 100 | ← 初期値: 100 | 2 | hatena | 1000 | ← 初期値: 1000 +----+--------+--------+
ただし,モデルの値は変化したままになる.
何とかならないのか?
ActiveRecord は何とかしてるぞ.
要調査.