好吧,我有一个模型的简化版本(Rails 3.2.13):
class Transfer < ActiveRecord::Base
attr_accessible :from,:to,:total
validates_presence_of :from,:to,:total
before_validation :positive_numbers, on: :create
before_validation :check_enough_balance, on: :create
after_validation :update_balances
private
def positive_numbers
unless self.total>0
errors.add(:total,"should be greater than 0")
return false
end
end
def check_enough_balance
@sender=User.find(self.from)
@receiver=User.find(self.to)
unless @sender.enough_balance(self.total)
errors.add(:base,"Not enough credit")
return false
end
end
def update_balances
@sender.balance -= self.total
@receiver.balance += self.total
@sender.save
@receiver.save
end
def another_action
puts 'does something'
end
end
每当total<0
时,实例返回一个false
,errors
数组已正确填充,并且不调用another_action
回调。
我想知道如何使用Rails的内置验证助手来获得同样的行为,我就是这样尝试的:
class Transfer < ActiveRecord::Base
attr_accessible :from,:to,:total
validates_presence_of :from,:to,:total
validates_numericality_of :total, greater_than: 0
before_validation :check_enough_balance, on: :create
after_validation :update_balances
private
def check_enough_balance
@sender=User.find(self.from)
@receiver=User.find(self.to)
unless @sender.enough_balance(self.total)
errors.add(:base,"Not enough credit")
return false
end
end
def update_balances
@sender.balance -= self.total
@receiver.balance += self.total
@sender.save
@receiver.save
end
end
class User<ActiveRecord::Base
attr_accessible :username
validates_presence_of :username, :balance
def enough_balance(amount)
self.balance >= amount
end
end
然而,在这种情况下,由于验证助手不是return false
,因此调用了以下自定义验证check_enough_balance
,我希望它的行为完全相同,并且我相信使用验证助手在某种程度上更优雅和简洁。
before_validation
回调的字面意思是"在检查验证之前运行此方法"。如果您希望验证在another_action
之前运行,请考虑将其移到另一个回调。根据您的示例,我认为您可能想要after_validation
回调,但也有其他受支持的回调可能效果更好。
after_validation :another_action, on: :create
您可以在这里找到支持回调的完整列表:ActiveRecord::回调。
验证规则应该是独立的单元,无论你的其他规则做什么,它都是有意义的。通常,最好所有的验证规则都运行并收集所有合并的错误,这样用户就可以同时修复所有问题。
对于您的特定情况,检查用户是否可以覆盖总共0或更少的内容似乎非常好。这只是另一个独立于其他规则的验证规则。也就是说,考虑将其从回调转移到实际的验证器:
class Transfer < ActiveRecord::Base
...
validate :enough_balance
private
def enough_balance
unless User.find(self.from).enough_balance(self.total)
errors.add(:base, "Not enough credit")
end
end
end
如果由于某些原因(例如性能),您需要不执行该检查,那么通过将条件更改为以下内容,可以很容易地再次检查无效条件,而无需同时处理错误:
unless self.total <= 0 || User.find(self.from).enough_balance(self.total)
您可以在此处找到有关自定义验证器的更多信息:活动记录验证-自定义验证器
保存记录不应作为验证的一部分(包括回调之前和之后)。例如,如果我们只想手动检查记录是否有效,我们都不希望出现这种副作用:
transfer = Transfer.new(...)
if transfer.valid?
# Stuff gets saved?!?!
...
end
相反,使用before_save
和after_save
回调来更新相关记录。只有当ActiveRecord
决定可以保存记录时,这些回调才会运行。如果作为save
调用的一部分验证失败,则不会运行这些回调。
after_save :update_balances
此外,当以这种方式执行额外保存时,通常最好使用save!
方法而不是save
方法,并将所有内容封装在事务中。当显式检查返回值时,应使用save
。当您假设一切都处于您期望的状态时,应该使用save!
。save!
引发的异常可用于回滚作为该事务一部分所做的所有其他更改。
一些例子:
transfer = Transfer.new(...)
if transfer.save # Good!
...
if transfer.save! # Bad, causes an exception when you might expect false
...
transfer.save # Bad, can silently fail
transfer.save! # Good, raises an exception if it unexpectedly fails
# All changes will be rolled back if any of the `save!` calls raise exceptions
Transfer.transaction do
transfer.save!
something_else.save!
yet_another_thing.save!
end
您通常会将Transfer.transaction
呼叫放在transfer.save
或transfer.save!
呼叫周围的控制器中。
有关事务的更多信息可以在这里找到:ActiveRecord::transactions::ClassMethods