如何validation付款永远不会导致发票金额小于零?

我有这堂课:

class Payment  { :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

特别是当同一张发票的多次付款可能时,这就变得棘手了。

我一直试图让我的头围绕这几个小时,但无济于事。 也许回滚数据库的回调可以在这里使用?

谢谢你的帮助。

一个可行的跨数据库解决方案是使用乐观锁定 。 从本质上讲,它需要一个特殊的lock_version列,每当进行更新时都会检查该列。 如果调用UPDATE时的lock_version与模型所期望的不同,则会抛出错误,指出此模型之外的某些内容导致记录发生更改(从而使更新无效)。 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 

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

应用程序/模型/ invoice.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 

如果您尝试创建超额付款,则会出现validation错误:

 ~/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 => # 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 => # 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 => # 1.9.3-p286 :005 > p2.errors => #, @messages={:amount=>["the invoice would be overpaid"]}> 1.9.3-p286 :006 >