0

我有这堂课:

class Payment < ActiveRecord::Base

  attr_accessible :amount, :invoice_id

  belongs_to :invoice

  validates :amount, :numericality => { :greater_than => 0, :less_than_or_equal_to => :maximum_amount }

  after_save    :update_amount_payable
  after_destroy :update_amount_payable

  private

  def maximum_amount
    invoice.amount_payable
  end

  def update_amount_payable
    invoice.update_amount_payable
  end

end

class Invoice < ActiveRecord::Base

  has_many :payments

  after_save :update_amount_payable

  def update_amount_payable
    update_column(:amount_payable_in_cents, new_amount_payable)
  end

  private

  def new_amount_payable
    (total - payments.map(&:amount).sum) * 100
  end

end

上面的代码有效。但是,我如何验证任何付款金额都不会invoice.amount_payable低于0

尤其是当可以为同一张发票进行多次付款时,这会变得很棘手。

几个小时以来,我一直试图解决这个问题,但无济于事。也许after可以在这里使用回滚数据库的回调?

谢谢你的帮助。

4

2 回答 2

1

一种可行的跨数据库解决方案是使用乐观锁定。本质上,它需要一个特殊的lock_version列,每当进行更新时都会对其进行检查。如果lock_version调用 UPDATE 的时间与模型所期望的不同,则会引发错误,指出此模型之外的某些内容导致记录更改(从而使更新无效)。ActiveRecord 开箱即用地支持这一点,如果您不介意完全阻止并发事务,它可能足以满足您的需求。

它不起作用的情况是您希望允许并发更新。在这种情况下,您需要在更新期间手动检查结果:

def update_amount_payable
  new_value = new_amount_payable
  raise "Payment amounts can't be greater than total invoice amount" if new_value < 0
  count = Invoice.where(id: id, amount_payable_in_cents: amount_payable_in_cents).
                  update_all(amount_payable_in_cents: new_value)
  raise ActiveRecord::StaleObjectError.new(self, 'update amount_payable_in_cents') if count != 1
end

private

def new_amount_payable
  (total - payments.sum(:amount)) * 100  # get the amount sum from the database
end
于 2013-05-13T19:14:17.163 回答
0

我会更改字段名称。但是鉴于当前的数据库模式,请尝试以下代码:

应用程序/模型/发票.rb

class Invoice < ActiveRecord::Base
  has_many :payments

  def still_open_amount
    self.amount_payable_in_cents - self.payments.sum('amount_in_cents')
  end
end

应用程序/模型/payment.rb

class Payment < ActiveRecord::Base
  belongs_to :invoice

  validates :amount_in_cents, :numericality => { :greater_than => 0 }

  before_validation :check_all_payments

  private
  def check_all_payments
    if self.new_record?
      if (self.invoice.payments.sum('amount_in_cents') + self.amount_in_cents) > self.invoice.amount_payable_in_cents
        errors.add(:amount, 'the invoice would be overpaid')
      end
    else
      if (self.invoice.payments.sum('amount_in_cents') - self.amount_in_cents_was + self.amount_in_cents) > self.invoice.amount_payable_in_cents
        errors.add(:amount, 'the invoice would be overpaid')
      end
    end
  end
end

如果您尝试创建多付的付款,这将通过验证错误:

~/Desktop/testapp  ᐅ rails c
Loading development environment (Rails 4.0.0.beta1)
1.9.3-p286 :001 > i = Invoice.create(amount_payable_in_cents: 100)
   (0.1ms)  begin transaction
  SQL (6.8ms)  INSERT INTO "invoices" ("amount_payable_in_cents", "created_at", "updated_at") VALUES (?, ?, ?)  [["amount_payable_in_cents", 100], ["created_at", Mon, 13 May 2013 19:23:24 UTC +00:00], ["updated_at", Mon, 13 May 2013 19:23:24 UTC +00:00]]
   (0.8ms)  commit transaction
 => #<Invoice id: 1, amount_payable_in_cents: 100, created_at: "2013-05-13 19:23:24", updated_at: "2013-05-13 19:23:24"> 
1.9.3-p286 :003 > p1 = i.payments.create(amount_in_cents: 90)
   (0.1ms)  begin transaction
  Invoice Load (0.2ms)  SELECT "invoices".* FROM "invoices" WHERE "invoices"."id" = ? ORDER BY "invoices"."id" ASC LIMIT 1  [["id", 1]]
   (0.2ms)  SELECT SUM("payments"."amount_in_cents") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = ?  [["invoice_id", 1]]
  SQL (0.4ms)  INSERT INTO "payments" ("amount_in_cents", "created_at", "invoice_id", "updated_at") VALUES (?, ?, ?, ?)  [["amount_in_cents", 90], ["created_at", Mon, 13 May 2013 19:24:10 UTC +00:00], ["invoice_id", 1], ["updated_at", Mon, 13 May 2013 19:24:10 UTC +00:00]]
   (1.0ms)  commit transaction
 => #<Payment id: 1, invoice_id: 1, amount_in_cents: 90, created_at: "2013-05-13 19:24:10", updated_at: "2013-05-13 19:24:10"> 
1.9.3-p286 :004 > p2 = i.payments.create(amount_in_cents: 20)
   (0.1ms)  begin transaction
  Invoice Load (0.2ms)  SELECT "invoices".* FROM "invoices" WHERE "invoices"."id" = ? ORDER BY "invoices"."id" ASC LIMIT 1  [["id", 1]]
   (0.1ms)  SELECT SUM("payments"."amount_in_cents") AS sum_id FROM "payments" WHERE "payments"."invoice_id" = ?  [["invoice_id", 1]]
   (0.1ms)  commit transaction
 => #<Payment id: nil, invoice_id: 1, amount_in_cents: 20, created_at: nil, updated_at: nil> 
1.9.3-p286 :005 > p2.errors
 => #<ActiveModel::Errors:0x007fd57b8e36d8 @base=#<Payment id: nil, invoice_id: 1, amount_in_cents: 20, created_at: nil, updated_at: nil>, @messages={:amount=>["the invoice would be overpaid"]}> 
1.9.3-p286 :006 >
于 2013-05-13T19:29:41.477 回答