PAYATHON 2026

REST 服务如何统一返回格式校验和数据库约束错误?

数据阿明

可以在同一个 errors 数组中返回字段格式错误和数据库约束相关错误。对客户端来说,它们都表示请求未能满足接口要求。服务端可以分阶段校验,再用统一结构描述错误。

题目中的组合是合理的:name 的类型和长度校验通过后,可以检查名称是否重复。即使 ownerId 格式错误,也可以执行这项独立检查,但应跳过针对 ownerId 的数据库存在性查询。

按依赖关系安排校验

可以按以下顺序处理:

  1. 解析请求:如果 JSON 无法解析,直接返回请求解析错误,无需继续校验字段。
  2. 检查字段结构:校验必填项、类型、长度和取值范围。ownerId 若对应 Long,通常还需要检查整数要求和可表示范围,不能只判断它是不是数字。
  3. 检查业务条件:对通过前置校验的字段,查询名称是否重复、关联记录是否存在。
  4. 尝试写入:只有前面没有错误时才执行写入,并处理数据库最终报告的约束冲突。

是否收集所有可检查的错误,取决于接口约定和查询成本。也可以在结构校验失败后立即返回,待客户端修正后再执行数据库检查。两种方式都可以,重要的是保持行为一致。

无需为了收集更多错误而检查无效字段。例如,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 RequestJSON 无法解析,或接口统一将请求校验失败归为 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 错误处理流程。若名称或关联记录的存在性涉及权限,还应先确定调用者是否有权获知这些信息。

备注:内容仅供参考。