Java增删改查类接口设计演进
前言
在Java企业级开发中,增删改查(CRUD,即Create、Read、Update、Delete)接口是永恒的主题。我经历过数十个项目,见过各式各样的接口设计:有的项目用RESTful规范行云流水,有的用GraphQL灵活高效,还有的坚持SOAP协议确保严格契约。这些设计没有绝对的对错,只有是否适合业务场景。
本文将从CRUD接口的基础概念出发,系统梳理常见的接口设计类型、核心架构模式、技术选型策略,以及从单体到微服务的演进路径。希望通过这篇文章,你能建立起完整的CRUD接口设计知识体系,在面对不同业务需求时,能够从容做出最合适的技术决策。
第一部分:CRUD接口设计的核心概念
1.1 CRUD的本质与业务价值
CRUD是软件工程中最基本的四个数据操作功能:
- Create(创建):向系统写入新的数据记录
- Read(读取):从系统查询已有数据
- Update(更新):修改系统中已有数据
- Delete(删除):从系统中移除数据
看似简单的四个操作,却是80%企业应用的核心功能。无论你是开发一个简单的博客系统,还是构建一个复杂的电商平台,最终80%的接口都属于CRUD范畴。因此,CRUD接口设计的质量,直接决定了系统的稳定性、可维护性和用户体验。
1.2 CRUD接口的演变历程
第一代:RPC风格(2000年前后)
采用HTTP + XML,接口命名如/userService?method=getUser&id=123,参数在URL或请求体中传递,缺乏统一规范。
第二代:RESTful风格(2010年代)
利用HTTP方法表达操作语义:GET查询、POST创建、PUT/PATCH更新、DELETE删除,资源通过URL标识。
第三代:GraphQL/API网关(2020年前后)
前端可以按需获取数据,一个请求获取多个资源,减少网络往返。
第四代:BFF(Backend For Frontend)模式(当前)
为不同前端设备(Web、Mobile、IoT)提供定制化API,后端微服务通过聚合层统一对外暴露。
了解这个演变过程,我们才能理解为什么今天会有这么多不同的接口设计风格,以及每种风格适用的场景。
第二部分:常见CRUD接口类型深度剖析
2.1 传统RESTful风格接口
RESTful是目前最主流的CRUD接口设计风格。它以资源为中心,用URL定位资源,用HTTP方法表达操作意图。
标准RESTful CRUD映射:
GET /api/v1/users - 查询用户列表(支持分页、排序、过滤)
GET /api/v1/users/{id} - 查询单个用户
POST /api/v1/users - 创建新用户
PUT /api/v1/users/{id} - 完整更新用户(替换)
PATCH /api/v1/users/{id} - 部分更新用户(修改部分字段)
DELETE /api/v1/users/{id} - 删除用户
关键设计细节:
幂等性保证:GET、PUT、DELETE、PATCH都应该是幂等的,多次请求与一次请求效果相同。POST则不具备幂等性(多次创建会生成多条记录)。对于POST,可以通过引入idempotency-key(幂等键)实现幂等。
状态码选择:
- 200 OK:查询、更新成功
- 201 Created:创建成功,返回Location头指向新资源
- 204 No Content:删除成功,无返回内容
- 400 Bad Request:客户端参数错误
- 404 Not Found:资源不存在
资源嵌套表示:对于关联资源,可以使用嵌套URL:
GET /api/v1/users/{userId}/orders - 查询用户的所有订单
POST /api/v1/users/{userId}/orders - 为用户创建订单
RESTful风格的优点:语义清晰、缓存友好、无状态、易于理解。缺点:一个业务操作可能需要多次请求,对前端不友好;没有标准的查询语言,复杂的过滤条件难以表达。
2.2 灵活查询接口(QueryDSL风格)
当表格查询条件极其复杂(几十个过滤字段、多表关联、聚合函数),RESTful的简单查询参数力不从心。这时就需要引入灵活的查询接口。
方案一:JSON查询对象
前端将完整的查询条件封装成一个JSON对象,通过POST方式发送到/api/v1/users/search这样的端点。
POST /api/v1/users/search
{
"filters": [
{"field": "name", "operator": "like", "value": "张"},
{"field": "age", "operator": "gte", "value": 18},
{"field": "age", "operator": "lte", "value": 60},
{"field": "status", "operator": "in", "value": [1, 2, 3]}
],
"sorts": [
{"field": "createTime", "direction": "DESC"}
],
"pagination": {
"page": 1,
"size": 20
},
"fields": ["id", "name", "age", "status"] // 指定返回字段
}
这种方案的优势是灵活、表达能力完全,挑战在于后端需要解析这个复杂的查询结构,构建对应的SQL,且字段名、操作符的安全校验更加重要。
方案二:OData(Open Data Protocol)
OData是一个开放协议,定义了标准的URI查询语法:
GET /api/v1/users?$filter=contains(name,'张') and age ge 18 and status in (1,2,3)&$orderby=createTime desc&$top=20&$skip=0&$select=id,name,age,status
OData的强大之处在于它是一个完整的生态系统,有标准的解析库(如Olingo),支持复杂的查询、关联展开、聚合等。但它的学习曲线较陡,且实现起来相对重量级。
方案三:GraphQL查询
GraphQL让前端可以精确指定需要的字段和关联关系:
query {
users(filter: {nameContains: "张", ageGte: 18}) {
id
name
age
status
orders(limit: 5) {
id
amount
}
}
}
GraphQL的优势:按需获取、一次请求多资源、强类型Schema。但引入了额外的学习成本和中间层开销。
2.3 批量操作接口
在管理后台、数据导入等场景中,批量操作是刚需。
批量查询:
GET /api/v1/users/batch?ids=1,2,3,4,5
返回ID为1到5的用户列表。
批量创建:
POST /api/v1/users/batch
[
{ "name": "用户1", "age": 20 },
{ "name": "用户2", "age": 25 }
]
一次创建多个用户,减少网络往返。
批量更新:
PATCH /api/v1/users/batch
{
"ids": [1, 2, 3],
"data": { "status": 1 } // 批量将这三个用户的状态改为1
}
批量删除:
DELETE /api/v1/users/batch?ids=1,2,3
或通过请求体传递ID列表。
批量操作的工程挑战:
- 事务边界:是全部成功或全部回滚,还是部分成功部分失败?需要根据业务场景决策。
- 执行效率:循环执行单条SQL会非常慢,应使用
INSERT ... VALUES (...), (...), ...或UPDATE ... WHERE id IN (...)等批量SQL。 - 结果反馈:批量操作需要返回每个操作的结果,如
successCount、failCount、failDetails。
2.4 软删除与归档接口
现代的CRUD接口通常不真正删除数据,而是使用软删除。
软删除设计:
- 表中增加
deleted字段(int/boolean)或delete_time字段(datetime) - DELETE操作本质是UPDATE,将
deleted置为1 - 查询时自动过滤
deleted=0的数据
MyBatis-Plus的软删除支持:
@TableLogic
private Integer deleted;
配置后,MyBatis-Plus的增删改查会自动处理软删除逻辑,开发者几乎无感知。
归档接口:将历史数据从主表迁移到归档表,减少主表数据量,提升查询性能。归档操作通常由定时任务触发,但也需要提供手动归档的接口。
2.5 导入导出接口
表格类系统几乎都离不开数据的导入导出。
导出接口设计:
@GetMapping("/export")
public ResponseEntity<Resource> exportUsers(ExportQuery query) {
// 1. 查询数据(可能包含大量数据)
List<UserVO> users = userService.queryForExport(query);
// 2. 生成Excel或CSV
byte[] bytes = excelExportService.export(users);
// 3. 构建下载响应
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=users_" + System.currentTimeMillis() + ".xlsx");
return ResponseEntity.ok()
.headers(headers)
.body(new ByteArrayResource(bytes));
}
导出优化要点:
- 大文件:使用流式写入(SXSSFWorkbook for Excel),避免OOM
- 异步导出:对于百万级数据,采用异步任务+通知下载
- 格式选择:Excel(用户友好) vs CSV(简单高效)
- 字段映射:支持用户选择导出哪些字段
导入接口设计:
@PostMapping("/import")
public ApiResponse<ImportResult> importUsers(@RequestParam MultipartFile file) {
// 1. 解析文件
List<UserImportDTO> rows = parseFile(file);
// 2. 校验数据
List<ValidationError> errors = validateRows(rows);
if (!errors.isEmpty()) {
return ApiResponse.fail("数据校验失败", errors);
}
// 3. 批量入库
int successCount = userService.batchInsert(rows);
return ApiResponse.success(new ImportResult(successCount));
}
导入关键考量:
- 模板下载:提供标准模板,让用户按格式准备数据
- 数据校验:逐行校验,给出明确的错误行号和原因
- 去重处理:导入时如何处理重复数据(覆盖/跳过)
- 事务控制:行级独立事务 vs 全部成功或全部回滚
2.6 草稿与版本管理接口
在内容管理、工作流类系统中,数据通常有草稿、已发布等状态,且需要保留历史版本。
草稿接口设计:
POST /api/v1/users/draft - 保存草稿
PUT /api/v1/users/{id}/publish - 发布草稿
PUT /api/v1/users/{id}/recall - 撤回发布
草稿实现方案:
- 方案一:在数据表中增加
status字段(draft/published) - 方案二:分离主表和草稿表,发布时复制到主表
版本管理:
GET /api/v1/users/{id}/versions - 获取所有历史版本
GET /api/v1/users/{id}/versions/{versionId} - 获取特定版本
PUT /api/v1/users/{id}/versions/{versionId}/restore - 回滚到指定版本
版本管理技术实现通常使用日志表或事件溯源模式。
2.7 多租户CRUD接口
在SaaS(Software as a Service,软件即服务)应用中,CRUD接口必须支持多租户隔离。
多租户隔离策略:
方案一:独立数据库
每个租户拥有独立数据库,物理隔离,安全性最高,但成本高昂。
方案二:共享数据库+独立Schema
每个租户在同一个数据库中有独立的Schema,管理和维护相对复杂。
方案三:共享数据库+共享Schema+租户ID字段
所有数据在同一个表中,通过tenant_id字段区分租户。这是最经济的方案,也是中小型SaaS应用的主流选择。
MyBatis-Plus多租户实现:
// 配置多租户拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(
new TenantLineHandler() {
@Override
public Expression getTenantId() {
// 从请求上下文获取当前租户ID
return new LongValue(TenantContextHolder.getCurrentTenantId());
}
@Override
public String getTenantIdColumn() {
return "tenant_id";
}
@Override
public boolean ignoreTable(String tableName) {
return "sys_config".equals(tableName); // 系统表不加租户过滤
}
}
));
return interceptor;
}
配置后,所有CRUD操作自动添加租户过滤,开发者完全无感知。
第三部分:CRUD接口的技术架构模式
3.1 经典的Controller-Service-DAO三层架构
这是Java CRUD接口最基础的架构模式:
Controller层:接收HTTP请求,参数校验,响应封装
↓
Service层:业务逻辑,事务管理,对象转换
↓
DAO层:数据访问,SQL执行
↓
Database
各层职责细化:
Controller层:
- 使用
@Valid进行参数校验 - 调用Service方法
- 使用统一响应结构
ApiResponse包装返回 - 不包含任何业务逻辑
Service层:
- 实现业务规则
- 管理事务边界(
@Transactional) - 执行Entity-DTO-VO转换
- 调用DAO层和其他Service
DAO层:
- 定义Mapper接口
- 编写SQL或使用MyBatis-Plus的Wrapper
- 返回Entity或基本类型
这种模式适用于:大多数传统企业应用,团队熟悉度高,维护成本低。
3.2 DDD分层架构
对于业务逻辑复杂的系统,DDD(Domain-Driven Design,领域驱动设计)提供了更精细的分层:
Interface层(接口层):REST API、消息队列监听、定时任务等
↓
Application层(应用层):协调领域对象完成用例,事务管理
↓
Domain层(领域层):核心业务逻辑,聚合根、实体、值对象
↓
Infrastructure层(基础设施层):数据库访问、外部服务调用
与三层架构的区别:
- 业务逻辑在Domain层而非Service层
- 使用聚合根封装业务规则和不变性
- 依赖倒置:Domain层不依赖Infrastructure层
CRUD在DDD中的实现:
- 创建:通过聚合根工厂方法创建,Application层调用Repository保存
- 查询:使用CQRS(命令查询职责分离)模式,查询走单独的Query模型
- 更新:通过聚合根的方法修改状态,Application层调用Repository更新
- 删除:聚合根提供删除方法,标记为已删除
适用场景:业务逻辑复杂、多变、需要长期演进的系统。
3.3 CQRS架构模式
CQRS(Command Query Responsibility Segregation,命令查询职责分离)将读操作和写操作完全分离:
写操作(Command):
POST /api/v1/users → Command → 写入主数据库(Event Sourcing可选)
读操作(Query):
GET /api/v1/users → Query → 从只读副本/缓存读取
核心思想:
- 读模型和写模型可以不同(甚至不同数据库)
- 读模型针对查询优化(冗余存储、预连接、物化视图)
- 写模型针对事务一致性优化(规范化存储)
实现方案:
- 写操作:使用JPA/Hibernate,保证事务一致性
- 读操作:使用MyBatis,编写高性能查询SQL
- 数据同步:通过事件驱动(如CDC工具Debezium)将数据同步到读库
优势:读性能和写性能都能独立优化;劣势:系统复杂度大幅增加,需要处理最终一致性。
3.4 充血模型 vs 贫血模型
在CRUD接口设计中,你一定会遇到这两个概念:
贫血模型(Anemic Domain Model):
- Entity/DTO只包含数据字段和getter/setter
- 所有业务逻辑在Service层
- 这是大多数Java项目的默认选择
// 贫血模型
@Entity
public class User {
private Long id;
private String name;
private Integer status;
// 只有getter/setter,无业务方法
}
@Service
public class UserService {
public void activeUser(Long userId) {
User user = userRepository.findById(userId);
user.setStatus(1); // 业务逻辑在Service
userRepository.save(user);
}
}
充血模型(Rich Domain Model):
- Entity包含业务方法,封装了业务规则
- Service层很薄,只负责协调和事务管理
// 充血模型
@Entity
public class User {
private Long id;
private String name;
private Integer status;
public void activate() {
if (this.status == 2) {
throw new BusinessException("已冻结用户无法激活");
}
this.status = 1;
// 触发领域事件
DomainEventPublisher.publish(new UserActivatedEvent(this.id));
}
}
@Service
public class UserService {
public void activeUser(Long userId) {
User user = userRepository.findById(userId);
user.activate(); // 业务逻辑在领域对象中
userRepository.save(user);
}
}
如何选择:对于简单CRUD(数据展示+基础操作),贫血模型足够。对于业务规则复杂的系统(如订单状态机、金融合规系统),充血模型能更好地维护业务一致性。
第四部分:CRUD接口的核心技术组件
4.1 对象映射与转换
CRUD接口涉及大量的对象转换:Request→DTO、DTO→Entity、Entity→VO。选择合适的对象映射方案至关重要。
MapStruct深度实践:
@Mapper(componentModel = "spring",
uses = {DateMapper.class, EnumMapper.class},
imports = {LocalDateTime.class})
public interface UserAssembler {
// Request → Entity(创建场景)
@Mapping(target = "createTime", expression = "java(LocalDateTime.now())")
@Mapping(target = "updateTime", expression = "java(LocalDateTime.now())")
@Mapping(target = "deleted", constant = "0")
UserEntity createReqToEntity(UserCreateRequest request);
// Request → Entity(更新场景)
@Mapping(target = "createTime", ignore = true)
@Mapping(target = "deleted", ignore = true)
@Mapping(target = "updateTime", expression = "java(LocalDateTime.now())")
void updateReqToEntity(UserUpdateRequest request, @MappingTarget UserEntity entity);
// Entity → VO(查询场景)
@Mapping(source = "createTime", target = "createTimeStr", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Mapping(source = "status", target = "statusDesc", qualifiedByName = "statusToDesc")
UserVO entityToVO(UserEntity entity);
// 批量转换
List<UserVO> entityListToVOList(List<UserEntity> entities);
}
MapStruct的优势:
- 编译期生成实现类,性能碾压反射工具
- 类型安全,编译期发现字段类型不匹配
- 支持复杂的映射逻辑(表达式、自定义方法、嵌套映射)
4.2 数据校验框架
Spring Validation高级用法:
分组校验:
public class UserRequest {
@NotNull(groups = {Update.class}, message = "ID不能为空")
private Long id;
@NotBlank(groups = {Create.class, Update.class}, message = "姓名不能为空")
private String name;
@NotNull(groups = {Create.class}, message = "年龄不能为空")
@Min(value = 1, groups = {Create.class, Update.class})
private Integer age;
}
// 使用
@Validated(Create.class)
public ApiResponse create(@RequestBody @Valid UserRequest request) { }
自定义校验注解:
@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) return true;
return PATTERN.matcher(value).matches();
}
}
4.3 分页与排序的实现
统一分页参数封装:
@Data
public class PageRequest {
@Min(1)
private Integer page = 1;
@Min(1)
@Max(200)
private Integer size = 20;
private String sort;
private String order = "DESC";
// 获取排序对象(带白名单校验)
public List<OrderItem> getOrderItems(Class<?> entityClass) {
if (StringUtils.isBlank(sort)) {
return Collections.emptyList();
}
// 白名单校验:只能使用Entity中存在的字段
try {
entityClass.getDeclaredField(sort);
} catch (NoSuchFieldException e) {
throw new BusinessException("排序字段不存在");
}
return Collections.singletonList(
"ASC".equalsIgnoreCase(order) ?
OrderItem.asc(sort) : OrderItem.desc(sort)
);
}
}
MyBatis-Plus分页查询:
public PageResult<UserVO> listUsers(PageRequest pageRequest, UserQuery query) {
// 构建分页对象
Page<UserEntity> page = new Page<>(pageRequest.getPage(), pageRequest.getSize());
// 构建查询条件
LambdaQueryWrapper<UserEntity> wrapper = Wrappers.lambdaQuery(UserEntity.class)
.like(StringUtils.isNotBlank(query.getName()), UserEntity::getName, query.getName())
.eq(query.getStatus() != null, UserEntity::getStatus, query.getStatus())
.between(query.getStartTime() != null && query.getEndTime() != null,
UserEntity::getCreateTime, query.getStartTime(), query.getEndTime());
// 添加排序
List<OrderItem> orderItems = pageRequest.getOrderItems(UserEntity.class);
if (!orderItems.isEmpty()) {
page.setOrders(orderItems);
} else {
page.addOrder(OrderItem.desc("create_time"));
}
// 执行查询
IPage<UserEntity> entityPage = userMapper.selectPage(page, wrapper);
// 转换结果
return PageResult.of(entityPage, userAssembler.entityListToVOList(entityPage.getRecords()));
}
4.4 缓存策略
缓存是CRUD性能优化的利器。
查询缓存:
@Cacheable(value = "user", key = "#id", unless = "#result == null")
public UserVO getUserById(Long id) {
UserEntity entity = userMapper.selectById(id);
return userAssembler.entityToVO(entity);
}
@CacheEvict(value = "user", key = "#id")
public void updateUser(Long id, UserUpdateRequest request) {
// 更新操作清除缓存
}
@CacheEvict(value = "user", allEntries = true)
public void deleteUser(Long id) {
// 删除操作清除所有用户缓存
}
列表缓存(复杂场景):
public PageResult<UserVO> listUsers(PageRequest pageRequest) {
// 使用缓存key包含分页参数
String cacheKey = "users:page:" + pageRequest.getPage() +
":size:" + pageRequest.getSize() +
":sort:" + pageRequest.getSort();
return redisTemplate.execute(session -> {
// 尝试从缓存获取
PageResult<UserVO> cached = (PageResult<UserVO>) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
// 缓存未命中,查询数据库
PageResult<UserVO> result = queryFromDatabase(pageRequest);
redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES);
return result;
});
}
缓存穿透防护:使用布隆过滤器或缓存空值。
缓存雪崩防护:设置不同的过期时间,添加随机偏移量。
4.5 事务管理
声明式事务:
@Transactional(rollbackFor = Exception.class)
public void createUserWithRoles(UserCreateRequest request, List<Long> roleIds) {
// 1. 创建用户
UserEntity user = userAssembler.createReqToEntity(request);
userMapper.insert(user);
// 2. 分配角色
if (CollectionUtils.isNotEmpty(roleIds)) {
roleIds.forEach(roleId -> {
UserRoleEntity relation = new UserRoleEntity();
relation.setUserId(user.getId());
relation.setRoleId(roleId);
userRoleMapper.insert(relation);
});
}
}
事务传播行为:
REQUIRED(默认):如果当前没有事务,就新建一个;否则加入当前事务REQUIRES_NEW:始终新建事务,挂起当前事务SUPPORTS:如果有事务就加入,否则不创建NESTED:嵌套事务,当前事务保存点
分布式事务(微服务场景):
- TCC模式(Try-Confirm-Cancel):手动实现补偿逻辑
- Saga模式:通过事件驱动进行最终一致性补偿
- Seata AT模式:自动代理数据源,实现分布式事务
第五部分:CRUD接口的安全性设计
5.1 数据权限控制
CRUD接口必须确保用户只能操作自己权限范围内的数据。
基于角色的权限(RBAC,Role-Based Access Control):
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ApiResponse deleteUser(@PathVariable Long id) {
// 只有管理员可以删除用户
userService.deleteUser(id);
return ApiResponse.success();
}
基于数据范围的权限(行级权限):
public PageResult<UserVO> listUsers(UserQuery query) {
// 获取当前用户的数据范围
Set<Long> accessibleDeptIds = getCurrentUserDeptIds();
LambdaQueryWrapper<UserEntity> wrapper = Wrappers.lambdaQuery(UserEntity.class)
.in(UserEntity::getDeptId, accessibleDeptIds) // 只查询有权限的部门
.like(...);
// 执行查询
return queryUsers(query, wrapper);
}
字段级权限(敏感字段脱敏):
@JsonSerialize(using = PhoneDesensitizeSerializer.class)
private String phone;
public class PhoneDesensitizeSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (value == null || value.length() < 7) {
gen.writeString(value);
return;
}
// 手机号脱敏:138****1234
gen.writeString(value.substring(0, 3) + "****" + value.substring(7));
}
}
5.2 接口限流
使用Guava RateLimiter(单机限流):
@RestController
public class UserController {
// 每秒允许100个请求
private final RateLimiter rateLimiter = RateLimiter.create(100.0);
@GetMapping
public ApiResponse listUsers() {
if (!rateLimiter.tryAcquire(1)) {
throw new RateLimitException("请求过于频繁,请稍后再试");
}
// 正常处理
}
}
使用Sentinel(分布式限流):
@SentinelResource(
value = "listUsers",
blockHandler = "handleBlock",
blockHandlerClass = UserBlockHandler.class
)
public PageResult<UserVO> listUsers(PageRequest pageRequest) {
return userService.listUsers(pageRequest);
}
5.3 SQL注入防护
黄金法则:永远不要拼接用户输入到SQL中。
安全写法:
// MyBatis-Plus安全写法
wrapper.eq("name", name); // 参数化
// MyBatis XML安全写法
<select id="findByName">
SELECT * FROM user WHERE name = #{name} <!-- 参数化 -->
</select>
// 安全写法:使用占位符
String sql = "SELECT * FROM user WHERE name = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, name);
危险写法(绝对禁止):
// 字符串拼接 - 高危!!!
String sql = "SELECT * FROM user WHERE name = '" + name + "'";
// MyBatis ${} - 高危!!!
<select id="findByName">
SELECT * FROM user WHERE name = '${name}'
</select>
排序字段处理(必须使用白名单):
private static final Set<String> ALLOWED_SORT_FIELDS =
Set.of("id", "name", "age", "createTime");
public void validateSortField(String field) {
if (!ALLOWED_SORT_FIELDS.contains(field)) {
throw new BusinessException("非法排序字段");
}
}
第六部分:接口性能优化实战
6.1 N+1问题解决
场景:查询用户列表后,需要获取每个用户的角色名称。
问题代码(N+1查询):
// 1次查询获取用户列表
List<UserEntity> users = userMapper.selectList(wrapper);
// N次查询获取角色名称
for (UserEntity user : users) {
String roleName = roleMapper.selectNameById(user.getRoleId());
// 设置到VO中
}
解决方案:
方案一:JOIN查询(最推荐)
SELECT u.*, r.name AS role_name
FROM user u
LEFT JOIN role r ON u.role_id = r.id
方案二:批量查询(使用IN)
List<UserEntity> users = userMapper.selectList(wrapper);
// 提取所有roleId
Set<Long> roleIds = users.stream().map(UserEntity::getRoleId).collect(Collectors.toSet());
// 批量查询角色(1次查询)
List<RoleEntity> roles = roleMapper.selectBatchIds(roleIds);
Map<Long, String> roleMap = roles.stream().collect(Collectors.toMap(RoleEntity::getId, RoleEntity::getName));
// 填充到VO
方案三:MyBatis嵌套查询(使用collection标签)
<resultMap id="userWithRoles" type="UserEntity">
<id column="id" property="id"/>
<result column="name" property="name"/>
<collection property="roles" ofType="RoleEntity"
select="selectRolesByUserId" column="id"/>
</resultMap>
6.2 深度分页优化
问题:LIMIT 100000, 20的性能极差,因为数据库需要扫描前100020条记录。
优化方案:
方案一:游标分页(使用ID或时间戳)
public PageResult<UserVO> listUsers(PageRequest pageRequest, UserQuery query) {
// 游标分页:基于上一页最后一条记录的ID
Long lastId = query.getLastId(); // 前端传回上一页的最后ID
LambdaQueryWrapper<UserEntity> wrapper = Wrappers.lambdaQuery(UserEntity.class)
.gt(lastId != null, UserEntity::getId, lastId) // 从上次位置开始
.orderByAsc(UserEntity::getId)
.last("LIMIT " + pageRequest.getSize());
// 这种方式的性能是O(1),与翻页深度无关
}
方案二:延迟关联(先查主键,再关联数据)
SELECT * FROM user
WHERE id IN (
SELECT id FROM user
WHERE condition = 'xxx'
ORDER BY id
LIMIT 100000, 20
)
方案三:缓存总记录数(避免COUNT查询)
// 使用Redis缓存总数
String countKey = "users:count:" + query.hashCode();
Long total = redisTemplate.opsForValue().get(countKey);
if (total == null) {
total = userMapper.selectCount(wrapper);
redisTemplate.opsForValue().set(countKey, total, 5, TimeUnit.MINUTES);
}
6.3 批量操作性能优化
批量插入优化:
// 批量插入使用 VALUES (?, ?), (?, ?)
public boolean batchInsert(List<UserEntity> users) {
String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
for (UserEntity user : users) {
sqlSession.insert("insertUser", user);
}
sqlSession.commit();
return true;
} finally {
sqlSession.close();
}
}
// MyBatis-Plus批量插入
userService.saveBatch(users, 1000); // 每1000条一批
批量更新优化:
-- 使用CASE WHEN批量更新
UPDATE user SET
status = CASE id
WHEN 1 THEN 0
WHEN 2 THEN 1
WHEN 3 THEN 0
END,
update_time = NOW()
WHERE id IN (1, 2, 3)
6.4 数据库连接池配置
spring:
datasource:
hikari:
# 最小空闲连接数
minimum-idle: 10
# 最大连接数
maximum-pool-size: 50
# 连接超时时间
connection-timeout: 30000
# 空闲连接超时
idle-timeout: 600000
# 连接最大生命周期
max-lifetime: 1800000
# 连接测试查询
connection-test-query: SELECT 1
第七部分:接口版本管理
7.1 URI版本号
/api/v1/users
/api/v2/users
@RestController
@RequestMapping("/api/v1/users")
public class UserV1Controller { }
@RestController
@RequestMapping("/api/v2/users")
public class UserV2Controller { }
优点:直观、易于理解。缺点:多个Controller冗余维护。
7.2 请求头版本号
@GetMapping
@ApiVersion("v1") // 自定义注解
public ApiResponse listUsersV1() { }
@GetMapping
@ApiVersion("v2")
public ApiResponse listUsersV2() { }
优点:保持URL干净。缺点:不够直观,客户端配置复杂。
7.3 兼容性演进策略
向后兼容原则:
- 新增字段:不影响老客户端,可以直接添加(设置默认值)
- 删除字段:需要先标记
@Deprecated,等待所有客户端升级后再删除 - 修改字段类型:需要提供转换逻辑,或新增字段替换
public class UserVO {
// 旧字段保留
private Integer status;
// 新增字段,替代旧字段
private String statusDesc;
// 字段变化:重构后保留兼容
private String phone;
@JsonProperty("phoneNumber") // 提供别名,兼容旧客户端
public String getPhoneNumber() {
return this.phone;
}
}
第八部分:CRUD接口的测试策略
8.1 单元测试(JUnit + Mockito)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserMapper userMapper;
@Mock
private UserAssembler userAssembler;
@InjectMocks
private UserServiceImpl userService;
@Test
void shouldCreateUserSuccessfully() {
// Given
UserCreateRequest request = new UserCreateRequest();
request.setName("张三");
request.setAge(25);
UserEntity entity = new UserEntity();
entity.setName("张三");
when(userAssembler.createReqToEntity(request)).thenReturn(entity);
when(userMapper.insert(entity)).thenReturn(1);
// When
userService.createUser(request);
// Then
verify(userMapper).insert(entity);
}
}
8.2 集成测试(Spring Boot Test + Testcontainers)
@SpringBootTest
@Testcontainers
class UserControllerIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldQueryUsersSuccessfully() {
// When
ResponseEntity<ApiResponse> response = restTemplate.getForEntity(
"/api/v1/users?page=1&size=10",
ApiResponse.class
);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getCode()).isEqualTo(200);
}
}
8.3 性能测试(JMeter/Gatling)
关键性能指标:
- TPS(Transactions Per Second,每秒事务数):接口吞吐量
- RT(Response Time,响应时间):接口响应延迟
- TP99(Top Percentile 99,99分位值):99%的请求在多少毫秒内完成
第九章:微服务时代的CRUD演进
9.1 从单体到微服务的CRUD变化
单体架构:
- 所有CRUD操作在同一个应用中
- 事务跨多个表由本地事务保证
- 部署简单,但扩展性差
微服务架构:
- 每个服务管理自己的数据(数据库隔离)
- 跨服务的事务由分布式事务或最终一致性保证
- 服务间通过HTTP/RPC通信
9.2 微服务CRUD的关键模式
API网关模式:
前端 → API Gateway → 服务A / 服务B / 服务C
网关负责:路由转发、认证鉴权、限流熔断、日志监控。
聚合服务模式:
前端 → 聚合服务 → 服务A
→ 服务B
→ 服务C
聚合服务负责聚合多个下游服务的数据,返回给前端一个完整的DTO。
事件驱动模式:
Service A (命令) → 发布事件 → Kafka → 消费事件 → Service B
用于解耦服务间的数据同步,实现最终一致性。
9.3 分布式数据访问的挑战
分布式ID生成:
- Snowflake算法(雪花算法)
- 号段模式(美团Leaf)
- UUID v7(时间排序)
分布式事务:
- Seata AT模式(自动代理)
- TCC模式(手动补偿)
- Saga模式(事件驱动)
数据一致性校验:
- 使用消息队列的可靠投递和消费
- 定期对账任务
- 分布式链路追踪(SkyWalking/Zipkin)
第十部分:实战总结与架构选型建议
10.1 不同场景下的CRUD架构选型
| 业务场景 | 推荐架构 | 核心原因 |
|---|---|---|
| 简单管理后台(<10万用户) | 三层架构 + MyBatis-Plus | 开发效率高,维护成本低 |
| 复杂业务系统(订单/风控) | DDD分层 + 充血模型 | 业务规则复杂,需要领域封装 |
| 高并发读系统(商品/内容) | CQRS + 读写分离 | 读写性能独立优化 |
| SaaS多租户系统 | 多租户拦截器 + 共享Schema | 经济高效,易扩展 |
| 微服务系统 | API网关 + 聚合服务 | 服务解耦,独立部署 |
10.2 技术栈推荐
快速开发推荐:
- Spring Boot 3.x + MyBatis-Plus + MapStruct + Redis + MySQL
高并发推荐:
- Spring Boot + MyBatis-Plus + Redisson(分布式锁)+ Seata(分布式事务)+ Sentinel(限流降级)
复杂业务推荐:
- Spring Boot + JPA/Hibernate(DDD)+ MapStruct + RabbitMQ/Kafka + Elasticsearch(查询)
10.3 CRUD接口设计检查清单
- 接口URL是否符合RESTful规范?
- 请求参数是否使用了分组校验?
- 分页参数是否设置了pageSize上限?
- 排序字段是否使用了白名单校验?
- 返回字段是否包含敏感信息?(脱敏处理)
- 是否实现了接口幂等性?(创建/更新)
- 是否做了数据权限过滤?(行级权限)
- 批量操作是否考虑了事务边界?
- 数据库查询是否避免了N+1问题?
- 查询性能是否使用EXPLAIN分析过?
- 接口是否有版本管理策略?
- 是否配置了全局异常处理和日志记录?
- 是否对热点数据应用了缓存?
- 是否处理了软删除逻辑?
- 接口文档是否自动生成并保持最新?
更多推荐


所有评论(0)