REST 服务如何统一返回格式校验和数据库约束错误?
可以在同一个 errors 数组中返回字段格式错误和数据库约束相关错误。对客户端来说,它们都表示请求未能满足接口要求。服务端可以分阶段校验,再用统一结构描述错误。
题目中的组合是合理的:name 的类型和长度校验通过后,可以检查名称是否重复。即使 ownerId 格式错误,也可以执行这项独立检查,但应跳过针对 ownerId 的数据库存在性查询。
按依赖关系安排校验
可以按以下顺序处理:
- 解析请求:如果 JSON 无法解析,直接返回请求解析错误,无需继续校验字段。
- 检查字段结构:校验必填项、类型、长度和取值范围。
ownerId若对应Long,通常还需要检查整数要求和可表示范围,不能只判断它是不是数字。 - 检查业务条件:对通过前置校验的字段,查询名称是否重复、关联记录是否存在。
- 尝试写入:只有前面没有错误时才执行写入,并处理数据库最终报告的约束冲突。
是否收集所有可检查的错误,取决于接口约定和查询成本。也可以在结构校验失败后立即返回,待客户端修正后再执行数据库检查。两种方式都可以,重要的是保持行为一致。
无需为了收集更多错误而检查无效字段。例如,ownerId 为 "abc" 时,针对该字段报告格式错误即可,不必再报告“关联记录不存在”。
用统一结构描述错误
可以保留题目中的 field 和 error,再增加稳定的 code,方便客户端根据错误类型处理,而不必匹配提示文字:
{
"errors": [
{
"field": "name",
"code": "name_already_exists",
"error": "该名称已存在。"
},
{
"field": "ownerId",
"code": "invalid_type",
"error": "ownerId 必须是整数。"
}
]
}
这些错误码是接口自行定义的示例。客户端不需要知道某条错误来自 DTO 校验器、业务查询还是数据库异常;服务端负责把这些结果转换为公开的接口错误。
跨字段错误可以使用单独的业务错误码,并按约定省略 field,不必强行归到某一个字段。
HTTP 状态码如何选择
一次 HTTP 响应只有一个状态码,不能为每条错误分别设置。可以按以下思路制定规则:
| 状态码 | 可采用的场景 |
|---|---|
400 Bad Request | JSON 无法解析,或接口统一将请求校验失败归为 400 |
422 Unprocessable Content | 请求内容能够解析,但字段值或业务条件不满足接口要求 |
409 Conflict | 创建操作与现有资源状态冲突,例如名称已被占用 |
对于题目中的混合错误,继续返回 400 是一种可行的接口约定。若希望区分解析失败与可解析内容的校验失败,也可以让后者统一返回 422。
如果决定将名称冲突单独映射为 409,还需要明确它与其他校验错误同时出现时采用哪个整体状态码。不要让状态码取决于校验器碰巧先发现哪条错误。
数据库约束仍然需要保留
写入前的查询可以改善错误提示,但不能保证并发情况下名称唯一。例如,两个请求可能同时查到名称可用,随后都尝试插入。关联记录也可能在存在性检查后、写入前被删除。
因此,仍需由数据库的唯一约束和外键约束提供最终保障。处理顺序可以用以下伪代码表示:
payload = parseRequest()
errors = validateFields(payload)
if namePassedFieldValidation(payload, errors):
if companyNameExists(payload.name):
errors.add("name", "name_already_exists")
if ownerIdPassedFieldValidation(payload, errors):
if not ownerExists(payload.ownerId):
errors.add("ownerId", "owner_not_found")
if errors is not empty:
return validationResponse(errors)
try:
company = createCompanyInTransaction(payload)
return createdResponse(company)
catch KnownConstraintViolation as violation:
return mappedConstraintResponse(violation)
这段代码只说明处理顺序。具体的事务回滚方式、异常类型和约束识别方式取决于使用的数据库、驱动及框架。
只转换明确识别的约束错误
通过数据库或驱动提供的结构化错误码、约束名称识别已知冲突,再映射为安全的业务提示。不要向客户端直接返回原始 SQL、表名或数据库异常消息,也不要把所有数据库异常都包装成字段校验失败。
连接失败、超时等服务端故障应走相应的 5xx 错误处理流程。若名称或关联记录的存在性涉及权限,还应先确定调用者是否有权获知这些信息。
备注:内容仅供参考。
