最常见的Rails技巧之一是用数据库级别的约束来支持你的ActiveRecord模型验证。
因为有些时候验证会被跳过,最好让你的数据库成为维护数据完整性的最后一道防线。你可以在你的模型中添加一个validates :name, presence: true 行,但是如果空值潜入你的数据库,如果你在name.downcase 上调用nil ,你的应用程序仍然会抛出一个异常。
最常见的例子是将presence 验证与非空列配对,以及unique 验证与唯一的数据库索引配对。
但是你知道吗,你可以使用一个叫做 "检查约束 "的数据库功能,更进一步。
使用方法
当你试图将数据存储到一个列中时,检查约束就会运行。如果数据违反了约束条件,就会出现错误,Rails会回滚交易。
在Arrows,我们通过创建自定义计划来帮助客户入职。计划是基于模板的,我们最近增加了一种方法,可以将截止日期设置为计划创建后的一个固定天数。
我们把最后期限的偏移量作为一个integer ,存储在数据库中。但在我们的应用中,这个数字不应该是负数。当我们从模板中创建一个计划时,我们想写这样的代码:
deadline = Date.current + @template.deadline_offset
但由于数据库中的integer 类型可以是负数,我们没有保证偏移量会是正数。很明显,我们不希望最后期限永远在计划创建之前。
我们可以在模型层面验证这一点,但我们也希望数据库能强制执行这一检查:
class Template < ApplicationRecord
validates :deadline_offset, numericality: {
only_integer: true,
greater_than_or_equal_to: 0
}
end
从Rails 6.1开始,你可以直接从迁移中具体检查约束条件:
class AddDeadlineOffsetCheckToTemplates < ActiveRecord::Migration[7.0]
def change
add_check_constraint :templates, "deadline_offset >= 0",
name: "deadline_offset_non_negative"
end
end
你为约束传递一个name ,然后为检查运行一个SQL条件。
现在,如果你忘记了ActiveRecord模型的验证,或者不小心通过跳过回调绕过了验证,那么这个检查就会在数据库层面上强制执行:
# Live dangerously and skip validations!
=> template.deadline_offset = -12
=> template.save(validate: false)
PG::CheckViolation: ERROR: new row for relation "templates" violates check constraint "deadline_offset_non_negative" (ActiveRecord::StatementInvalid)
其他常见的例子
下面是一些你可能想使用检查约束的其他地方:
- 确保最低价格作为一种安全机制:检查
price > 100,以确保没有人意外地添加一个低于最低水平的产品 - 验证固定格式。储存一个美国邮编?确保
char_length(zipcode) = 5 - 强制执行两列之间的关系:添加一个约束条件,
start_date < end_date或sale_price <= price
你应该总是添加检查约束吗?我从来没有遇到过在数据完整性方面有额外保护的情况,我对此感到不高兴。如果你有关键任务的数据要求,我会强烈建议添加检查约束。
它们对每一件小事都是严格必要的吗?也许不是。但你可以权衡一下修复数据问题的麻烦和为你的应用程序中的每个验证添加约束的成本。