MyBatisPlus



1. 概念

是MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,简化开发提高效率。

特性:

无侵入,损耗小,强大的 CRUD 操作,支持 Lambda 形式调用,

支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题

支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强 大的 CRUD 操作

支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )

内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用 (MyBatis的逆向工程只能生成Mapper,Medel)

内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询,分页插件支持多种数据库

内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询

内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作


2. 入门案例

  1. 建立测试表user

    CREATE DATABASE `mybatis_plus` 
    use `mybatis_plus`;
    CREATE TABLE `user` (
        `id` bigint(20) NOT NULL COMMENT '主键ID',       # 注意是bigint
        `name` varchar(30) DEFAULT NULL COMMENT '姓名',
        `age` int(11) DEFAULT NULL COMMENT '年龄',
        `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
  2. 创建SpringBoot项目

    mysql驱动,lombok,mybatis-plus

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.25</version>
    </dependency>
    
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <!-- 引入 MyBatis-Plus 之后请不要再次引入 MyBatis 以及 MyBatis-Spring,以避免因版本差异导致的问题-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId> 
        <version>3.5.1</version>
    </dependency>
    
  3. 配置数据源

    spring:
      datasource:
        type: com.zaxxer.hikari.HikariDataSource   # Spring默认的数据源
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/mybatis_plus
        username: root
        password: 9527
    
  4. 创建实体类User

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User {
        private Long id;  // 注意是Long,MyBtisPlus使用雪花算法算的id
        private String name;
        private Integer age;
        private String email;
    }
    
  5. 创建Mapper接口

    // BaseMapper是MyBatisPlus提供的(里面对单表的增删查改方法),泛型是要操作的实体类的类型
    public interface UserMapper extends BaseMapper<User> {
        
    }
    
  6. 在SpringBoot启动类上面添加Mapper扫描

    @MapperScan("com.sutong.mapper")  // 用于扫描Mapper接口,或者在每个Mapper上加上@Mapper注解
    
  7. 测试

    @SpringBootTest
    public class MyBatisPlusTest {
        @Resource
        UserMapper userMapper;
    
        @Test
        public void test01() {
            // 参数里面是查询条件,没有则填null就是查询全部
            List<User> users = userMapper.selectList(null);
            System.out.println(users);
        }
    }
    

我们没有写sql,也没给MyBatisPlus说操作那个表,也没有说字段和那个属性映射,只是指定了一个泛型就可以使用!

如果想要看sql语句则下面这样配置(日志功能):

mybatis-plus:
 configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 输出sql信息

上面测试的sql语句:SELECT id,name,age,email FROM user,则默认操作表是实体类的名称,查询字段则是属性名

MySQL新老版本区别⭐

1、驱动类

spring boot 2.0(内置jdbc5驱动), spring boot 2.1及以上(内置jdbc8驱动)

driver-class-name: com.mysql.jdbc.Driver

driver-class-name: com.mysql.cj.jdbc.Driver 否则运行测试用例的时候会有 WARN 信息

2、连接地址url

MySQL5.7版本的url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&useSSL=false

MySQL8.0版本的url: jdbc:mysql://localhost:3306/mybatis_plus?serverTimezone=GMT%2B8&characterEncoding=utf-8&useSSL=false

如果上面设置还是相差八小时的话使用:serverTimezone=Asia/Shanghai


3. CRUD

Ⅰ BaseMapper

方法:

int insert(T entity); 

int deleteById(Serializable id);
int deleteById(T entity);
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
int delete(@Param("ew") Wrapper<T> queryWrapper);  
int deleteBatchIds(@Param("coll") Collection<?> idList);

int updateById(@Param("et") T entity);
int update(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

T selectById(Serializable id);
List<T> selectBatchIds(@Param("coll") Collection<? extends Serializable> idList);
List<T> selectByMap(@Param("cm") Map<String, Object> columnMap);

default T selectOne(@Param("ew") Wrapper<T> queryWrapper) {  // 根据条件构造器进行单个查询
    List<T> ts = this.selectList(queryWrapper);
    if (CollectionUtils.isNotEmpty(ts)) {
        if (ts.size() != 1) {
            throw ExceptionUtils.mpe("...", new Object[0]);
        } else {
            return ts.get(0);
        }
    } else {
        return null;
    }
}

default boolean exists(Wrapper<T> queryWrapper) {  // 根据条件构造器判断是否存在
    Long count = this.selectCount(queryWrapper);
    return null != count && count > 0L;
}

Long selectCount(@Param("ew") Wrapper<T> queryWrapper); // 查询符合条件的数据数量
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper); // 根据条件构造器查询数据,封装为List
// 根据条件查询数据,每条封装为Map,整体是个List
List<Map<String, Object>> selectMaps(@Param("ew") Wrapper<T> queryWrapper);
List<Object> selectObjs(@Param("ew") Wrapper<T> queryWrapper);  // 查询多个

// 分页的
<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper); 
<P extends IPage<Map<String, Object>>> P selectMapsPage(P page, @Param("ew") Wrapper<T> queryWrapper);

Ⅱ 插入 删除 修改

@Test
public void test02() {
    // sql:INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? )
    User su = new User(null, "su", 18, "su@qq.com");
    int res = userMapper.insert(su);
    System.out.println(res + " " + su.getId()); // id很长(1499732145958776834)
}
@Test
public void test03() {
    userMapper.deleteById(1L);  // 如果很大的话需要1499732145958776834L这样!Long类型
    userMapper.deleteById(new User(1499732145958776834L, null, null, null)); 
    userMapper.deleteBatchIds(Arrays.asList(1L, 2L)); // sql:... WHERE id IN (?, ?)
    
    Map<String, Object> map = new HashMap<>();
    map.put("name", "su");
    map.put("age", 18); 
    userMapper.deleteByMap(map); // 删除名字是su的,年龄是18的
    // 带条件的下面讲!!
}
@Test
public void test04() {
    // userMapper.update(); // 第一个是数据,第二是条件Wrapper(后面讲)
    userMapper.updateById(new User(1L, "tong", null, "tong@qq.com")); // 通过id修改, age不修改
}

Ⅲ 查询

@Test
public void test05() {
    User user = userMapper.selectById(1L);
    List<User> users01 = userMapper.selectBatchIds(Arrays.asList(1L, 2L)); // sql: ... WHERE id INT(?,?)
    
    Map<String, Object> map = new HashMap<>();
    map.put("name", "su");
    map.put("age", 18);
    List<User> users02 = userMapper.selectByMap(map);  // map的k-v作为条件查询,和delete一样

    List<User> users03 = userMapper.selectList(null);
}

Ⅳ 拓展

如果BaseMapper的方法不满意,我们可以自定义,和MyBatis的用法一样的

指定Mapper映射文件的路径:(Mapper映射文件的名字要和Mapper接口名一样)

mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml  # 默认就是classpath*:/mapper/**/*.xml,mapper目录下还可以有目录

接口:

public interface UserMapper extends BaseMapper<User> {

    // 根据id查询用户信息为一个Map集合 k-v 列名-值
    Map<String, Object> selectMapById(Long id);
}

映射文件:

<mapper namespace="com.sutong.mapper.UserMapper">
    <select id="selectMapById" resultMap="map">
        select id,name,age,email from user where id = #{id}
    </select>
</mapper>

测试:

@Test
public void test06() {
    System.out.println(userMapper.selectMapById(1L));
    // {name=Jone, id=1, age=18, email=test1@baomidou.com}
}

Ⅴ 通用Service

  • 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用save保存, get 查询单行, remove 删除, list 查询集合, page 分页 前缀命名方式区分 Mapper 层避免混淆
  • 泛型 T 为任意实体对象,对象 Wrapper 为 条件构造
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类

IService -> ServiceImp (实际中不建议直接使用IServiceImp因为无法满足我们的业务)

public interface IService<T> {}

public class ServiceImpl<M extends BaseMapper<T>, T> implements IService<T> {}

使用⭐:

// 建议自己创建一个Service,这样既可以使用MyBatisPlus的,也可以自定义方法!!
public interface UserService extends IService<User> {
    
} 

// ServiceImpl第一个泛型是要操作Mapper接口的类型,第二个实体类的类型
// 这样UserServiceImp就能用MyBatisPlus提供的通过Service方法了!!!
@Service
public class UserServiceImp extends ServiceImpl<UserMapper, User> implements UserService {
    
}

测试:

@SpringBootTest
public class ServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void test01() {
        long count = userService.count(); // 查询总记录数 SELECT COUNT( * ) FROM user

        List<User> users = new ArrayList<>();
        // INSERT INTO user ( id, name, age, email ) VALUES ( ?, ?, ?, ? ) 
        // 是根据单条添加进行批量添加的,每1000条提交一次
        users.add(new User(null, "11", 20, "11@qq.com"));
        users.add(new User(null, "22", 10, "22@qq.com"));
        userService.saveBatch(users);   // BaseMapper里面没有批量添加,在Service才有
        // saveOrUpdateBatch 是根据id有没有,进行添加或者修改
    }
}

4. 注解

Ⅰ @TableName

如果数据库的表名和实体类名不一样,怎么办?

  • 使用 @TableName

    @TableName("t_user")  // 设置实体类所对应的表名
    public class User {
        private Long id;
        private String name;
        private Integer age;
        private String email;
    }
    
  • 全局配置

    可以配置实体类对应数据表名的前缀(就不用每个实体类都加注解的)

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

Ⅱ @TableId

MyBatisPlus默认是以id作为主键,如果数据库中uid是主键怎么办? @TableId

public class User {
    @TableId  //将当前属性对应的字段指定为主键
    private Long uid;
    
    private String name;
    private Integer age;
    private String email;
}
  • value属性,如果实体类中叫id,数据库中叫uid,怎么映射?

    public class User {
        @TableId(value = "uid")  // 用于指定主键的字段
        private Long id;
        
        ...
    }
    
  • type属性(表示主键生成的策略,默认是雪花算法)

    如果想要使用MySql是主键自动递增(首先要在数据库设置主键自动递增):

    public class User {
        @TableId(type = IdType.AUTO)  // 再指定主键自动生成策略 (ASSIGN_ID代表雪花算法,与数据库递增无关)
        private Long id;
        
        ...
    }
    

    递增添加的时候则不为id赋值。而雪花算法是先生成id,再为id字段赋值!

    全局配置

    mybatis-plus:
      global-config:
        db-config:
          id-type: auto
    

Ⅲ @TableField

其他字段的字段名和实体类的属性名不对应时,怎么办?

当数据库是下划线命名法,属性是驼峰命名法,我们不需要使用这个,自动帮我们映射!!!

当命名完全没关系时,则可以使用下面的注解:

@TableField("user_name")  // 指定属性对应的字段名
private String name;

@TableField(exist = false)  // 这个字段在表中不存在,否则就会报错了!
private String password;

Ⅳ @TableLogic

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

1.表中添加一个字段,is_delete 默认0,没被删除

2.实体类中也添加上

public class User {
    ...
    @TableLogic  // 代表是逻辑删除的字段
    private Integer isDelete;
}

3.再次执行通用service的删除方法就是逻辑删除

@Test
public void test02() {
    // UPDATE user SET is_delete=1 WHERE id=? AND is_delete=0
    // 将@TableLogic标注的字段修改为1
    System.out.println(userService.removeById(1)); // 不再是删除,而是修改sql(MyBatisPlus自动帮我们转化操作)

    // sql: SELECT id,name,age,email,is_delete FROM user WHERE id=? AND is_delete=0
    User user = userService.getById(1); // null,是查不到的
}

雪花算法

数据库分表(需要选择合适的方案去应对数据规模的增长)

  • 垂直拆分:将不重要的字段和占了大量空间的拆分掉

  • 水平拆分:适合表行数特别大的表,例如超过 5000 万就要分表

    水平分表相比垂直分表,会引入更多的复杂性,例如要求全局唯一的数据id该如何处理

    • 主键递增

      1 ~ 999999 放到表 1中, 1000000 ~ 1999999 放到表2中,以此类推。

      优点:可以随着数据的增加平滑地扩充新的表。 缺点:分布不均匀。复杂点:分段大小的选取。

    • 取模

      假如有 10 个数据库表,用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中。

      优点:表分布比较均匀。 缺点:扩充新的表很麻烦,所有数据都要重分布。复杂点:初始表数量的确定。

    • 雪花算法

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

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

雪花算法核心思想(了解):

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

首先是一个符号位,1bit标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负 数是1,所以id一般是正数,最高位是0。

41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年。

10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)。

12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)。


5. 条件构造器

继承逻辑:

Wrapper : 条件构造抽象类,最顶端父类

  • AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
    • QueryWrapper : 查询条件封装
    • UpdateWrapper : Update 条件封装
    • AbstractLambdaWrapper : 使用Lambda 语法
      • LambdaQueryWrapper :用于Lambda语法使用的查询Wrapper
      • LambdaUpdateWrapper : Lambda 更新封装Wrapper

QueryWrapper 查询和删除(修改也能用)

UpdateWrapper 修改

Ⅰ QueryWrapper

测试BaseMapper的方法:

简单的:

// 组装查询条件
@Test
public void test01() {
    // 查询用户名包含t,年龄再20-30,emil不为空的信息 (column参数是字段名,并不是属性名)
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.like("name", "t").between("age", 20, 30).isNotNull("email");
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);
    
    // SELECT id,name,age,email,is_delete FROM user 
    // 		WHERE is_delete=0 AND (name LIKE ? AND age BETWEEN ? AND ? AND email IS NOT NULL)
    // Parameters: %t%(String), 20(Integer), 30(Integer)
}


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


// 组装删除条件
@Test
public void test03() {
    // 删除email为null的用户
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.isNull("email");
    int line = userMapper.delete(queryWrapper);
}

复杂一点:

// 组装修改条件!!!!
@Test
public void test04() {
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    
    // 将 (name包含a并且age大于20) 或者 email为空的信息进行修改 (不调用or则默认为使用and连接)
    queryWrapper.like("name", "a")
            .gt("age", 20)
            .or()
            .isNull("email");

    // 将 name包含a 并且 (age大于20或者email为空) 的信息进行修改
    // and()可传入一个Consumer(or也有),消费者模型,消费者的参数就是条件构造器,Lambda表达式中的条件优先执行
    queryWrapper.like("name", "a")  
            .and(i -> {
                i.gt("age", 20).or().isNull("email"); 
            });   
    
    User user = new User(null, "小明", null, "ming@qq.com", 0); // 修改user不为空的字段
    int line = userMapper.update(user, queryWrapper); // 第一个参数用户填充,第二个是要修改的条件
    
    
    // 第一个:UPDATE user SET name=?, email=? 
    //  		WHERE is_delete=0 AND (age > ? AND name LIKE ? OR email IS NULL)
    // 第二个:UPDATE user SET name=?, email=? 
    // 			WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}

查询部分字段:

// 组装select字句
@Test
public void test05() {
    // 查询age大于20用户的的name,email信息
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.gt("age", 20).select("name", "email");
    List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
    maps.forEach(System.out::println);
    
    // SELECT name,email FROM user WHERE is_delete=0 AND (age > ?)
}

子查询:

@Test
public void test06() {
    // 查询id小于100的的用户信息(可以不使用子查询)
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.inSql("id", "select id from user where id <= 100");
    List<User> users = userMapper.selectList(queryWrapper);
    users.forEach(System.out::println);

    // SELECT id,name,age,email,is_delete FROM user 
    // 		WHERE is_delete=0 AND (id IN (select id from user where id <= 100))
}

com.baomidou.mybatisplus.core.toolkit.StringUtils; 中isNotBlank()判断字符串是否为null,空字符串,空白符。Spring中也有StringUtils工具类

组装前判断,condition参数:

在实际中,我们要根据用户浏览器发来的参数,进行组装条件,组装前要对每个参数进行条件判断(下面是判断简单的方法)

@Test
public void test07() {
    String username = "a"; Integer ageBegin = 30; Integer ageEnd = 50; // 用户请求参数信息
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.like(StringUtils.isNotBlank(username), "name", username) // 第一个参数是condition布尔类型
            .ge(ageBegin != null, "age", ageBegin)
            .le(ageEnd != null, "age", ageEnd);
    List<User> users = userMapper.selectList(queryWrapper);
}

Ⅱ UpdateWrapper

@Test
public void test08() {
    // 将 name包含a 并且 (age大于20或者email为空) 的信息进行修改
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();  // UpdateWrapper!!
    
    // 修改条件
    updateWrapper.like("name", "a")
            .and(i -> {    
                i.gt("age", 20).or().isNull("email");
            });
    // 修改数据
    updateWrapper.set("name", "小白").set("email", "bai@qq.com");
    userMapper.update(null, updateWrapper); // 修改不使用实体类对象了,更简单一点
    
    // UPDATE user SET name=?,email=? 
    //		WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}

Ⅲ LambdaQueryWrapper

上面我们的字段名也容易写错,所以有了LambdaQueryWrapper

和上面不同的是,字段那个字符串参数,变成了一个函数式接口,SFunction

public interface Function<T, R> {
	R apply(T t);
}

public interface SFunction<T, R> extends Function<T, R>, Serializable {
    
}

T就是实体类的类型,通过这个就可以直接拿到实体类属性所对应的字段名!!

自动获取属性所对应的字段名来作为组装sql的字段!!(使用@TableField注解时会自动去映射字段名)

测试:

@Test
public void test09() {
    String username = "a"; Integer ageBegin = 30; Integer ageEnd = 50;
    
    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);
    List<User> users = userMapper.selectList(lambdaQueryWrapper);
    
    // SELECT id,name,age,email,is_delete FROM user 
    //		WHERE is_delete=0 AND (name LIKE ? AND age >= ? AND age <= ?)
}

Ⅳ LambdaUpdateWrapper

@Test
public void test10() {
    // 将 (name包含a并且age大于20) 或者 email为空的信息进行修改
    LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();  
    // 修改条件
    lambdaUpdateWrapper.like(User::getName, "a")
            .and(i -> {
                i.gt(User::getAge, 20).or().isNull(User::getEmail);
            });
    // 设置数据也能用Lambda方式
    lambdaUpdateWrapper.set(User::getName, "小白").set(User::getEmail, "bai@qq.com");
    userMapper.update(null, lambdaUpdateWrapper); 
    
    // UPDATE user SET name=?,email=? 
    // 		WHERE is_delete=0 AND (name LIKE ? AND (age > ? OR email IS NULL))
}

6. 插件

Ⅰ 分页插件

MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能!!

  1. 配置类

    @Configuration
    public class MyBatisPlusConfig {
        // @return  MybatisPlusInterceptor是配置MyBatis中的插件的
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            // 添加插件。分页插件的有参构造最后加上数据库类型,因为不同的数据库分页功能实现不同!
            interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
            return interceptor;
        }
    }
    
  2. 测试

    @Test
    public void test01() {
        // 泛型是实体类类型,Page<T> implements IPage<T>。参数:current是当前第几页,size是每页显示的数据数
        Page<User> page = new Page<>(2, 3);
        userMapper.selectPage(page, null); // 查询所有数据的分页数据,该方法返回还是Page,数据都在这里面
        
        List<User> users = page.getRecords(); // 当前页的数据
        page.getCurrent(); page.getSize();
        long pageNum = page.getPages(); // 总页数
        long total = page.getTotal(); // 总记录数
        page.hasNext(); page.hasPrevious(); // 是否有下一条/上一条数据
    
        // SELECT id,name,age,email,is_delete FROM user WHERE is_delete=0 LIMIT ?,?
        // Parameters: 3(Long), 3(Long)  
        // limit后面第一个占位是当前页的起始索引 = (current - 1) * size,所以两个参数都是3
    }
    

自定义分页功能

Mapper接口:

public interface UserMapper extends BaseMapper<User> {

    // 参考selectPage()方法,返回值必须是Page对象。想要分页插件作用我们自己的方法,第一个参数必须是Page类型!!!
    // 根据年龄查询用户信息,进行分页
    Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
}

映射文件:

<!-- resultType是实体类对象类型就行,select标签里面的sql不需要我们自己写分页的逻辑-->
<select id="selectPageVo" resultType="com.sutong.bean.User">
    select id,name,age,email,is_delete from user where age > #{age} 
</select>

测试:

@Test
public void test02() {
    Page<User> page = new Page<>(2, 3);
    userMapper.selectPageVo(page, 10);   // 当然这个方法可以不用自己写,可以传入条件构造器就行
    List<User> list = page.getRecords(); // 年龄大于10分页后的数据
    list.forEach(System.out::println);

    // SELECT COUNT(*) AS total FROM user WHERE age > ?
    // select id,name,age,email,is_delete from user where age > ? LIMIT ?,?     在我们写的sqsl后面加上limit
}

Ⅱ 乐观锁

一件商品,成本价是80元,售价是100元。如果小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就 完全被小王的覆盖了。

  1. 悲观锁

    小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证 最终的价格是120元。

  2. 乐观锁

    小王保存价格前,会检查下价格是否被人修改过了(查看版本号字段)。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。

模拟修改的冲突:

  1. 创建商品表

    CREATE TABLE product(
        id BIGINT(20) NOT NULL COMMENT '主键ID',
        NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称',
        price INT(11) DEFAULT 0 COMMENT '价格',
        VERSION INT(11) DEFAULT 0 COMMENT '乐观锁版本号',
        PRIMARY KEY (id)
    );
    
    INSERT INTO t_product (id, NAME, price) VALUES (1, '外星人笔记本', 100);
    
  2. 创建实体类Product,ProductMapper

  3. 测试(最终价格会变为70)

    @Test
    public void test01() {
        Product productLi = productMapper.selectById(1);
        Integer priceLi = productLi.getPrice(); // 100
    
        Product productWang = productMapper.selectById(1);
        Integer priceWang = productWang.getPrice(); // 100
    
        // 修改
        productLi.setPrice(priceLi + 50);
        productMapper.updateById(productLi); // 150
        productWang.setPrice(priceWang - 30);
        productMapper.updateById(productWang); // 70
    }
    

解决上面修改冲突:(使用MyBatisPlus提供的乐观锁插件)

乐观锁实现流程:

  • 在表中添加version字段

  • 取出记录时,获取当前version

    SELECT id,name,price,version FROM product WHERE id=1

  • 更新时,version + 1,如果where语句中的version版本不对,则更新失败

    UPDATE product SET price=price+50, version=version + 1 WHERE id=1 AND version=1

  1. 配置类中配置插件

    @Configuration
    public class MyBatisPlusConfig {
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 添加乐观锁插件
            return interceptor;
        }
    }
    
  2. 在实体类表示版本号的属性上面加上@Version注解

    public class Product {
        ...
        @Version  // 标识乐观锁版本号字段
        private Integer version;
    }
    
  3. 运行上面的测试

    @Test
    public void test01() {
        Product productLi = productMapper.selectById(1); 
        Integer priceLi = productLi.getPrice(); // 100
        Product productWang = productMapper.selectById(1); 
        Integer priceWang = productWang.getPrice(); // 100
    
        productLi.setPrice(priceLi + 50);
        productMapper.updateById(productLi); // 150 
        // UPDATE product SET name=?, price=?, version=? WHERE id=? AND version=?
        // 参数:外星人笔记本(String), 150(Integer), 1(Integer), 1(Long), 0(Integer)
        
        productWang.setPrice(priceWang - 30);
        productMapper.updateById(productWang); // 版本号不对,更新失败!!!
        // UPDATE product SET name=?, price=?, version=? WHERE id=? AND version=?
        // 参数: 外星人笔记本(String), 70(Integer), 1(Integer), 1(Long), 0(Integer)
                
        Product productBoss = productMapper.selectById(1);
        System.out.println(productBoss.getPrice()); // 最终会是150
    }
    

    但这样并没有实现我们想要的价格!!

  4. 优化

    @Test
    public void test01() {
        Product productLi = productMapper.selectById(1);
        Integer priceLi = productLi.getPrice(); // 100
        Product productWang = productMapper.selectById(1);
        Integer priceWang = productWang.getPrice(); // 100
    
        productLi.setPrice(priceLi + 50);
        productMapper.updateById(productLi); // 150
    
        productWang.setPrice(priceWang - 30);
        int res = productMapper.updateById(productWang);
        if (res == 0) {  // 操作失败重试,获取新的版本号,进行修改
            Product productNew = productMapper.selectById(1);
            Integer priceNew = productNew.getPrice();
            productNew.setPrice(priceNew - 30);
            productMapper.updateById(productNew); // 再次进行修改!!
        }
    
    
        Product productBoss = productMapper.selectById(1);
        System.out.println(productBoss.getPrice()); // 最终就是120了!!
    }
    

7. 通用枚举

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

  1. 在user表上添加sex字段,是int型的(默认是把枚举的名放到数据库的,而我们想要放01,数据库的sex字段是int类型的!)

  2. 创建SexEnum枚举类

    @Getter  // 只设置get进行了
    public enum SexEnum {
        MALE(1, "男"), FEMALE(0, "女");
    
        @EnumValue // 将注解所标志属性的值存储到数据库中
        private Integer sex;
        private String sexName;
        
        SexEnum(Integer sex, String sexName) {
            this.sex = sex;
            this.sexName = sexName;
        }
    }
    
  3. 在User实体类加上sex属性

    public class User {
        ...
        private SexEnum sex;  // 枚举类型的sex
        @TableLogic 
        private Integer isDelete;
    }
    
  4. 扫描通用枚举!!

    mybatis-plus:
      type-enums-package: com.sutong.enums
    
  5. 测试

    @Test
    public void test01() {
        User user = new User(null, "admin", 20, "admin@qq.com", SexEnum.MALE, 0);
        userMapper.insert(user);  // 这时数据库中存的标识MALE字符串,而是1数字!!
    }
    

很简单就两步

1.@EnumValue

2.扫描通用枚举


8. 代码生成器

逆向工程:根据表逆向生成实体类和Mapper接口和映射文件

这个和逆向工程差不多,但要比MyBatis生成的要多一点!!

  1. 依赖

    <dependency>
        <groupId>com.baomidou</groupId>  <!-- 代码生成器的依赖-->
        <artifactId>mybatis-plus-generator</artifactId>
        <version>3.5.1</version>
    </dependency>
    
    <dependency>
        <groupId>org.freemarker</groupId>  <!-- 生成过程中需要使用FreeMarker引擎模板-->
        <artifactId>freemarker</artifactId>
        <version>2.3.31</version>
    </dependency>
    
  2. 创建测试类,直接执行下面代码就行

    public class FastAutoGeneratorTest {
        public static void main(String[] args) {
            FastAutoGenerator.create("jdbc:mysql://localhost:3306/mybatis_plus", "root", "9527")
                    .globalConfig(builder -> {
                        builder.author("sutong")    // 设置作者
                                .enableSwagger()    // 开启 swagger 模式 (可关)
                                .fileOverride()     // 覆盖已生成文件
                                .outputDir(System.getProperty("user.dir") + "/src/main/java"); // 指定输出目录
                    })
                    .packageConfig(builder -> {
                        builder.parent("com.sutong")        // 设置父包名
                                .moduleName("mybatisplus")  // 设置父包模块名(可不设置)
                                .pathInfo(Collections.singletonMap(OutputFile.mapperXml,
                                                                   System.getProperty("user.dir")
                                                                   + "/src/main/resources/mapper")); 
                        		// 设置mapperXml生成路径
                    })
                    .strategyConfig(builder -> {
                        builder.addInclude("t_dept")        	// 设置需要生成的表名
                               .addTablePrefix("t_", "c_")   	// 设置过滤表前缀
                               .entityBuilder().enableLombok()  // 使用lombok
                               .controllerBuilder().enableRestStyle(); // 开启生成@RestController控制器
                    })
                    .templateEngine(new FreemarkerTemplateEngine())
    	            //使用Freemarker引擎模板,默认的是Velocity引擎模板 
                    .execute();
        }
    }
    
  3. controller,entity,mapper,service,xxxMapper.xml 都自动生成了!!

    命名会自动转化,下划线命名自动转化为驼峰命名!


9. 多数据源

适用于多种场景:纯粹多库、 读写分离、 一主多从、 混合模式等

我们创建两个库,分别为:mybatis_plus(以前的库不动)与mybatis_plus_1(新建),将 mybatis_plus库的product表移动到mybatis_plus_1库,这样每个库一张表,通过一个测试用例 分别获取用户数据与商品数据,如果获取到说明多库模拟成功

  1. 创建数据库和表

  2. 引入多数据源场景!!starter

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.5.0</version>
    </dependency>
    
  3. 配置多数据源

    spring:
      datasource:
        dynamic:
          primary: master  #设置默认的数据源或者数据源组,默认值即为master
          strict: false    #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
          datasource:
            master:
              url: jdbc:mysql://localhost:3306/mybatis_plus
              username: root
              password: zgq20020820
              driver-class-name: com.mysql.jdbc.Driver  # 3.2.0开始支持SPI可省略此配置
            slave_1:
              url: jdbc:mysql://localhost:3306/mybatis_plus_1
              username: root
              password: zgq20020820
              driver-class-name: com.mysql.jdbc.Driver
            slave_2:
              url: ENC(xxxxx) # 内置加密,使用请查看详细文档
              username: ENC(xxxxx)
              password: ENC(xxxxx)
              driver-class-name: com.mysql.jdbc.Driver
            #......省略
            #以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
    
    # 多主多从                      纯粹多库(记得设置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:
    
  4. 创建UserService

    public interface UserService extends IService<User> { }
    
    @DS("master") //指定所操作的数据源!!! 没有@DS使用默认数据源
    @Service
    public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements
    UserService { }
    
  5. 创建ProductService

    public interface ProductService extends IService<Product> { }
    
    @DS("slave_1")
    @Service
    public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product>
    implements ProductService { }
    

@DS注意:方法上的注解优先于类上注解。强烈建议只在service的类和方法上添加注解,不建议在mapper上添加注解。配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。

如果我们实现读写分离,将写操作方法加上主库数据源,读操作方法加上从库数据源,自动切换,就能实现读写分离!!


10. MyBatisX

MyBatis-Plus为我们提供了强大的mapper和service模板,能够大大的提高开发效率 但是在真正开发过程中,MyBatis-Plus并不能为我们解决所有问题,例如一些复杂的SQL,多表联查,我们就需要自己去编写代码和SQL语句,我们该如何快速的解决这个问题呢,这个时候可 以使用MyBatisX插件 MyBatisX一款基于 IDEA 的快速开发插件,为效率而生!

使用文档:https://baomidou.com/pages/ba5b24/

  1. IDEA中安装MyBatisX插件

  2. 想要使用MyBatisX的代码生成需要使用IDEA右边的DataBase连接上数据库

  3. 连上后选择表,右键MyBatisX Generator

  4. 设置一些生成的包/路径,字段忽略的前后缀,表明忽略的前后缀,然后next

  5. annotation选择MyBatisPlus 3

    options按需选择就行,comment是注释,lombok是使用它

    template是生成的模板,选择MyBatisPlus 3

    然后下面有Mapper接口,MapperXML,serviceImpl,service接口的生成位置,和我们自己写的一样

  6. 生成的东西和MyBatis的代码生成器差不多!

    MapperXML里面还给我们把字段抽取出来了,还提供了一个基础的resultMap

快速生成CRUD⭐,我们不需要写返回值,只需要写方法名(insert, select, delete, update开头…),插件就给我们提示了

然后按alt+enter,选择第二个MyBatisX-Generator sql,帮我们自动补全方法和映射文件中的sql

例如:

public interface DeptMapper extends BaseMapper<Dept> {
    // 有选择性的添加
    int insertSelective(Dept dept);  

    // 通过各个字段删除都可以,输入And后面也自动提示!!
    int deleteByIdAndDeptName(@Param("id") Long id, @Param("deptName") String deptName);
    
    // 修改那个字段都行,直接And拼接,条件通过By拼接!!
    int updateAddressById(@Param("address") String address, @Param("id") Long id);
    
    //selectAllById              通过id查所有字段
    //selectAddressByAgeBetween  通过年龄区间查地址字段,参数会有beginAge和endAge
    //selectAllOrderByAgeDesc    年龄升序
    List<Dept> selectDeptNameAndAddressById(@Param("id") Long id); //通过id查询名字和地址字段
}
<insert id="insertSelective">
    insert into t_dept
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="id != null">id,</if>
        <if test="deptName != null">dept_name,</if>
        <if test="address != null">address,</if>
    </trim>
    values
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="id != null">#{id,jdbcType=BIGINT},</if>
        <if test="deptName != null">#{deptName,jdbcType=VARCHAR},</if>
        <if test="address != null">#{address,jdbcType=VARCHAR},</if>
    </trim>
</insert>

<delete id="deleteByIdAndDeptName">
    delete from t_dept
    where
    id = #{id,jdbcType=NUMERIC}
    AND dept_name = #{deptName,jdbcType=VARCHAR}
</delete>

<update id="updateAddressById">
    update t_dept
    set address = #{address,jdbcType=VARCHAR}
    where
    id = #{id,jdbcType=NUMERIC}
</update>

<select id="selectDeptNameAndAddressById" resultMap="BaseResultMap">
    select dept_name, address
    from t_dept
    where
    id = #{id,jdbcType=NUMERIC}
</select>

命名推荐:

Dao 接口命名

  • insert
  • batchInsert
  • selectOne (selectByXXX)
  • count
  • list
  • listPage
  • update
  • delete

Service 接口命名

  • add
  • findOne (findByXXX)
  • findAll
  • modify
  • remove

阿里的规范Dao / Service:

获取单个对象,get

获取多个对象,listUsers

统计,count

插入,insert / save

删除,delete / remove

修改,update

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值