Skip to content

校验与检查

框架提供了 @DatabaseExistsById 注解和 CheckProvider 接口体系,用于简化数据校验和业务检查逻辑。

核心特性

  • @DatabaseExistsById:自动校验数据库中是否存在指定 ID 的数据
  • CheckProvider:提供存在性检查、幂等性检查、操作结果检查
  • 集成 Bean Validation:与 @Valid 无缝配合
  • 自定义异常:支持自定义错误提示和异常类型
  • 批量校验:支持单个 ID 和 ID 集合

@DatabaseExistsById 注解

功能介绍

@DatabaseExistsById 是基于 Jakarta Bean Validation 的自定义校验注解,用于在参数校验阶段自动检查数据库中是否存在指定 ID 的数据。

适用场景

  • 创建订单时校验商品 ID 是否存在
  • 分配角色时校验用户 ID 是否存在
  • 关联数据时校验外键 ID 是否合法

快速开始

1. 定义请求 DTO

java
import host.springboot.framework.mybatisplus.validation.annotation.DatabaseExistsById;
import host.springboot.framework3.core.mvc.annotation.TrimWhiteSpace;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
public class CreateOrderRequest {
    
    /**
     * 用户 ID(必须在数据库中存在)
     */
    @NotNull(message = "用户ID不能为空")
    @DatabaseExistsById(
        repository = UserService.class,
        message = "用户不存在"
    )
    private Long userId;
    
    /**
     * 商品 ID(必须在数据库中存在)
     */
    @NotNull(message = "商品ID不能为空")
    @DatabaseExistsById(
        repository = ProductService.class,
        message = "商品不存在"
    )
    private Long productId;
    
    /**
     * 收货地址
     */
    @TrimWhiteSpace
    @NotBlank(message = "收货地址不能为空")
    private String address;
    
    /**
     * 购买数量
     */
    @NotNull(message = "数量不能为空")
    private Integer quantity;
}

2. Controller 使用

java
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/order")
public class OrderController {
    
    private final OrderService orderService;
    
    /**
     * 创建订单
     * 会自动校验 userId 和 productId 是否存在
     */
    @PostMapping
    public BaseVO create(@Valid @RequestBody CreateOrderRequest request) {
        orderService.createOrder(request);
        return R.ok("创建成功");
    }
}

校验时机

@DatabaseExistsById 在 Controller 参数绑定时自动执行,无需在 Service 层再次校验。

注解属性

属性类型必填默认值说明
repositoryClass<? extends BaseService<?>>-Service 类,用于查询数据
messageString"数据不存在"校验失败提示信息
groupsClass<?>[]{}校验分组
payloadClass<? extends Payload>[]{}负载

使用场景

场景1:单个 ID 校验

java
@Data
public class AssignRoleRequest {
    
    /**
     * 用户 ID
     */
    @NotNull(message = "用户ID不能为空")
    @DatabaseExistsById(
        repository = UserService.class,
        message = "用户不存在,无法分配角色"
    )
    private Long userId;
    
    /**
     * 角色 ID
     */
    @NotNull(message = "角色ID不能为空")
    @DatabaseExistsById(
        repository = RoleService.class,
        message = "角色不存在"
    )
    private Long roleId;
}

场景2:批量 ID 校验

java
@Data
public class BatchDeleteRequest {
    
    /**
     * 用户 ID 集合(每个 ID 都必须存在)
     */
    @NotNull(message = "用户ID列表不能为空")
    @Size(min = 1, message = "至少选择一个用户")
    @DatabaseExistsById(
        repository = UserService.class,
        message = "部分用户不存在"
    )
    private List<Long> userIds;
}

校验逻辑

  • 集合中所有 ID 都必须在数据库中存在
  • 任一 ID 不存在,校验失败

场景3:与其他校验注解组合

java
@Data
public class UpdateProductRequest {
    
    /**
     * 商品 ID(必须存在 + 必须为正数)
     */
    @NotNull(message = "商品ID不能为空")
    @Min(value = 1, message = "商品ID必须为正数")
    @DatabaseExistsById(
        repository = ProductService.class,
        message = "商品不存在"
    )
    private Long productId;
    
    /**
     * 商品名称(自动去除首尾空格 + 不能为空)
     */
    @TrimWhiteSpace
    @NotBlank(message = "商品名称不能为空")
    private String productName;
    
    /**
     * 价格(必须为正数)
     */
    @NotNull(message = "价格不能为空")
    @DecimalMin(value = "0.01", message = "价格必须大于0")
    private BigDecimal price;
}

校验顺序

  1. @NotNull - 检查是否为 null
  2. @Min - 检查是否为正数
  3. @DatabaseExistsById - 检查数据库中是否存在

场景4:校验分组

java
public interface CreateGroup {}
public interface UpdateGroup {}

@Data
public class ProductRequest {
    
    /**
     * 商品 ID(更新时必须存在)
     */
    @NotNull(groups = UpdateGroup.class, message = "商品ID不能为空")
    @DatabaseExistsById(
        groups = UpdateGroup.class,
        repository = ProductService.class,
        message = "商品不存在"
    )
    private Long productId;
    
    /**
     * 商品名称
     */
    @TrimWhiteSpace
    @NotBlank(groups = {CreateGroup.class, UpdateGroup.class}, message = "商品名称不能为空")
    private String productName;
}
java
@RestController
@RequestMapping("/product")
public class ProductController {
    
    private final ProductService productService;
    
    /**
     * 创建商品(不校验 productId)
     */
    @PostMapping
    public BaseVO create(@Validated(CreateGroup.class) @RequestBody ProductRequest request) {
        productService.create(request);
        return R.ok("创建成功");
    }
    
    /**
     * 更新商品(校验 productId 是否存在)
     */
    @PutMapping
    public BaseVO update(@Validated(UpdateGroup.class) @RequestBody ProductRequest request) {
        productService.update(request);
        return R.ok("更新成功");
    }
}

校验原理

DatabaseExistsByIdValidator 的校验逻辑:

java
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
    // null 值跳过(由 @NotNull 校验)
    if (Objects.isNull(value)) {
        return true;
    }
    
    // 集合校验
    if (value instanceof Collection<?> collection) {
        if (collection.isEmpty()) {
            return true;
        }
        // 所有 ID 都必须存在
        return repository.countByIdIgnore(collection) == collection.size();
    }
    
    // 单个 ID 校验
    if (value instanceof Serializable) {
        return repository.countByIdIgnore((Serializable) value) > 0;
    }
    
    throw new ValidationException("元素类型不支持");
}

关键点

  • null 值返回 true,交由 @NotNull 处理
  • 使用 countByIdIgnore 查询,避免异常
  • 集合必须全部存在才通过校验

异常处理

校验失败时,会抛出 MethodArgumentNotValidException,建议使用全局异常处理器统一处理:

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public BaseVO handleValidationException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        FieldError error = result.getFieldError();
        String message = error != null ? error.getDefaultMessage() : "参数校验失败";
        return R.error(message);
    }
}

返回示例

json
{
  "code": 400,
  "message": "商品不存在",
  "data": null
}

CheckProvider 接口体系

接口继承关系

CheckProvider (统一入口)
    ├── CheckExistsService      (存在性检查)
    ├── CheckIdempotencyService (幂等性检查)
    └── CheckOperationService   (操作结果检查)
          └── BaseCheckService  (基础检查)

BaseService 已经实现了 CheckProvider 接口,因此所有继承 BaseService 的 Service 都可以直接使用检查方法。

CheckExistsService(存在性检查)

用于检查数据是否存在,不存在时抛出 MybatisServiceException

核心方法

方法参数说明
checkExists(Boolean)布尔值检查布尔结果
checkExists(Long/Integer)数量检查数量是否大于 0
checkExists(Boolean, String)布尔值、提示信息自定义提示信息
checkExists(Boolean, Supplier<E>)布尔值、异常供应商自定义异常
checkExistsAndGet(T)对象检查对象是否为 null,非 null 则返回

使用场景

场景1:检查用户是否存在

java
@Service
public class OrderServiceImpl extends BaseServiceImpl<OrderMapper, Order> 
        implements OrderService {
    
    private final UserService userService;
    private final ProductService productService;
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        // 检查用户是否存在
        boolean userExists = userService.isExistsByIdValidate(request.getUserId());
        checkExists(userExists, "用户不存在,无法创建订单");
        
        // 检查商品是否存在
        boolean productExists = productService.isExistsByIdValidate(request.getProductId());
        checkExists(productExists, "商品不存在");
        
        // 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setQuantity(request.getQuantity());
        this.saveValidate(order);
    }
}

场景2:获取并检查

java
@Service
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> 
        implements UserService {
    
    public void updateUserEmail(Long userId, String newEmail) {
        // 获取用户,不存在则抛异常
        User user = checkExistsAndGet(
            this.getByIdIgnore(userId),
            "用户不存在,无法更新邮箱"
        );
        
        user.setEmail(newEmail);
        this.updateByIdValidate(user);
    }
}

场景3:自定义异常

java
@Service
public class ProductServiceImpl extends BaseServiceImpl<ProductMapper, Product> 
        implements ProductService {
    
    public void deductStock(Long productId, Integer quantity) {
        // 检查商品是否存在,使用自定义异常
        boolean exists = this.isExistsByIdValidate(productId);
        checkExists(exists, () -> new BusinessException(
            ErrorCode.PRODUCT_NOT_FOUND,
            "商品不存在,无法扣减库存"
        ));
        
        // 获取商品并锁定
        Product product = this.getAndLockValidate(productId);
        
        // 扣减库存
        if (product.getStock() < quantity) {
            throw new BusinessException("库存不足");
        }
        product.setStock(product.getStock() - quantity);
        this.updateByIdValidate(product);
    }
}

CheckIdempotencyService(幂等性检查)

用于检查操作是否重复,已存在时抛出 MybatisServiceException

核心方法

方法参数说明
checkIdempotency(Boolean)布尔值检查是否重复(true 表示重复)
checkIdempotency(Long/Integer)数量检查数量是否大于 0(大于 0 表示重复)
checkIdempotency(Boolean, String)布尔值、提示信息自定义提示信息
checkIdempotency(Boolean, Supplier<E>)布尔值、异常供应商自定义异常

使用场景

场景1:防止重复创建

java
@Service
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> 
        implements UserService {
    
    @Transactional(rollbackFor = Exception.class)
    public void register(RegisterRequest request) {
        // 检查用户名是否已存在
        long count = this.count(Wrappers.<User>query()
            .eq("username", request.getUsername()));
        checkIdempotency(count, "用户名已存在");
        
        // 检查邮箱是否已存在
        count = this.count(Wrappers.<User>query()
            .eq("email", request.getEmail()));
        checkIdempotency(count, "邮箱已被注册");
        
        // 创建用户
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        this.saveValidate(user);
    }
}

场景2:防止重复提交订单

java
@Service
public class OrderServiceImpl extends BaseServiceImpl<OrderMapper, Order> 
        implements OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        // 检查是否已存在相同订单(幂等性校验)
        long count = this.count(Wrappers.<Order>query()
            .eq("user_id", request.getUserId())
            .eq("product_id", request.getProductId())
            .eq("order_no", request.getOrderNo()));
        checkIdempotency(count, "订单已存在,请勿重复提交");
        
        // 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setOrderNo(request.getOrderNo());
        this.saveValidate(order);
    }
}

场景3:自定义异常

java
@Service
public class CouponServiceImpl extends BaseServiceImpl<CouponMapper, Coupon> 
        implements CouponService {
    
    public void receiveCoupon(Long userId, Long couponId) {
        // 检查用户是否已领取过该优惠券
        long count = userCouponMapper.selectCount(Wrappers.<UserCoupon>query()
            .eq("user_id", userId)
            .eq("coupon_id", couponId));
        
        checkIdempotency(count, () -> new BusinessException(
            ErrorCode.COUPON_ALREADY_RECEIVED,
            "您已经领取过该优惠券了"
        ));
        
        // 发放优惠券
        UserCoupon userCoupon = new UserCoupon();
        userCoupon.setUserId(userId);
        userCoupon.setCouponId(couponId);
        userCouponMapper.insert(userCoupon);
    }
}

CheckOperationService(操作结果检查)

用于检查数据库操作是否成功,失败时抛出 MybatisServiceException

核心方法

方法参数说明
checkOperation(Boolean)布尔值检查操作结果
checkOperation(Boolean, String)布尔值、提示信息自定义提示信息
checkOperation(Boolean, Supplier<E>)布尔值、异常供应商自定义异常

使用场景

场景1:检查更新结果

java
@Service
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> 
        implements UserService {
    
    public void updateUserStatus(Long userId, Integer status) {
        // 更新用户状态
        boolean success = this.update(Wrappers.<User>update()
            .set("status", status)
            .eq("id", userId));
        
        // 检查更新是否成功
        checkOperation(success, "更新用户状态失败");
    }
}

场景2:检查删除结果

java
@Service
public class ProductServiceImpl extends BaseServiceImpl<ProductMapper, Product> 
        implements ProductService {
    
    public void deleteProduct(Long productId) {
        // 检查商品是否存在
        checkExists(this.isExistsByIdValidate(productId), "商品不存在");
        
        // 删除商品
        boolean success = this.removeByIdValidate(productId);
        
        // 检查删除是否成功
        checkOperation(success, "删除商品失败");
    }
}

场景3:批量操作检查

java
@Service
public class OrderServiceImpl extends BaseServiceImpl<OrderMapper, Order> 
        implements OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void batchUpdateStatus(List<Long> orderIds, Integer status) {
        // 批量更新订单状态
        boolean success = this.update(Wrappers.<Order>update()
            .set("status", status)
            .in("id", orderIds));
        
        // 检查批量更新是否成功
        checkOperation(success, () -> new BusinessException(
            ErrorCode.BATCH_UPDATE_FAILED,
            "批量更新订单状态失败"
        ));
    }
}

最佳实践

✅ 推荐做法

  1. 参数校验使用 @DatabaseExistsById

    java
    @DatabaseExistsById(repository = UserService.class, message = "用户不存在")  // ✅
    private Long userId;
  2. Service 层使用 CheckProvider

    java
    checkExists(userService.isExistsByIdValidate(userId), "用户不存在");  // ✅
  3. 自定义错误提示

    java
    checkExists(exists, "用户不存在,无法创建订单");  // ✅ 提示明确
  4. 组合使用多个检查

    java
    checkExists(userExists, "用户不存在");           // ✅ 先检查存在性
    checkIdempotency(orderExists, "订单已存在");     // ✅ 再检查幂等性
    checkOperation(updateSuccess, "更新失败");       // ✅ 最后检查操作结果
  5. 使用校验分组

    java
    @DatabaseExistsById(groups = UpdateGroup.class, ...)  // ✅ 只在更新时校验

❌ 不推荐做法

  1. ❌ 不使用注解,手动校验

    java
    // ❌ 繁琐且容易遗漏
    if (!userService.isExistsByIdValidate(userId)) {
        throw new BusinessException("用户不存在");
    }
  2. ❌ 不提供错误信息

    java
    @DatabaseExistsById(repository = UserService.class)  // ❌ 使用默认消息
  3. ❌ 在 Service 层重复校验

    java
    // Controller 已经用 @DatabaseExistsById 校验过
    // Service 层不应再次校验
    if (!userService.isExistsByIdValidate(userId)) {  // ❌ 重复校验
        throw new BusinessException("用户不存在");
    }
  4. ❌ 混用 Validate 和 CheckProvider

    java
    User user = userService.getByIdValidate(userId);  // ❌ 已经抛异常了
    checkExists(user != null, "用户不存在");          // ❌ 多余的检查

常见问题

Q: @DatabaseExistsById 和 getByIdValidate 有什么区别?

A: 使用时机不同

对比项@DatabaseExistsByIdgetByIdValidate
使用位置Controller 参数校验Service 业务逻辑
执行时机参数绑定阶段调用时执行
返回值校验失败抛异常查询结果或抛异常
性能只查询 count查询完整对象

使用建议

  • 只需要校验存在性 → 用 @DatabaseExistsById
  • 需要获取数据 → 用 getByIdValidate

Q: CheckProvider 什么时候用?

A: 适用场景

  • 复杂的业务校验逻辑
  • 多条件组合校验
  • 自定义异常类型
  • Service 层内部校验

示例

java
// ✅ 复杂业务校验
checkExists(userExists && userService.isActive(userId), "用户不存在或未激活");

// ✅ 自定义异常
checkExists(exists, () -> new CustomException("自定义错误"));

Q: 批量校验时,部分 ID 不存在如何处理?

A: @DatabaseExistsById 要求所有 ID 都存在,如果需要部分存在,手动处理:

java
@Service
public class UserServiceImpl extends BaseServiceImpl<UserMapper, User> 
        implements UserService {
    
    public void batchDelete(List<Long> userIds) {
        // 查询存在的用户
        List<User> users = this.listByIdIgnore(userIds);
        
        if (users.size() != userIds.size()) {
            // 找出不存在的 ID
            Set<Long> existIds = users.stream()
                .map(User::getId)
                .collect(Collectors.toSet());
            
            List<Long> notExistIds = userIds.stream()
                .filter(id -> !existIds.contains(id))
                .toList();
            
            throw new BusinessException("部分用户不存在: " + notExistIds);
        }
        
        // 批量删除
        this.removeByIdValidate(userIds);
    }
}

Q: 如何全局处理校验异常?

A: 使用全局异常处理器:

java
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    /**
     * 处理参数校验异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public BaseVO handleValidationException(MethodArgumentNotValidException e) {
        BindingResult result = e.getBindingResult();
        
        // 收集所有错误信息
        List<String> errors = result.getFieldErrors().stream()
            .map(FieldError::getDefaultMessage)
            .toList();
        
        return R.error(String.join(", ", errors));
    }
    
    /**
     * 处理 MybatisServiceException
     */
    @ExceptionHandler(MybatisServiceException.class)
    public BaseVO handleMybatisServiceException(MybatisServiceException e) {
        log.error("数据库操作异常", e);
        return R.base(e.getErrorCode(), e.getErrorMessage(), e.getUserTip());
    }
}

Q: CheckProvider 能否用于 Controller?

A: 不推荐。CheckProvider 是为 Service 层设计的,Controller 应该使用 Bean Validation 注解。

java
// ❌ 不推荐在 Controller 使用
@RestController
public class UserController {
    
    @GetMapping("/{id}")
    public SingleVO<User> getById(@PathVariable Long id) {
        checkExists(id != null, "ID不能为空");  // ❌
        // ...
    }
}

// ✅ 推荐使用 @Valid
@RestController
public class UserController {
    
    @PostMapping
    public BaseVO create(@Valid @RequestBody CreateUserRequest request) {  // ✅
        // ...
    }
}

下一步

扩展阅读

Released under the Apache-2.0 License