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项目是一个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
- AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件
删除用的是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