MyBatisPlus 用法详解

news2024/11/26 14:40:30

文章目录

  • 一、快速入门
    • 1.1 引入依赖:
    • 1.2 定义 Mappper:
    • 1.3 使用演示:
    • 1.4 常见注解:
      • 1.4.1 @TableName:
      • 1.4.2 @TableId:
      • 1.4.3 @TableField:
    • 1.5 常见配置:
  • 二、核心功能
    • 2.1 条件构造器:
      • 2.1.1 QueryWrapper:
      • 2.1.2 UpdateWrapper:
      • 2.1.3 LambdaQueryWrapper || LambdaUpdateWrapper:
    • 2.2 自定义 SQL:
      • 2.2.1 基本用法:
      • 2.2.2 多表关联:
    • 2.3 Service 接口:
      • 2.3.1 CRUD:
      • 2.3.2 基本用法:
      • 2.3.3 Lambda:
      • 2.3.4 批量新增:
  • 三、扩展功能
    • 3.1 代码生成插件:
      • 3.1.1 安装插件:
      • 3.1.2 使用演示:
    • 3.2 静态工具类:
    • 3.3 逻辑删除
    • 3.4 通用枚举:
      • 3.4.1 创建枚举对象并添加 @EnumValue 注解
      • 3.4.2 配置枚举处理器:
      • 3.4.3 @JsonValue 注解:
    • 3.5 JSON 类型处理器:
      • 3.5.1 定义存储实体:
      • 3.5.2 使用类型处理器:
  • 四、分页插件
    • 4.1 配置分页插件:
    • 4.2 使用演示:

一、快速入门

1.1 引入依赖:

MybatisPlus 提供了 starter,实现了自动 Mybatis 以及MybatisPlus 的自动装配功能,坐标如下:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

这个 starter 包含对 mybatis 的自动装配,因此完全可以替换掉 Mybatis 的starter。

1.2 定义 Mappper:

为了简化单表的 CRUD,MybatisPlus 提供了一个基础的BaseMapper接口,其中已经实现了单表的 CRUD,具体方法如下图所示:

image-20241108140332697

因此我们自定义的 Mapper 只要继承了这个BaseMapper<T>,就无需自己实现单表 CRUD 了。

注意:继承 baseMapper 需要指定对应的实体类。

1.3 使用演示:

//Mapper:
public interface UserMapper extends BaseMapper<User> {
}
//实体类
@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String phone;
    private String info;
    private Integer status;
    private Integer balance;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

//测试类
@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testInsert() {
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userMapper.insert(user);// MybatisPlus 提供的
    }

    @Test
    void testSelectById() {
        User user = userMapper.selectById(5L);// MybatisPlus 提供的
        System.out.println("user = " + user);
    }


    @Test
    void testQueryByIds() {
        List<User> users = userMapper.selectBatchIds(List.of(1L, 2L, 3L, 4L));// MybatisPlus 提供的
        users.forEach(System.out::println);
    }

    @Test
    void testUpdateById() {
        User user = new User();
        user.setId(5L);
        user.setBalance(20000);
        userMapper.updateById(user);// MybatisPlus 提供的
    }

    @Test
    void testDeleteUser() {
        userMapper.deleteById(5L);// MybatisPlus 提供的
    }
}

效果如下图所示:由于方法格式过多,所以下面这张图片只展示更新部分的。

image-20241108162919294

1.4 常见注解:

MybatisPlus 就是根据 PO 实体(BaseMapper<T>中的T)的信息来推断出表的信息,从而生成 SQL 的。默认情况下:

  • MybatisPlus 会把 PO 实体的类名驼峰转下划线作为表名。
  • MybatisPlus 会把 PO 实体的所有变量名驼峰转下划线作为表的字段名,并根据变量类型推断字段类型。
  • MybatisPlus 会把名为 id 的字段作为主键。

但很多情况下,默认的实现与实际场景不符,因此 MybatisPlus 提供了一些注解便于我们声明表信息。

1.4.1 @TableName:

使用方式:表名注解,标识实体类对应的表。

使用位置:实体类。

使用场景:当实体类的名称和表名的命名不一致时(驼峰和下划线的区别另外),使用该注解。

TableName 注解除了指定表名以外,还可以指定很多其它属性:

属性类型必须指定默认值描述
valueString“”表名
schemaString“”schema
keepGlobalPrefixbooleanfalse是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMapString“”xml 中 resultMap 的 id(用于满足特定类型的实体类对象绑定)
autoResultMapbooleanfalse是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludePropertyString[]{}需要排除的属性名 @since 3.3.1

1.4.2 @TableId:

使用方式:主键注解,标识实体类中的主键字段。

使用位置:实体类的主键字段。

使用场景:需要指定主键时,如果是按照规范进行操作,不需要使用该注解指定,有默认值。

TableId注解支持两个属性:

属性类型必须指定默认值描述
valueString“”主键字段名
typeEnumIdType.NONE指定主键类型

IdType支持的类型有:

描述
AUTO数据库 ID 自增
NONE无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 ASSIGN_ID)
INPUTinsert 前自行 set 主键值
ASSIGN_ID分配 ID(主键类型为 Number(Long 和 Integer)或 String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法),简单理解为程序自动利用雪花算法,帮助我们生成 id。
ASSIGN_UUID分配 UUID,主键类型为 String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认 default 方法)

这里比较常见的有三种:

  • AUTO:利用数据库的id自增长
  • INPUT:手动生成id
  • ASSIGN_ID:雪花算法生成Long类型的全局唯一id,这是默认的ID策略。

1.4.3 @TableField:

使用方式:普通字段注解,标识实体类中的普通属性。

使用位置:实体类的属性。

使用场景:当实体类的命名和数据库的字段名有出路时,使用该注解。

一般情况下我们并不需要给字段添加@TableField注解,一些特殊情况除外(非常重要):

  • 成员变量名与数据库字段名不一致
  • 成员变量是以isXXX命名,按照JavaBean的规范,MybatisPlus识别字段时会把is去除,这就导致与数据库不符。
  • 成员变量名与数据库一致,但是与数据库的关键字冲突。使用@TableField注解给字段名添加转义字符:``

支持的其它属性如下:

属性类型必填默认值描述
valueString“”数据库字段名
existbooleantrue是否为数据库表字段
conditionString“”字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s},参考(opens new window)
updateString“”字段 update set 部分注入,例如:当在version字段上注解update=“%s+1” 表示更新时会 set version=version+1 (该属性优先级高于 el 属性)
insertStrategyEnumFieldStrategy.DEFAULT举例:NOT_NULL insert into table_a(column) values (#{columnProperty})
updateStrategyEnumFieldStrategy.DEFAULT举例:IGNORED update table_a set column=#{columnProperty}
whereStrategyEnumFieldStrategy.DEFAULT举例:NOT_EMPTY where column=#{columnProperty}
fillEnumFieldFill.DEFAULT字段自动填充策略
selectbooleantrue是否进行 select 查询
keepGlobalFormatbooleanfalse是否保持使用全局的 format 进行处理
jdbcTypeJdbcTypeJdbcType.UNDEFINEDJDBC 类型 (该默认值不代表会按照该值生效)
typeHandlerTypeHander类型处理器 (该默认值不代表会按照该值生效)
numericScaleString“”指定小数点后保留的位数

1.5 常见配置:

MybatisPlus 也支持基于 yaml 文件的自定义配置。

大多数的配置都有默认值,因此我们都无需配置。但还有一些是没有默认值的,例如:

  • 实体类的别名扫描包(用来简化,后续引用实体类的路径。这个即使配置,我还是建议大家把路径写全,因为这个有时会不生效,而且编译器会更加支持写全)。

  • 全局 id 类型。

mybatis-plus:
  type-aliases-package: xxxxxx # 填写实体类对应的包名
  global-config:
    db-config:
      id-type: auto # 全局id类型为自增长

需要注意的是,MyBatisPlus 也支持手写 SQL 的,而 mapper 文件的读取地址可以自己配置:

mybatis-plus:
  mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper.xml文件地址,当前这个是默认值。

二、核心功能

刚才的案例中都是以 id 为条件的简单 CRUD,一些复杂条件的 SQL 语句就要用到一些更高级的功能了。

2.1 条件构造器:

除了新增以外,修改、删除、查询的 SQL 语句都需要指定 where 条件。因此 BaseMapper 中提供的相关方法除了以id作为where条件以外,还支持更加复杂的where条件。

image-20241109094626662

参数中的Wrapper就是条件构造的抽象类,其下有很多默认实现,继承关系如图:

image-20241109094741439

Wrapper的子类AbstractWrapper提供了where中包含的所有条件构造方法:(下面的这些方法中,一般来说,第一个参数是 boolean 类型的,是用来做出动态 SQL 中 if 标签同样的效果,如果为 true,表示该条件会添加到 SQL 中,如果为 false,就不会在 SQL 中显示该条件。

image-20241109094833251

而 QueryWrapper 在 AbstractWrapper 的基础上拓展了一个 select 方法,允许指定查询字段:

image-20241109095236012

而 UpdateWrapper 在 AbstractWrapper 的基础上拓展了一个 set 方法,允许指定 SQL 中的 SET 部分:

image-20241109095317601

2.1.1 QueryWrapper:

无论是修改、删除、查询,都可以使用 QueryWrapper 来构建查询条件。

下面举个例子:

查询:查询出名字中带o的,存款大于等于 1000 元的人的 id,username,info,balance。代码如下:

@Test
void testQueryWrapper() {
    //1. 构造查询条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<User>()
            .select("id","username","info","balance")
            .like("username", "o")
            .ge("balance", 1000);
    //2. 进行查询
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
}

更新:更新用户名为 jack(Mysql 中,是不区分大小写的) 的用户的余额为 2000,代码如下:

@Test
void testUpdateByQueryWrapper(){
    //1. 构造更新条件
    //1.1 构造更新参数
    User user = new User();
    user.setBalance(2000);
    //1.2 构造更新条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<User>()
            .eq("username","jack");
    //2. 进行更新
    userMapper.update(user, queryWrapper);
}

2.1.2 UpdateWrapper:

上面 BaseMapper 中的 update 方法传入 QueryWrapper 的条件更新只能直接进行复制,对于一些复杂的需求就难以实现。

例如:更新id为1,2,4的用户的余额,扣 200,对应的 SQL 应该是:

UPDATE user SET balance = balance - 200 WHERE id in (1, 2, 4)

SET 的赋值结果是基于字段现有值的,这个时候就要利用 UpdateWrapper 中的 setSql 功能了:

@Test
void testUpdateWrapper(){
    //1. 构造更新条件
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<User>()
            .setSql("balance = balance - 200")
            .in("id",List.of(1L,2L,4L));
    //2. 进行更新
    userMapper.update(null, updateWrapper);
}

2.1.3 LambdaQueryWrapper || LambdaUpdateWrapper:

无论是 QueryWrapper 还是 UpdateWrapper 在构造条件的时候都需要写死字段名称,这在编程规范中显然是不推荐的。

那怎么样才能不写字段名,又能知道字段名呢?

一种办法是基于变量的gettter方法结合反射技术。因此我们只要将条件对应的字段的getter方法传递给 MybatisPlus,它就能计算出对应的变量名,从而知道字段名。而传递方法可以使用 JDK8 中的方法引用Lambda表达式。 因此 MybatisPlus 又提供了一套基于Lambda 的 Wrapper ,包含两个:

  • LambdaQueryWrapper
  • LambdaUpdateWrapper

分别对应 QueryWrapper 和 UpdateWrapper。

LambdaQueryWrapper 的使用演示如下:

@Test
void testLambdaQueryWrapper() {
    //1. 构造查询条件
    LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<User>()
            .select(User::getId, User::getUsername, User::getInfo, User::getBalance)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000);
    //2. 进行查询
    List<User> users = userMapper.selectList(lambdaQueryWrapper);
    users.forEach(System.out::println);
}

LambdaUpdateWrapper 的使用演示如下:(需要注意的是,如果是使用 setSql 的话,是不能使用 Lambda 表达式的,如果需要达成在原来的基础上进行扣减,且不使用 setSql 的话,可以从 Java 代码的业务逻辑出发,先查出当前 user 的信息,在对该 user 的余额进行扣减,再更新回去即可,实现起来并不难,所以下面演示的代码就简单的进行赋值)。

@Test
void testLambdaUpdateWrapper() {
    // 1. 构造更新条件
    LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<User>()
            .set(User::getBalance, 1000)
            .in(User::getId, List.of(1L, 2L, 3L));
    //2. 进行更新
    userMapper.update(null, lambdaUpdateWrapper);
}

2.2 自定义 SQL:

在演示 UpdateWrapper 的案例中,我们在代码中编写了更新的 SQL 语句:

image-20241109110422232

这种写法在某些企业也是不允许的,因为 SQL 语句最好都维护在持久层,而不是业务层。 就当前案例来说,由于条件是 in 语句,只能将SQL 写在 Mapper.xml 文件,利用 foreach 来生成动态 SQL。 这实在是太麻烦了。假如查询条件更复杂,动态 SQL 的编写也会更加复杂。

所以,MybatisPlus 提供了自定义 SQL 功能,可以让我们利用 Wrapper 生成查询条件,再结合 Mapper.xml 编写 SQL。这样的做法算是综合了一下利弊,将 where 之前的 sql 语句放在 Mapper 层,进行编写,where 后面的语句在业务层进行编写,虽然还是不符合规范,但是较之前全部 sql 都放在业务层,也算是有了提升,MybatisPlus 的优势在于编写 where 条件非常的方便,使用 xml 文件进行动态 sql 编写,太繁琐了。

2.2.1 基本用法:

@Test
void testCustomWrapper(){
    //1. 构造 Wrapper 条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<User>()
            .in("id", List.of(1L, 2L, 4L));
    //2. 进行扣除余额
    userMapper.deductBalanceByIds(queryWrapper,200);
}
// Mapper 层
void deductBalanceByIds(@Param("ew") QueryWrapper<User> queryWrapper, @Param("balance") int balance);
// xml 文件
<update id="deductBalanceByIds">
        update user set balance = balance - #{balance} ${ew.customSqlSegment}
</update>

注意:@Param(“ew”) QueryWrapper queryWrapper,中 @Param(“ew”) 是固定写法,${ew.customSqlSegment} 也是固定写法。

2.2.2 多表关联:

理论上来讲 MyBatisPlus 是不支持多表查询的,不过我们可以利用 Wrapper 中自定义条件,结合自定义 SQL 来实现多表查询的效果。例如,我们要查询出所有收货地址在北京的并且用户 id 在1、2、4之中的用户,要是自己基于 mybatis 实现 SQL,大概是这样的:

<select id="queryUserByIdAndAddr" resultType="User">
      SELECT *
      FROM user u
      INNER JOIN address a ON u.id = a.user_id
      WHERE u.id
      <foreach collection="ids" separator="," item="id" open="IN (" close=")">
          #{id}
      </foreach>
      AND a.city = #{city}
  </select>

可以看出其中 where 的编写还是挺复杂的,如果业务复杂一些,这里的 SQL 会更变态。

但是基于自定义 SQL 结合 Wrapper 的玩法,我们就可以利用 Wrapper 来构建查询条件,然后手写 SELECT 及 FROM 部分,实现多表查询。

对应的代码如下:

// VO 实体类
@Data
public class UserVO extends User {
    private String city;
}
// Test 测试类
@Test
void testCustomJoinWrapper(){
    //1. 构造 Wrapper 条件
    QueryWrapper<UserVO> queryWrapper = new QueryWrapper<UserVO>()
        .in("u.id",List.of(1L,2L,4L))
        .eq("a.city","北京");
    //2. 进行查询
    List<UserVO> users = userMapper.queryUserAndAddressByWrapper(queryWrapper);
}
// Mapper 层代码
List<UserVO> queryUserAndAddressByWrapper(@Param("ew") QueryWrapper<UserVO> queryWrapper); 

// xml 文件中的对应代码
<select id="queryUserAndAddressByWrapper" resultType="com.gobeyye.mp.domain.vo.UserVO">
    select u.*,a.city from user u inner join address a
    on u.id = a.user_id ${ew.customSqlSegment}
</select>

对于上面这段 Mybatis plus 使用自定义 SQL 写多表查询,我还是更加建议使用原来的 Mybatis,配合使用 AI ,也不会太繁琐,且代码的可读性更好,不用一条 SQL 多个文件里面去找。

2.3 Service 接口:

MybatisPlus 不仅提供了 BaseMapper,还提供了通用的 Service 接口及默认实现,封装了一些常用的 service 模板方法。

通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:

  • save:新增
  • remove:删除
  • update:更新
  • get:查询单个结果
  • list:查询集合结果
  • count:计数
  • page:分页查询

2.3.1 CRUD:

这里带着大家一起看看 Mybatis plus 提供的方法都有什么,并解释一些常用的方法。

新增:

image-20241109151016086

  • save是新增单个元素
  • saveBatch是批量新增
  • saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
  • saveOrUpdateBatch是批量的新增或修改

删除:

image-20241109151106235

  • removeById:根据id删除
  • removeByIds:根据id批量删除
  • removeByMap:根据Map中的键值对为条件删除
  • remove(Wrapper<T>):根据Wrapper条件删除
  • removeBatchByIds:也是根据 id 批量进行删除,但是删除的实现方式不一样removeByIds:使用一条 SQL 语句,配合 in 子句一次性执行删除操作,性能比较高。removeBatchByIds:会生成多条的 SQL 语句,每条 SQL 语句逐个执行,性能比 removeByIds 差,但是适用于数据量非常大的时候,可以有效避免一条 SQL 语句过长的情况)。

修改:

image-20241109151933564

  • updateById:根据id修改
  • update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含setwhere部分
  • update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据
  • updateBatchById:根据id批量修改

Get:

image-20241109152108760

  • getById:根据id查询1条数据
  • getOne(Wrapper<T>):根据Wrapper查询1条数据
  • getBaseMapper:获取Service内的BaseMapper的子类,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper

List:

image-20241109152322943

  • listByIds:根据id批量查询
  • list(Wrapper<T>):根据Wrapper条件查询多条数据
  • list():查询所有

Count:

image-20241109152348078

  • count():统计所有数量
  • count(Wrapper<T>):统计符合Wrapper条件的数据数量

getBaseMapper:

当我们在 service 中要调用 Mapper 中自定义 SQL 时,就必须获取 service 对应的 Mapper,就可以通过这个方法:

image-20241109152710289

2.3.2 基本用法:

由于Service中经常需要定义与业务有关的自定义方法,因此我们不能直接使用IService,而是自定义Service接口,然后继承IService的拓展方法,由于都是接口,继承了之后,根据 Java 语法,需要我们自己实现,这显然不符合 Mybatis plus 的创建初衷,于是 Mybatis plus 自己提供了一个 IService 的实现类 ServiceImpl,我们自己的实现类只要继承该实现类,就可以直接使用方法了。

上面的这段文字可以抽象成下面的这张图片:

image-20241109153358606
// UserService 的接口
public interface IUserService extends IService<User> {
}

// UserService 的实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService{
}

由于这部分的代码调用演示和上面的都差不多,无非就是一个是在 Controller 层调用,一个是在 Service 层进行调用,所以这里就不再进行演示。

2.3.3 Lambda:

IService 中还提供了 Lambda 功能来简化我们的复杂查询及更新功能。

下面通过两个两个案例分别学习 lambdaQuery() 和 lambdaUpdate()。

案例一:

实现一个根据复杂条件查询用户的接口,查询条件如下:

  • name:用户名关键字,可以为空
  • status:用户状态,可以为空
  • minBalance:最小余额,可以为空
  • maxBalance:最大余额,可以为空

代码如下:

// UserQuery 实体
@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}

// Controller 层的代码
@GetMapping("/list")
    @ApiOperation("通过 UserQuery 查询用户")
    public List<User> queryUser(UserQuery query) {
        //1. 准备工作
        String username = query.getName();
        Integer status = query.getStatus();
        Integer minBalance = query.getMinBalance();
        Integer maxBalance = query.getMaxBalance();

        //2. 构造条件并进行查询
        List<User> users = userService.lambdaQuery()
                .like(username != null, User::getUsername, username)
                .eq(status != null, User::getStatus, status)
                .ge(minBalance != null, User::getBalance, minBalance)
                .le(maxBalance != null, User::getBalance, maxBalance)
                .list();
        //3. 返回结果
        return users;
    }

可以发现 lambdaQuery 方法中除了构建条件,还需要在链式编程的最后添加一个list(),这是在告诉 MybatisPlus,我们的调用结果需要是一个 list 集合。这里不仅可以用list(),可选的方法有:

  • .one():最多1个结果
  • .list():返回集合结果
  • .count():返回计数结果

MybatisPlus 会根据链式编程的最后一个方法来判断最终的返回结果。

案例二:

根据 id 修改用户余额,如果扣减后余额为 0,则将用户 status 修改为冻结状态。

代码实现如下:

@Transactional
@Override
public void deductBalance(Long id, Integer money) {
    //1. 检查用户是否存在
    User user = this.getById(id);
    if(user == null){
        throw new RuntimeException("用户不存在");
    }
    //2. 检查用户状态
    if(user.getStatus() == 2){
        throw new RuntimeException("用户状态异常");
    }
    //3. 检查用户余额是否足够
    if(user.getBalance() - money < 0){
        throw new RuntimeException("用户余额不足");
    }
    // 剩余的金额
    int newMoney = user.getBalance() - money;
    //4. 扣减余额
    this.lambdaUpdate()
            .set(User::getBalance,newMoney)
            .set(newMoney == 0, User::getStatus,2)// 动态判断是否要更新状态
            .eq(User::getId,id)
            .eq(User::getBalance,user.getBalance())// 乐观锁,防止在多线程的情况下出现异常。
            .update();
}

2.3.4 批量新增:

IService 中的批量新增功能使用起来非常方便,但有一点注意事项,我们先来测试一下。

MybatisPlus 的批处理(插入 10 万条数据):

@Test
void testSaveBatch() {
    // 准备10万条数据
    List<User> list = new ArrayList<>(1000);
    long b = System.currentTimeMillis();
    for (int i = 1; i <= 100000; i++) {
        list.add(buildUser(i));
        // 每1000条批量插入一次
        if (i % 1000 == 0) {
            userService.saveBatch(list);
            list.clear();
        }
    }
    long e = System.currentTimeMillis();
    System.out.println("耗时:" + (e - b));
}
private User buildUser(int i) {
    User user = new User();
    user.setUsername("user_" + i);
    user.setPassword("123");
    user.setPhone("" + (18688190000L + i));
    user.setBalance(2000);
    user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
    user.setCreateTime(LocalDateTime.now());
    user.setUpdateTime(user.getCreateTime());
    return user;
}

执行最终耗时如下:

image-20241109180510879

MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条 insert 语句,逐条插入数据。SQL 类似这样:

Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01

而如果想要得到最佳性能,最好是将多条 SQL 合并为一条,像这样:

INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES 
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);

这要怎么进行转化呢?

MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements。顾名思义,就是重写批处理的statement语句。

这个参数的默认值是 false,我们需要修改连接参数,将其配置为 true。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root

再次测试插入10万条数据,可以发现速度有非常明显的提升:

image-20241109181407054

三、扩展功能

3.1 代码生成插件:

在使用MybatisPlus以后,基础的MapperServicePO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成POMapperService等相关代码。只不过代码生成器同样要编码使用,也很麻烦。

这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。

3.1.1 安装插件:

Idea的 plugins 市场中搜索并安装MyBatisPlus插件:

在这里插入图片描述

然后记得重启 IDEA。

3.1.2 使用演示:

首先需要配置数据库地址,在 Idea 顶部菜单中,找到tools,选择Config Database

image-20241109212533553

在弹出的窗口中填写数据库连接的基本信息:

image-20241109212351008

点击 OK 保存。

然后再次点击 IDEA 顶部菜单中的 tools,然后选择Code Generator

image-20241109212620168

在弹出的表单中填写信息:

image-20241109213050055

最终,代码自动生成到指定的位置了。

3.2 静态工具类:

有的时候 Service 之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus 提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致(大多数需要多传递一个,数据库对应实体类的字节码),也可以帮助我们实现 CRUD 功能:

image-20241109213213879

示例:

@Test
void testDbGet() {
    User user = Db.getById(1L, User.class);
    System.out.println(user);
}

@Test
void testDbList() {
    // 利用Db实现复杂条件查询
    List<User> list = Db.lambdaQuery(User.class)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000)
            .list();
    list.forEach(System.out::println);
}

@Test
void testDbUpdate() {
    Db.lambdaUpdate(User.class)
            .set(User::getBalance, 2000)
            .eq(User::getUsername, "Rose");
}

3.3 逻辑删除

对于一些比较重要的数据,我们往往会采用逻辑删除的方案,即:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为 true
  • 查询时过滤掉标记为 true 的数据

一旦采用了逻辑删除,所有的查询和删除逻辑都要跟着变化(原先没有采用逻辑删除),非常麻烦。

为了解决这个问题,MybatisPlus 就添加了对逻辑删除的支持。

注意:只有MybatisPlus生成的SQL语句才支持自动的逻辑删除,自定义SQL需要自己手动处理逻辑删除。

具体操作如下:

首先需要给表和对应的实体类添加对应的逻辑删除字段。

接下来,我们要在application.yml中配置逻辑删除字段:(对 Mybatis 的版本有要求,大于 3.3.0)

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

测试代码如下:

@Test
void testDeleteById(){
    Db.removeById(60L, Address.class);
}

代码执行结果如下:

image-20241109220105664

综上, 开启了逻辑删除功能以后,我们就可以像普通删除一样做 CRUD,基本不用考虑代码逻辑问题。还是非常方便的。

注意:

逻辑删除本身也有自己的问题,比如:

  • 会导致数据库表垃圾数据越来越多,从而影响查询效率。
  • SQL 中全都需要对逻辑删除字段做判断,影响查询效率。

因此,如果数据不能删除,可以采用把数据迁移到其它表的方法。

3.4 通用枚举:

MybatisPlus 提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换

3.4.1 创建枚举对象并添加 @EnumValue 注解

要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。

MybatisPlus提供了@EnumValue注解来标记枚举属性:

@Getter
public enum UserStatus {
    NORMAL(1,"正常"),
    FREEZE(2,"冻结"),
    ;

    @EnumValue
    private final Integer code;
    private final String desc;

    UserStatus(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

3.4.2 配置枚举处理器:

在application.yaml文件中添加配置:

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler

测试代码:

@Test
void testService(){
    List<User> list = userService.list();
    list.forEach(System.out::println);
}

image-20241110084900278

image-20241110085312142

因此我们可以知道,在配置完枚举处理器和添加 @EnumValue 注解后,MybatisPlus 会自动帮助我们完成 Java 枚举对象和数据库列字段的转化。

3.4.3 @JsonValue 注解:

如果我们想要枚举对象在前端展示对象其中的一个属性,可以通过 @JsonValue 添加在对应的属性上,MybatisPlus 会自动帮助我们进行转化。

image-20241110085834121

未添加 @JsonValue 注解之前。

image-20241110085814010

添加 @JsonValue 注解之后。

image-20241110090018010

3.5 JSON 类型处理器:

数据库的 user 表中有一个info字段,是 JSON 类型:

image-20241110090155800

格式类似这样:

{"age": 20, "intro": "佛系青年", "gender": "male"}

但是我们 Java 当中没有 Json 这个类型,所以我们只能采用 String 来进行接收。

image-20241110090322161

这样一来,我们要读取 info 中的属性时就非常不方便。如果要方便获取,info 的类型最好是一个Map或者实体类。

而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

因此 MybatisPlus 提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理 JSON 就可以使用JacksonTypeHandler处理器。

3.5.1 定义存储实体:

@Data
public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
}

3.5.2 使用类型处理器:

接下来,将 User 类的 info 字段修改为 UserInfo 类型,并声明类型处理器:

image-20241110092022769

注意:由于这样是对象套对象,我们需要处理映射关系(resultMap),这样才能把数据传输到 info 属性上。

MybatisPlus 帮助我们处理了对象套对象的映射关系,我们只需要在对应类上添加注解 @TableName(autoResultMap = true)。即可。

image-20241110092627426

效果如下:

image-20241110092851916

四、分页插件

MybatisPlus 提供了很多的插件功能,进一步拓展其功能。目前已有的插件有:

  • PaginationInnerInterceptor:自动分页
  • TenantLineInnerInterceptor:多租户
  • DynamicTableNameInnerInterceptor:动态表名
  • OptimisticLockerInnerInterceptor:乐观锁
  • IllegalSQLInnerInterceptor:sql 性能规范
  • BlockAttackInnerInterceptor:防止全表更新与删除

这里我们以分页插件来学习插件的用法。

在未引入分页插件的情况下,MybatisPlus是不支持分页功能的,IServiceBaseMapper中的分页方法都无法正常起效。所以,我们必须配置分页插件。

4.1 配置分页插件:

在项目中新建一个配置类:

image-20241110093749899

其代码如下:

@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        //1. 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //2. 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        //3. 返回核心插件
        return interceptor;
    }
}

4.2 使用演示:

核心在于 Page 类,下面是 page 类的所有属性,既有分页查询所需的条件,也有分页查询返回的结果。

image-20241110095028300

注意:分页查询传递的参数 page 和接收的 page 是同一个对象。

@Test
void testPageQuery(){
    // 1.分页查询,new Page()的两个参数分别是:页码、每页大小
    Page<User> p = userService.page(new Page<>(2, 2));
    // 2.总条数
    System.out.println("total = " + p.getTotal());
    // 3.总页数
    System.out.println("pages = " + p.getPages());
    // 4.数据
    List<User> records = p.getRecords();
    records.forEach(System.out::println);
}

效果如下:

image-20241110094913700

这里用到了分页参数,Page,即可以支持分页参数,也可以支持排序参数。常见的 API 如下:

其中 OrderItem 的第二个参数,如果为 true 表示为升序,如果为 false 表示为降序。page 可以添加多个排序规则,当第一个排序规则相等时,采用第二个排序规则,以此类推。

int pageNo = 1, pageSize = 5;
// 分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 排序参数, 通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
//.....可以有很多个
page.addOrder(....)
// 进行分页查询
userService.page(page);

参考文献:

  • 黑马程序员

结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话,还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2237521.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python小游戏23——捕鱼达人

首先&#xff0c;你需要安装Pygame库。如果你还没有安装&#xff0c;可以使用以下命令进行安装&#xff1a; 【bash】 pip install pygame 运行效果展示 接下来是示例代码&#xff1a; 【python】 import pygame import random # 初始化Pygame pygame.init() # 屏幕尺寸 SCREEN…

库打包工具 rollup

库打包工具 rollup 摘要 **概念&#xff1a;**rollup是一个模块化的打包工具 注&#xff1a;实际应用中&#xff0c;rollup更多是一个库打包工具 与Webpack的区别&#xff1a; 文件处理&#xff1a; rollup 更多专注于 JS 代码&#xff0c;并针对 ES Module 进行打包webpa…

基于SSM+VUE小学生素质成长记录平台JAVA|VUE|Springboot计算机毕业设计源代码+数据库+LW文档+开题报告+答辩稿+部署教+代码讲解

源代码数据库LW文档&#xff08;1万字以上&#xff09;开题报告答辩稿 部署教程代码讲解代码时间修改教程 一、开发工具、运行环境、开发技术 开发工具 1、操作系统&#xff1a;Window操作系统 2、开发工具&#xff1a;IntelliJ IDEA或者Eclipse 3、数据库存储&#xff1a…

【架构设计常见技术】

EJB EJB是服务器端的组件模型&#xff0c;使开发者能够构建可扩展、分布式的业务逻辑组件。这些组件运行在EJB容器中&#xff0c;EJB将各功能模块封装成独立的组件&#xff0c;能够被不同的客户端应用程序调用&#xff0c;简化开发过程&#xff0c;支持分布式应用开发。 IOC …

优选算法 - 1 ( 双指针 移动窗口 8000 字详解 )

一&#xff1a;双指针 1.1 移动零 题目链接&#xff1a;283.移动零 class Solution {public void moveZeroes(int[] nums) {for(int cur 0, dest -1 ; cur < nums.length ; cur){if(nums[cur] 0){}else{dest; // dest 先向后移动⼀位int tmp nums[cur];nums[cur] num…

鸿蒙操作系统是什么?与安卓系统有什么区别?

鸿蒙操作系统 鸿蒙操作系统&#xff08;HarmonyOS&#xff09;是华为公司发布的一款基于微内核的面向全场景的分布式操作系统。 发展历程&#xff1a; 早期规划&#xff1a;华为从2012 年开始规划自有操作系统&#xff0c;并在芬兰赫尔辛基设立智能手机研发中心&#xff0c;招…

现场工程师日记-MSYS2迅速部署PostgreSQL主从备份数据库

文章目录 一、概要二、整体架构流程1. 安装 MSYS2 环境2. 安装postgresql 三、技术名词解释1.MSYS22.postgresql 四、技术细节1. 创建主数据库2.添加从数据库复制权限3. 按需修改参数&#xff08;1&#xff09;WAL保留空间&#xff08;2&#xff09;监听地址 4. 启动主服务器5.…

第二届计算机网络技术与电子信息工程国际学术会议(CNTEIE 2024,12月6-8日)

第二届计算机网络技术与电子信息工程国际学术会议&#xff08;CNTEIE 2024&#xff09; 2024 2nd International Conference on Computer Network Technology and Electronic and Information Engineering 重要信息 会议官网&#xff1a;www.cnteie.org 2024 2nd Internation…

Git 入门篇(一)

前言 操作系统&#xff1a;win11 64位 与gitee搭配使用 Git 入门篇&#xff08;一&#xff09; Git 入门篇&#xff08;二&#xff09; Git 入门篇&#xff08;三&#xff09; 目录 git下载、安装与配置 下载 安装 配置 git下载、安装与配置 下载 官网&#xff1a;git-…

WPS文档中的“等线”如何删除

如何删除“等线”占用的行如何删除表格之间的空行WPS文档中的“等线”是什么如果删除脚注文本占用的行 如下这种&#xff0c;在文档中添加了表格和脚注&#xff0c;发现上下表格之间有多行空行&#xff0c;鼠标选中&#xff0c;显示是“等线”&#xff0c;那么如何去除等线占用…

题目讲解15 合并两个排序的链表

原题链接&#xff1a; 合并两个排序的链表_牛客题霸_牛客网 思路分析&#xff1a; 第一步&#xff1a;写一个链表尾插数据的方法。 typedef struct ListNode ListNode;//申请结点 ListNode* BuyNode(int x) {ListNode* node (ListNode*)malloc(sizeof(ListNode));node->…

计算机网络基本概念总结

IP地址 概念 使网络中的设备都有唯一的地址标识&#xff0c;用于表示其在网络中的位置。 格式 IP地址是一个32位的二进制数&#xff0c;通常被分割为4个8位二进制数&#xff08;也就是4个字节&#xff09;&#xff0c;如&#xff1a;01100100.00001000.00001010.00000110。通常…

Pandas | 特征列大量数据异常需要填充数据时注意事项

问题描述 一组数据如下&#xff1a; df.isnull().sum()城市 0 名称 0 星级 1529 评分 0 价格 1 销量 1 省/市/区 0 坐标 0 简介 41 是否免费 0 具体地址 3 dtype: int64df[星级]0…

Science Robotics 综述揭示演化研究新范式,从机器人复活远古生物!

在地球46亿年的漫长历史长河中&#xff0c;生命的演化过程充满着未解之谜。如何从零散的化石证据中还原古生物的真实面貌&#xff1f;如何理解关键演化节点的具体过程&#xff1f;10月23日&#xff0c;Science Robotics发表重磅综述&#xff0c;首次系统性提出"古生物启发…

[编译报错]ImportError: No module named _sqlite3解决办法

1. 问题描述&#xff1a; 在使用python进行代码编译时&#xff0c;提示下面报错&#xff1a; "/home/bspuser/BaseTools/Source/Python/Workspace/WorkspaceDatabase.py", line 18, in <module>import sqlite3File "/usr/local/lib/python2.7/sqlite3/_…

EasyExcel的AbstractColumnWidthStyleStrategy注入CellStyle不生效

设置背景色 CellStyle style workbook.createCellStyle();style.setFillForegroundColor(IndexedColors.RED.getIndex()); // 是设置前景色不是背景色style.setFillPattern(FillPatternType.SOLID_FOREGROUND)EasyExcel.writerTable(0).head(Head1.class).registerWriteHandl…

iphone怎么删除重复的照片的新策略

Phone用户常常面临存储空间不足的问题&#xff0c;其中一个主要原因是相册中的重复照片。这些重复项不仅占用了大量的存储空间&#xff0c;还会影响设备的整体性能。本文将向您展示iphone怎么删除重复的照片的方法&#xff0c;包括一些利用工具来自动化这个过程的创新方法。 识…

AI4SCIENSE(鄂维南院士:再谈AI for Science)

鄂维南院士&#xff1a;再谈AI for Science_哔哩哔哩_bilibili 以往处理高维问题 量子力学&#xff1a;单变量乘积 统计学&#xff1a;旋转 AI4S 处理数据 蛋白质折叠&#xff1f; 不是纯粹的数据驱动 物理学等学科基本原理 例&#xff1a;分子动力学 数据模型 流程图 这…

learn C++ NO.31——类型转换

C语言中的类型转换 在C语言中&#xff0c;当赋值符号两边的类型不匹配的时候&#xff0c;或者是形参类型和实参类型不匹配时&#xff0c;返回值类型与接受返回值类型不匹配时&#xff0c;都会需要类型转换。C语言的类型转换有两种&#xff1a;显示类型转换和隐式类型转换。 显…

基于Spring Boot的养老保险管理系统的设计与实现,LW+源码+讲解

摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统养老保险管理系统信息管理难度大&#xff0c;容错率低&a…