验证规则
本指南涵盖 Lucid 添加到 VineJS 的验证规则。你将学会如何:
- 验证某个值在数据库表中是唯一的
- 验证某个值在数据库表中已存在
- 在更新时从唯一性检查中排除当前行
- 通过选项或回调自定义查询
- 覆盖默认错误消息
概述
Lucid 使用两个数据库感知的验证规则扩展了 VineJS:unique 和 exists。这两个规则都是 VineString 和 VineNumber 上的宏,在 Lucid 的服务提供者启动时自动注册,因此你可以将它们链接到 schema 中的任何字符串或数字字段上。
import vine from '@vinejs/vine'
export const createUserValidator = vine.create({
email: vine.string().email().unique({ table: 'users', column: 'email' }),
referralCode: vine.string().exists({ table: 'referrals', column: 'code' }),
})
两个规则都支持两种调用形式。options 对象描述表、列和任何额外的约束,Lucid 为你运行查询。回调给你数据库服务,让你自己编写查询,这在 options 形式无法覆盖你的需求时很有用。
每个规则对每个验证值都针对数据库运行一个单独的查询,因此验证多个此类字段的请求会运行多个查询。参见行为与性能说明了解其影响。
unique 规则
当数据库还没有包含匹配该值的行时,unique 规则通过,当包含时报告验证错误。用于必须在表中唯一的字段,如电子邮件地址或 slug。
const createUserValidator = vine.create({
email: vine.string().email().unique({ table: 'users', column: 'email' }),
})
上面的 options 形式在规则执行时发出以下 SQL:
SELECT "email" FROM "users" WHERE "email" = ? LIMIT 1
当 SELECT 返回一行时,规则报告 "{{ field }} 已被占用"。当没有返回行时,验证通过。
exists 规则
exists 规则是相反的:当数据库确实包含匹配行时通过,当不包含时报告错误。用于验证对现有记录的引用,如表单中提交的分类 slug 或外键。
const subscribeValidator = vine.create({
plan: vine.string().exists({ table: 'plans', column: 'slug' }),
})
SQL 在结构上与 unique 相同:
SELECT "slug" FROM "plans" WHERE "slug" = ? LIMIT 1
当 SELECT 没有返回行时,规则失败,消息为 "所选的 {{ field }} 无效"。
更新时排除当前行
unique 的纯 options 形式对于创建请求是正确的,但对于更新是错误的。当用户编辑他们的个人资料并提交他们已经拥有的相同电子邮件时,规则会找到他们自己的行并报告电子邮件已被占用。解决方法是 filter 选项,结合 VineJS 元数据,它允许你从唯一性检查中排除当前行。
定义一个带有元数据类型的验证器,以便在验证时接受当前用户的 ID。
import vine from '@vinejs/vine'
export const updateUserValidator = vine
.withMetaData<{ userId: number }>()
.create({
email: vine.string().email().unique({
table: 'users',
column: 'email',
filter: (db, _value, field) => {
db.whereNot('id', field.meta.userId)
},
}),
})
从控制器调用验证器时,将用户的 ID 作为元数据传递。
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
import { updateUserValidator } from '#validators/users_validator'
export default class UsersController {
async update({ params, request }: HttpContext) {
const user = await User.findOrFail(params.id)
const payload = await updateUserValidator.validate(request.all(), {
meta: { userId: user.id },
})
user.merge(payload)
await user.save()
return user
}
}
filter 回调接收底层查询构建器并就地修改它。发出的 SQL 变为:
SELECT "email" FROM "users" WHERE "email" = ? AND "id" <> ? LIMIT 1
用户自己的行现在被排除,因此规则仅在电子邮件属于其他人时失败。
选项参考
unique 和 exists 都接受相同的 options 对象。
-
table
-
要查询的数据库表。必需。
-
column
-
要比较值的列。省略时,规则使用 schema 中的字段名,如果你的字段名和列名不同,这可能是错误的。当字段是 camelCase(如
emailAddress)而列是 snake_case(如email_address)时,显式传递它。 -
connection
-
要查询的数据库连接名称。默认为应用的主连接。当被检查的表位于非默认连接上时使用此选项。
-
caseInsensitive
-
当为
true时,比较的两侧都被包装在LOWER(...)中。规则发出WHERE LOWER("column") = LOWER(?)而不是WHERE "column" = ?。参见行为与性能说明了解对索引的影响。 -
filter
-
一个回调
(query, value, field) => void | Promise<void>,允许你向查询附加额外的WHERE子句。直接修改查询构建器,而不是返回新查询。使用此选项在更新时排除当前行,将检查范围限定到租户,或应用任何其他 options 形式无法表达的约束。
使用回调
回调形式接受一个函数 (db, value, field) => Promise<boolean>,由你自己运行整个查询。当 options 形式无法描述你的需求时使用它:多子句 WHERE、JOIN、软删除处理或任何模型特定的内容。
对于 unique,当值唯一时(验证通过)回调返回 true,当值已存在时(验证失败)返回 false。对于 exists,语义是相反的:true 表示值存在。
import vine from '@vinejs/vine'
import User from '#models/user'
const createUserValidator = vine.create({
email: vine.string().email().unique(async (_db, value) => {
const existing = await User.findBy('email', value)
return existing === null
}),
})
回调可以直接使用 Lucid 模型(如上所示),或使用 db 参数以原始查询构建器形式编写查询。
.unique(async (db, value) => {
const row = await db
.from('users')
.where('email', value)
.whereNull('deleted_at')
.first()
return row === null
})
自定义错误消息
默认消息是 unique 的 "{{ field }} 已被占用" 和 exists 的 "所选的 {{ field }} 无效"。通过 VineJS 的消息自定义机制,使用规则键 database.unique 和 database.exists 来覆盖它们。
import { SimpleMessagesProvider } from '@vinejs/vine'
export const messagesProvider = new SimpleMessagesProvider({
'database.unique': '已有用户注册了此 {{ field }}',
'database.exists': '未找到给定的 {{ field }} 对应的记录',
})
参见 VineJS 错误消息指南了解完整模式,包括按字段和按规则的覆盖。
行为与性能说明
关于规则如何执行的一些值得了解的细节。
前面的规则失败会跳过数据库查询。 如果某个字段在 unique 或 exists 规则运行之前已经无效(值缺失、格式错误或其他规则报告了错误),该规则会短路并且不会命中数据库。这意味着 unique 和 exists 错误不会叠加在其他错误之上。一个提交空值给 vine.string().minLength(3).unique({...}) 的表单会报告 minLength 错误,并且永远不会运行唯一性检查。
不区分大小写的比较会禁用索引使用。 设置 caseInsensitive: true 会将两侧包装在 LOWER(...) 中。列上的标准 B-tree 索引无法用于此比较,因此数据库会回退到顺序扫描。为了在大型表上保持规则快速,创建 LOWER(column) 的函数索引(PostgreSQL、MySQL 8+)或使用带有常规索引的生成列。
每个规则运行一个单独的查询。 在一个 schema 中用 unique/exists 规则验证五个字段会产生五次独立的数据库往返。对于典型表单来说,这很少成为瓶颈,但对于验证许多此类字段针对大型表的端点来说,值得了解。