当前位置: 代码网 > it编程>编程语言>Java > Java增删改查类接口设计深入讲解

Java增删改查类接口设计深入讲解

2026年07月19日 Java 我要评论
前言在java企业级开发中,增删改查(crud,即create、read、update、delete)接口是永恒的主题。我经历过数十个项目,见过各式各样的接口设计:有的项目用restful规范行云流水

前言

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

总结 

到此这篇关于java增删改查类接口设计的文章就介绍到这了,更多相关java增删改查类接口内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com