MyBatisPlus

MyBatisPlus

MybatisPlus简介

Mybatis-Plus (简称MP)是 MyBatis 的一个增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生

MyBatis-Plus提供了通用的mapper和service,可以在不编写任何SQL语句的情况下,快速的实现对单表的CRUD、批量、逻辑删除、分页等操作

润物无声

只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑

效率至上

只需简单配置,即可快速进行单表CRUD操作,从而节省大量时间

功能丰富

代码生成、自动分页、逻辑删除、自动填充等功能一应俱全

特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作

支持数据库

任何能使用 MyBatis 进行 CRUD, 并且支持标准 SQL 的数据库

框架体系

1、扫描实体(ScanEntity)

第一步会先扫描当前所操作的实体类,扫描器会通过反射机制对实体类中的属性进行抽取

2、分析表与实体类之间的关系

分析表与实体类的关系,实体类中的属性与字段之间的关系

3、分析我们当前所调用的方法

根据我们当前所调用的方法生成对应的sql语句,将生成的sql语句注入到mybatis的容器中实现最终的功能

实体类简化

Lombok

Lombok官网

Lombok项目是一个java库,它可以自动插入到编辑器和构建工具中,增强java的性能,不需要再写getter、setter或equals方法,只要有一个注解,你的类就有一个功能齐全的构建器、自动记录变量等等

@NoArgsConstructor注解

生成字节码时自动为实体类生成无参构造

@AllArgsConstructor注解

自动生成全参构造

@Data

整合了Getter、Setter、ToString、EqualsAndHashCode、RequiredArgsConstructor注解

@Getter

快速构建Getter方法

@Setter

快速构建Setter方法

@ToString

快速将当前对象转换成字符串类型,便于log

@EqualsAndHashCode

快速进行相等判断

实体类与表的匹配关系

设置表名

@TableName注解

创建实体时我们使用的主键是Long类型,数据库表主键字段是BigInt类型

是因为mybatisPlus使用的主键id是用雪花算法生成的

在mybatisPlus执行查询的时候指定实体匹配的表名

package com.os467.pojo;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;

@Data
@TableName("tb_user")
public class User {

    private Long id;

    private String username;

    private String password;

}

使用全局配置

设置实体类所对应表的统一前缀

mybatis-plus.global-config.db-config.table-prefix=tb_

设置主键

@TableId注解

设置实体类中的指定属性对应的字段为主键id

注解中的value属性可以设置当前属性所对应的表中的字段名

type属性

指定主键的生成方法,默认使用雪花算法IdType.ASSIGN_ID

如果要指定自动递增,首先表中的主键也必须是自动递增的

需要把type属性指定为IdType.AUTO

@TableId(value = "id",type = IdType.AUTO)

设置统一的主键生成策略

mybatis-plus.global-config.db-config.id-type=auto

雪花算法

背景

  • 需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量
  • 数据库的扩展方式主要包括:业务分库、主从复制,数据库分表

数据库分表

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

单表数据拆分有两种方式:垂直分表和水平分表

垂直分表

垂直分表适合将表中某些不常用且占了大量空间的列拆分出去

水平分表

  • 水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以 作为参考,但并不是绝对标准,关键还是要看表的访问性能
  • 对于一些比较复杂的表,可能超过 1000 万就要分表了,而对于一些简单的表,即使存储数据超过 1 亿行,也可以不分表

主键自增

​ 优点:可以随着数据的增加平滑地扩充新的表

​ 例如,现在的用户是 100 万,如果增加到 1000 万, 只需要增加新的表就可以了,原有的数据不需要动

取模

​ 优点:表分布比较均匀

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

雪花算法

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

  • 长度共64bit(一个long型)
    • 首先是一个符号位,1bit标识
    • 41bit时间截(毫秒级),存储的是时间截的差值(当前时间截 - 开始时间截),结果约等于69.73年
    • 10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID,可以部署在1024个节点)
    • 12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID)

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

设置字段名

@TableField注解

指定属性所对应的字段名,value值为指定的表中的字段名

逻辑删除

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

@TableLogic注解

此字段需要有默认值

比如未删除状态字段值默认值为0

delete语句会变成修改操作,会将删除状态字段数值改变为1

下次执行查询就无法查询到删除状态字段数值为1的数据

BaseMapper

MyBatis-Plus中的基本增删改查在内置的BaseMapper类中都已得到了实现,我们可以直接使用

我们在让Mapper接口继承BaseMapper类的时候需要提供一个实体类的泛型

package com.os467.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.os467.pojo.User;

public interface UserMapper extends BaseMapper<User> {

}

在springboot启动类中开启映射包扫描

如果基于springboot开发,我们需要指定mapper接口所在的包以及mapper映射文件所在的路径

@MapperScan 用于扫描指定包下的mapper接口

将mapper包下由mybatis动态生成的接口代理类交由spring容器管理

package com.os467.mybatisplus;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.os467.mapper")
public class MybatisplusApplication {

    public static void main(String[] args) {
        SpringApplication.run(MybatisplusApplication.class, args);
    }

}

测试单元,测试是否能查询到数据

表和实体的名字需要相同才能被匹配到

关于Autowired报错,其实是idea编译器在检测时以为接口实例还没被创建出来,在运行时是不会报错的

package com.os467.mybatisplus;

import com.os467.mapper.UserMapper;
import com.os467.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;


import java.util.List;

@SpringBootTest
class MybatisplusApplicationTests {

    @Autowired
    private UserMapper userMapper;

    @Test
    void test01() {

        //通过条件构造器查询一个list集合,如果没有条件,则传入null为参数
        List<User> userList = userMapper.selectList(null);

        userList.forEach(System.out::println);

    }

}

mybatis中内置的显示sql日志

mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

insert语句

@Test
public void test_insert(){

    User user = new User();

    user.setUsername("Tom001");
    user.setPassword("123456");

    int result = userMapper.insert(user);

    System.out.println("result"+result);

    System.out.println("uid:"+user.getId());


}

删除语句

根据id删除数据

@Test
public void test_deleteById(){

    //id是long类型所以在后面加L
    int result = userMapper.deleteById(1554390450146725889L);

    System.out.printf("result:"+result);

}

根据map集合提供的条件删除数据

@Test
public void test_deleteById(){

    Map<String,Object> map = new HashMap<>();

    map.put("username","张三");
    map.put("password",123);

    int result = userMapper.deleteByMap(map);

    System.out.println("result:"+result);

}

根据多个id条件删除数据

deleteBatchIds()这个方法使用的是sql中的in()函数

@Test
public void test_deleteById(){

    List<Long> longs = Arrays.asList(1L, 2L, 3L);

    int result = userMapper.deleteBatchIds(longs);

    System.out.println("result:"+result);

}

修改语句

根据id修改用户数据

@Test
public void testUpdate(){

    User user = new User();
    user.setId(2L);
    user.setUsername("小明");
    user.setPassword("1234567");

    int result = userMapper.updateById(user);

    System.out.println("result:"+result);
}

查询方法

通过id来查询用户信息

@Test
public void testSelect(){

    User user = userMapper.selectById(1L);
    System.out.println(user);

}

根据多个id来查询用户信息

@Test
public void testSelect(){

    List<Long> longs = Arrays.asList(1L, 2L, 3L);
    List<User> users = userMapper.selectBatchIds(longs);
    users.forEach(System.out::println);

}

根据Map集合中的内容查询

@Test
public void testSelect(){

    Map<String,Object> map = new HashMap<>();

    Object put = map.put("username", "张三");
    Object password = map.put("password", "123");

    List<User> users = userMapper.selectByMap(map);

    users.forEach(System.out::println);

}

mybatisPlus中自定义sql

需要创建mapper文件

不配置,默认是类路径下的mapper文件,此文件下的所有xml都会被当作映射文件

mybatis-plus.mapper-locations=文件路径

Service CRUD接口

通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆

MyBatis-Plus中有一个接口 IService和其实现类 ServiceImpl封装了常见的业务层逻辑

我们通常会选择创建一个Service接口来继承mybatisPlus的IService接口

package com.os467.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.os467.pojo.User;

public interface UserService extends IService<User> {
}

我们在使用自己的service实现类的时候想要使用IService接口中的方法,但是有些方法我们又不想去重写,因此我们只需要继承一个IService提供的实现类即可

注意需要传入两个泛型,一个是当前类的父接口以及需要被操作的实体类

package com.os467.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.os467.mapper.UserMapper;
import com.os467.pojo.User;
import com.os467.service.UserService;

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

service接口查询总记录数

@Test
public void testGetCount(){

    int count = userService.count();

    System.out.println("总记录数:"+count);

}

批量添加SQL

底层通过baseMapper中的单个添加的sql语句循环来实现的

@Test
public void insertMore(){

    List<User> list = new ArrayList<>();

    for (int i = 0; i <= 10; i++) {

        User user = new User();

        user.setUsername("os"+i);
        user.setPassword("os467"+i);

        list.add(user);

    }

    boolean b = userService.saveBatch(list);

    System.out.println(b);

}

Wapper介绍

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

删除用的是QueryWrapper

QueryWrapper

查询wrapper测试

@Test
public void test01(){

    //用户名包含a
    //密码不为空
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.like("username","小")
            .between("id",1,10)
            .isNotNull("password");

    List<User> users = userMapper.selectList(queryWrapper);

    users.forEach(System.out::println);

}

排序的方法

@Test
public void test02(){

    //查询用户信息按照id降序排序
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.orderByDesc("id");

    List<User> users = userMapper.selectList(queryWrapper);

    users.forEach(System.out::println);

}

删除wrapper测试

因为设置了逻辑删除,此测试实际上是将删除状态从0变为了1

@Test
public void test03(){

    //删除名字包含o的
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.like("username","o");

    int result = userMapper.delete(queryWrapper);

    System.out.println("result:"+result);

}

修改wrapper测试

第一个user对象是存放需要修改的信息,第二个wrapper是修改条件

@Test
public void test04(){

    //id大于1并且密码为123或名字包含"小"
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.gt("id",1).eq("username","123").
            or().like("username","小");

    User user = new User();
    user.setPassword("123");

    int result = userMapper.update(user, queryWrapper);

    System.out.println("result:"+result);


}

优先条件设置

在and方法中使用lambda表达式

@Test
public void test05(){

    //名字包含"小"(id大于1或密码为123)
    //lambda中的条件优先执行
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.like("username","小").
            and(i->i.gt("id",1).or().eq("password","123"));

    User user = new User();
    user.setPassword("1234");
    int result = userMapper.update(user, queryWrapper);

    System.out.println("result:"+result);

}

指定需要查询的字段

@Test
public void test06(){
    
    //查询用户的用户名,年龄,邮箱信息
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    
    //指定需要查询的字段
    queryWrapper.select("username","password");

    List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
    
    maps.forEach(System.out::println);

}

子查询

@Test
public void test07(){

    //查询id小于100的用户信息,用子查询
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.inSql("id","select id from tb_user where id < 100");

    List<User> users = userMapper.selectList(queryWrapper);

    users.forEach(System.out::println);

}

updateWrapper

同时设置修改条件与修改字段

@Test
public void test08(){

    //名字包含"小"(id大于1或密码为1234)
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();

    updateWrapper.like("username","小").
            and(i->i.gt("id",1).or().eq("password","1234"));

    //设置需要修改的字段
    updateWrapper.set("username","小黑");

    int result = userMapper.update(null, updateWrapper);

    System.out.println("result:"+result);

}

组装条件生产sql语句

此方法需要用if语句判断较为麻烦

@Test
public void test09(){

    String username = "";
    String password = "123";

    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    //mybatis-plus内置字符串工具类,判断某个字符串不为空字符串,不为null,不为空白符
    if (StringUtils.isNotBlank(username)){

        queryWrapper.like("username","小");

    }

    if (password != null){

        queryWrapper.eq("password","1234");

    }

    List<User> users = userMapper.selectList(queryWrapper);

    users.forEach(System.out::println);

}

与上面的代码实现相同,但是更加简洁

@Test
public void test10(){

    String username = "";
    String password = "123";

    QueryWrapper<User> queryWrapper = new QueryWrapper<>();

    queryWrapper.like(StringUtils.isNotBlank(username),"username","小").
            eq(password != null,"password","1234");

    List<User> users = userMapper.selectList(queryWrapper);

    users.forEach(System.out::println);

}

lambdaWrapper

LambdaQueryWrapper

能够防止字段名写错,此方法使用函数式接口,mybatis-plus会自动去匹配对应的字段

@Test
public void test11(){

    String username = "";
    String password = "123";

    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

    queryWrapper.like(StringUtils.isNotBlank(username),User::getUsername,username).
            eq(password != null,User::getPassword,password);

    List<User> users = userMapper.selectList(queryWrapper);

    users.forEach(System.out::println);

}

分页插件

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

需要创建一个spring核心配置类

@Configuration
@MapperScan("com.os467.mapper")
public class MyBatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){

        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));

        return interceptor;

    }

}

测试分页

@Test
void test01() {

    Page<User> page = new Page<>(2,3);

    userMapper.selectPage(page,null);

    System.out.println(page.getRecords());
    System.out.println(page.getPages());

}

自定义查询分页

接口中定义方法

@Param注解 为参数命名,通过OGNL表达式获取

Page对象必须在参数第一个位置

/**
 * 通过id查询用户信息然后分页
 * @param page MyBatis-Plus所提供的分页对象,必须在参数第一个位置
 * @param id
 * @return
 */
Page<User> selectPageVo(@Param("page") Page<User> page, Integer id);

测试

@Test
void test02() {

    Page<User> page = new Page<>(2,3);

    userMapper.selectPageVo(page, 3);

    System.out.println(page);
    
}

追加自定义sql条件

last 在sql语句后面追加条件

//在查询条件后追加自定义条件
lambdaWrapper.last("limit 1");

乐观锁

场景

一件商品,成本价是80元,售价是100元。

老板先是通知小李,说你去把商品价格增加50元。小 李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太 高,可能会影响销量。又通知小王,你把商品价格降低30元。

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

现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。

乐观锁与悲观锁

乐观锁

上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过 了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库

悲观锁

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

乐观锁实现流程

数据库中添加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

Mybatis-Plus实现乐观锁

修改实体类

为version字段添加**@Version注解** 标识乐观锁版本号字段

package com.atguigu.mybatisplus.entity;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
@Data
public class Product {
private Long id;
private String name;
private Integer price;
@Version
private Integer version;
}

在spring核心配置类中的MybatisPlusInterceptor拦截器中配置

在之前的分页插件之后继续配置

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页插件
interceptor.addInnerInterceptor(new
PaginationInnerInterceptor(DbType.MYSQL));
//添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}

因为乐观锁版本号改变而导致的操作失败如何解决?

//假设版本号改变操作失败,返回result为0
int result = productMapper.updateById(1);

//判断影响记录条数
if(result == 0){
    
    //操作失败,重试,查询查询获取到版本号
    Product productNew = productMapper.selectById(1);
    
    productNew.setPrice(productNew.getPrice()-30);
    
    productMapper.updateById(productNew);
}

通用枚举

需要在配置文件中设置扫描通用枚举包

需要传入通用枚举类型送在包

mybatis-plus.type-enums-package=com.os467.enums

设置性别枚举类型

@EnumValue注解 将此注解标注的枚举类型属性值存入数据库中

package com.os467.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;

@Getter
public enum SexEnum {

    MALE(1,"男"),
    FEMALE(2,"女");

    @EnumValue
    private Integer sex;
    private String sexName;

    SexEnum(Integer sex, String sexName) {
        this.sex = sex;
        this.sexName = sexName;
    }

}

测试

@Test
public void test(){
    User user = new User();
    user.setId(1L);
    user.setSex(SexEnum.FEMALE);
    int result = userMapper.updateById(user);

    System.out.println("result:"+result);
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com

文章标题:MyBatisPlus

字数:5.3k

本文作者:Os467

发布时间:2022-08-01, 11:15:26

最后更新:2022-09-05, 00:08:59

原始链接:https://os467.github.io/2022/08/01/MyBatisPlus/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏