DataMapper を使う (Transactions)

今回は Transaction 機能について.

モデル定義

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 は何とかしてるぞ.
要調査.