校验与检查
框架提供了 @DatabaseExistsById 注解和 CheckProvider 接口体系,用于简化数据校验和业务检查逻辑。
核心特性
- ✅ @DatabaseExistsById:自动校验数据库中是否存在指定 ID 的数据
- ✅ CheckProvider:提供存在性检查、幂等性检查、操作结果检查
- ✅ 集成 Bean Validation:与
@Valid无缝配合 - ✅ 自定义异常:支持自定义错误提示和异常类型
- ✅ 批量校验:支持单个 ID 和 ID 集合
@DatabaseExistsById 注解
功能介绍
@DatabaseExistsById 是基于 Jakarta Bean Validation 的自定义校验注解,用于在参数校验阶段自动检查数据库中是否存在指定 ID 的数据。
适用场景:
- 创建订单时校验商品 ID 是否存在
- 分配角色时校验用户 ID 是否存在
- 关联数据时校验外键 ID 是否合法
快速开始
1. 定义请求 DTO
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 使用
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 层再次校验。
注解属性
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
repository | Class<? extends BaseService<?>> | 是 | - | Service 类,用于查询数据 |
message | String | 否 | "数据不存在" | 校验失败提示信息 |
groups | Class<?>[] | 否 | {} | 校验分组 |
payload | Class<? extends Payload>[] | 否 | {} | 负载 |
使用场景
场景1:单个 ID 校验
@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 校验
@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:与其他校验注解组合
@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;
}校验顺序:
@NotNull- 检查是否为 null@Min- 检查是否为正数@DatabaseExistsById- 检查数据库中是否存在
场景4:校验分组
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;
}@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 的校验逻辑:
@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,建议使用全局异常处理器统一处理:
@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);
}
}返回示例:
{
"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:检查用户是否存在
@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:获取并检查
@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:自定义异常
@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:防止重复创建
@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:防止重复提交订单
@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:自定义异常
@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:检查更新结果
@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:检查删除结果
@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:批量操作检查
@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,
"批量更新订单状态失败"
));
}
}最佳实践
✅ 推荐做法
参数校验使用 @DatabaseExistsById
java@DatabaseExistsById(repository = UserService.class, message = "用户不存在") // ✅ private Long userId;Service 层使用 CheckProvider
javacheckExists(userService.isExistsByIdValidate(userId), "用户不存在"); // ✅自定义错误提示
javacheckExists(exists, "用户不存在,无法创建订单"); // ✅ 提示明确组合使用多个检查
javacheckExists(userExists, "用户不存在"); // ✅ 先检查存在性 checkIdempotency(orderExists, "订单已存在"); // ✅ 再检查幂等性 checkOperation(updateSuccess, "更新失败"); // ✅ 最后检查操作结果使用校验分组
java@DatabaseExistsById(groups = UpdateGroup.class, ...) // ✅ 只在更新时校验
❌ 不推荐做法
❌ 不使用注解,手动校验
java// ❌ 繁琐且容易遗漏 if (!userService.isExistsByIdValidate(userId)) { throw new BusinessException("用户不存在"); }❌ 不提供错误信息
java@DatabaseExistsById(repository = UserService.class) // ❌ 使用默认消息❌ 在 Service 层重复校验
java// Controller 已经用 @DatabaseExistsById 校验过 // Service 层不应再次校验 if (!userService.isExistsByIdValidate(userId)) { // ❌ 重复校验 throw new BusinessException("用户不存在"); }❌ 混用 Validate 和 CheckProvider
javaUser user = userService.getByIdValidate(userId); // ❌ 已经抛异常了 checkExists(user != null, "用户不存在"); // ❌ 多余的检查
常见问题
Q: @DatabaseExistsById 和 getByIdValidate 有什么区别?
A: 使用时机不同:
| 对比项 | @DatabaseExistsById | getByIdValidate |
|---|---|---|
| 使用位置 | Controller 参数校验 | Service 业务逻辑 |
| 执行时机 | 参数绑定阶段 | 调用时执行 |
| 返回值 | 校验失败抛异常 | 查询结果或抛异常 |
| 性能 | 只查询 count | 查询完整对象 |
使用建议:
- 只需要校验存在性 → 用
@DatabaseExistsById - 需要获取数据 → 用
getByIdValidate
Q: CheckProvider 什么时候用?
A: 适用场景:
- 复杂的业务校验逻辑
- 多条件组合校验
- 自定义异常类型
- Service 层内部校验
示例:
// ✅ 复杂业务校验
checkExists(userExists && userService.isActive(userId), "用户不存在或未激活");
// ✅ 自定义异常
checkExists(exists, () -> new CustomException("自定义错误"));Q: 批量校验时,部分 ID 不存在如何处理?
A: @DatabaseExistsById 要求所有 ID 都存在,如果需要部分存在,手动处理:
@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: 使用全局异常处理器:
@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 注解。
// ❌ 不推荐在 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) { // ✅
// ...
}
}下一步
- 自动填充 - 了解自动填充机制
- 增强 Service 层 - 回顾
BaseService的 CRUD 方法 - 基础实体类 - 回顾
BaseDO和BaseAssignDO
