前言

在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。
  • 结果反馈:批量操作需要返回每个操作的结果,如successCountfailCountfailDetails

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分析过?
  • 接口是否有版本管理策略?
  • 是否配置了全局异常处理和日志记录?
  • 是否对热点数据应用了缓存?
  • 是否处理了软删除逻辑?
  • 接口文档是否自动生成并保持最新?

更多推荐