【MyBatisPlus】一文带你快速上手MyBatisPlus

news2025/2/25 13:13:29

文章目录

  • MyBatisPlus学习笔记
    • 前言
    • 1、MyBatisPlus概述
    • 2、快速体验
    • 3、CRUD接口
      • 3.1 Mapper层CRUD接口
        • 3.1.1 Insert
        • 3.1.2 Delete
        • 3.1.3 Update
        • 3.1.4 Select
      • 3.2 Service层CRUD接口
        • 3.2.1 Save
        • 3.2.2 Remove
        • 3.2.3 Update
        • 3.2.4 Get
    • 3.3 自定义SQL接口
    • 4、常用注解和配置
      • 4.1 @TableId
      • 4.2 @TableField
      • 4.3 @TableLogic
    • 5、条件构造器和常用接口
      • 5.1 QueryWrapper
      • 5.2 UpdateWrapper
      • 5.3 使用Condition动态组装条件
      • 5.4 LamdaQueryWrapper
      • 5.5 LamdaUpdateWrapper
    • 6、插件
      • 6.1 MyBatisPlus分页插件
      • 6.2 悲观锁与乐观锁
      • 6.3 MyBatisX插件
    • 7、通用枚举
    • 8、代码生成器
      • 8.1 快速生成
      • 8.2 交互式生成
    • 9、多数据源

MyBatisPlus学习笔记

img

前言

  本文是参考MyBatisPlus官网对MyBatisPlus的一个学习笔记,主要是对MyBatisPlus的一个简单的入门学习,大致对MyBatisPlus有一个整体认知,熟悉使用MyBatisPlus提供的各种API(比如MyBatisPlus提供的增删改查接口),以及各种便利的特性和插件(比如自动生成代码、MyBatisPlus分页插件)。当然主要目的是想利用费曼学习方法将学到的知识进行一个输出,这样能够加深我对知识有一个更加深刻的认知,因为这些知识在官方是都能够查看到的,我用自己的认知、自己的排版对这些知识进行重复输出。

1、MyBatisPlus概述

  • MyBatisPlus是什么

    MyBatisPlus(简称MP)是MyBatis的增强版,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。和MyBatis一样是一款数据访问层的框架,可以大大简化对SQL的操作

    • 官方文档地址:MyBatis-Plus

    • Gitee开源地址:mybatis-plus: mybatis

    • GitHub开源地址:baomidou/mybatis-plus

    • 创始人:青苗

  • MyBatisPlus的t特点

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

    • 国外:MySQL,Oracle,DB2,H2,HSQL,SQLite,PostgreSQL,SQLServer,Phoenix,Gauss ,ClickHouse,Sybase,OceanBase,Firebird,Cubrid,Goldilocks,csiidb,informix,TDengine,redshift
    • 国内:达梦数据库,虚谷数据库,人大金仓数据库,南大通用(华库)数据库,南大通用数据库,神通数据库,瀚高数据库,优炫数据库
  • 框架结构

    image-20230113153145218

2、快速体验

搭建环境
编写配置文件
编写实体类
编写Mapper
编写测试类
扫描Mapper
  • Step1:搭建环境

    1)建表(使用的是MySQL)

    建立一张 user 表

    DROP TABLE IF EXISTS user;
    
    CREATE TABLE user
    (
        id BIGINT(20) NOT NULL COMMENT '主键ID',
        name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
        age INT(11) NULL DEFAULT NULL COMMENT '年龄',
        email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
        PRIMARY KEY (id)
    );
    

    往表中添加数据:

    DELETE FROM user;
    
    INSERT INTO user (id, name, age, email)
    VALUES (1, 'Jone', 18, 'test1@baomidou.com'),
           (2, 'Jack', 20, 'test2@baomidou.com'),
           (3, 'Tom', 28, 'test3@baomidou.com'),
           (4, 'Sandy', 21, 'test4@baomidou.com'),
           (5, 'Billie', 24, 'test5@baomidou.com');
    

    2)导入依赖

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.5.2</version>
            </dependency>
    

    3)编写配置文件

    application.yml

    server:
      port: 8080
    
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis-plus_study?serverTimezone=UTC
        username: root
        password: 32345678
        
    mybatis-plus:
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启标准日志
    
  • Step2:编写实体类

    @Data
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private String email;
    }
    
  • Step3:编写Mapper

    package com.hhxy.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.hhxy.entity.User;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface UserMapper extends BaseMapper<User> {
        /*
        说明: BaseMapper<T> 
        BaseMapper是MP封装了通用CRUD的接口,Mapper只需要集成BaseMapper
        T为任意实体类,只要让Mapper集成BaseMapper并且关联对应的实体类,就能使用Mapper对象操作实体类对应的表了
        */
    }
    
  • Step4:扫描Mapper

    在SpringBoot的启动类上添加**@MapperScan("com.hhxy.mapper")**注解

    PS:如果我们在Mapper接口上添加了@Mapper注解,则这个注解可以省略

  • Step5:编写测试类

    @SpringBootTest
    public class SampleTest {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        public void testSelect() {
            System.out.println(("----- selectAll method test ------"));
            List<User> userList = userMapper.selectList(null);
            Assertions.assertEquals(5, userList.size());
            userList.forEach(System.out::println);
        }
        
    }
    

    image-20230113164200227

知识拓展:关于MP中的SQL是如何确定要操作的表是谁

我们在使用MP时,我们自己编写的Mapper必须继承BaseMapper,通过这一步,我们MP底层会自动将T映射为SQL操作表,举个例子吧😄:当我们的T是User时,那么SQL操作的表就是user

那如果数据库中的表是tb_user,如何确保SQL操作的表是tb_user呢?

  • 方案一:最直接的方法肯定是修改entity的格式,比如可以将entity修改成tb_user、TbUser、tbUser、Tb_User、Tb_User。

  • 方案二:通过@TableName注解映射表名,比如@TableName("tb_user")

    image-20230115224423439

  • 方案二:通过编写配置文件,给user添加一个前缀

    mybatis-plus:
    global-config:
     db-config:
       table-prefix: tb_
    

    需要注意的是,通过配置文件是全局有效的,也就是所有的实体类都会增加一个前缀tb_

3、CRUD接口

MP在MyBatis的基础上做了增强,底层封装了大量通用的SQL,主要有BaseMapper<T>IService<T>两个CRUD接口,其中IService的实现类是ServiceImpl<M, T>,BaseMapper中的方法以insertdeleteupdateselect开始,IService中的方法以saveremoveget开始

  • insert、delete、update的返回值都是int类型,返回值为0说明无数据更新,此时SQL执行失败
  • save、remove的返回值都是boolean类型,返回值为false说明SQL执行失败

3.1 Mapper层CRUD接口

参数说明

类型参数名描述
Tentity实体对象
Wrapperwrapper实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
Collection<? extends Serializable>idList主键 ID 列表(不能为 null 以及 empty)
Serializableid主键 ID(可以是任何实现了序列化接口的数据类型)
Map<String, Object>columnMap表字段 map 对象
IPagepage分页查询条件(可以为 RowBounds.DEFAULT)

备注:基本数据类型的包装类都实现了序列化接口,String也实现了序列化接口

3.1.1 Insert

  • int insert(T entity):插入一条记录,返回值为更新数据的条数
    @Test
    public void testInsert(){
        User user = new User("张三", 23, "123@qq.com");
//        INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? );
        int result = userMapper.insert(user);
        System.out.println("insert = " + result); // result = 1
        // MP很贴心,它会将Insert后自动生成的id,重新赋值给user对象(不用向MyBatis还要进行配置才能获取生成id)
        System.out.println("MP自动生成的id为: "+user.getId());
    }

备注:不止是BaseMapper的Insert方法能够通过getId直接获取自动生成的Id,IService的save方法也能够通过getId直接获取自动生成的Id。总的来讲MP真的简化了MyBatis,让开发变得更简单😄

3.1.2 Delete

  • int deleteById(Serializable id):根据id删除一条记录,返回值为更新数据的条数

  • int deleteById(T entity):根据entity的id删除一条记录,返回值为更新数据的条数

    注意事项:entity必须具有getId()这个方法,否则会报错

    PS:其实可以通过@TableId配置自定义的主键,详情键第四节【常用注解和配置】

  • int deleteByMap(Map<String, Object> columnMap):以map的value作为条件进行删除

    注意事项:map的key必须要与表中的字段进行对应(不区分大小写)

  • int deleteBachIds(Cellection<?> idList):批量删除

  • int delete(Wrapper<T> queryWrapper):条件删除

int result = 0;

// DELETE FROM user WHERE id=?;
result = userMapper.deleteById(1613825708092678146L);

User user = new User(1L,"张三", 23, "123@qq.com");
// DELETE FROM user WHERE id=?;
result = userMapper.deleteById(user);

Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
map.put("age", 23);
// DELETE FROM user WHERE name=? AND age=?;
result = userMapper.deleteByMap(map);

List<Long> idList = Arrays.asList(1L, 2L, 3L);
// DELETE FROM user WHERE id IN (?, ?, ?);
result = userMapper.deleteBatchIds(idList);


3.1.3 Update

  • int updateById(T entity):根据id进行修改

    entity必须具有getId()这个方法,否则会报错

  • int update(T entity, Wrapper<T> updateWrapper):根据条件进行修改

int result = 0;

User user = new User(1L,"张三", 23, "123@qq.com");
// UPDATE user SET name=?, age=?, email=? WHERE id=?;
result = userMapper.updateById(user);

3.1.4 Select

  • T selectById(Serializable id):根据id进行查询

  • List<T> selectBatchIds(Collection<? extends Serializable> idList):批量查询

  • List<T> selectByMap(Map<String, Object> columnMap):根据map的value进行条件查询

    注意事项:map的key必须要与表中的字段进行对应(不区分大小写)

  • List<T> selectList(Wrapper<T> queryWrapper):条件查询

// SELECT id,name,age,email FROM user WHERE id=?;
User user = userMapper.selectById(1L);

List<Long> idList = Arrays.asList(1l, 2l, 3l);
// SELECT id,name,age,email FROM user WHERE id IN ( ? , ? , ? );
List<User> users = userMapper.selectBatchIds(idList);

Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
map.put("age", 23);
// SELECT id,name,age,email FROM user WHERE name = ? AND age = ?;
List<User> users = userMapper.selectByMap(map);

// SELECT id,name,age,email FROM user;
List<User> users1 = userMapper.selectList(null);

3.2 Service层CRUD接口

类型参数名描述
intbatchSize插入批次数量
Tentity实体对象
WrapperupdateWrapper实体对象封装操作类 UpdateWrapper
CollectionentityList实体对象集合
Collection<? extends Serializable>idListidList
Function<? super Object, V>mapper转换函数

3.2.1 Save

  • boolean save(T entity):新增一条记录

  • boolean saveBatch(Collection<T> entityList):批量添加

    温馨提示

    1. 使用saveBatch,最好在数据库连接的url中添加一个rewriteBatchedStatements=true参数,实现高性能的批量插入

    2. 使用saveBatch,底层使用了事务,执行多条新增只会提交一次事务;但是如果在for循环中使用,会提交多次事务(不建议在循环中使用saveBatch方法)

    3. saveBatch(Collection entityList)底层就是调用saveBatch(Colection entityList, int batchSize),只是前一个他设置了默认值batchSize=1000,也就是一个批次会插入1000条,超过一千条需要等待下一批次执行

      image-20230115091505858

      备注:saveBatch底层是通过JDBC的executeBatch实现的。批次只针对增删改操作,它是指将执行SQL的语句的请求先存起来,等到达到一定数量,就当作一批发送给MySQL服务器,让MySQL服务器一次将这些SQL执行完,这样做可以避免频繁和MySQL数据库交互,造成效率低下

    参考文章:

    • MyBatis-plus批量写入数据方法saveBatch速度很慢原因排查
    • Mybatis-Plus批量插入的简单自测
    • Mybatis Plus saveBatch批量插入如何高效
    • Java-Mysql之批处理_veejaLiu的博客-CSDN博客
    • 什么是批处理?
  • boolean saveBatch(Colection<T> entityList, int batchSize):批量添加

    当batchSize=1时,都是一条一条执行单一的insert语句

    image-20230115093843353

    当batchSize>=list.size()时,底层通过executeBatch方法将list中的数据拼接成一条SQL,最终执行性一条SQL

    image-20230115093849845

  • boolean saveOrUpdate(T entity)

  • boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper)

  • boolean saveOrUpdateBatch(Collection<T> entityList)

  • boolean saveOrUpdateBatch(Collection<T>entityList, int batchSize)

// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? );
User user = new User("张三", 22, "123@qq.com");
boolean result = userService.save(user);

List<User> users = new ArrayList<>();
for (int i = 0; i < 3; i++) {
    User user = new User("张三", 18, "123@qq.com");
    users.add(user);
}
// INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? ),( ?, ?, ?, ? ),( ?, ?, ?, ? );
boolean result = userService.saveBatch(users);

// 执行三次: INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? );
boolean result = userService.saveBatch(users, 1);


3.2.2 Remove

  • boolean removeById(Serializable id):根据id删除数据
  • boolean removeById(T entity):根据id删除数据
  • boolean removeById(Serializable id, boolean useFill)
  • boolean removeBatchByIds(Collection<?> list):更具id进行批量删除
  • boolean removeBatchByIds(Collection<?> list, int batchSize)
  • boolean removeBatchByIds(Collection<?> list, boolean useFill)
  • boolean removeBatchByIds(Collection<?> list, int batchSize, boolean useFill)

3.2.3 Update

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereWrapper 条件,更新记录
boolean update(T updateEntity, Wrapper<T> whereWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

3.2.4 Get

// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);
// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

3.3 自定义SQL接口

这个和MyBatis中是高度重合的,我就不在这里继续赘述了,如果想了解可以参考这篇文章:初识MyBatisPlus

MP充分践行了它的理念:只做增强,不做修改

4、常用注解和配置

此外还有@TableName注解比较常用,这个已经在前面学习过了

4.1 @TableId

  • @TableId:用于映射主键

    MP默认将id作为注解,如果数据库中主键非id,会报错。比如我们数据库中的注解为uid,实体类的字段也为uid,此时需要在实体类的uid上添加一个@TableId注解,告诉MP uid是主键

    • value属性

      前面是数据库的主键为uid,实体类中的属性也是uid。但如果数据库中主键是uid,实体类中的主键是id呢?此时就需要使用@TableId注解的value属性了。具体使用方式:

      给实体类的uid属性上添加@TableId(value = "uid"),只有value一个属性,还可以省略为@TableId(“uid”)

    • type属性

      IdType说明
      AUTO使用id自增策略(前提是数据库也要开启id自增)
      NONE不手动设置主键值,MP将默认给出一个 Long 类型的字符串作为主键
      INPUT手动设置主键值(不设置就没有)
      ASSIGN_ID使用雪花算法生成主键值
      ASSIGN_UUID使用uuid生成主键值

      MP的id生成策略默认是雪花算法,如果我们不想使用雪花算法,需要使用@TableId注解的type属性进行配置

      给实体类的id属性上添加@TableId(type = idType.AUTO)

      注意:手动设置id的优先级要高于MP的id生成策略

      知识拓展

      • 拓展一:通过配置文件配置主键生成策略

        mybatis-plus:
          global-config:
            db-config:
              id-type: auto # assign_id、assign_uuid、input、none
        
      • 拓展二:雪花算法

        简介:雪花算法是由Twitter公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的 主键的有序性

        特点:全局唯一性(生成的id与当前时间相关)、递增性、高可用性(能够确保任何时候都能生成正确的id)、高性能(特别适合高并发场景)

        核心思想

        长度共64bit(一个long型)。

        首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负 数是1,所以id一般是正数,最高位是0。 41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。 10bit作为机器的ID(5个bit是数据中心,5bit的机器ID,可以部署在1024个节点)。 12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)

        为了应对数据规模增长导致的压力,我们需要对数据库进行扩展。常见的扩展方式有:业务分库、主从复制、数据库分表

        image-20230115122227373

        优点:整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞,并且效率较高

        1)数据库分表

        将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务 继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据, 如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进 行拆分。

        单表数据拆分有两种方式:垂直分表水平分表。示意图如下:

        image-20230115103140283

        垂直分表:拆分字段。垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。 例如,前面示意图中的 nickname 和 description 字段,假设我们是一个婚恋网站,用户在筛选其他用 户的时候,主要是用 age 和 sex 两个字段进行查询,而 nickname 和 description 两个字段主要用于展 示,一般不会在业务查询中用到。description 本身又比较长,因此我们可以将这两个字段独立到另外 一张表中,这样在查询 age 和 sex 时,就能带来一定的性能提升。

        水平分表:拆分记录。水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以 作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000 万就要分表了;而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表。 但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性 能瓶颈或者隐患。 水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理

        体验分表:以主键id为例

        1)方式一:通过分段的方式分表。

        选取适当的分段范围,比如可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1中, 1000000 ~ 1999999 放到表2中,以此类推。

        复杂点:分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会 导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适 的分段大小。

        优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万, 只需要增加新的表就可以了,原有的数据不需要动

        缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1 条,而 另外一个分段实际存储的数据量有 1000 万条

        2)方式二:通过取模的方式分表。假如我们一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来 表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号 为 6 的子表中。

        复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题

        优点:表分布比较均匀

        缺点:扩充新的表很麻烦,所有数据都要重分布

4.2 @TableField

前面我们遇到主键不一致,我们可以使用@TableId注解进行映射,如果我们的其它属性和表的字段不一致,我们则需要使用@TableField注解进行映射

  • 情况一:MP自动映射驼峰和下划线

    若实体类中的属性使用的是驼峰命名风格,而表中的字段使用的是下划线命名风格。例如实体类属性userName,表中字段user_name ,此时MyBatis-Plus会自动将下划线命名风格转化为驼峰命名风格(这个在MyBatis中需要手动配置,而MP中则默认配置了)

  • 情况二:使用@TableField注解映射字段

    若实体类中的属性和表中的字段不满足情况一,例如实体类属性name,表中字段username,此时需要在实体类属性上使用@TableField("username")设置属性所对应的字段

  • 情况三:使用@TableField注解自动填充

    在项目开发时,对于一些字段(比如:create_time、update_time……),每次执行添加、修改操作时,都需要去填充,如果使用注入的方式会显得比较麻烦,所以我们需要设置字段自动填充,当进行该操作时,就自动为这些字段进行赋值

    字段自动填充主要使用到了@TableField注解的fill属性,该属性有以下取值:

    image-20230116110727289

    • Step1:环境搭建

      ……

    • Step2:创建实体类,并在实体类需要填充的字段添加上@TableField

          @TableField(fill = FieldFill.INSERT_UPDATE) // 当进行更新(插入、删除、修改)操作时,就会进行自动填充
          private LocalDateTime updateTime;
      
          @TableField(fill = FieldFill.INSERT) // 当进行插入操作时,就会进行自动填充
          private LocalDateTime createTime;
      
    • Step3:编写元数据处理器,用于设置填充的数据

      @Component
      public class MyMetaObjectHandler implements MetaObjectHandler {
          /**
           * 插入时填充字段
           * @param metaObject
           */
          @Override
          public void insertFill(MetaObject metaObject) {
              if (metaObject.hasSetter("createTime")){
                  metaObject.setValue("createTime", LocalDateTime.now());
                  /* this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); */ //也可以使用这个,这是官方给出的自动填充方式
              }
              if (metaObject.hasSetter("updateTime")){
                  metaObject.setValue("updateTime", LocalDateTime.now());
              }
          }
      
          /**
           * 更新时填充字段(包括修改、删除)
           * @param metaObject
           */
          @Override
          public void updateFill(MetaObject metaObject) {
              if(metaObject.hasSetter("updateTime")){
                  metaObject.setValue("updateTime", LocalDateTime.now());
              }
          }
      }
      

      备注:如果实体类中只存在updateTime一个属性时,会报错,必须要同时具有updateTime和createTime两个属性(在实体类中添加@TableField注解进行自动填充,SpringBoot会自动调用MyMetaObjectHandler中所有的方法)。为了解决这个问题,我们可以添加一个if判断,判断该类是否具有该属性,然后再进行赋值操作。

      注意事项

      1. 填充的数据类型要和实体类的数据类型保持一致,否则填充结果为null

      2. 注意区别官方给出的三个填充的方法

        // 这个是通用的,插入和更新都可以使用 但是当字段存在值 的时候不进行填充
        this.fillStrategy(metaObject, "createTime", LocalDateTime.now());
        // 这个是insert的时候用的,插入的时候时候强制进行填充
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        // update的时候使用,更新的时候强制进行填充
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); 
        

        备注:以上方法都需要MyBatisPlus3.3.0版本才能进行使用,值得一提的是fillStrategy再3.3.0版本中有bug,需要使用更高的版本(PS:反正我现在使用的是MP3.5😄问题不大)

4.3 @TableLogic

  • 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为“被删除状态”,之后在数据库 中仍旧能看到此条数据记录
  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据

如果我们想要被删除的数据能够恢复,就需要使用到逻辑删除

示例

  • Step1:创建一个逻辑删除字段 is_deleted,同时设置默认值为0(0表示未删除,1表示删除)

    image-20230115123737893

  • Step2:实体类中也添加一个逻辑删除的属性isDeleted

  • Step3:给实体类的逻辑删除属性上添加一个@TableLogic注解

  • Step4:测试

    int result = userMapper.deleteById(1613825708092678146L);
    

    可以看到此时,删除的SQL并不是以前的DELETE FROM user WHERE id=?;了,而是换成了UPDATE tb_user SET is_deleted=1 WHERE id=? AND is_deleted=0;

    要恢复逻辑删除的数据也能简单,直接在数据库中将is_deleted的值改成0就好了,需要注意的是逻辑删除后的数据,查询语句是无法查到的,同时update语句也是无法进行修改的

5、条件构造器和常用接口

image-20230115131032432

  • Wrapper : 条件构造抽象类,最顶端父类
    • AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
      • QueryWrapper : 查询条件封装
      • UpdateWrapper : Update 条件封装
      • AbstractLambdaWrapper : 使用Lambda 语法
        • LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
        • LambdaUpdateWrapper : Lambda 更新封装Wrapper

5.1 QueryWrapper

  • QueryWrapper

    img

    示例一:使用QueryMapper进行查询

        @Test
        public void test1() {
            // 查询姓张的,年龄在20~30之间,并且email字段不为空的用户信息
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.like("name", "张")
                    .between("age", 20, 30)
                    .isNotNull("email");
            /*
            SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 AND
            (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
             */
            List<User> users = userMapper.selectList(queryWrapper);
            users.forEach(System.out::println);
        }
    

    image-20230115132610042

    进行指定字段查询:

        @Test
        public void test6(){
            // 查询指定字段(用户名、年龄、邮箱)
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.select("name", "age", "email");
    //        SELECT name,age,email FROM user
            List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
            maps.forEach(System.out::println);
        }
    

    image-20230115141123466

    使用QueryMapper实现子查询:

        @Test
        public void test7(){
            // 查询id小于100的用户信息
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.inSql("id","select id from user where id < 100");
    //        SELECT id,name,age,email,is_deleted FROM user WHERE (id IN (select id from user where id < 100))
            List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
            maps.forEach(System.out::println);
        }
    

    image-20230115142624942

    示例二:使用QueryMapper进行条件删除

        @Test
        public void test2(){
            // 查询用户信息,按照年龄降序排序,如果年龄相同则按照id升序排序
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.orderByDesc("age")
                    .orderByAsc("id");
    //        SELECT id,name,age,email,is_deleted FROM user WHERE is_deleted=0 ORDER BY age DESC,id ASC
            List<User> users = userMapper.selectList(queryWrapper);
            users.forEach(System.out::println);
        }
    

    image-20230115134018294

    示例三:使用QueryMapper进行条件修改

        @Test
        public void test4(){
            // 修改(年龄大于20并且姓张)或邮箱为null的用户信息
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.gt("age", 20)
                    .like("name", "张")
                    .or() // QueryMapper的连接条件默认是 AND
                    .isNull("email");
            User user = new User("张三", 22, "123@qq.com");
    //        UPDATE user SET name=?, age=?, email=? WHERE (age > ? AND name LIKE ? OR email IS NULL)
            userMapper.update(user, queryWrapper);
        }
    

    image-20230115135216822

        @Test
        public void test5(){
            // 修改年龄大于20并且(姓张或邮箱为null)的用户信息
            QueryWrapper<User> queryWrapper = new QueryWrapper<>();
            queryWrapper.gt("age", 20)
                    .and(qw->qw.like("name", "张")
                            .or()
                            .isNull("email"));
            User user = new User("张三", 22, "123@qq.com");
    //        UPDATE user SET name=?, age=?, email=? WHERE (age > ? AND (name LIKE ? OR email IS NULL))
            userMapper.update(user, queryWrapper);
        }
    

    image-20230115140253455

5.2 UpdateWrapper

示例一

    @Test
    public void test8(){
        // 修改 用户名中含有张并且(年龄大于20或者邮箱null)的用户信息
        UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
        updateWrapper.like("name", "张")
                .and(uw -> uw.gt("age", 20)
                        .or()
                        .isNull("email"));
        updateWrapper.set("name", "李四").set("age", 22);
//        UPDATE user SET name=?,age=? WHERE (name LIKE ? AND (age > ? OR email IS NULL))
        int result = userMapper.update(null, updateWrapper);
        System.out.println("result = " + result);
    }

image-20230115155249946

某个字段自增或自减,MyBatiPlusm目前只有两种方式实现

  • 使用UpdateWrapper拼接

    // 使用UpdateWrapper
    UpdateWrapper wrapper = new UpdateWrapper();
    wrapper.eq("id",【实体类对象】.getBid());
    wrapper.setSql("'自增字段' = '自增字段' + 1"); // 注意SQL中没有 += 这个符号
    【service对象】.update(wrapper);
    // 直接调用update方法
    【service对象】.update().
        setSql("stock = stock -1")
        .eq("voucher_id", voucherId)
        .update();
    
  • 直接写原生sql到xml中

    略……

方式三:自定义一个自增自减字段的方法,参考文章:mybatis-plus 自定义UpdateWrapper(一)实现列自增

        // 库存充足,秒杀券库存减一
        // 方式一:使用Service对象的update方法
        /*boolean flag = seckillVoucherService.update().
                setSql("stock = stock -1")
                .eq("voucher_id", voucherId)
                .update();*/
        // 方式二:使用LambdaUpdateWrapper的setSql方法
        UpdateWrapper updateWrapper = new UpdateWrapper();
        updateWrapper.eq("voucher_id", voucherId);
        updateWrapper.setSql("stock = stock -1");
        boolean flag = seckillVoucherService.update(updateWrapper);

        // 方式三:使用自定义的LambdaUpdateWrapper
        /*CustomLambdaUpdateWrapper<SeckillVoucher> updateWrapper = new CustomLambdaUpdateWrapper<>();
        updateWrapper.descField(SeckillVoucher::getStock, 1)
                .eq(SeckillVoucher::getVoucherId, voucherId);
        boolean flag = seckillVoucherService.update(updateWrapper);*/
        // 方式四:在xml中编写SQL语句

备注:相关完整代码请参考博主Gitee仓库中的hmdp项目

5.3 使用Condition动态组装条件

前面我们学习了QueryWrapper的常用方法,在这些方法的基础上,我们可以都可以通过添加一个condition条件,进行判断 是否需要添加改条件,这个类似于MyBatis中的动态SQL的标签,只有满足condition条件,才能够将改条件组装到SQL上

    @Test
    public void test9(){
        // 模拟从前端接收参数
        String username = "";
        Integer ageBegin = 20;
        Integer ageEnd = 30;
        // 模糊查询姓某某的人,同时20<age<30的用户信息
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.like(StringUtils.isNotBlank(username), "name", username)
                .ge(ageBegin != null, "age", ageBegin)
                .le(ageEnd != null, "age", ageEnd);
        // SELECT id,name,age,email,is_deleted FROM user WHERE (age >= ? AND age <= ?)
        List<User> users = userMapper.selectList(queryWrapper);
        users.forEach(System.out::println);
    }

5.4 LamdaQueryWrapper

前面我们使用QueryWrapper和UpdateWrapper时,字段都是自己来编写的,如果你尝试过就一定知道,自己通过字符串的形式编写是没有一点提示的,这样很容易出现字段名写错,导致SQL执行失败的情况。而MP的开发者也是知道这一点的,于是就提供了LamdaQueryWrapper,这样我们可以通过lamda表达式的形式确定字段名,这在IDEA中是有提示的,基本上可以杜绝字段写错的问题。这个完全可以替代QueryWrapper,前提是你的JDK必须大于1.8,因为Lamda表达式是JDK1.8引入的

创建条件构造器
添加条件
执行SQL
    @Test
    public void test10(){
        // 模拟从前端接收参数
        String username = "";
        Integer ageBegin = 20;
        Integer ageEnd = 30;
        // 模糊查询姓某某的人,同时20<age<30的用户信息
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.like(StringUtils.isNotBlank(username), User::getName, username)
                .ge(ageBegin != null, User::getAge, ageBegin)
                .le(ageEnd != null, User::getAge, ageEnd);
        // SELECT id,name,age,email,is_deleted FROM user WHERE (age >= ? AND age <= ?)
        List<User> users = userMapper.selectList(lambdaQueryWrapper);
        users.forEach(System.out::println);
    }

5.5 LamdaUpdateWrapper

同样的这个也可以完全替代UpdateWrapper,推荐使用这个😃

创建条件构造器
添加条件
执行SQL
    @Test
    public void test11(){
        // 修改 用户名中含有张并且(年龄大于20或者邮箱null)的用户信息
        LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
        lambdaUpdateWrapper.like(User::getName, "张")
                .and(uw -> uw.gt(User::getAge, 20)
                        .or()
                        .isNull(User::getEmail));
        lambdaUpdateWrapper.set(User::getName, "李四").set(User::getAge, 22);
//        UPDATE user SET name=?,age=? WHERE (name LIKE ? AND (age > ? OR email IS NULL))
        int result = userMapper.update(null, lambdaUpdateWrapper);
        System.out.println("result = " + result);
    }

6、插件

分页插件和乐观锁插件都是内置再MP中的,只需要配置以下就能使用了,而MyBatisX插件需要到插件商店中进行下载

6.1 MyBatisPlus分页插件

  • Step1:搭建环境

    请参考第二节【快速体验】

  • Step2:创建MP配置类,配置拦截器

    @Configuration
    public class MyBatisPlusConfig {
        
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor(){
            // 1、创建MP拦截器对象
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            // 2、将MP的分页插件添加到拦截器对象中
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            return interceptor;
        }
    }
    

    备注:如果没有加@Configuration注解,则需要在启动类上添加@MapperScan注解扫描该类

  • Step3:测试

        @Test
        public void testPage(){
            // 创建分页构造器
            Page<User> page = new Page<>(2,3);
            // 执行SQL:分页查询所有条件(还可以使用QueryWrapper对象进行条件过滤)
            userMapper.selectPage(page, null);
            
            // 这里和前面获取生成id是一样的,MP将查询的数据重新赋值给了Page对象
            System.out.println("当前页码 = " + page.getCurrent());
            System.out.println("当前页展示的记录条数 = " + page.getSize());
            System.out.println("当前页展示的记录: ");
            page.getRecords().forEach(System.out::println);
            System.out.println("分页查询的总页数 = " + page.getPages());
            System.out.println("分页查询的总记录数 = " + page.getTotal());
            System.out.println("是否存在上一页: " + page.hasPrevious());
            System.out.println("是否存在下一页: " + page.hasNext());
        }
    

    image-20230115164728524

    image-20230115165249398

知识拓展:XML自定义分页

如果在我们像自定义一个分页查询的SQL,并且需要使用MP内置的分页插件

UserMapper:

 /**
     * 通过年龄查询用户信息并进行分页
     * @param page 如果想使用MP内置的分页插件,则分页对象必须放在第一个
     * @param age
     * @return
     */
    Page<User> selectPageByAge(@Param("page") Page<User> page, @Param("age") Integer age);
    <select id="selectPageByAge" resultType="User">
        select id, name, age, email
        from user
        where age > #{age}
    </select>

备注:这里配置了实体类的别名

image-20230115171052020

测试结果:

image-20230115170916134

6.2 悲观锁与乐观锁

在学习悲观锁和乐观锁之前,我们先模拟一个场景:

一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小 李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太 高,可能会影响销量。又通知小王,你把商品价格降低30元。 此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王 也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据 库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就 完全被小王的覆盖了。 现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1 万多。

像上面那样,不加锁,就会导致操作并发时出现严重故障,所以我们对于这种事务性操作是一定要进行枷锁校验的,这样才能保证数据的一致性,不然系统的使用者蒙受巨大损失

  • 乐观锁:对数据的处理持乐观态度,乐观的认为数据一般情况下不会发生冲突,只有提交数据更新时,才会对数据是否冲突进行检测。乐观锁的实现不依靠数据库提供的锁机制,需要我们自已实现,实现方式一般是记录数据版本,一种是通过版本号,一种是通过时间戳。
  • 悲观锁:对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要将数据锁定。通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select … for update来实现悲观锁。需要注意的是使用悲观锁时,需要关闭数据库自动提交功能,即:set autocommit = 0;

如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过 了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。 如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证 最终的价格是120元。

示例一

模拟数据冲突

数据冲突包括:丢失修改、不可重复读、脏读

  • Step1:环境搭建

    1)建表

    2)插入数据

    3)导入依赖

    4)编写配置文件

  • Step2:编码

    1)编写实体类

    2)编写Mapper

  • Step3:测试

        @Test
        public void testDataConflict(){
            // 小李查看价格
            Product p1 = productMapper.selectById(1L);
            System.out.println("小李取出的价格:" + p1.getPrice());
    
            // 小王查看价格
            Product p2 = productMapper.selectById(1L);
            System.out.println("小王取出的价格:" + p2.getPrice());
    
            // 小李将价格加了50元,存入了数据库
            p1.setPrice(p1.getPrice() + 50);
            int result1 = productMapper.updateById(p1);
            System.out.println("小李修改结果:" + result1);
    
            // 小王将商品减了30元,存入了数据库
            p2.setPrice(p2.getPrice() - 30);
            int result2 = productMapper.updateById(p2);
            System.out.println("小王修改结果:" + result2);
    
            // 获取商品最终的结果
            Product p3 = productMapper.selectById(1L);
            System.out.println("最后的结果:" + p3.getPrice());
        }
    
    image-20230115215638335

示例二

使用悲观锁解决数据冲突

  • Step1:搭建环境

    略……

  • Step2:给表添加一个version字段。

  • Step3:在编写实体类时,使用@Version注解标记version属性。取出记录时,获取当前version;更新时,version + 1,如果where语句中的version版本不对,则更新失败

  • Step4:编写配置类

    @Configuration
    public class MyBatisPlusConfig {
        
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor(){
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            // 添加乐观锁插件
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
            return interceptor;
        }
        
    }
    
  • Step5:测试

    测试代码和示例一相同,略……

    image-20230115220758726

    小李进行操作时,此时小王还没进行更新操作,version字段不变;当小王操作时,因为前面小李进行了更新操作,所以version+1了,和小王获取的version不同,更新操作失败!

示例三

使用悲观锁解决数据冲突

  • Step1:搭建环境

    略……

  • Step2

6.3 MyBatisX插件

MyBatisX插件主要具有以下功能

  1. 通过Mapper接口快速在Mapper配置文件中生成statement
  2. 快速生成代码

image-20230116114750159

image-20230116114722997

image-20230116115419567

image-20230116115521336

7、通用枚举

表中的有些字段值是固定的,例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举 来实现

  • Step1:搭建环境

    1)建表(将sex的类型设置为int)

    2)插入数据

    3)导入依赖

    4)编写配置文件

  • Step2:编码

    1)编写枚举类

    @Getter // 需要提供get方法,让外界能够获取到枚举对象
    public enum SexEnum {
    
        MALE(1, "男"),
        FEMALE(2, "女");
    
        @EnumValue // 将注解所标识的值存储到数据库中(不添加这个注解,则添加的值是MALE或FEMALE)
        private Integer sex;
        private String sexName;
    
        SexEnum(Integer sex, String sexName) {
            this.sex = sex;
            this.sexName = sexName;
        }
    
    }
    

    备注:MP3.5.2版本以前(3.5.2开始就已经废弃了,不需要配置就能直接使用通用没觉了),还需要再配置文件中进行配置,才能成功使用通用枚举

    image-20230115223715462

    2)编写实体类

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private String email;
        private SexEnum sex;
    }
    

    3)编写Mapper

  • Step3:测试

        @Test
        public void testEnum(){
            User user = new User("张三", 23, "123@qq.com", SexEnum.MALE);
            int result = userMapper.insert(user);
            System.out.println("result = " + result);
        }
    

    image-20230115223937727

8、代码生成器

在MyBatis中我们可以通过逆向工程快速生成代码,从而节省大量的时间,但是MyBatis的逆向工程配置起来较为麻烦,所以MP简化了配置,让代码生成变得更加简单,堪称码农神器。让我们不必过多地重复哪些CRUD操作,只专注于核心业务。

代码生成器模板结构:

    FastAutoGenerator.create("url", "username", "password")
        //2、全局配置
        .globalConfig(...)
        //3、包配置
        .packageConfig(...)
        //4、策略配置
        .strategyConfig(...)
        //5、模板引擎配置
        .templateEngine(...)
        //6、执行
        .execute();

8.1 快速生成

示例

  • Step1:搭建环境

    1)建库、建表

    2)创建SpringBoot项目

    3)引入依赖

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <!--knife4j对应的starter,用于生成接口文档(knife4j集成了Swagger)-->
            <dependency>
                <groupId>com.github.xiaoymin</groupId>
                <artifactId>knife4j-spring-boot-starter</artifactId>
                <version>3.0.3</version>
            </dependency>
    
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.5.2</version>
            </dependency>
    
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>3.5.2</version>
            </dependency>
    
            <dependency>
                <groupId>org.freemarker</groupId>
                <artifactId>freemarker</artifactId>
                <version>2.3.31</version>
            </dependency>
    

    4)编写配置文件

    # 配置端口
    server:
      port: 8080
    
    # Spring相关配置
    spring:
      # 数据源相关配置
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis-plus_study?serverTimezone=UTC
        username: root
        password: 32345678
    
    # MyBatisPlus相关配置
    mybatis-plus:
      configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启标准日志
      type-aliases-package: com.hhxy.entity # 配置类型别名
    
  • Step2:编写生成器

    注意:一定要使用绝对路径

    public class FastAutoGeneratorTest {
        public static void main(String[] args) {
            // 1、配置数据源
            FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis-plus_study?serverTimezone=UTC", "root", "32345678")
                    // 2、全局配置
                    .globalConfig(builder -> {
                        builder.author("ghp") // 设置作者
                                .enableSwagger() // 开启 swagger 模式
                                .fileOverride() // 覆盖已生成文件
                                .outputDir("/src/main/java"); // 指定输出目录
                    })
                    // 3、包配置
                    .packageConfig(builder -> {
                        builder.parent("com.hhxy") // 设置父包名
                                .moduleName("") // 设置父包模块名
                                .pathInfo(Collections.singletonMap(OutputFile.xml, "/src/main/resources/mapper")); // 设置mapperXml生成路径
                    })
                    // 4、策略配置
                    .strategyConfig(builder -> {
                        builder.addInclude("user") // 设置需要生成的表名(数据库中必须存在该表)
                                .addTablePrefix("tb_", "t_", "tbl_"); // 设置过滤表前缀
                    })
                    // 5、模板引擎配置
                    .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                    // 6、执行
                    .execute();
        }
    }
    
    

    备注:MyBatisPlus5.2开始,fileOverride方法被弃用了

  • Step3:运行方法

    image-20230116101109289

image-20230116100607012

使用MyBatisX插件生成自定的SQL

我们需要通过insertdeleteupdateselect等关键次开头,IDEA会有提示

image-20230116141021961

image-20230116141726325

8.2 交互式生成

public class InteractiveAutoGeneratorTest {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("=====================数据库配置=======================");
        System.out.println("请输入 URL");
        String url = scan.next();
        System.out.println("请输入 username");
        String username = scan.next();
        System.out.println("请输入 password");
        String password = scan.next();

        FastAutoGenerator.create(url, username, password)
                // 全局配置
                .globalConfig((scanner, builder) -> builder.author(scanner.apply("=====================全局配置=======================\n请输入作者名称"))
                        .outputDir(System.getProperty("user.dir") + "/src/main/java")
                        .commentDate("yyyy-MM-dd hh:mm:ss")
                        .dateType(DateType.TIME_PACK)
                        .enableSwagger()
                        .fileOverride()
                        .enableSwagger()
                        .disableOpenDir()
                )
                // 包配置
                .packageConfig((scanner, builder) -> builder.parent(scanner.apply("=====================包配置=======================\n请输入包名?"))
                        .moduleName(scanner.apply("请输入父包模块名?"))
                        .entity("entity")
                        .service("service")
                        .serviceImpl("serviceImpl")
                        .mapper("mapper")
                        .xml("mapper")
                        .other("utils")
                        .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir")+"/src/main/resources/mapper"))
                )
                // 策略配置
                .strategyConfig((scanner, builder) -> {
                    builder.addInclude(getTables(scanner.apply("=====================策略配置=======================\n请输入表名,多个参数使用逗号分隔")))
                            .serviceBuilder()
                            .formatServiceFileName("%sService")
                            .formatServiceImplFileName("%sServiceImpl")
                            .entityBuilder()        //实体类策略配置
                            .enableLombok()         //开启 Lombok
                            .disableSerialVersionUID()
                            .logicDeleteColumnName("deleted")        //逻辑删除字段
                            .naming(NamingStrategy.underline_to_camel)
                            .columnNaming(NamingStrategy.underline_to_camel)
                            .addTableFills(new Column("create_time", FieldFill.INSERT), new Column("modify_time", FieldFill.INSERT_UPDATE))
                            .enableTableFieldAnnotation()       // 开启生成实体时生成字段注解
                            .controllerBuilder()
                            .formatFileName("%sController")
                            .enableRestStyle()
                            .mapperBuilder()
                            .superClass(BaseMapper.class)
                            .formatMapperFileName("%sMapper")
                            .enableMapperAnnotation()       //@mapper
                            .formatXmlFileName("%sMapper");
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }

    // 处理 all 情况
    protected static List<String> getTables(String tables) {
        return "all".equals(tables) ? Collections.emptyList() : Arrays.asList(tables.split(","));
    }
}

9、多数据源

一个项目中经常会使用到多个数据库,比如主从复制,主库来进行更新操作,从库用来进行查询操作,从而有效降低数据库库的负担,一定程度提高系统的效率。

关于主从复制相关配置可以参考:MySQL学习笔记

多数据源主要存在以下三种配置方式:

# 多主多从                      纯粹多库(记得设置primary)                   混合配置
spring:                               spring:                               spring:
  datasource:                           datasource:                           datasource:
    dynamic:                              dynamic:                              dynamic:
      datasource:                           datasource:                           datasource:
        master_1:                             mysql:                                master:
        master_2:                             oracle:                               slave_1:
        slave_1:                              sqlserver:                            slave_2:
        slave_2:                              postgresql:                           oracle_1:
        slave_3:                              h2:                                   oracle_2:

主从复制相关配置:

spring:
# 配置数据源信息
  datasource:
    dynamic:
    # 设置默认的数据源或者数据源组, 默认值即为master
#    primary: master
    # 严格匹配数据源,默认false.true未匹配到指定数据源时抛异常,false使用默认数据源
#    strict: false
    datasource:
      names:
        master,slave # 设置别名,可以随便取,但要与后面的前缀对应(可以配置多个从库)
      master:
        url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: 123456
      slave:
        url: jdbc:mysql://localhost:3306/mybatis_plus_1?characterEncoding=utf-8&useSSL=false
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: 123456

image-20230116105235620

@Service
@DS("master") // 使用主库
public class UserServiceImpl implements UserService {

  @Autowired
  private JdbcTemplate jdbcTemplate;

  public List selectAll() {
    return  jdbcTemplate.queryForList("select * from user");
  }
  
  @Override
  @DS("slave") // 使用从库
  public List selectByCondition() {
    return  jdbcTemplate.queryForList("select * from user where age >10");
  }
}

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

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

相关文章

【C++】模板进阶(非类型模板参数、类模板的特化和模板的分离编译)

之前我们讲解过模板初阶&#xff0c;没有阅读过的童鞋可以先去阅读之前的博文----->模板初阶 本章我们将针对模板进行进一步的讲解。 目录 &#xff08;一&#xff09;非类型模板参数 &#xff08;二&#xff09;模板的特化 &#xff08;1&#xff09;概念 &#xff0…

我的面试八股(Java集合篇)

Java集合 两个抽象接口派生&#xff1a;一个是Collection接口,存放单一元素&#xff1b;一个是Map接口存放键值对。 Vector为什么是线程安全 简单&#xff0c;因为官方在可能涉及到线程不安全的操作都进行了synchronized操作&#xff0c;就自身源码就给你加了把锁。 Vector…

Stacking:解决机器学习进行多模型组合的实用工具

文章目录1 Stacking原理第一步&#xff1a;生成预测结果第二步&#xff1a;整合预测结果2 使用Python实现Stacking第一步&#xff1a;生成预测结果第二步&#xff1a;整合预测结果借助sklearn实现stacking3 各领域内的一些实际应用在机器学习领域&#xff0c;算法的选择和参数的…

前端--文件上传--文件切片--利用FileReader()中的readAsDataURL()做缩略图--多文件上传--formData--切片上传实现

一、文件上传 <template><div><input type"file" name"file" change"fileChange" /><button click"submit">提交</button></div> </template><script setup>function fileChange(e)…

ROS开发之如何制作launch启动文件?

文章目录0、引言1、Launch文件语法2、Launch示例0、引言 笔者因研究课题涉及ROS开发&#xff0c;学习了古月居出品的ROS入门21讲&#xff0c;为巩固launch的知识&#xff0c;本文将ROS的launch启动文件制作一讲内容进行总结。launch文件通过XML文件实现多节点的配置和启动&…

Compose (11/N) - 手势

一、点击 1.1 可点击 Modifier.clickable( ) 允许应用检测对该元素的点击。 Composable fun ClickableSample() {val count remember { mutableStateOf(0) }Text(text count.value.toString(),modifier Modifier.clickable { count.value 1 }) } 1.2 手势检测 Modifier.p…

【技术分享】接口自动化测试中,如何做断言验证?

在服务端自动化测试过程中&#xff0c;发起请求之后还需要对响应值进行验证。验证响应信息符合预期值之后&#xff0c;这一条接口自动化测试用例才算完整的通过。所以这一章节&#xff0c;将会讲解在接口自动化测试中&#xff0c;如何对服务端返回的响应内容做断言验证。 实战…

C语言函数大全-- i 开头的函数

C语言函数大全 本篇介绍C语言函数大全– i 开头的函数 1. imagesize 1.1 函数说明 函数声明函数功能unsigned imagesize(int left, int top, int right, int bottom);获取保存位图像所需的字节数 1.2 演示示例 #include <graphics.h> #include <stdlib.h> #in…

【Java数据结构】链表OJ提交小记

目录 1.删除链表中所有值为val的节点 2.反转单链表 3.返回链表的中间节点 4.返回链表倒数第k个节点 5.按次序合并链表 6.按值分割链表 7.判断链表是否为回文 1.删除链表中所有值为val的节点 1. 删除链表中所有值为val的节点https://leetcode.cn/problems/remove…

考研数据结构-绪论

绪论 文章目录绪论1. 什么是数据结构2. 基本概念数据结构的四类基本结构&#xff08;逻辑结构&#xff09;存储结构顺序存储和链式存储比较分析3. 算法概念特征优点&#xff08;也是要求&#xff09;算法效率的度量概念时间复杂度空间复杂度(了解)1. 什么是数据结构 数据结构是…

【Python】字符串 ⑦ ( input 字符串输入 | input 函数自带提示参数 | input 函数接收的变量类型 )

文章目录一、input 字符串输入二、代码示例三、input 函数自带提示参数四、input 函数接收的变量类型一、input 字符串输入 在命令行中 , 使用 printf 可以输出数据 , 将 变量 , 字面量 , 表达式 输出到命令行中 ; 在命令行中 , 使用 input 语句可以 在 命令行 中 , 从键盘获取…

2023年第五届传智杯前四题题解(后俩没写出来)

比赛链接&#xff1a;第五届“传智杯”全国大学生计算机大赛&#xff08;决赛B组&#xff09; - 比赛详情 - 洛谷 时效「月岩笠的诅咒」 题目背景 蓬莱之药&#xff0c;被诅咒的不死之药。 奉命将蓬莱之药投入富士山中销毁的月岩笠&#xff0c;最终打算把蓬莱之药改投入八岳销…

STM32Cube的debug和release切换

一&#xff0c; Debug / Release版本区别 来源&#xff1a;STM32CUBEIDE中 Debug 和 Release 的作用/区别/使用场景 - svchao - 博客园 (cnblogs.com) 二&#xff0c;Debug / Release使用。 1&#xff0c;在编译的时候可以选择Debug 还是 Release . 2,使用stm32CubeIDE调试或运…

【模型复现】resnet,使用net.add_module()的方法构建模型。小小的改进大大的影响,何大神思路很奇妙,基础很扎实

从经验来看&#xff0c;网络的深度对模型的性能至关重要&#xff0c;当增加网络层数后&#xff0c;网络可以进行更加复杂的特征模式的提取&#xff0c;所以当模型更深时理论上可以取得更好的结果。但是更深的网络其性能一定会更好吗&#xff1f;实验发现深度网络出现了退化问题…

Git的安装与基本使用

Git是一个分布式版本控制工具&#xff0c;可以快速高效地处理从小型到大型的各种项目。 1.Git的安装 官网下载地址 &#xff1a;https://git-scm.com/ 安装过程 选择 Git 安装位置&#xff0c;要求是非中文并且没有空格的目录&#xff0c;然后下一步。 Git 选项配置&#xf…

ChatGPT搭建语音智能助手

环境 python&#xff1a;3 ffmpeg:用于处理视频和语音 gradio:UI界面和读取语音 概述 我们的目的是做一个语音智能助手 下面我们开始 准备工作 下载Visual Studio Code Visual Studio Code 因为需要写python代码&#xff0c;用Visual Studio Code比较方便。 安装pytho…

( “树” 之 DFS) 101. 对称二叉树 ——【Leetcode每日一题】

101. 对称二叉树 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#xff1a;false 提示&#xff1a…

webgl-画任意多边形

注意&#xff1a; let canvas document.getElementById(webgl) canvas.width window.innerWidth canvas.height window.innerHeight let radio window.innerWidth/window.innerHeight; let ctx canvas.getContext(webgl) 由于屏幕长宽像素不一样&#xff0c;导致了长宽像素…

移远云服务QuecCloud正式发布,一站式为全球客户提供创新有效的解决方案

4月12日&#xff0c;在“万物智联共数未来”移远通信物联网生态大会上&#xff0c;移远通信宣布正式推出其物联网云服务——QuecCloud。QuecCloud具备智能硬件开发、物联网开放平台、行业解决方案三大能力&#xff0c;可为开发者和企业用户提供从硬件接入到软件应用的全流程解决…

Java 进阶(5) Java IO流

⼀、File类 概念&#xff1a;代表物理盘符中的⼀个⽂件或者⽂件夹。 常见方法&#xff1a; 方法名 描述 createNewFile() 创建⼀个新文件。 mkdir() 创建⼀个新⽬录。 delete() 删除⽂件或空⽬录。 exists() 判断File对象所对象所代表的对象是否存在。 getAbsolute…