当前位置: 代码网 > it编程>编程语言>Java > SpringBoot中高效CRUD与进阶的实战

SpringBoot中高效CRUD与进阶的实战

2026年08月27日 Java 我要评论
1. 项目概述:为什么是 mybatis-plus?如果你正在用 java 做后端开发,尤其是和数据库打交道,那你肯定绕不开 mybatis。它是个好东西,灵活、强大,但写多了 xml 映射文件,尤其

1. 项目概述:为什么是 mybatis-plus?

如果你正在用 java 做后端开发,尤其是和数据库打交道,那你肯定绕不开 mybatis。它是个好东西,灵活、强大,但写多了 xml 映射文件,尤其是那些简单的增删改查(crud),难免会觉得有点“啰嗦”。一个简单的 user 表,你得写 insert deletebyid updatebyid selectbyid 这些 sql,虽然不复杂,但架不住表多啊,每个表来一套,代码量就上去了,而且重复性极高。

mybatis-plus(简称 mp)就是为了解决这个“啰嗦”而生的。它不是要取代 mybatis,而是在 mybatis 的基础上做增强,核心目标就一个: 简化开发、提高效率 。你可以把它理解成 mybatis 的“瑞士军刀”扩展包,内置了大量开箱即用的功能,最招牌的就是 通用 crud 操作 。你只需要定义好实体类,继承 mp 提供的 basemapper ,那些基础的增删改查方法就直接可用了,一行 xml 都不用写。这对于快速构建业务原型、开发管理后台这类 crud 密集型的应用来说,效率提升是立竿见影的。

这个教程,我会从一个实际开发者的角度,带你从零开始,手把手搭建一个整合了 mybatis-plus 的 spring boot 项目,并完成一套完整的、包含一些进阶技巧的 crud 操作。我们不止讲“怎么做”,更会聊清楚“为什么这么做”,以及在实际项目中我踩过哪些坑、有哪些最佳实践。目标是让你看完就能在自己的项目里用起来,并且用得明白、用得踏实。

2. 环境准备与项目初始化

工欲善其事,必先利其器。我们先来把项目架子搭好。我习惯用 spring boot 来快速构建项目,这里以目前比较稳定的 spring boot 2.7.x 版本和 mysql 数据库为例。

2.1 依赖引入与关键配置

首先,创建一个新的 spring boot 项目。你可以用 idea 的 spring initializr,或者直接在 pom.xml 里添加依赖。核心依赖就三个:spring boot web starter(用于构建web应用)、mysql driver(数据库驱动)、以及 mybatis-plus 的 spring boot starter。

这里有一个细节需要注意: mybatis-plus 已经包含了 mybatis 的核心依赖,所以你不需要再单独引入 mybatis-spring-boot-starter ,引入反而可能引起版本冲突。

我的 pom.xml 关键依赖部分通常长这样:

<dependencies>
    <!-- spring boot web -->
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-web</artifactid>
    </dependency>
    <!-- mybatis-plus 起步依赖 (关键!) -->
    <dependency>
        <groupid>com.baomidou</groupid>
        <artifactid>mybatis-plus-boot-starter</artifactid>
        <version>3.5.3.1</version> <!-- 请使用当时最新稳定版 -->
    </dependency>
    <!-- mysql 驱动 -->
    <dependency>
        <groupid>mysql</groupid>
        <artifactid>mysql-connector-java</artifactid>
        <scope>runtime</scope>
    </dependency>
    <!-- lombok,用于简化实体类代码,非必须但强烈推荐 -->
    <dependency>
        <groupid>org.projectlombok</groupid>
        <artifactid>lombok</artifactid>
        <optional>true</optional>
    </dependency>
    <!-- 测试 -->
    <dependency>
        <groupid>org.springframework.boot</groupid>
        <artifactid>spring-boot-starter-test</artifactid>
        <scope>test</scope>
    </dependency>
</dependencies>

注意 :mybatis-plus 的版本请务必去官方仓库查看最新稳定版。版本不匹配是启动报错的常见原因之一。

依赖搞定后,就是 application.yml (或 application.properties )的配置了。基础的数据库连接和 mp 的配置必不可少:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.driver
    url: jdbc:mysql://localhost:3306/mp_demo?useunicode=true&characterencoding=utf-8&usessl=false&servertimezone=asia/shanghai
    username: root
    password: your_password
# mybatis-plus 配置
mybatis-plus:
  configuration:
    # 控制台打印执行 sql 及参数,开发环境非常有用
    log-impl: org.apache.ibatis.logging.stdout.stdoutimpl
  global-config:
    db-config:
      # 全局表前缀,如果所有表都有共同前缀如 `t_`,可以在这里配置
      # table-prefix: t_
      # 全局主键类型,auto 表示数据库自增,input 表示手动输入,assign_id 表示雪花算法id
      id-type: assign_id

这里我强烈建议在开发阶段开启 log-impl 配置,它会在控制台打印出 mp 最终执行的 sql 语句和参数,对于调试和理解 mp 的行为至关重要。看到生成的 sql,你心里才踏实。

2.2 实体类与 mapper 接口定义

接下来,我们创建一个简单的 user 实体类,对应数据库中的 user 表。这里会用到 mp 的核心注解。

package com.example.mpdemo.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.data;
import java.time.localdatetime;

@data // lombok 注解,自动生成 getter/setter/tostring 等方法
@tablename("user") // 指定关联的数据库表名,如果类名和表名一致(忽略大小写),可省略
public class user {
    /**
     * 主键
     * @tableid 注解用于标识主键
     * type = idtype.assign_id:使用雪花算法生成 long 类型 id (默认策略)
     * 如果数据库是自增,则使用 idtype.auto
     */
    @tableid(type = idtype.assign_id)
    private long id;

    /**
     * 用户名
     */
    private string username;

    /**
     * 密码
     */
    private string password;

    /**
     * 年龄
     */
    private integer age;

    /**
     * 邮箱
     */
    private string email;

    /**
     * 创建时间
     * @tablefield 注解用于配置非主键字段
     * fill = fieldfill.insert:在插入操作时自动填充
     */
    @tablefield(fill = fieldfill.insert)
    private localdatetime createtime;

    /**
     * 更新时间
     * fill = fieldfill.insert_update:在插入和更新操作时自动填充
     */
    @tablefield(fill = fieldfill.insert_update)
    private localdatetime updatetime;

    /**
     * 逻辑删除标识(0-未删除,1-已删除)
     * @tablelogic 注解标识逻辑删除字段
     * value = “0” 表示未删除的值
     * delval = “1” 表示已删除的值
     */
    @tablelogic(value = “0”, delval = “1”)
    private integer deleted;
}

实体类定义好了,mapper 接口就简单得不可思议。你只需要创建一个接口,继承 mp 提供的 basemapper ,并指定泛型为你的实体类。

package com.example.mpdemo.mapper;

import com.baomidou.mybatisplus.core.mapper.basemapper;
import com.example.mpdemo.entity.user;

// 无需写任何方法,继承 basemapper 即拥有了基础的 crud 方法
public interface usermapper extends basemapper<user> {
}

是的,就这样。 basemapper<user> 这个泛型接口,已经为你预定义了数十个通用方法。你不需要写实现类,mp 会在运行时通过动态代理帮你生成。记得在 spring boot 主类或者配置类上加上 @mapperscan(“com.example.mpdemo.mapper”) 注解,告诉 spring 去哪里扫描这些 mapper 接口。

3. 核心 crud 操作详解与实战

环境搭好,基础代码写完,现在我们来真正操作数据库。我会把增、删、改、查分开讲,并融入一些实际开发中更实用的技巧。

3.1 插入(create)操作的多场景应用

基础的插入就是 insert 方法。但实际项目中,我们往往有更多需求。

基础插入:

@autowired
private usermapper usermapper;

public void testinsert() {
    user user = new user();
    user.setusername(“张三”);
    user.setpassword(“123456”);
    user.setage(25);
    user.setemail(“zhangsan@example.com”);
    // createtime 和 updatetime 由 mp 的自动填充处理器处理,这里不用set

    int rows = usermapper.insert(user); // 返回影响的行数
    system.out.println(“插入成功,影响行数:” + rows);
    system.out.println(“插入后,用户id为:” + user.getid()); // mp 会自动回填主键 id
}

执行后,控制台会打印类似 insert into user ( id, username, password, age, email, create_time, update_time ) values ( ?, ?, ?, ?, ?, ?, ? ) 的 sql。你会发现,即使我们没有设置 id 、 create_time 、 update_time ,mp 也帮我们处理了。 id 根据 @tableid 的策略(这里是雪花算法)生成并回填到实体对象中,时间字段则由我们后面要配置的“元对象处理器”自动填充。

批量插入: 对于一次性插入多条数据,使用 insert 方法循环效率较低。mp 提供了 insertbatchsomecolumn 方法(需配合 sql 注入器,默认已注入),但更通用和高效的做法是使用 service 层的 savebatch 方法(后面会讲到 service)。这里先提一个思路:在实际高性能场景下,可以考虑使用 mp 的 sqlinjector 配合自定义方法,或者直接使用 mybatis 的 foreach 标签编写 xml。对于大多数业务, service.savebatch 足够用。

插入时忽略字段: 假设 user 对象是从前端接收的,里面可能包含一个前端生成的临时id,而数据库id是自增或雪花算法生成的。你希望插入时忽略实体类中的某个字段(比如一个非数据库字段的 token ),可以使用 @tablefield(exist = false) 注解标记该字段,mp 在构建 sql 时会自动忽略它。

3.2 查询(read)操作的丰富 api

查询是业务中最频繁的操作。 basemapper 提供了从根据id查询到复杂条件组合查询的一系列方法。

根据 id 查询:

user user = usermapper.selectbyid(1544865477235572737l);

非常简单直接。

根据条件查询单个:

querywrapper<user> querywrapper = new querywrapper<>();
querywrapper.eq(“username”, “张三”) // where username = ‘张三'
             .eq(“age”, 25);        // and age = 25
user user = usermapper.selectone(querywrapper);

selectone 期望查询结果只有一条。如果查询到多条会抛出 toomanyresultsexception 。 querywrapper 是 mp 条件构造器的核心,用于构建 where 子句。

根据条件查询列表:

querywrapper<user> querywrapper = new querywrapper<>();
querywrapper.like(“username”, “张”) // where username like ‘%张%'
             .gt(“age”, 20)         // and age > 20
             .orderbydesc(“age”);   // order by age desc
list<user> userlist = usermapper.selectlist(querywrapper);

like 、 gt (greater than)、 orderbydesc 这些方法语义非常清晰,像拼积木一样构建查询条件。

分页查询: 分页是后台管理系统标配。mp 提供了强大的分页插件,需要先配置。

  1. 配置分页插件: 在配置类中注入 paginationinnerinterceptor 。
    @configuration
    public class mybatisplusconfig {
        @bean
        public mybatisplusinterceptor mybatisplusinterceptor() {
            mybatisplusinterceptor interceptor = new mybatisplusinterceptor();
            // 添加分页插件
            interceptor.addinnerinterceptor(new paginationinnerinterceptor(dbtype.mysql));
            return interceptor;
        }
    }
    
  2. 使用分页查询:
    @test
    public void testselectpage() {
        page<user> page = new page<>(1, 10); // 查询第1页,每页10条
        querywrapper<user> querywrapper = new querywrapper<>();
        querywrapper.isnotnull(“email”);
        page<user> userpage = usermapper.selectpage(page, querywrapper);
        system.out.println(“总记录数:” + userpage.gettotal());
        system.out.println(“总页数:” + userpage.getpages());
        system.out.println(“当前页数据:” + userpage.getrecords());
    }
    
    执行后,mp 会先执行一条 count(*) 查询总记录数,再执行带 limit 的分页查询。这是物理分页,数据量大时要注意性能。

选择特定字段查询: 有时我们只需要部分字段,避免 select * 。 querywrapper 的 select 方法可以指定字段。

querywrapper<user> querywrapper = new querywrapper<>();
querywrapper.select(“id”, “username”, “email”) // 只查询这三个字段
             .like(“username”, “admin”);
list<user> userlist = usermapper.selectlist(querywrapper);

这生成的 sql 是 select id, username, email from user where username like ‘%admin%’ ,对于大表或宽表能有效减少网络传输和内存占用。

3.3 更新(update)操作与乐观锁实践

更新操作最常用的是根据 id 更新。但直接全字段更新可能带来风险,mp 推荐使用 updatewrapper 进行动态更新。

根据 id 更新实体:

user user = new user();
user.setid(1544865477235572737l);
user.setage(26);
user.setemail(“new_email@example.com”);
int rows = usermapper.updatebyid(user); // update user set age=?, email=? where id=?

这里只会更新 user 对象中非 null 的字段。这是 mp 的默认策略,非常智能。

根据条件更新:

updatewrapper<user> updatewrapper = new updatewrapper<>();
updatewrapper.eq(“age”, 25) // where age = 25
             .set(“age”, 26) // set age = 26
             .set(“email”, “updated@example.com”);
int rows = usermapper.update(null, updatewrapper); // 第一个参数为 null 表示不传入实体

或者使用 lambda 表达式,避免字段名写错字符串:

lambdaupdatewrapper<user> lambdaupdatewrapper = new lambdaupdatewrapper<>();
lambdaupdatewrapper.eq(user::getage, 25)
                   .set(user::getage, 26)
                   .set(user::getemail, “updated@example.com”);
int rows = usermapper.update(null, lambdaupdatewrapper);

乐观锁实现: 在高并发更新场景下,防止数据覆盖是关键。mp 内置了乐观锁支持。

  1. 在实体类版本字段上加 @version 注解。
    @version
    private integer version;
    
  2. 在配置类中注册乐观锁插件。
    // 在刚才的 mybatisplusconfig 的拦截器里添加
    interceptor.addinnerinterceptor(new optimisticlockerinnerinterceptor());
    
  3. 更新时,mp 会自动使用版本号。
    user user = usermapper.selectbyid(1l); // 此时 user.version = 0
    user.setage(user.getage() + 1);
    usermapper.updatebyid(user); // sql: update user set age=?, version=? where id=? and version=?
    // 如果此时该记录已被其他线程修改(version已变),则本次更新影响行数为0,可据此判断更新失败。
    
    乐观锁能有效解决“丢失更新”问题,是并发编程的常用手段。

3.4 删除(delete)操作与逻辑删除配置

删除分为物理删除和逻辑删除。物理删除直接从数据库抹掉数据,风险高。生产环境更推荐使用逻辑删除。

物理删除:

// 根据id删除
int rows = usermapper.deletebyid(1l);
// 根据条件删除
querywrapper<user> querywrapper = new querywrapper<>();
querywrapper.lt(“age”, 18); // where age < 18
int rows = usermapper.delete(querywrapper);

逻辑删除: 逻辑删除是更新一个标记字段,而非真正删除记录。我们之前在 user 实体中定义的 deleted 字段就是用于此目的。

  1. 配置全局逻辑删除规则(在 application.yml 中):
    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: deleted # 全局逻辑删除的实体字段名
          logic-delete-value: 1 # 逻辑已删除值(默认为 1)
          logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
  2. 在实体类字段上添加 @tablelogic 注解(前面已加)。
  3. 执行删除操作:
    usermapper.deletebyid(1l);
    
    此时,mp 实际执行的 sql 是: update user set deleted=1 where id=? and deleted=0 。所有自动生成的查询语句,也会自动加上 and deleted=0 条件。这样,对于业务层来说,调用 delete 方法“删除”了数据,调用 select 方法也查不到被“删除”的数据,实现了无感知的逻辑删除。如果需要查询包含已逻辑删除的数据,需要使用自定义 sql 或 wrapper 手动覆盖条件。

4. 进阶技巧与 service 层封装

直接使用 mapper 进行 crud 在简单场景下没问题,但业务复杂后,我们通常会在 mapper 之上再封装一层 service 。mp 同样为我们提供了强大的 iservice 接口和 serviceimpl 实现类。

4.1 创建 service 接口与实现

// userservice 接口,继承 mp 的 iservice
public interface userservice extends iservice<user> {
    // 这里可以定义自己的业务方法
    list<user> selectusersbycomplexcondition(customquery query);
}

// userserviceimpl 实现类
@service
public class userserviceimpl extends serviceimpl<usermapper, user> implements userservice {
    @autowired
    private someothercomponent component; // 可以注入其他组件

    @override
    public list<user> selectusersbycomplexcondition(customquery query) {
        // 实现自定义复杂业务逻辑
        lambdaquerywrapper<user> wrapper = new lambdaquerywrapper<>();
        // ... 构建复杂条件
        return this.list(wrapper);
    }
}

service 层继承了 iservice ,它提供了比 basemapper 更丰富的业务方法,例如 savebatch (批量插入)、 listbyids (根据id集合查询)、 updatebatchbyid (根据id批量更新)等,这些方法都经过了优化。

4.2 自动填充与类型处理器

自动填充(metaobjecthandler): 我们之前实体类中的 createtime 和 updatetime 字段标记了 @tablefield(fill = …) ,现在需要实现处理器来为它们赋值。

@component // 必须声明为 spring 组件
public class mymetaobjecthandler implements metaobjecthandler {
    @override
    public void insertfill(metaobject metaobject) {
        this.strictinsertfill(metaobject, “createtime”, localdatetime.class, localdatetime.now());
        this.strictinsertfill(metaobject, “updatetime”, localdatetime.class, localdatetime.now());
        // 也可以填充其他字段,例如当前用户id
        // this.strictinsertfill(metaobject, “createby”, string.class, getcurrentuserid());
    }

    @override
    public void updatefill(metaobject metaobject) {
        this.strictupdatefill(metaobject, “updatetime”, localdatetime.class, localdatetime.now());
    }
}

这样,在执行 insert 或 update 方法时,mp 会自动调用相应方法为指定字段填充值。 strictinsertfill 和 strictupdatefill 是 mp 3.3.0 后推荐的方法,它能进行严格的类型匹配,避免出错。

类型处理器(typehandler): 如果数据库字段类型和 java 属性类型不是常规映射(比如把对象存成 json 字符串),可以使用 @tablefield(typehandler = …) 指定类型处理器。mp 内置了常用的处理器,如 jacksontypehandler 用于 json。

4.3 条件构造器的 lambda 表达式写法

之前我们用 querywrapper 时,条件字段名是用字符串表示的,这容易写错且重构不友好。mp 强烈推荐使用基于 lambda 表达式的条件构造器。

lambdaquerywrapper<user> lambdaquerywrapper = new lambdaquerywrapper<>();
lambdaquerywrapper.eq(user::getusername, “张三”) // 方法引用,编译期安全
                  .between(user::getage, 20, 30)
                  .isnotnull(user::getemail)
                  .orderbydesc(user::getcreatetime);
list<user> list = usermapper.selectlist(lambdaquerywrapper);

lambdaupdatewrapper 用法类似。这种方式利用 java 编译器的类型检查,彻底告别字段名的魔法字符串,是提升代码健壮性的最佳实践。

5. 常见问题排查与性能优化心得

在实际使用中,你肯定会遇到一些“坑”。这里我总结几个高频问题和我的处理经验。

5.1 启动或运行时常见异常

  1. invalid bound statement (not found) :

    • 原因 :这是最经典的 mybatis 异常。mp 动态代理 mapper 接口,需要找到对应的 sql 映射。可能的原因有:
      • mapper 接口没有被扫描到。检查 @mapperscan 注解的路径是否正确。
      • mybatisplusconfig 配置类没有被 spring 管理(缺少 @configuration )。
      • 实体类的主键注解 @tableid 配置错误,或者实体类与数据库表映射有问题。
    • 排查 :首先检查控制台启动日志,看是否有 mapped “…” 这样的语句,确认你的 mapper 被成功加载。开启 mp 的 sql 日志,看执行时是否生成了正确的 sql。
  2. 插入时主键冲突:

    • 原因 : @tableid 策略配置与数据库不匹配。例如,数据库字段是 auto_increment ,但实体类配置了 @tableid(type = idtype.assign_id) (雪花算法),插入时 mp 会尝试设置一个 long 型 id,与数据库自增冲突。
    • 解决 :确保 id-type 配置与数据库表设计一致。自增用 auto ,手动赋值用 input ,分布式场景用 assign_id (雪花算法)或 assign_uuid 。
  3. 字段更新为 null 失效:

    • 现象 :使用 updatebyid(entity) 时,即使 entity 的某个字段为 null ,该字段在数据库中也未被更新为 null 。
    • 原因 :mp 的默认更新策略是“非 null 更新”,即只更新实体对象中不为 null 的字段。这是为了防止意外覆盖。
    • 解决 :
      • 如果确实需要更新为 null ,使用 updatewrapper 的 set 方法: .set(“fieldname”, null) 。
      • 或者,在字段上使用 @tablefield(strategy = fieldstrategy.ignored) ,但这样会全局忽略该字段的 null 判断,需谨慎。

5.2 性能优化与最佳实践

  • 慎用 select * : 尽量使用 querywrapper.select(…) 指定需要查询的字段,尤其是在表字段很多或有关联查询时。这能显著减少数据库服务器和网络传输的数据量。
  • 分页查询优化: mp 的物理分页( page )在数据量极大(百万级以上)时, count(*) 操作可能会很慢。对于不需要精确总数的情况,可以考虑使用“滚动查询”或者前端“加载更多”的模式。如果需要精确分页且 count 慢,可以尝试对条件字段加索引,或者使用其他近似计数方案。
  • 批量操作使用 service 方法: iservice 提供的 savebatch 、 updatebatchbyid 等方法,内部使用了 executortype.batch 模式,比在循环中单条执行 insert/update 性能高得多。对于大批量数据操作,务必使用这些批量方法。
  • 复杂查询或联表查询: mp 的条件构造器对于单表动态 sql 构建是利器,但对于复杂的多表关联查询,或者非常定制化的 sql(如窗口函数、复杂子查询),它的表达能力会受限。 不要强行用 wrapper 去拼装过于复杂的 sql ,那样代码可读性会变得很差。这时,退一步,回到 mybatis 的老本行——在 xml 映射文件或注解中编写清晰、可维护的 sql,是更明智的选择。mp 和原生 mybatis 是兼容的,你完全可以在同一个 mapper 接口中,既有 mp 生成的通用方法,也有自己写的自定义 sql 方法。
  • 监控生成的 sql: 始终在开发环境开启 mybatis-plus.configuration.log-impl 配置。观察生成的 sql 是否符合你的预期,特别是条件构造器生成的 where 子句、分页的 limit 语句。这是调试和性能分析的第一步。

最后,我想说的是,mybatis-plus 是一个极大地提升了开发效率的工具,但它不是银弹。理解它的原理,知道它的边界(比如复杂 sql),在合适的场景使用它,才能让它真正成为你开发中的得力助手,而不是新的负担。从简单的 crud 开始,逐步尝试条件构造器、分页、乐观锁、自动填充这些特性,你会慢慢体会到它“简化开发”的哲学。

到此这篇关于springboot中高效crud与进阶的实战的文章就介绍到这了,更多相关springboot 高效crud内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

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

发表评论

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