新的一年学习 Mybatis_Plus

本人学习于B站的 三更草堂

新的一年努力学习,明年2-3 月,高薪工作我来了~

MyBatis-Plus 轻松掌握‍:

新的一年学习 Mybatis_Plus_第1张图片

官方图标是一个 魂斗罗 表示:Mybatis 和 Plus就像兄弟一样,相辅相成

介绍:

MyBatis-Plus 简称 MP, 是一个MyBatis 的增强工具 官方网站

在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生

  • MyBatis 大家都了解吧,对于Java开发者已经是 家喻户晓,ORM 对象关系映射的,半自动化,持久层的框架
  • MyBatis-plus 是国人研发,简化了 MyBatis 的开发代码...

特性:

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  • 内置代码生成器,分页插件,支持多种数据库:MySQL Oracle DB2 HSQL
  • 内置性能分析插件 可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

SpringBoot 快速入门:

准备工作‍:

本人使用的数据库,.sql文件

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_name` varchar(20) NOT NULL COMMENT '用户名',
  `password` varchar(20) NOT NULL COMMENT '密码',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `address` varchar(100) DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

insert  into `user`(`id`,`user_name`,`password`,`name`,`age`,`address`) values 
(1,'ruiwen','123','瑞文',12,'诺克萨斯'),
(2,'gailun','1332','盖伦',13,'德玛西亚'),
(3,'timu','123','提姆',22,'约德尔'),
(4,'daji','1222','亚索',221,'艾欧尼亚');

创建一个SpringBoot 的 Maven 工程:

image-20211230005147061

① 添加依赖

pom.xml



    org.springframework.boot
    spring-boot-starter-parent
    2.5.0




    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    
    
    
        org.projectlombok
        lombok
        true
    

    
    
        com.baomidou
        mybatis-plus-boot-starter
        3.4.3
    
    
    
        mysql
        mysql-connector-java
    

② SpringBoot 启动类:

这里不过多的解释SpringBoot 了, 需要学习的小伙伴可以借鉴:Java_慈祥学习笔记

SpringBootRun.Java

//SpringBoot 启动类注解~
@SpringBootApplication
public class SpringBootRun {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootRun.class);
    }
}

③ entity 实体类:

com.wsm.entity 包下的实体类:User.Java

  • 主要是,提供JavaBean 与要查询的数据库,属性/列进行关联...
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String userName;
    private String password;
    private String name;
    private Integer age;
    private String address;
}

④ mapper 接口文件:

com.wsm.mapper 包下的 xxxMapper 接口

  • MyBatis 提倡 面向接口编程 提供每一个实体类对应的接口,与对应的Mapper.xml 映射文件进行 sql 的实现;
  • 而,MyBatis-plus 对其进行了封装

    Mapper接口,extends继承 BaseMapper 并通过 泛型指定对应的实体类... 使 xxxMapper接口, 拥有BaseMapper的所有方法();

    MyBatis-plus 对其中的方法都有其对应的实现映射,所以,只需要 继承BaseMapper 就实现了大量的常用方法, 这就是MP的简单强大之处,省去开发者的大量重复工作

    新的一年学习 Mybatis_Plus_第2张图片

    MP 提供了大量的方法, 各种的 CRUD

UserMapper.Java

@Mapper
//@Mapper 注解,使当前的Mapper 接口,被Spring进行管理,不然需要在,启动类上声明 @MapperScan("com.wsm.mapper") 类扫描指定包下,mapper接口文件;
public interface UserMapper  extends BaseMapper {
    //Mapper 接口 extends集成 BaseMapper 泛型对应的实体类;
    //  Ctrl+右击, 进入BaseMapper 中可以看到, MP 默认给对应实体类提供好的实现方法();
    //  增删改查... 即各种的, 重载 CRUD 的操作;
    
    //如果,BaseMapper 中,没有提供的,后面还可以在,该 xxxMapper 文件中, 自定义自己需要的方法();
}

⑤ SpringBoot 配置文件⚙:

application.yml 的语法结构,比较清晰明了

spring:
  # 配置SpringBoot 连接的数据源,注意 这里的Mysql连接 用户名 密码 要根据自己的实际情况来~
  datasource:
    url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: ok
    driver-class-name: com.mysql.cj.jdbc.Driver

⑥ 运行测试:

,一切准备就绪运行SpringBoot 启动类:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.0)

启动成功, 准备Maven 测试类,进行测试程序:

Maven test模块下测试程序:

test模块,就Maven包下的专门用来测试程序的模块

com.wsm.MPTest.Java 测试程序,第一个MP程序:查询所有的User表

/* MP的测试类; **/
@SpringBootTest
//@SpringBootTest JUnit等其他测试框架结合起来,提供了便捷高效的测试手段.
//使用@SpringBootTest后,Spring将加载所有被管理的bean,基本等同于启动了整个服务,此时便可以开始功能测试. 需要引入: spring-boot-starter-test 依赖;
public class MPTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectList(){
        System.out.println("测试 selectList(null); 查询全部!");
            // selectList(queryWrapper); 参数需要是一个 查询添加Wrapper; 不设置,则无条件查询全部!

        List users = userMapper.selectList(null);
        users.forEach(System.out::println);
    }
}

运行‍♂️:

9997d0baaf25396c5fc484d1067c0af

总结:

MyBatis-Plus是如此的简单,BaseMapper接口类中,已经默认集成了很多单表的 CRUD的操作,我们只需要去调用即可 轻松的完成:增删改查

MP 常用配置⚙:

MP 具有强大的注解 / yml的全局配置 使框架具有更加强大的功能!

表/实体 映射:@TableName

默认情况下MP操作的表名就是实体类的类名,但是如果表名和类名不一致就需要我们自己设置映射规则

局部设置~单独,针对某些表~:

  • MP 可以通过 @TableName注解 进行,==Java实体 与 数据库之间的相互映射==

    注解在类上,指定类和数据库表的映射关系

    如果,实体类 类名——>转换 小写后——> 和数据库映射表相同,可以不指定该注解; MP 默认就是这样映射的;

  • 这里文章中就不贴代码了, 可以私聊或自行下载... 这里介绍一下,验证思路:

    同样的库中,重新创建一个 除表名外,表结构相同的,数据不同的表 通过 @TableName 来进行切换执行查看数据;

全局设置~表前缀~:

  • 上面的 @TableName 可以设置,数据库与实体的 表名进行映射

    一般一个项目表名的前缀都是统一风格的,这个时候如果一个个设置就太麻烦了。我们可以通过配置来设置全局的表名前缀

  • 例如:

    如果一个项目中所有的表名相比于类名都是多了个前缀: tb_ 这可以使用如下方式配置:

    # MP 参数设置:
    mybatis-plus:
      global-config:
        db-config:
          # 设置数据库映射 实体时候添加的 前缀;
          table-prefix: tb_

    这样,MP 在通过实体映射 数据库表的时候,会在前面自动添加 tb_,可以大量节省开发者的工作...(只限于,存在规律的表;

  • 验证思路:

    在数据库中,创建一个 表名前缀 tb_ ,表结构相同的,数据不同的表 执行查看运行数据!

设置主键生成策略:@TableId

注解在实体类的某一字段上,表示这个字段对应数据库表的主键

  • 当数据库表字段 和 实体类属性名都是 id 时候,无需使用改注解进行指定, MP会自定进行关联; 且默认使用的是 雪花算法
  • 如果: 数据库表字段 和 实体类属性名 不匹配/不是 id 时候需要使用,@TableId注解的 value属性进行关联~ type属性:主键策略

type 主键生成策略:IdType

主键生成策略的值,是一个枚举类型,全都定义在 idType枚举类中,取值如下:

AUTO 自增

  • 数据库ID自增,依赖于数据库。该类型请确保数据库设置了 ID自增 否则无效默认采用 雪花算法

NONE 默认

  • 未设置主键类型,默认采用雪花算法
  • 注意:

    要注意数据库主键列的 长度要11 位,不然数据库新增列不够长报错!长度太小!

    Java 的字段要是 Long 长整型

INPUT 手动输入

  • 需要手动设置主键,若不设置,插入操作生成SQL语句时,主键这一列的值会是null

ASSIGN_ID 手动+默认

  • 当没有手动设置主键,即实体类中的主键属性为空时,才会自动填充,使用雪花算法

ASSIGN_UUID 手动+uuid

  • 当实体类的主键属性为空时,才会自动填充,使用UUID
  • 注意:

    uuid 是一个带有字母的字符串,数据库的字段需要是 varchar 字符类型,长度40 Java字段要是String字符类型;

uuid 和 雪花算法:❄

首先,雪花算法 和 uuid 都是为了保证,在分布式环境下,保证数据库表中,主键唯一!

雪花算法:

  • 通常长度 11 个数字组成,分布式环境下,有序且唯一的全局id
  • 生成方式:时间戳 + 机器id + 毫秒序列号, 一般常用的有了分布式保证数据唯一且自增,有的公司第一个数据使用 雪花算法 ,后面的数据采用auto 自增
  • 1478677587608748035 1478677587608748036 ...

uuid

  • JDK1.5 Java又了其包装类,来生成uuid String uuid = UUID.randomUUID().toString();
  • uuid 有很多版本,不同版本有不同的生成策略,但都是保证唯一 这里不细致介绍:

    UUID总长度 36,由 32 个 16 进制字符和 4 个连字符组成,例如: 5c6aeee6-00f1-45b1-aafa-d615a18217aa

全局设置 主键生成策略:

mybatis-plus:  global-config:    db-config:      # 设置全局 主键生成策略;      id-type: auto

表字段/实体属性 映射:@TableFieid

与@TableName 类型 注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系

  • MP 默认开启 表列/实体字段的 驼峰映射

    即:数据库中的 user_name 字段,会自动与 Java实体的 userName 进行映射匹配

  • 而,对于某些完全不一样的数据库列/实体字段 可以通过 @TableFieid进行关联,注解声明在要匹配的字段名上

    value 属性指定表的列名

    fill 属性指定,字段为空时会进行自动填充的值

    exist 属性,设置之后表示该,实体属性,不和任何数据库列匹配 CRUD的Sql 会忽略这个字段~

    exist 也可以通过其它方式来完成,如使用 static transient 关键字的属性,不过不是很合理;

MP 打印日志:

如果需要打印MP操作对应的SQL语句等,可以配置日志输出:

mybatis-plus:  configuration:    # 设置MP 打印SQL 语句日志;    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Mapper CRUD接口:

MP 封装了一些最基础的CRUD方法,Mapper接口只需要继承 BaseMapperMP 在程序运行时候,会自动给 Mapper接口,完成对应的实现~

  • MP 为了方便操作后面又提供了 Service CRUD接口 还有 Wrapper条件构造器 方便组装SQL where条件

BaseMapper 接口方法:

  • insert(T entity) 插入一条记录
  • deleteById(Serializable id) 根据主键id删除一条记录
  • delete(Wrapper wrapper) 根据条件构造器wrapper进行删除
  • selectById(Serializable id) 根据主键id进行查找
  • selectBatchIds(Collection idList) 根据主键id进行批量查找
  • selectList(Wrapper wrapper) 根据条件构造器wrapper进行查询
  • selectMaps(Wrapper wrapper) 根据 wrapper 条件,查询记录,将查询结果封装为一个Map,Map的key为结果的列,value为值
  • update(T entity, Wrapper wrapper) 根据条件构造器wrapper进行更新
  • updateById(T entity) 传入对象类型,必须给主键列赋值,修改非主键列的字段...
  • ..... 等:

插入数据: Insert

通过 BaseMapper的 insert(); 方法, 传入一个对象, 对其进行新增入库;

test模块:com.wsm.MPTest.Java

// insert(); 新增用户@Testpublic void testInsert(){    User user = new User();    user.setName("wsm");    user.setAge(540);    user.setPassword("qwer");    user.setUserName("wsm");    user.setAddress("皮尔及沃特");    // 讲创建的对象,新增入库,并返回影响行数;    int insert = userMapper.insert(user);    // 判断影响行数,是否新增成功!    if(insert >0)        System.out.println("新增成功");    else        System.out.println("新增失败");}

删除数据: Deletexxx

test模块:com.wsm.MPTest.Java

// deleteById(); 根据id 删除数据;@Testpublic void testDelByid(){    // deleteById(Serializable id);    // Java 数值类型 继承了 Number抽象类 实现了 Serializable序列化接口,所以传入一个 Serializable对象;    int del = userMapper.deleteById(1);    // 判断影响行数,是否删除成功!    if(del >0)        System.out.println("删除成功");    else        System.out.println("删除失败");}// deleteBatchIds(Collection); 根据 id 集合,批量删除数据!@Testpublic void testDelBat(){    List ids = new ArrayList<>();    ids.add(2);    ids.add(3);    ids.add(4);    // 批量删除 2 3 4 主键列的数据;    int del = userMapper.deleteBatchIds(ids);    // 判断影响行数,是否删除成功!    if(del >0)        System.out.println("批量删除成功");    else        System.out.println("批量删除失败");}// deleteByMap(Map); 根据传入Map 指定的K作为列名和V作为列值进行等值匹配查找;@Testpublic void testDelByMap(){    HashMap map = new HashMap<>();    map.put("name","wsm");    map.put("age",540);    // 删除 name = wsm age = 540 的数据;    int del = userMapper.deleteByMap(map);    // 判断影响行数,是否删除成功!    if(del >0)        System.out.println("删除匹配的数据");    else        System.out.println("未删除数据");}

修改数据: Updatexxx

test模块:com.wsm.MPTest.Java

// updateByid(T) 根据传入对象,id属性来修改对应数据的对应字段值...@Testpublic void updByid(){    User user = new User();    user.setId(2L);    user.setUserName("wsm");    // 修改数据库 id=2 的数据,user_name 值为wsm 其它属性未赋值,数据库不会改动~    int upd = userMapper.updateById(user);    // 判断影响行数,是否修改成功!    if(upd >0)        System.out.println("数据修改成功!");    else        System.out.println("数据修改失败!");}

Wrapper 调节构造器:

我们在实际操作数据库的时候会涉及到很多的条件,MP为我们提供了一个功能强大的条件构造器 Wrapper

新的一年学习 Mybatis_Plus_第3张图片

Wrapper 是一个 抽象类,

其子类 AbstractWrapper抽象类 中提供了很多用于构造Where条件的方法

AbstractWrapper的子类QueryWrapper则额外提供了用于针对Select语法的select方法。可以用来设置查询哪些列;

AbstractWrapper的子类UpdateWrapper则额外提供了用于针对SET语法的set方法。可以用来设置对哪些列进行更新;

Condition

所有条件构造器的方法中();

  • 都可以指定一个 boolean类型的参数,condition 可以,用来决定该条件是否加入最后生成的WHERE语句中

举例:

// 假设name变量是一个外部传入的参数String name = ""; QueryWrapper wrapper = new QueryWrapper<>();wrapper.like(StringUtils.hasText(name), "name", name);    // 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中// 其实就是对下面代码的简化if (StringUtils.hasText(name)) {    wrapper.like("name", name);}// StringUitils.hasText("");     里面的值为 null 、"" 、 " ",那么返回值为false... // 即:上面的表示,name 不存在则不加该条件, 当然判断是否成立的方式有很多: Lambda函数式接口...

常用AbstractWrapper方法

eq    等于 =    eq("name", "老王") ---> name = '老王'ne  不等于 <>    ne("name", "老王")---> name <> '老王'gt    大于 >    gt("age", 18) ---> age > 18ge    大于等于≥    ge("age", 18) ---> age >= 18lt    小于<    lt("age", 18) ---> age < 18le    小于等于≤    le("age", 18) ---> age <= 18between        相当于SQL中的BETWEEN AND                 between("age", 18, 30) ---> age between 18 and 30  (18 ≤ age ≥ 30)notBetween     相当于between 取反            notBetween("age", 18, 30) ---> age not between 18 and 30like        模糊匹配            like("name", "王") ---> name like '%王%'notLike     模糊匹配取反            notLike("name", "王") ---> name not like '%王%'likeRight    模糊匹配右半边            likeRight("name", "王") ---> name like '王%'likeLeft    模糊匹配左半边            likeLeft("name", "王") ---> name like '%王'isNull         判断字段为空的匹配            isNull("name") ---> name is nullisNotNull    字段不为空的匹配            isNotNull("name") ---> name is not nulland            嵌套            and(i -> i.eq("name", "李白").ne("status", "活着")) ---> and (name = '李白' and status <> '活着')or            拼接            eq("id",1).or().eq("name","老王") ---> id = 1 or name = '老王'in                                in("age",{1,2,3})--->age in (1,2,3)groupBy        分组            groupBy("id", "name")--->group by id,nameorderByAsc     正排序            orderByAsc("id", "name")--->order by id ASC,name ASCorderByDesc 倒排            orderByDesc("id", "name")--->order by id DESC,name DESC

更多请参考官方

AbstractWrapper 案例:

SQL语句如下:

SELECT     id,user_name,PASSWORD,NAME,age,address FROM     tb_user WHERE     age > 18 or address like 'tb_德玛%'

Wrapper写法如下:

/** AbstractWrapper 使用 **/@Testpublic void testAbsWra(){    QueryWrapper query = new QueryWrapper();    query.gt("age", 18);    query.or();    query.likeRight("address", "tb_德玛");    // 查询传入 wrapper 条件构造器,查询: 年龄大于18 或 地址是 tb_德玛 开头的;    List users = userMapper.selectList(query);    users.forEach(System.out::println);}

image-20220105231737837

常用QueryWrapper方法

QueryWrapper的 select 可以设置要查询的列

示例一:

select(String... sqlSelect) 方法的指定要查询的列名

SQL语句如下:

SELECT id,`name`FROM tb_user WHERE age > 18

Wrapper写法如下:

// select(String... sqlSelect) 方法的指定要查询的列名public void testQueryWra1(){    // 创建QueryWrapper 实例;    QueryWrapper query = new QueryWrapper<>();    // .select('列1','列2',...); 指定要查询的列, QueryWrapper支持链式编程...    query.select("id","name").gt("age", 18);    // 传入QueryWrapper 开始查询~    List users = userMapper.selectList(query);    users.forEach(System.out::println);}

示例二:

select(Class entityClass, Predicate predicate)

  • 方法第一个参数为实体类的字节码对象
  • 第二个参数为Predicate类型,可以使用lambda的写法,过滤要查询的字段 (主键除外) 因为有局限性,并不是常用了解即可!

SQL语句如下:

SELECT id,user_name,`name`,age,address FROM tb_user WHERE age > 18

Wrapper写法如下:

// select(Class entityClass, Predicate predicate)@Testpublic void testQueryWra2(){    // 创建QueryWrapper 实例;    QueryWrapper query = new QueryWrapper<>();    // .select(类对象,Predicate); 内部类实现;    //        query.select(User.class, new Predicate() {    //            @Override    //            public boolean test(TableFieldInfo tableFieldInfo) {    //                return !"password".equals(tableFieldInfo.getColumn());    //            }    //        }).gt("age", 18);    // .select(类对象,Predicate); 支持使用Lambda表达式实现;    // Predicate中的test(); 方法返回 boolean类型, 程序会循环对每个字段进行比较,为 true 的才会查询该列~    query.select(User.class,u-> !"password".equals(u.getColumn())).gt("age", 18);    // 传入QueryWrapper 开始查询~    List users = userMapper.selectList(query);    users.forEach(System.out::println);}

常用UpdateWrapper方法

我们前面在使用update方法时需要创建一个实体类对象传入,用来指定要更新的列及对应的值

  • 但是如果需要更新的列比较少时,创建这么一个对象显的有点麻烦和复杂
  • 我们可以使用UpdateWrapper的set方法来设置要更新的列及其值。同时这种方式也可以使用Wrapper去指定更复杂的更新条件

sql语句如下:

-- 小于等于 18 岁的用户,都更新为 540;UPDATE tb_user SET age = 540 WHERE age <= 18

Wrapper写法如下:

/** UpdateWrapper使用 **/@Testpublic void tetsUpdWra(){    UpdateWrapper updwra = new UpdateWrapper<>();    // 小于等于 18 岁的用户,都更新为 540    updwra.le("age",1).set("age","540");    // 传入UpdateWrapper 开始查询~    int update = userMapper.update(null, updwra);    // 判断影响行数,是否修改成功!    if(update >0)        System.out.println("数据修改成功!");    else        System.out.println("数据修改失败!");}

LambdaQueryWrapper 使用

我们前面在使用条件构造器时列名都是用字符串的形式去指定,这种方式无法在编译期确定列名的合法性 无法更加准确的保证列匹配正确;

MP提供了一个Lambda条件构造器可以让我们直接以实体类的方法引用的形式来指定列名

SQL语句如下:

SELECT id,user_name,`name`,age,address FROM tb_user WHERE age > 18

Wrapper写法如下:

/** LambdaQueryWrapper使用 **/// Lamdba 表达式实现的@Testpublic void testQueryWralmd(){    LambdaQueryWrapper lam = new LambdaQueryWrapper<>();    // gt(User::getAge,18); 使用方法引用的形式,对参数进行绑定,避免了编译期不确定数据库列,而造成的失误~    lam.select(User.class,u-> !"password".equals(u.getColumn())).gt(User::getAge,18);    // 传入QueryWrapper 开始查询~    List users = userMapper.selectList(lam);    users.forEach(System.out::println);}

自定义Mapper sql:

虽然MP为我们提供了很多常用的方法,并且也提供了条件构造器

  • 但是如果真的遇到了复杂的SQL时,我们还是需要自己去定义方法,自己去写对应的SQL,这样SQL也更有利于后期维护
  • 因为MP是对mybatis做了增强,所以还是支持之前Mybatis的方式去自定义方法
  • 同时也支持在使用Mybatis的自定义方法时使用MP的条件构造器帮助我们进行条件构造

MP 中使用 Mybatis 方式:

其实本质上是没有太大变化的,还是正常的:定义Mapper接口 创建对应的映射文件 映射文件中编写sql 调用测试

定义Mapper接口:

com.wsm.mapper 包下的 xxxMapper 接口

@Mapper//@Mapper 注解,使当前的Mapper 接口,被Spring进行管理,不然需要在,启动类上声明 @MapperScan("com.wsm.mapper") 类扫描指定包下,mapper接口文件;public interface UserMapper  extends BaseMapper {    //Mapper 接口 extends集成 BaseMapper 泛型对应的实体类;    //  Ctrl+右击, 进入BaseMapper 中可以看到, MP 默认给对应实体类提供好的实现方法();    //  增删改查... 即各种的, 重载 CRUD 的操作;    //如果,BaseMapper 中,没有提供的,后面还可以在,该 xxxMapper 文件中, 自定义自己需要的方法();    /** 自定义方法 **/    // 根据id查询对象;    User findMyUser(Long id);}

创建对应的映射文件, 编写sql:

为了方便管理在: resources 资源目录下创建 mapper 文件夹,中创建 对应的sql映射文件 UserMapper.xml

        

yaml 设置项目 扫描对应的 sql映射文件:

mybatis-plus:  # 设置扫描的sql映射文件,加载至环境中;  mapper-locations: classpath*:/mapper/**/*.xml

程序测试:

test模块下:com.wsm.MPTest.Java

/** 测试自定义方法查询结果 **/@Testpublic void testfindMyUser(){    User myUser = userMapper.findMyUser(1L);    System.out.println(myUser);}

运行,ok 可以查询到数据!

总结:

MyBatis-plus 归根结底底层也还是 Mybatis 所以,按照正常 Mybatis 写法来对MP进行扩展Mybatis写法,没有任何影响

Mybatis方式结合 MP条件构造器

我们在使用上述方式自定义方法时, 如果也希望我们的自定义方法能像MP自带方法一样使用条件构造器来进行条件构造的话只需要使用如下方式即可

在SQL语句中获取Warpper拼接的SQL片段进行拼接

添加Warpper类型的参数,并且要注意给其指定参数名 UserMapper.Java

@Mapper//@Mapper 注解,使当前的Mapper 接口,被Spring进行管理,不然需要在,启动类上声明 @MapperScan("com.wsm.mapper") 类扫描指定包下,mapper接口文件;public interface UserMapper  extends BaseMapper {    //Mapper 接口 extends集成 BaseMapper 泛型对应的实体类;    //  Ctrl+右击, 进入BaseMapper 中可以看到, MP 默认给对应实体类提供好的实现方法();    //  增删改查... 即各种的, 重载 CRUD 的操作;    //如果,BaseMapper 中,没有提供的,后面还可以在,该 xxxMapper 文件中, 自定义自己需要的方法();    /** 自定义方法 **/    // 根据id查询对象;    User findMyUser(Long id);    // 自定义方法,使用MP 的Wrapper    List findUsers(@Param(Constants.WRAPPER) Wrapper wrapper);  //wrapper 是MP包下的依赖~}

方法定义中添加Warpper类型的参数

UserMapper.xml

                

程序测试:

test模块下:com.wsm.MPTest.Java

/** 自定义方法查询 +MP的 wrapper 条件构造 **/@Testpublic void testfindusers(){    // 定义QueryWrapper 构造器,查询 age 大于 18 的数据;    QueryWrapper query = new QueryWrapper<>();    query.gt("age", 18);    // 调用 自定义的方法(wrapper 参数);    List users = userMapper.findUsers(query);    users.forEach(System.out::println);}

运行,ok 可以查询到数据!

分页查询 Page

单表分页查询:

分页查询是一个使用非常频繁的功能,通常实现方式:

  • 定义一个Page分页类,类中属性:当前页int 每页行int 总记录数int 总页数int 每页的数据集合List
  • 分页前先根据条件查询出总记录数,给Page对象的属性赋值,同时根据分页算法

    总记录数%每页行==0?总记录数/每页行:总记录数/每页行+1 得总页数赋值 总记录 整除 每页行 不整除+1 得总页数;

  • MySQL分页: 使用 limit x,y 关键字:获取查询结果的 第x行 往下 y个记录数;

    当前端,请求后端传入:分页条件 查询第几页x 每页几行y

    后台Java 会根据:x=(x-1)乘y 得到limit 起始行,0开始 y 并将 x y传入sql 中执行 limit x,y 返回分页的结果集存入 Page类对象的每页的数据集合List

MP 为了方便外面操作,页对分页进行了封装, 我们不需要关注太多就可以完成分页操作!

① 配置分页查询拦截器

MP 使用拦截器进行分页处理,所以创建一个 util工具包来, 存放分页配置类

MPPageConfig.Java

  • MP不同的版本,分页的配置也有些区别,不过这个不是我们关系的copy 过来能用就了
/** MP 分页,配置类 */// @Configuration 将类加载至Spring容器中去;@Configurationpublic class MPPageConfig {    /**     * 3.4.0之前的版本     * @return     *///    @Bean//    public PaginationInterceptor paginationInterceptor(){//        return  new PaginationInterceptor();//    }    /**     * 3.4.0之后版本     * @return     */    @Bean    public MybatisPlusInterceptor mybatisPlusInterceptor(){        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());        return mybatisPlusInterceptor;    }}

② 进行分页查询

/** selectPage(); Mapper 单表分页 */@Testpublic void testPage(){    // 定义分页对象    Page userPage = new Page<>();    // 设置分页参数    // 每页行数    userPage.setSize(2);    // 当前查询的第几页    userPage.setCurrent(1L);    //后台会自动根据 每页行 第几页 计算拼接limit sql;    // 执行分页查询 selectPage(page,wrapper); 两个参数: 分页对象,条件构造器;    userMapper.selectPage(userPage, null);        // 返回的分页对象,就算传入的对象 userPage    System.out.println("获取当前页的数据");    System.out.println(userPage.getRecords());    System.out.println("获取总记录数");    System.out.println(userPage.getTotal());    System.out.println("当前页码");    System.out.println(userPage.getCurrent());}

多表分页查询:

准备工作:

定义需要的数据库sql

CREATE TABLE `orders` (  `id` bigint(20) NOT NULL AUTO_INCREMENT,  `price` int(11) DEFAULT NULL COMMENT '价格',  `remark` varchar(100) DEFAULT NULL COMMENT '备注',  `user_id` int(11) DEFAULT NULL COMMENT '用户id',  `update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',  `create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',  `version` int(11) DEFAULT '1' COMMENT '版本',  `del_flag` int(1) DEFAULT '0' COMMENT '逻辑删除标识,0-未删除,1-已删除',  `create_by` varchar(100) DEFAULT NULL COMMENT '创建人',  `update_by` varchar(100) DEFAULT NULL COMMENT '更新人',  PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;/*Data for the table `orders` */insert  into `orders`(`id`,`price`,`remark`,`user_id`,`update_time`,`create_time`,`version`,`del_flag`,`create_by`,`update_by`) values (1,2000,'无',2,'2021-08-24 21:02:43','2021-08-24 21:02:46',1,0,NULL,NULL),(2,3000,'无',3,'2021-08-24 21:03:32','2021-08-24 21:03:35',1,0,NULL,NULL),(3,4000,'无',2,'2021-08-24 21:03:39','2021-08-24 21:03:41',1,0,NULL,NULL);

com.wsm.entity 包下的实体类:Orders.Java

@Data@NoArgsConstructor@AllArgsConstructor// 因为设置了全局的表前缀 tb_ 为了方便操作,@TableName 指定表;@TableName("orders")public class Orders {    // Orders 表属性:    private Long id;    private Integer price;    private String remark;    private Integer userId;    private LocalDateTime updateTime;    private LocalDateTime createTime;    private Integer version;    private Integer delFlag;    // 多表查询,User表扩展属性;    // MP 默认的CRUD 不对该属性进行sql映射,自定义Mapper 可以通过同名/取别名 自动映射;    @TableField(exist = false)    public String userName;}

多表 sql 准备:

新的一年学习 Mybatis_Plus_第4张图片

① 定义OrdersMapper.Java 接口

OrdersMapper.Java

  • 因为是多表分页,所以需要自定义sql 和 Mapper接口,自定义分页需要传入参数 Page 并且返回类型也是 Page
@Mapperpublic interface OrdersMapper extends BaseMapper {    // 自定义多表分页方法    public Page selPageOrdUs(Page page);}

② 定义OrderMapper.xml 映射文件

OrdersMapper.xml

        

③ 编写测试程序:

MPTest.Java

/** selectPage(); Mapper 多表分页 *///定义 OrdersMapper 对象;@Autowired        OrdersMapper ordersMapper;@Testpublic void testselPageOrdUs(){     // 定义MP 分页对象,并设置分页属性    Page ordersPage = new Page<>();    // 每页行,当前页    ordersPage.setSize(2);    ordersPage.setCurrent(1);    // 进行查询,返回分页对象, 因为: 引用类型的实参改变形参会受影响~所以, ordersPage == ordersIPage    Page ordersIPage = ordersMapper.selPageOrdUs(ordersPage);    System.out.println("ordersPage == ordersIPage是否相等:"+(ordersPage == ordersIPage));    // 返回的结果    System.out.println("获取总记录数");    System.out.println(ordersIPage.getTotal());    System.out.println("获取当前页的数据");    System.out.println(ordersIPage.getRecords());}

④ 运行测试:

ordersPage == ordersIPage是否相等:true获取总记录数3获取当前页的数据[Orders(id=1, price=2000, remark=无, userId=2, updateTime=2021-08-24T21:02:43, createTime=2021-08-24T21:02:46, version=1, delFlag=0, userName=gailun), Orders(id=3, price=4000, remark=无, userId=2, updateTime=2021-08-24T21:03:39, createTime=2021-08-24T21:03:41, version=1, delFlag=0, userName=gailun)]

Service CRUD接口:

MP 也为我们提供了Service 层的接口来完成 CRUD的操作:

为什么MP 有了 Mapper接口, 还要 Service接口:

Why?,多次一举?最开始学习的时候,我也很疑惑,对呀为啥呢?

因为:为了方便开发程序现在程序大部分都是 三层架构`Dao持久 Service业务 Controller 控制`

  • 而,定义的Mapper接口,很多时候又要在Service 里面进行重新调用 而,很多时候Servcie又很简单只能调用了Mapper的方法();
  • MP 为了简化开发者工作,对Service接口也进行了继承,这样对于一些简单的功能,只需要编写 Controller控制层就可以完成开发,开发者不需要在写简单的Service代码
  • 还有,Service 中有很多是对Mapper 的整合方法();

    SaveOrUpdate(T entity) 更新记录T 如果不存在,插入一条记录;

    saveOrUpdate(T entity, Wrapper updateWrapper); 根据条件修改一条数据, 如果没有匹配则删除

    saveOrUpdateBatch(Collection entityList); 批量修改插入

    Service 对 Mapper 多了更多的组合批量操作 算是, 节省了开发者的工作量; 官方‍

编写User Service 的CRUD操作:

image-20220106215914269

在使用MP Service 的 CRUD 之前还是需要确保,Mapper 继承 BaseMapper

① 编写 Service

UserService.Java

  • 创建 Service 接口,extends继承 IService<操作的实体类T> IService类中,定义了Service的很多批量CRUD方法~
// Service接口 继承MP的 IService  T泛型,要CRUD对应映射的实体;public interface UserService extends IService {       }

② 编写 Service 实现 ServiceImpl

UserServiceImpl.Java

// Spring注解,表示改类是一个 Service 业务逻辑类,并交给Spring容器管理;@Service// ServiceImpl 是 Service 的实现// 继承MP 的ServiceImpl<对应的Mapper,映射表的实体类> // 因为是 Service的实现, 实现对应的接口 implement Servicepublic class UserServiceImpl extends ServiceImpl implements UserService {   }

③ 测试:

MPTest.Java

/** Service 的CRUD **/// 从Spring容器中获取Service对象;@AutowiredUserService userService;// 只获取匹配的第一条数据;@Testpublic void testGetOne(){    LambdaQueryWrapper wrapper = Wrappers.lambdaQuery();    wrapper.gt(User::getAge, 28);    User one = userService.getOne(wrapper, false); // 第二参数指定为false,使得在查到了多行记录时,不抛出异常,而返回第一条记录    System.out.println(one);}

自定义 Service

就是正常的引用 Mapper

① 编写 Service

UserService.Java

// Service接口 继承MP的 IService  T泛型,要CRUD对应映射的实体;public interface UserService extends IService {    //自定义Service 实现:    List selall();}

② 编写 Service 实现 ServiceImpl

UserServiceImpl.Java

// Spring注解,表示改类是一个 Service 业务逻辑类,并交给Spring容器管理;@Service// ServiceImpl 是 Service 的实现// 继承MP 的ServiceImpl<对应的Mapper,映射表的实体类>// 因为是 Service的实现, 实现对应的接口 implement Servicepublic class UserServiceImpl extends ServiceImpl implements UserService {    // 创建UserMapper 对象实例;    @Autowired    UserMapper userMapper;    @Override    public List selall() {        // 直接调用Mapper 的查询全部~ 当然Service业务逻辑层,可以写很多更加复杂的操作...        return userMapper.selectList(null);    }}

MP 的代码生成器:

MP提供了一个代码生成器,可以让我们一键生成实体类,Mapper接口,Service,Controller等全套代码

  • 只需要,小手一点:实体 mapper service controller 啥啥都不需要自己写了! 实在是太牛逼了!

① 添加依赖

    com.baomidou    mybatis-plus-generator    3.4.1    org.freemarker    freemarker

② 添加生成类,运行生成文件

一般情况下,可以将这个文件放在 项目Util 包下,作为一个工具类使用:

  • 甚至,可以不声明在项目中,因为它可以指定 代码生成的地址... 创建完成之后,将需要的东西拖到项目中也可以
  • 很多时候,有些特别长的表的实体,用这个生成,实在是可以省下很多时间

GeneratorTest.Java

public class GeneratorTest {    @Test    public void generate() {        AutoGenerator generator = new AutoGenerator();                // 全局配置        GlobalConfig config = new GlobalConfig();        String projectPath = System.getProperty("user.dir");                // 设置输出到的目录: 可以更改为任何路径 D盘 C盘...        // config.setOutputDir("D:/MP");        config.setOutputDir(projectPath + "/src/main/java");        // 生成的作者名        config.setAuthor("wsm");        // 生成结束后是否打开文件夹        config.setOpen(false);        // 全局配置添加到 generator 上        generator.setGlobalConfig(config);        // 数据源配置: 配置自己的数据库要生成的数据库 用户/密码         DataSourceConfig dataSourceConfig = new DataSourceConfig();        dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&serverTimezone=UTC");        dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");        dataSourceConfig.setUsername("root");        dataSourceConfig.setPassword("ok");        // 数据源配置添加到 generator        generator.setDataSource(dataSourceConfig);        // 包配置, 生成的代码放在哪个包下        PackageConfig packageConfig = new PackageConfig();        packageConfig.setParent("com.wsm");        // 包配置添加到 generator        generator.setPackageInfo(packageConfig);                // 策略配置        StrategyConfig strategyConfig = new StrategyConfig();        // 下划线驼峰命名转换        strategyConfig.setNaming(NamingStrategy.underline_to_camel);        strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);        // 开启lombok,生成的实体类上面就会又 lombok注解;        strategyConfig.setEntityLombokModel(true);        // 开启RestController        strategyConfig.setRestControllerStyle(true);        generator.setStrategy(strategyConfig);        generator.setTemplateEngine(new FreemarkerTemplateEngine());        // 开始生成        generator.execute();    }}

生成的路径:D:/MP

新的一年学习 Mybatis_Plus_第5张图片

MP高级 自动填充

在实际项目中表不仅仅会有开发中需要的功能字段有时候还会需要很多的附属字段:

  • 更新时间 创建时间 创建人 更新人 逻辑删除列 乐观锁Version 备用1 备用2...
  • 而,这些字段需要我们手动进行维护会很麻烦,每个数据新增 修改都要进行手工维护;
  • MP 提供了 自动填充 来完成对这些数据的操作

实例Demo

① 在需要操作的实体上, 添加 @TableFieId注解

在对应字段上增加注解,@TableFieIdfill属性来设置字段的自动填充; Orders为例子

  • fill 属性:是一个枚举FieldFill

    DEFAULT默认值无任何处理 INSERT新增触发 UPDATE修改时触发 INSERT_UPDATE 新增或修改时触发

② 编写适配器:

MetaObjectHandler.Java

@Componentpublic class MyMetaObjectHandler implements MetaObjectHandler {    // 新增时候触发,并设置新增时候对应数据要赋的值    @Override    public void insertFill(MetaObject metaObject) {        this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);        this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);    }    // 修改时候触发,并设置修改时候对应数据要赋的值    @Override    public void updateFill(MetaObject metaObject) {        this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);    }}

③ 测试:

/** MP 自动填充: **/@Testpublic void testinsertOrd(){    // 创建新增对象    Orders orders = new Orders();    orders.setPrice(1000);    orders.setRemark("无");    orders.setUserId(2);    // 执行新增    int insert = ordersMapper.insert(orders);    if(insert>0)        System.out.println("新增成功");    else        System.out.println("新增失败");}

新增成功, 查看数据库结果集!

MP高级 逻辑删除

我们深处大数据时代,一般企业的数据都是不允许真实删除的,这样后面找都不好找

  • 所以 很多公司的数据库都会添加一个字段逻辑删除,用来判断数据是否删除,一般只有两个值:0|1
  • 0: 就是正常的数据
  • 1: 就算被删除的数据,并不会真的删除,而是查询时候默认就不查询 1 的数据;

实现:

通常逻辑删除 只需要在 yaml 配置一下即可

注意:3.3.0版本之前还需要在对应的字段上加上@TableLogic注解

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: delFlag      # 全局逻辑删除的实体字段名, 3.3.0配置后可以不添加注解,之前的还需要添加注解 @Tablelogic)
      logic-delete-value: 1         # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0     # 逻辑未删除值(默认为 0)

测试:

/** MP 逻辑删除 **/
@Test
public void delOrd(){
    // 配置了逻辑删除之后,直接调用MP 的删除方法就是进行逻辑删除了! 注意: 自定义sql的操作还需要自己完成注意!
    int i = ordersMapper.deleteById(1);
    if(i>0)
        System.out.println("逻辑删除成功");
    else
        System.out.println("逻辑删除失败");
}

MP高级 乐观锁

在程序开发中大家应该都很了解吧,为了避免多线程情况下数据紊乱需要对数据进行加锁

  • 而当,多个线程同时操作一个数据时候可能出现数据被重复修改的情况 典型的冲突
  • 丢失更新

    一个事务的更新覆盖了其它事务的更新结果,就是所谓的更新丢失

    用户A把值从6改为2,用户B把值从2改为6,则用户A丢失了他的更新

  • 脏读

    当一个事务读取其它完成一半事务的记录时,就会发生脏读取

    用户A,B看到的值都是6,用户B把值改为2,用户A读到的值仍为6

乐观锁 和 悲观锁

乐观锁:

  • 比较乐观 假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。乐观锁不能解决脏读的问题
  • 通常使用数据版本(Version)记录机制实现,

    通过为数据库表增加一个数字类型的 “version” 字段来实现,当读取数据时,将version字段的值一同读出,`数据每更新一次,对此version值加一

    Update set version=version+1 where version = version 每次更新前都要判断传入的 版本 是否是现在最新版本!

    A B 同时要更新数据 1 都获取了version版本 1

    A 先更新:Update set version=version+1 where version = 1 version就是1 所以更新成功!

    B 在更新:Update set version=version+1 where version = 1 version已经被A +1 所以version 是2 where 2=1 不成立 B更新失败

悲观锁:

  • 比较悲观,认为一定会发送数据改变
  • 在对数据库的一条记录进行修改时,先直接加锁(数据库的锁机制),锁定这条数据,然后再进行操作. 同一时间只能,允许一个人来修改这条数据!

总结:

  • 在读多写少的场景下,乐观锁比较适用,能够减少加锁操作导致的性能开销,提高系统吞吐量
  • 在写多读少的场景下,悲观锁比较使用,否则会因为乐观锁不断失败重试,反而导致性能下降

MP 为了方便操作就对次进行了封装处理,更加方便的进行了操作;

MP 实现:

① 配置乐观锁插件

@Configuration
public class MybatisPlusConfig {
    /**
     * 旧版
     */
    @Bean
    public OptimisticLockerInterceptor optimisticLockerInterceptor() {
        return new OptimisticLockerInterceptor();
    }
    
    /**
     * 新版
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

目前项目中直接运行会报错,因为 MybatisPlusInterceptor 会在很多地方使用到:MP分页 MP乐观锁 ...

  • 只需要配置一个即可,将 mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); 定义在对应的 MybatisPlusInterceptor
  • 就 了,保证Spring 一次只注入一个对象类型...

② 在实体类中表示版本的字段上添加注解@Version

Orders.Java 省略其它未更改代码;

// 添加乐观锁的注解,使用MP 的方式实现乐观锁 保证数据安全;
// version字段,类型只支持int,long,Date,Timestamp,LocalDateTime
@Version
private Integer version;

③ 操作:

注意:

  • 在更新前我们一定要先查询到version设置到实体类上再进行更新才能生效 传入的对象一定要携带 Version列有值
  • 乐观锁插件仅支持updateById(id)update(entity, wrapper)方法 wrapper不能复用!会出现重复参数使用;

MPTest.Java

/** MP 乐观锁 */
// 操作前要去抱必须获取到数据最新的 version: 先查询在修改:
@Test
public void testupdVer(){
    // 先查询:
    Orders orders = ordersMapper.selectById(1);
    // 设置更新列
    orders.setPrice(123);
    System.out.println(orders);

    // 修改: 乐观锁插件仅支持`updateById(id)`与`update(entity, wrapper)`方法
    int i = ordersMapper.updateById(orders);
    if(i>0)
        System.out.println("修改成功,version+1");
    else
        System.out.println("修改失败");
}

cmd 控制台输出:

查询
==>  Preparing: SELECT id,price,remark,user_id,update_time,create_time,version,del_flag,create_by,update_by FROM orders WHERE id=? AND del_flag=0
==> Parameters: 1(Integer)
<==    Columns: id, price, remark, user_id, update_time, create_time, version, del_flag, create_by, update_by
<==        Row: 1, 2000, 无, 2, 2021-08-24 21:02:43, 2021-08-24 21:02:46, 1, 0, null, null
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@772caabe]
Orders(id=1, price=123, remark=无, userId=2, updateTime=2021-08-24T21:02:43, createTime=2021-08-24T21:02:46, version=1, delFlag=0, createBy=null, updateBy=null, userName=null)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@51da32e5] was not registered for synchronization because synchronization is not active

修改
JDBC Connection [HikariProxyConnection@2069678360 wrapping com.mysql.cj.jdbc.ConnectionImpl@4538856f] will not be managed by Spring
==>  Preparing: UPDATE orders SET price=?, remark=?, user_id=?, update_time=?, create_time=?, version=?, update_by=? WHERE id=? AND version=? AND del_flag=0
==> Parameters: 123(Integer), 无(String), 2(Integer), 2022-01-07T00:54:39.578(LocalDateTime), 2021-08-24T21:02:46(LocalDateTime), 2(Integer), www(String), 1(Long), 1(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@51da32e5]
修改成功,version+1

,MP 的学习就到这里了, 感谢点赞

本人对每个案例Demo 都进行了, 本地Git管理,并在 wlog.md 中有更详细的使用说明:

网盘链接:https://pan.baidu.com/s/1fwm9...
提取码:2540

新的一年学习 Mybatis_Plus_第6张图片

你可能感兴趣的