SpringBoot学习小结之MyBatisPlus

25 篇文章 3 订阅
18 篇文章 0 订阅

前言

​ 在使用Mybatis时,最先开始的事情就是实体类对应的增删改查代码的编写,而且还不能省略,你不知道下个需求需不需要这个方法。几乎每个表都需要编写一套最基本的增删改查方法,主要就是 DAO 接口和 mapper.xml 文件的编写,如果表中的字段进行了修改,那么实体类,mapper 文件甚至 DAO 接口都要进行修改, 这样比较麻烦。

​ 虽然有 MyBatis Generator 这样的插件在,可以自动生成,但是会覆盖自定义的方法。有没有类似 JPA 那样不用编写sql语句的框架库呢?

​ 有的,MyBatis-Plus [简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。MP 专注于单表,可以实现让你不用写SQL, 只需要简单配置就可以 CRUD 操作,从而节省大量时间。

​ MP可以实现单表操作不需要编写 sql 语句,但是如果多表 join 的话,还是需要自己编写 sql 语句。

MP的愿景是成为 MyBatis 最好的搭档,就像 魂斗罗中的 1P、2P,基友搭配,效率翻倍。

一、Pom依赖

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

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

二、简单使用

2.1 配置

application.yml

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test
  sql:
    init:
      platform: h2
      # 项目启动后自动执行的DDL语句
      schema-locations: classpath:db/schema.sql
      # 项目启动后自动执行的DML语句
      data-locations: classpath:db/data.sql
  h2:
    console:
      enabled: true #开启web console功能
mybatis-plus:
  #配置sql打印日志
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    database-id: h2

db/scheme.sql

DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` ( id bigint, username varchar(30), age int, email varchar(50), primary key (id));

db/data.sql

INSERT INTO `user_account`(id, username, age, email) VALUES
(1, 'Eli', 18, 'Eli@example.com'),
(2, 'Jack', 10, 'Jack@example.com'),
(3, 'Tom', 28, 'Tom@example.com'),
(4, 'Sandy', 21, 'Sandy@example.com'),
(5, 'Billie', 24, 'Billie@example.com');

2.2 类

实体类 UserAccount

@Data
@Builder
//@TableName("user_account")
public class UserAccount {
    private Long id;
    private String username;
    private Integer age;
    private String email;
}

DAO 接口 UserAccountDao

@Mapper
public interface UserAccountDao extends BaseMapper<UserAccount> {
}

启动类

@SpringBootApplication
@MapperScan
public class MybatisplusdemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybatisplusdemoApplication.class, args);
    }
}

2.3 CRUD

@SpringBootTest
class MybatisplusdemoApplicationTests {

    private static final Logger logger = LoggerFactory.getLogger(MybatisplusdemoApplicationTests.class);

    @Autowired
    private UserAccountDao userAccountDao;

    @Test
    void crudTest() {
		
        UserAccount userAccount = UserAccount.builder().id(6L).username("张三").age(20).email("zs@163.com").build();
        userAccountDao.insert(userAccount);
        UserAccount zs = userAccountDao.selectOne(new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getUsername, "张三"));
        logger.info("{}", zs);
        assertThat(zs, is(userAccount));


        LambdaQueryWrapper<UserAccount> id6Wrapper = new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getId, 6L);
        LambdaQueryWrapper<UserAccount> id1Wrapper = new LambdaQueryWrapper<UserAccount>().eq(UserAccount::getId, 1L);

        
        userAccount.setAge(30);
        userAccountDao.updateById(userAccount);
        assertThat(userAccountDao.selectOne(id6Wrapper).getAge(), is(30));
        
        UserAccount update = UserAccount.builder().id(1L).username("李四").build();
        userAccountDao.update(update, id1Wrapper);
        assertThat(userAccountDao.selectOne(id1Wrapper).getUsername(), is("李四"));
        assertThat(userAccountDao.selectOne(id1Wrapper).getEmail(), is(notNullValue()));

        
        List<UserAccount> userAccounts = userAccountDao.selectList(null);
        assertThat(userAccounts, is(hasSize(6)));
        LambdaQueryWrapper<UserAccount> ageMoreThan20Wrapper = new LambdaQueryWrapper<UserAccount>().ge(UserAccount::getAge, 20);
        List<UserAccount> userAccounts2 = userAccountDao.selectList(ageMoreThan20Wrapper);
        assertThat(userAccounts2, is(hasSize(4)));

        
        userAccountDao.deleteById(userAccount.getId());
        assertThat(userAccountDao.selectOne(id6Wrapper), is(nullValue()));
    }
}

三、高级特性

3.1 分页

分页需要加上分页插件

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return interceptor;
}
@Test
void pageTest() {

    LambdaQueryWrapper<UserAccount> ageMoreThan20Wrapper = new LambdaQueryWrapper<UserAccount>()
            .ge(UserAccount::getAge, 20)
            .orderBy(true, true, UserAccount::getId);

    Page<UserAccount> userAccountPage = userAccountDao.selectPage(Page.of(2, 2), ageMoreThan20Wrapper);
    List<UserAccount> records = userAccountPage.getRecords();
    assertThat(records, is(hasSize(1)));
}

3.2 逻辑删除

开启逻辑删除功能后,查找更新自动会排除掉已逻辑删除的字段. 如果想真实删除,可以使用原生 MyBatis 功能实现

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,可以不用在实体类字段上加上@TableLogic注解)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
@Data
@Builder
//@TableName("user_account")
public class UserAccount {
    private Long id;
    private String username;
    private Integer age;
    private String email;
    private Integer flag;
}
DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` ( id bigint, username varchar(30), age int, email varchar(50), flag int default 0, primary key (id));
@Test
void logicDeleteTest() {
    userAccountDao.deleteById(1L);
    List<UserAccount> userAccounts = userAccountDao.selectList(null);
    assertThat(userAccounts, hasSize(4));
}

3.3 密码加密

v3.3.2 开始支持,可以用来加密yml中数据库配置 url, username, password

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: mpw:a9yJMQVA0N44amxF8sMVNKMo732YV7JfLJUuv8Ke82k=
#    url: jdbc:h2:mem:test

jar 包启动需要加上参数 --mpw.key=5d1bbd7c53c59da2

@SpringBootTest(args = "--mpw.key=5d1bbd7c53c59da2")
class MybatisplusdemoApplicationTests {
    @Test
    void logicDeleteTest() {
        userAccountDao.deleteById(1L);
        List<UserAccount> userAccounts = userAccountDao.selectList(null);
        assertThat(userAccounts, hasSize(4));
        String randomKey = AES.generateRandomKey();
        String encrypt = AES.encrypt("jdbc:h2:mem:test", randomKey);
        logger.info("{}, {}", randomKey, encrypt);
    }
}

3.4 自定义ID生成器

添加了 @TableId(type = IdType.ASSIGN_ID) 或者 ASSIGN_UUID 即可生成 ID

从 MybatisParameterHandler 源码可以看出

默认ASSIGN_ID使用https://gitee.com/yu120/sequence,ASSIGN_UUID 使用UUID(不含中划线)

也可以自定义生成器,需要实现 IdentifierGenerator 接口

@Data
@Builder
//@TableName("user_account")
public class UserAccount {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    private String username;
    private Integer age;
    private String email;
    private Integer flag;
}

@TableId(type = IdType.Auto) 可实现ID自增,需要在建表时指定,否则无效

db2 主键自增设置如下,实际操作时发现db2设置主键自增后,insert就不能insert id了

id bigint generated always as identity (INCREMENT BY +1)

3.5 自动填充

MP 支持类似更新时间,创建时间属性的自动填充

DROP TABLE IF EXISTS `user_account`;
CREATE TABLE `user_account` ( id bigint, username varchar(30), age int,
email varchar(50), flag int default 0, create_time datetime, update_time datetime, primary key (id));
@Component
public class DateTimeMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
    }
}
@Data
@Builder
//@TableName("user_account")
public class UserAccount {
    private Long id;
    private String username;
    private Integer age;
    private String email;
    private Integer flag;
    
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.UPDATE)
    private LocalDateTime updateTime;
}
@Test
void fillTest() {
    UserAccount userAccount = UserAccount.builder().id(6L).username("王五").age(25).email("12@qq.com").build();
    userAccountDao.insert(userAccount);
    logger.info("{}", userAccount);
    assertThat(userAccount.getCreateTime(), is(notNullValue()));

    userAccount.setAge(10);
    userAccountDao.updateById(userAccount);

    logger.info("{}", userAccount);
    assertThat(userAccount.getUpdateTime(), is(notNullValue()));
}

四、源码探秘

本节只探究 MybatisPlus BaseMapper 方法实现和 LambdaQueryWrapper , 毕竟这两部分使用的最多,源码版本为最新版 v3.5.2

4.1 BaseMapper

在使用 MyBatisPlus 时,只需要将DAO接口继承 BaseMapper ,就可以使用框架自带的 CRUD 方法了,很是方便,所以想了解它是如何实现的。

众所周知,Mybatis 是通过解析XML文件(新版本也可用注解),用动态代理来实现接口中的方法,而·MyBatisPlus不需要xml和注解也可以直接使用,下面从源码开始调试探秘

  1. 从 MapperProxyFactory 开始,这是 MyBatis 动态代理类,猜测 MyBatisPlus 对 MyBatis 增强的话,一定会在生成代理类之前将 BaseMapper 中方法的 Sql 语句生成好

  2. 发现 MyBatisPlus 从 Mybatis 复制过来 MybatisMapperProxyFactory,发现 MybatisMapperRegistry , MybatisConfiguration 都继承的是 Mybatis 里的类,这几个类都是 MyBatis 流程中很重要的类,那了解 MyBatis 的话,就可以很好过一下这个流程

  3. 首先是 MybatisMapperRegistry.addMapper ,这里会将代理类工厂包装自定义 DAO 接口,然后添加到 knownMappers ,然后通过 MybatisMapperAnnotationBuilder.parse 开始解析

    mybatis_plus_reigstter

  4. MybatisMapperAnnotationBuilder.parse 会解析 MyBatis 自带的一些注解,而 BaseMapper 自带的方法会通过 sql 注入的方式实现,在 parserInjector 这个方法处理,并且 parserInjector 这个方法默认调用了 DefaultSqlInjector.inspectInject 方法

    mybatis_plus_annotation_builder

  5. DefaultSqlInjector.inspectInject 实际调用的是父类 AbstractSqlInjector.inspectInject 方法,会对DefaultSqlInjector.getMethodList 进行注入

    在这里插入图片描述

  6. 通过AbstractMethod的各个子类(包含主键默认的话16个)进行注入,以 delete 为例,会生成xml脚本

    在这里插入图片描述

  7. 最终会将生成的语句加入到 Mybatis 容器中

    在这里插入图片描述

4.2 LambdaQueryWrapper

LambdaQueryWrapper 的父类的父类是 AbstractWrapper, 它用来构建 sql 的 where 部分, 具体如何构建详细信息可以查看 https://baomidou.com/pages/10c804/#abstractwrapper

下面是继承 BaseMapper 的 UserAccountDao 中 selectList 生成的 mybatis xml 脚本 ,可以看出 LambdaQueryWrapper 是如何作用于最终SQL生成

BaseMapper @Param(Constants.WRAPPER) Wrapper<T> queryWrapper Constants 接口中 String WRAPPER = “ew”,下表的 ew 代表 Wrapper 类

<script>
<if test="ew != null and ew.sqlFirst != null">
    ${ew.sqlFirst}
</if> 
SELECT 
<choose>
    <when test="ew != null and ew.sqlSelect != null">
        ${ew.sqlSelect}
    </when>
    <otherwise>id,username,age,email,flag,create_time,update_time</otherwise>
</choose> 
FROM user_account
<where>
    <choose>
        <when test="ew != null">
            <if test="ew.entity != null">
                <if test="ew.entity.id != null">id=#{ew.entity.id}</if>
                <if test="ew.entity['username'] != null"> AND username=#{ew.entity.username}</if>
                <if test="ew.entity['age'] != null"> AND age=#{ew.entity.age}</if>
                <if test="ew.entity['email'] != null"> AND email=#{ew.entity.email}</if>
                <if test="ew.entity['createTime'] != null"> AND create_time=#{ew.entity.createTime}</if>
                <if test="ew.entity['updateTime'] != null"> AND update_time=#{ew.entity.updateTime}</if>
            </if>
            AND flag=0
            <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.nonEmptyOfNormal">
                AND ${ew.sqlSegment}
            </if>
            <if test="ew.sqlSegment != null and ew.sqlSegment != '' and ew.emptyOfNormal">
                ${ew.sqlSegment}
            </if>
        </when>
        <otherwise>flag=0</otherwise>
    </choose>
</where>  
<if test="ew != null and ew.sqlComment != null">
    ${ew.sqlComment}
</if>
</script>

参考

  1. https://baomidou.com/
  2. https://github.com/baomidou/mybatis-plus
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aabond

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值