网站建设 请示,密云建设网站公司,政务服务网站 建设方案,呼市做开发网站的公司MybatiPlus学习 一、MybatiPlus简介1.1 入门案例1.2 mybatisPlus概述1.3 总结 二、标准数据层开发2.1 标准的CRUD使用2.2 新增2.3 删除2.4 修改2.5 根据Id查询2.6 查询全部2.7 Lombok2.8 分页功能 三、DQL控制3.1 条件查询方式3.1.1 构建条件查询3.1.2 多条件查询3.1.3 null值判… MybatiPlus学习 一、MybatiPlus简介1.1 入门案例1.2 mybatisPlus概述1.3 总结 二、标准数据层开发2.1 标准的CRUD使用2.2 新增2.3 删除2.4 修改2.5 根据Id查询2.6 查询全部2.7 Lombok2.8 分页功能 三、DQL控制3.1 条件查询方式3.1.1 构建条件查询3.1.2 多条件查询3.1.3 null值判定 3.2 查询投影3.2.1 查询指定字段3.2.2 聚合查询3.2.3 分组查询 3.3 查询条件设定3.3.1 等值查询3.3.2 范围查询3.3.3 模糊查询3.3.4 排序查询 3.4 字段映射与表名映射 四、DQL控制4.1 id生成策略控制4.1.1 AUTO4.1.2 INPUT4.1.3 ASSIGN_ID4.1.4 ASSIGN_UUID4.1.5 ID生成策略对比4.1.6 简化配置 4.2 多记录操作4.3 逻辑删除4.4 乐观锁4.4.1 实现思路4.4.2 实现步骤 五、快速开发5.1 代码生成器实现5.2 MP中Service的CRUD 六、复习 一、MybatiPlus简介
1.1 入门案例
MyBatisPlus简称MP是基于MyBatis框架基础上开发的增强型工具旨在简化开发提高效率开发方式 基于MyBatis使用MyBatisPlus基于Spring使用MyBatisPlus基于SpringBoot使用MyBatisPlus(重点)
现在直接使用SpringBoot来构建项目官网的快速开始也是直接用的SpringBoot 步骤一创建数据库和表 CREATE TABLE user (
id bigint(20) primary key auto_increment,
name varchar(32) not null,
password varchar(32) not null,
age int(3) not null ,
tel varchar(32) not null
);
insert into user values(1,Tom,tom,3,18866668888);
insert into user values(2,Jerry,jerry,4,16688886666);
insert into user values(3,Jock,123456,41,18812345678);
insert into user values(4,略略略,nigger,15,4006184000);步骤二创建SpringBoot工程 只需要勾选MySQL不用勾选MyBatis了 步骤三补全依赖 导入德鲁伊和MyBatisPlus的坐标 dependency
groupIdcom.baomidou/groupId
artifactIdmybatis-plus-boot-starter/artifactId
version3.4.1/version
/dependency
dependency
groupIdcom.alibaba/groupId
artifactIddruid/artifactId
version1.1.16/version
/dependency步骤四编写数据库连接四要素 还是将application的后缀名改为yml以后配置都是用yml来配置 注意要设置一下时区不然可能会报错指高版本的mysql spring:datasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot_db?serverTimezoneUTCusername: rootpassword: YOUSONOFABTICH.## mybatis的日志信息l
mybatis-plus:
configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl步骤五根据数据表来创建对应的模型类 注意id是Long类型至于为什么是Long接着往下看 public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;Override
public String toString() {return User{ id id , name name \ , password password \ , age age , tel tel \ };
}public Long getId() {return id;
}public void setId(Long id) {this.id id;
}public String getName() {return name;
}public void setName(String name) {this.name name;
}public String getPassword() {return password;
}public void setPassword(String password) {this.password password;
}public Integer getAge() {return age;
}public void setAge(Integer age) {this.age age;
}public String getTel() {return tel;
}public void setTel(String tel) {this.tel tel;}
}步骤六创建dao接口 Mapper
public interface UserDao extends BaseMapperUser{
}这样写就完事儿了 只需要在类上方加一个Mapper注解同时继承BaseMapper泛型写创建的模型类的类型 然后这样就能完成单表的CRUD了 步骤七测试 懒死以后连简单的CRUD都不用写了 SpringBoot的测试类也是简单的一批只需要一个SpringBootTest注解就能完成创建SpringBoot工程的时候已经帮我们自动弄好了 测试类里需要什么东西就用Autowired自动装配测试方法上用Test注解 SpringBootTest
class MybatisplusApplicationTests {Autowired
private UserDao userDao;Test
void contextLoads() {ListUser users userDao.selectList(null);for (User b : users) {System.out.println(b);}}
}selectList() 方法的参数为 MP 内置的条件封装器 Wrapper所以不填写就是无任何条件 1.2 mybatisPlus概述
MyBatisPlus的官网 因为域名被抢注了但是粉丝也捐赠了一个 https://mybatis.plus 域名
MP旨在成为MyBatis的最好搭档而不是替换掉MyBatis从名称上来看也是这个意思一个MyBatis的plus版本在原有的MyBatis上做增强其底层仍然是MyBatis的东西所以我们当然也可以在MP中写MyBatis的内容
对于MP的深入学习可以多看看官方文档锻炼自己自学的能力毕竟不是所有知识都有像这样的网课更多的还是自己看文档挖源码。
MP的特性
无侵入只做增强不做改变引入它不会对现有工程产生影响如丝般顺滑损耗小启动即会自动注入基本 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 操作智能分析阻断也可自定义拦截规则预防误操作
1.3 总结
SpringBoot集成MyBatisPlus非常的简单只需要导入MyBatisPlus的坐标然后令dao类继承BaseMapper写上泛型类上方加Mapper注解
可能存在的疑问
我甚至都没写在哪个表里查为什么能自动识别是在我刚刚创建的表里查 注意我们创建的表和对应的模型类是同一个名默认情况是在同名的表中查找 那我要是表明和模型类的名不一样那咋整 在模型类的上方加上TableName注解 例如数据表叫tb_user但数据类叫User那么就在User类上加TableName(tb_user)注解
二、标准数据层开发
2.1 标准的CRUD使用
功能自定义接口MP接口新增boolean save(T t)int insert(T t)删除boolean delete(int id)int deleteById(Serializable id)修改boolean update(T t)int updateById(T t)根据id查询T getById(int id)T selectById(Serializable id)查询全部List getAll()List selectList()分页查询PageInfo getAll(int page,int size)IPage selectPage(IPage page)按条件查询List getAll(Condition condition)IPage selectPage(WrapperqueryWrapper)
2.2 新增
int insert(T t)参数类型是泛型也就是我们当初继承BaseMapper的时候填的泛型返回值是int类型0代表添加失败1代表添加成功
Test
void testInsert(){User user new User();user.setName(magua);user.setAge(23);user.setTel(4005129421);user.setPassword(MUSICIAN);userDao.insert(user);
}随便写一个User的数据运行程序然后去数据库看看新增是否成功 1572364408896622593 magua MUSICIAN 23 4005129421 这个主键自增id看着有点奇怪但现在你知道为什么要将id设为long类型了吧
2.3 删除
int deleteByIds(
//Serializable id
)参数类型为什么是一个序列化类Serializable 通过查看String的源码你会发现String实现了Serializable接口而且Number类也实现了Serializable接口Number类又是FloatDoubleLong等类的父类那现在能作为主键的数据类型都已经是Serializable类型的子类了MP使用Serializable类型当做参数类型就好比我们用Object类型来接收所有类型一样 返回值类型是int 数据删除成功返回1未删除数据返回0。 那下面我们就来删除刚刚添加的数据注意末尾加个L
Test
void testDelete(){userDao.deleteById(1572364408896622593L);
}2.4 修改
int updateById(T t);T:泛型需要修改的数据内容注意因为是根据ID进行修改所以传入的对象中需要有ID属性值int:返回值 修改成功后返回1未修改数据返回0
Test
void testUpdate(){User user new User();user.setId(1L);user.setName(Alen);userDao.updateById(user);
}修改功能只修改指定的字段未指定的字段保持原样与比较之前方便许多之前修改的话要加很多判断语句是否为空。
2.5 根据Id查询
T selectById (Serializable id)T selectById (Serializable id)
Test
void testSelectById(){User user userDao.selectById(1);System.out.println(user);
}2.6 查询全部
ListT selectList(WrapperT queryWrapper)Wrapper用来构建条件查询的条件目前我们没有可直接传为Null
Test
void testSelectAll() {ListUser users userDao.selectList(null);for (User u : users) {System.out.println(u);}
}方法都测试完了那你们有没有想过这些方法都是谁提供的呢
想都不用想肯定是我们当初继承的BaseMapper
2.7 Lombok 代码写到这我们发现之前的dao接口都不用我们自己写了只需要继承BaseMapper用他提供的方法就好了 但是现在我还想偷点懒毕竟懒是第一生产力之前我们手写模型类的时候创建好对应的属性然后用IDEA的AltInsert快捷键快速生成get和set方法toSring各种构造器有需要的话等 项目做这么久写模型类都给我写烦了有没有更简单的方式呢 答案当然是有的可以使用Lombok一个Java类库提供了一组注解来简化我们的POJO模型类开发
具体步骤如下 步骤一添加Lombok依赖 dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId!--version1.18.12/version--
/dependency版本不用写SpringBoot中已经管理了lombok的版本 步骤二在模型类上添加注解 Lombok常见的注解有: Setter:为模型类的属性提供setter方法Getter:为模型类的属性提供getter方法ToString:为模型类的属性提供toString方法EqualsAndHashCode:为模型类的属性提供equals和hashcode方法Setter
Getter
ToString
EqualsAndHashCode
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}Data:是个组合注解包含上面的注解的功能NoArgsConstructor:提供一个无参构造函数AllArgsConstructor:提供一个包含所有参数的构造函数 Data
AllArgsConstructor
NoArgsConstructor
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
}说明:Lombok只是简化模型类的编写我们之前的方法也能用 例如你有特殊的构造器需求只想要name和password这两个参数那么可以手写一个
public User(String name, String password) {this.name name;this.password password;
}2.8 分页功能
基础的增删改查功能就完成了现在我们来进行分页功能的学习
IPageT selectPage(IPageT page, WrapperT queryWrapper)IPage用来构建分页查询条件Wrapper用来构建条件查询的条件暂时没有条件可以传一个null返回值IPage是什么意思后面我们会说明
具体的使用步骤如下 步骤一调用方法传入参数获取返回值 Test
void testSelectPage() {
IPageUser page new Page(1, 3);
userDao.selectPage(page, null);
System.out.println(当前页码 page.getCurrent());
System.out.println(本页条数 page.getSize());
System.out.println(总页数 page.getPages());
System.out.println(总条数 page.getTotal());
System.out.println(page.getRecords());
}步骤二设置分页拦截器 public class MybatisPlusConfig {
Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1.设置一个MP的拦截器MybatisPlusInterceptor myInterceptor new MybatisPlusInterceptor();//2.添加具体的拦截器小的myInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());return myInterceptor;}
}步骤三运行测试程序
开启日志可以查看其运行的整个sql语句
三、DQL控制
增删改查四个操作中查询是非常重要的也是非常复杂的操作这部分我们主要学习的内容有:
3.1 条件查询方式 MP将复杂的SQL查询语句都做了封装使用编程的方式来完成查询条件的组合
之前我们在写CRUD时都看到了一个Wrapper类我们当初都是赋一个null值但其实这个类就是用来查询的
3.1.1 构建条件查询 QueryWrapper 小于用lt大于用gt 回想之前我们在html页面中如果需要用到小于号或者大于号需要用对应的html实体来替换 小于号的实体是 lt;大于号的实体是gt; Test
void testQueryWrapper(){
QueryWrapperUser qw new QueryWrapper();
//条件为 age字段小于18
qw.lt(age,18);
ListUser userList userDao.selectList(qw);
System.out.println(userList);
}这种方法有个弊端那就是字段名是字符串类型没有提示信息和自动补全如果写错了那就查不出来 QueryWrapper的基础上使用lambda Test
void testQueryWrapper(){
QueryWrapperUser qw new QueryWrapper();
qw.lambda().lt(User::getAge,18);
ListUser userList userDao.selectList(qw);
System.out.println(userList);
}User::getAget,为lambda表达式中的类名::方法名 LambdaQueryWrapper 方式二解决了方式一的弊端但是要多些一个lambda()那方式三就来解决方式二的弊端使用LambdaQueryWrapper就可以不写lambda() Test
void testQueryWrapper(){LambdaQueryWrapperUser lqw new LambdaQueryWrapper();lqw.lt(User::getAge,18);ListUser userList userDao.selectList(lqw);System.out.println(userList);
}3.1.2 多条件查询
上面三种都是单条件的查询那我们现在想进行多条件的查询该如何编写代码呢
需求查询表中年龄在10~30岁的用户信息
Test
void testQueryWrapper(){LambdaQueryWrapperUser lqw new LambdaQueryWrapper();//大于10lqw.gt(User::getAge,10);//小于30lqw.lt(User::getAge,30);ListUser userList userDao.selectList(lqw);System.out.println(userList);
}构建多条件的时候我们还可以使用链式编程
Test
void testQueryWrapper() {LambdaQueryWrapperUser lqw new LambdaQueryWrapper();lqw.gt(User::getAge, 10).lt(User::getAge, 30);ListUser userList userDao.selectList(lqw);System.out.println(userList);
}可能存在的疑问 MP怎么就知道你这俩条件是AND的关系呢那我要是想用OR的关系该咋整 解答 默认就是AND的关系如果需要OR关系用or()链接就可以了
lqw.gt(User::getAge, 10).or().lt(User::getAge, 30);需求查询年龄小于10或者年龄大于30的用户信息
Test
void testQueryWrapper() {LambdaQueryWrapperUser lqw new LambdaQueryWrapper();lqw.lt(User::getAge, 10).or().gt(User::getAge, 30);ListUser userList userDao.selectList(lqw);System.out.println(userList);
}3.1.3 null值判定 在做条件查询的时候一般都会有很多条件供用户查询 这些条件用户可以选择用也可以选择不用 之前我们是通过动态SQL来实现的 select idselectByPageAndCondition resultMapbrandResultMap
select *
from tb_brand
whereif testbrand.brandName ! null and brand.brandName ! and brand_name like #{brand.brandName}/ifif testbrand.companyName ! null and brand.companyName ! and company_name like #{brand.companyName}/ifif testbrand.status ! nulland status #{brand.status}/if
/where
limit #{begin} , #{size}
/select那现在我们来试试在MP里怎么写 需求:查询数据库表中根据输入年龄范围来查询符合条件的记录 用户在输入值的时候 如果只输入第一个框说明要查询大于该年龄的用户 如果只输入第二个框说明要查询小于该年龄的用户 如果两个框都输入了说明要查询年龄在两个范围之间的用户 问题一后台如果想接收前端的两个数据该如何接收? 我们可以使用两个简单数据类型也可以使用一个模型类但是User类中目前只有一个age属性 TableName(tb_user)
Data
public class User {private Long id;private String name;private String password;private Integer age;private String tel;
}使用一个age属性如何去接收页面上的两个值呢?这个时候我们有两个解决方案 方案一添加属性age2,这种做法可以但是会影响到原模型类的属性内容方案二新建一个模型类,让其继承User类并在其中添加age2属性UserQuery在拥有User属性后同时添加了age2属性。 Data
TableName(tb_user)
public class UserQuery extends User{private Integer age2;
}环境准备好后我们来实现下刚才的需求 Test
void testQueryWrapper() {
LambdaQueryWrapperUser lqw new LambdaQueryWrapper();
UserQuery uq new UserQuery();
uq.setAge(10);
uq.setAge2(30);
if (null ! uq.getAge()) {lqw.gt(User::getAge, uq.getAge());
}
if (null ! uq.getAge2()) {lqw.lt(User::getAge, uq.getAge2());
}
for (User user : userDao.selectList(lqw)) {System.out.println(user);}
}上面的写法可以完成条件为非空的判断但是问题很明显如果条件多的话每个条件都需要判断代码量就比较大来看MP给我们提供的简化方式lt还有一个重载的方法当condition为true时添加条件为false时不添加条件 public Children lt(boolean condition, R column, Object val) {return this.addCondition(condition, column, SqlKeyword.LT, val);
}故我们可以把if的判断操作放到lt和gt方法中当做参数来写 Test
void testQueryWrapper() {
LambdaQueryWrapperUser lqw new LambdaQueryWrapper();
UserQuery uq new UserQuery();
uq.setAge(10);
uq.setAge2(30);
lqw.gt(null ! uq.getAge(), User::getAge, uq.getAge()).lt(null ! uq.getAge2(), User::getAge, uq.getAge2());
for (User user : userDao.selectList(lqw)) {System.out.println(user);}
}3.2 查询投影
3.2.1 查询指定字段
目前我们在查询数据的时候什么都没有做默认就是查询表中所有字段的内容我们所说的查询投影即不查询所有字段只查询出指定内容的数据。
具体如何来实现?
Test
void testQueryWrapper() {LambdaQueryWrapperUser lqw new LambdaQueryWrapper();lqw.select(User::getName,User::getName);for (User user : userDao.selectList(lqw)) {System.out.println(user);}
}select(…)方法用来设置查询的字段列可以设置多个
lqw.select(User::getName,User::getName);如果使用的不是lambda就需要手动指定字段
Test
void testQueryWrapper() {QueryWrapperUser qw new QueryWrapper();qw.select(name, age);for (User user : userDao.selectList(qw)) {System.out.println(user);}
}3.2.2 聚合查询 需求:聚合函数查询完成count、max、min、avg、sum的使用 count:总记录数max:最大值min:最小值avg:平均值sum:求和 count Test
void testQueryWrapper() {
QueryWrapperUser qw new QueryWrapper();
qw.select(count(*) as count);
for (MapString, Object selectMap : userDao.selectMaps(qw)) {System.out.println(selectMap);}
}max Test
void testQueryWrapper() {
QueryWrapperUser qw new QueryWrapper();
qw.select(max(age) as maxAge);
for (MapString, Object selectMap : userDao.selectMaps(qw)) {System.out.println(selectMap);}
}min Test
void testQueryWrapper() {
QueryWrapperUser qw new QueryWrapper();
qw.select(min(age) as minAge);
for (MapString, Object selectMap : userDao.selectMaps(qw)) {System.out.println(selectMap);}
}avg Test
void testQueryWrapper() {
QueryWrapperUser qw new QueryWrapper();
qw.select(avg(age) as avgAge);
for (MapString, Object selectMap : userDao.selectMaps(qw)) {System.out.println(selectMap);
}
}sum Test
void testQueryWrapper() {
QueryWrapperUser qw new QueryWrapper();
qw.select(sum(age) as sumAge);
for (MapString, Object selectMap : userDao.selectMaps(qw)) {System.out.println(selectMap);}
}3.2.3 分组查询
Test
void testQueryWrapper() {QueryWrapperUser qw new QueryWrapper();qw.select(max(age) as maxAge);qw.groupBy(tel);for (MapString, Object selectMap : userDao.selectMaps(qw)) {System.out.println(selectMap);}
}注意
聚合与分组查询无法使用lambda表达式来完成MP只是对MyBatis的增强如果MP实现不了我们可以直接在DAO接口中使用MyBatis的方式实现 3.3 查询条件设定
前面我们只使用了lt()和gt(),除了这两个方法外MP还封装了很多条件对应的方法
范围匹配 、 、between模糊匹配like空判定null包含性匹配in分组group排序order……
3.3.1 等值查询
需求:根据用户名和密码查询用户信息
Test
void testQueryWrapper() {LambdaQueryWrapperUser qw new LambdaQueryWrapper();qw.eq(User::getName,Seto).eq(User::getPassword,MUSICIAN);User user userDao.selectOne(qw);System.out.println(user);
}eq() 相当于 ,对应的sql语句为 SELECT * FROM tb_user WHERE name seto AND password MUSICIAN;selectList查询结果为多个或者单个 selectOne:查询结果为单个
3.3.2 范围查询
需求:对年龄进行范围查询使用lt()、le()、gt()、ge()、between()进行范围查询
Test
void testQueryWrapper() {LambdaQueryWrapperUser qw new LambdaQueryWrapper();qw.between(User::getAge,10,30);ListUser users userDao.selectList(qw);for (User u : users) {System.out.println(u);}
}gt():大于()ge():大于等于()lt():小于()lte():小于等于()between():between ? and ?
3.3.3 模糊查询
需求:查询表中name属性的值以J开头的用户信息,使用like进行模糊查询
Test
void testQueryWrapper() {LambdaQueryWrapperUser qw new LambdaQueryWrapper();qw.likeRight(User::getName,J);ListUser users userDao.selectList(qw);for (User u : users) {System.out.println(u);}
}like():前后加百分号,如 %J%相当于包含J的namelikeLeft():前面加百分号,如 %J相当于J结尾的namelikeRight():后面加百分号,如 J%相当于J开头的name
需求:查询表中name属性的值包含e的用户信息,使用like进行模糊查询
Test
void testQueryWrapper() {LambdaQueryWrapperUser qw new LambdaQueryWrapper();qw.like(User::getName,e);ListUser users userDao.selectList(qw);for (User u : users) {System.out.println(u);}
}3.3.4 排序查询
需求:查询所有数据然后按照age降序
Test
void testQueryWrapper() {LambdaQueryWrapperUser qw new LambdaQueryWrapper();/*** condition 条件返回boolean当condition为true进行排序如果为false则不排序* isAsc:是否为升序true为升序false为降序* columns需要操作的列*/qw.orderBy(true,false,User::getAge);ListUser users userDao.selectList(qw);for (User u : users) {System.out.println(u);}
}遇到想用的功能先自己用一个试试方法名和形参名都很见名知意遇到不确定的用法再去官方文档查阅资料
3.4 字段映射与表名映射
我们做查询的时候数据表中的字段名与模型类中的属性名一致查询的时候没有问题那么问题就来了 问题一表字段与模型类编码属性不一致 当表的列名和模型类的属性名发生不一致就会导致数据封装不到模型对象这个时候就需要其中一方做出修改那如果前提是两边都不能改又该如何解决?MP给我们提供了一个注解TableField,使用该注解可以实现模型类属性名和表的列名之间的映射关系例如表中密码字段为pwd而模型类属性名为password那我们就可以用TableField注解来实现他们之间的映射关系 问题二编码中添加了数据库中未定义的属性 当模型类中多了一个数据库表不存在的字段就会导致生成的sql语句中在select的时候查询了数据库不存在的字段程序运行就会报错错误信息为:Unknown column 多出来的字段名称 in field list具体的解决方案用到的还是TableField注解它有一个属性叫exist设置该字段是否在数据库表中存在如果设置为false则不存在生成sql语句查询的时候就不会再查询该字段了。 问题三采用默认查询开放了更多的字段查看权限 查询表中所有的列的数据就可能把一些敏感数据查询到返回给前端这个时候我们就需要限制哪些字段默认不要进行查询。解决方案是TableField注解的一个属性叫select该属性设置默认是否需要查询该字段的值true(默认值)表示默认查询该字段false表示默认不查询该字段。例如像密码这种的敏感字段不应该查询出来作为JSON返回给前端不安全
知识点TableField
名称TableField类型属性注解位置模型类属性定义上方作用设置当前属性对应的数据库表中的字段关系相关属性value(默认)设置数据库表字段名称 exist:设置属性在数据库表字段中是否存在默认为true此属性不能与value合并使用 select:设置属性是否参与查询此属性与select()映射配置不冲突
问题四表名与编码开发设计不同步 这个问题其实我们在一开始就解决过了现在再来回顾一遍该问题主要是表的名称和模型类的名称不一致导致查询失败这个时候通常会报如下错误信息Table ‘databaseName.tableNaem’ doesn’t exist解决方案是使用MP提供的另外一个注解TableName来设置表与模型类之间的对应关系。
知识点TableName
名称TableName类型类注解位置模型类定义上方作用设置当前类对应于数据库表关系相关属性value(默认)设置数据库表名称
四、DQL控制
4.1 id生成策略控制
前面我们在新增数据的时候主键ID是一个很长的Long类型我们现在想要主键按照数据表字段进行自增长在解决这个问题之前我们先来分析一下ID的生成策略 不同的表应用不同的id生成策略 日志自增1 2 3 4购物订单特殊规则线下购物发票下次可以留意一下外卖订单关联地区日期等信息这个我熟举个例子10 04 20220921 13 14例如10表示北京市04表示朝阳区20220921表示日期等关系表可以省略ID …… 不同的业务采用的ID生成方式应该是不一样的那么在MP中都提供了哪些主键生成策略以及我们该如何进行选择? 在这里我们又需要用到MP的一个注解叫TableId
知识点TableId
名称TableId类型属性注解位置模型类中用于表示主键的属性定义上方作用设置当前类中主键属性的生成策略相关属性value(默认)设置数据库表主键名称type:设置主键属性的生成策略值查照IdType的枚举值
4.1.1 AUTO 步骤一设置生成策略为AUTO TableName(tb_user)
Data
public class User {
TableId(type IdType.AUTO)
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
TableField(exist false)
private Integer online;
}步骤二设置自动增量为5将4之后的数据都删掉防止影响我们的结果 步骤三运行新增方法 Test
void testInsert(){
User user new User();
user.setName(Helsing);
user.setAge(531);
user.setPassword(HELL_SING);
user.setTel(4006669999);
userDao.insert(user);
}会发现新增成功并且主键id也是从5开始
我们进入源码来看看还有什么生成策略
public enum IdType {AUTO(0),NONE(1),INPUT(2),ASSIGN_ID(3),ASSIGN_UUID(4),/** deprecated */DeprecatedID_WORKER(3),/** deprecated */DeprecatedID_WORKER_STR(3),/** deprecated */DeprecatedUUID(4);private final int key;private IdType(int key) {this.key key;}public int getKey() {return this.key;}
}NONE: 不设置id生成策略INPUT:用户手工输入idASSIGN_ID:雪花算法生成id(可兼容数值型与字符串型)ASSIGN_UUID:以UUID生成算法作为id生成策略其他的几个策略均已过时都将被ASSIGN_ID和ASSIGN_UUID代替掉。 拓展: 分布式ID是什么? 当数据量足够大的时候一台数据库服务器存储不下这个时候就需要多台数据库服务器进行存储比如订单表就有可能被存储在不同的服务器上如果用数据库表的自增主键因为在两台服务器上所以会出现冲突这个时候就需要一个全局唯一ID,这个ID就是分布式ID。 4.1.2 INPUT 步骤一将ID生成策略改为INPUT TableName(tb_user)
Data
public class User {
TableId(type IdType.INPUT)
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
TableField(exist false)
private Integer online;
}步骤二运行新增方法 注意这里需要手动设置ID了 Test
void testInsert(){
User user new User();
user.setId(6L);
user.setName(Helsing);
user.setAge(531);
user.setPassword(HELL_SING);
user.setTel(4006669999);
userDao.insert(user);
}查看数据库ID确实是我们设置的值
4.1.3 ASSIGN_ID 步骤一设置生成策略为ASSIGN_ID TableName(tb_user)
Data
public class User {
TableId(type IdType.ASSIGN_ID)
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
TableField(exist false)
private Integer online;
}步骤二运行新增方法 这里就不要手动设置ID了 Test
void testInsert(){
User user new User();
user.setName(Helsing);
user.setAge(531);
user.setPassword(HELL_SING);
user.setTel(4006669999);
userDao.insert(user);
}查看结果生成的ID就是一个Long类型的数据生成ID时使用的是雪花算法 雪花算法(SnowFlake),是Twitter官方给出的算法实现 是用Scala写的。其生成的结果是一个64bit大小整数
1bit,不用,因为二进制中最高位是符号位1表示负数0表示正数。生成的id一般都是用整数所以最高位固定为0。41bit-时间戳用来记录时间戳毫秒级10bit-工作机器id用来记录工作机器id,其中高位5bit是数据中心ID其取值范围0-31低位5bit是工作节点ID其取值范围0-31两个组合起来最多可以容纳1024个节点序列号占用12bit每个节点每毫秒0开始不断累加最多可以累加到4095一共可以产生4096个ID
4.1.4 ASSIGN_UUID 步骤一设置生成策略为ASSIGN_UUID TableName(tb_user)
Data
public class User {
TableId(type IdType.ASSIGN_UUID)
private String id;
private String name;
private String password;
private Integer age;
private String tel;
TableField(exist false)
private Integer online;
}步骤二修改表的主键类型 主键类型设置为varchar长度要大于32因为UUID生成的主键为32位如果长度小的话就会导致插入失败。 步骤三运行新增方法 Test
void testInsert(){
User user new User();
user.setName(Helsing);
user.setAge(531);
user.setPassword(HELL_SING);
user.setTel(4006669999);
userDao.insert(user);
}4.1.5 ID生成策略对比
介绍完了这些主键ID的生成策略那么以后我们开发用哪个呢
NONE不设置ID生成策略MP不自动生成约定于INPUT所以这两种方式都需要用户手动设置SET方法但是手动设置的第一个问题就是容易出错加了相同的ID造成主键冲突为了保证主键不冲突就得做很多判定实现起来较为复杂AUTO数据库ID自增这种策略适合在数据库服务器只有一台的情况下使用不可作为分布式ID使用ASSIGN_UUID可以在分布式的情况下使用而且能够保证ID唯一但是声称的主键是32位的字符串长度过长占用空间而且不能排序查询性能也慢ASSIGN_ID可以在分布式的情况下使用生成的是Long类型的数字可以排序性能也高但是生成的策略与服务器时间有关如果修改了系统时间也有可能出现重复的主键综上所述每一种主键的策略都有自己的优缺点根据自己的项目业务需求的实际情况来使用才是最明智的选择
4.1.6 简化配置 模型类主键策略设置 如果要在项目中的每一个模型类上都需要使用相同的生成策略比如你有Book表User表Student表Score表等好多个表如果你每一个表的主键生成策略都是ASSIGN_ID那我们就可以用yml配置文件来简化开发不用在每一个表的id上都加上TableId(type IdType.ASSIGN_ID) mybatis-plus:global-config:db-config:id-type: assign_id数据库表与模型类的映射关系 MP会默认将模型类的类名名首字母小写作为表名使用假如数据库表的名称都以tb_开头那么我们就需要将所有的模型类上添加TableName(tb_TABLENAME)这样做很繁琐有没有更简单的方式呢 我们可以在配置文件中设置表的前缀 mybatis-plus:global-config:db-config:id-type: assign_idtable-prefix: tb_设置表的前缀内容这样MP就会拿 tb_加上模型类的首字母小写就刚好组装成数据库的表名前提是你的表名得规范命名别瞎起花里胡哨的名。将User类的TableName注解去掉再次运行新增方法
Data
public class User {TableId(type IdType.ASSIGN_ID)private Long id;private String name;private String password;private Integer age;private String tel;
}4.2 多记录操作 需求:根据传入的id集合将数据库表中的数据删除掉。
deleteBatchIdsTest
void testDeleteByIds(){
ArrayListLong list new ArrayList();
list.add(1572543345085964289L);
list.add(1572554951983460354L);
list.add(1572555035978534913L);
userDao.deleteBatchIds(list);
}执行成功后数据库表中的数据就会按照指定的id进行删除。上面三个数据是我之前新增插入的可以随便换成数据库中有的id 需求根据传入的ID集合查询用户信息 selectBatchIds
Test
void testSelectByIds() {ArrayListLong list new ArrayList();list.add(1L);list.add(2L);list.add(3L);for (User user : userDao.selectBatchIds(list)) {System.out.println(user);}
}4.3 逻辑删除
逻辑删除是删除操作中比较重要的一部分先来讲个案例 这是一个员工和其所办理的合同表一个员工可以办理多张合同表 员工ID为1的张业绩办理了三个合同但是她现在想离职跳槽了我们需要将员工表中的数据进行删除执行DELETE操作 如果表在设计的时候有主外键关系那么同时也要将合同表中的张业绩的数据删掉 后来公司要统计今年的总业绩发现这数据咋对不上呢业绩这么少原因是张业绩办理的合同信息被删掉了 如果只删除员工却不删除员工对应的合同表数据那么合同的员工编号对应的员工信息不存在那么就会产生垃圾数据出现无主合同根本不知道有张业绩这个人的存在 经过我们的分析之后我们不应该将表中的数据删除掉得留着但是又得把离职的人和在职的人区分开这样就解决了上述问题 区分的方式就是在员工表中添加一列数据deleted如果为0说明在职员工如果离职则将其改完10和1所代表的含义是可以自定义的 所以对于删除操作业务问题来说有: 物理删除:业务数据从数据库中丢弃执行的是delete操作逻辑删除:为数据设置是否可用状态字段删除时设置状态字段为不可用状态数据保留在数据库中执行的是update操作 MP中逻辑删除具体该如何实现? 步骤一修改数据库表添加deleted列 字段名任意类型int长度1默认值0个人习惯你随便 步骤二实体类添加属性 还得修改对应的pojo类增加delete属性属性名也任意对不上用TableField来添加映射关系 标识新增的字段为逻辑删除字段使用TableLogic //表名前缀和id生成策略在yml配置文件写了
Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
//新增delete属性
//value为正常数据的值在职delval为删除数据的值离职
TableLogic(value 0,delval 1)
private Integer deleted;
}步骤三运行删除方法 Test
void testLogicDelete(){userDao.deleteById(1);
}从测试结果来看逻辑删除最后走的是update操作执行的是UPDATE tb_user SET deleted1 WHERE id? AND deleted0会将指定的字段修改成删除状态对应的值。
思考逻辑删除对查询有没有影响呢? 执行查询操作 Test
void testSelectAll() {
for (User user : userDao.selectList(null)) {System.out.println(user);}
}从日志中可以看到执行的SQL语句如下WHERE条件中规定只查询deleted字段为0的数据
SELECT id,name,password,age,tel,deleted FROM tb_user WHERE deleted0输出结果当然也没有ID为1的数据了
如果还是想把已经删除的数据都查询出来该如何实现呢?
Mapper
public interface UserDao extends BaseMapperUser {//查询所有数据包含已经被删除的数据Select(select * from tb_user)public ListUser selectAll();
}如果每个表都要有逻辑删除那么就需要在每个模型类的属性上添加TableLogic注解如何优化? 在配置文件中添加全局配置如下: mybatis-plus:global-config:db-config:## 逻辑删除字段名logic-delete-field: deleted## 逻辑删除字面值未删除为0logic-not-delete-value: 0## 逻辑删除字面值删除为1logic-delete-value: 1使用yml配置文件配置了之后就不需要在模型类上用TableLogic注解了
介绍完逻辑删除逻辑删除的本质为修改操作。如果加了逻辑删除字段查询数据时也会自动带上逻辑删除字段。 执行的SQL语句为:
UPDATE tb_user SET deleted1 WHERE id? AND deleted0知识点TableLogic
名称TableLogic类型属性注解位置模型类中用于表示删除字段的属性定义上方作用标识该字段为进行逻辑删除的字段相关属性value逻辑未删除值delval:逻辑删除值
4.4 乐观锁
在学乐观锁之前我们还是先由一个案例来引入 业务并发现象带来的问题秒杀 加入有100个商品在售为了保证每个商品只能被一个人购买如何保证不会超买或者重复卖对于这一类的问题其实有很多的解决方案可以使用第一个最先想到的就是锁锁在一台服务器中是可以解决的但是如果在多台服务器下就没办法控制比如12306有两台服务器再进行卖票在两台服务器上都添加锁的话那也有可能会在同一时刻有两个线程在卖票还是会出现并发问题我们接下来介绍的这种方式就是针对于小型企业的解决方案因为数据库本身的性能就是个瓶颈如果对其并发超过2000以上的就需要考虑其他解决方案了
简单来说乐观锁主要解决的问题是当要更新一条记录的时候希望这条记录没有被别人更新
4.4.1 实现思路
数据库表中添加version字段比如默认值给个1第一个线程要修改数据之前取出记录时获取当前的version1第二个线程要修改数据之前取出记录时获取当前的version1 第一个线程执行更新时 set version newVersion where version oldVersion newVersion version 1oldVersion version 第二个线程执行更新时 set version newVersion where version oldVersion newVersion version 1oldVersion version 假如这两个线程都来更新数据第一个和第二个线程都可能先执行 假如第一个线程先执行更新会将version改为2 那么第二个线程再更新的时候set version 2 where version 1此时数据库表的version已经是2了所以第二个线程修改失败 假如第二个线程先执行更新会将version改为2 那么第一个线程再更新的时候set version 2 where version 1此时数据库表的version已经是2了所以第一个线程修改失败
4.4.2 实现步骤 步骤一数据库表添加列 加一列version长度给个11默认值设为1 步骤二在模型类中添加对应的属性 Data
public class User {
private Long id;
private String name;
private String password;
private Integer age;
private String tel;
TableLogic(value 0, delval 1)
private Integer deleted;
Version
private Integer version;
}步骤三添加乐观锁拦截器 Configuration
public class MpConfig {
Bean
public MybatisPlusInterceptor mpInterceptor() {//1.定义Mp拦截器MybatisPlusInterceptor mpInterceptor new MybatisPlusInterceptor();//2.添加乐观锁拦截器mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return mpInterceptor;}
}步骤四执行更新操作 Test
void testUpdate(){
//1. 先通过要修改的数据id将当前数据查询出来
User user userDao.selectById(1L);
//2. 修改属性
user.setName(Person);
userDao.updateById(user);
}我们传递的是1(oldVersion)MP会将1进行加1变成2然后更新回到数据库中(newVersion)
大概分析完乐观锁的实现步骤以后我们来模拟一种加锁的情况看看能不能实现多个人修改同一个数据的时候只能有一个人修改成功。
Test
void testUpdate() {User userA userDao.selectById(1L); //version1User userB userDao.selectById(1L); //version1userB.setName(Jackson);userDao.updateById(userB); //B修改完了之后version2userA.setName(Person);//A拿到的version是1但现在的version已经是2了那么A在执行 UPDATE ... WHERE version 1时就必然会失败userDao.updateById(userA);
}至此乐观锁的实现就已经完成了
五、快速开发
官方文档地址 通过观察我们之前写的代码会发现其中有很多重复的内容于是MP抽取了这些重复的地方做成了一个模板供我们使用 要想完成代码自动生成我们需要有以下内容:
模板: MyBatisPlus提供可以自己提供但是麻烦不建议数据库相关配置:读取数据库获取表和字段信息开发者自定义配置:手工配置比如ID生成策略 5.1 代码生成器实现 步骤一创建一个Maven项目 步骤二导入对应的jar包 ?xml version1.0 encodingUTF-8?project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd
modelVersion4.0.0/modelVersion
parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.5.1/version
/parent
groupIdcom.blog/groupId
artifactIdmybatisplus_04_generator/artifactId
version0.0.1-SNAPSHOT/version
propertiesjava.version1.8/java.version
/properties
dependencies!--spring webmvc--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--mybatisplus--dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.4.1/version/dependency!--druid--dependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.1.16/version/dependency!--mysql--dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdscoperuntime/scope/dependency!--test--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-test/artifactIdscopetest/scope/dependency!--lombok--dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.12/version/dependency!--代码生成器--dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-generator/artifactIdversion3.4.1/version/dependency!--velocity模板引擎--dependencygroupIdorg.apache.velocity/groupIdartifactIdvelocity-engine-core/artifactIdversion2.3/version/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/plugin/plugins
/build/project步骤三编写引导类 SpringBootApplication
public class Mybatisplus04GeneratorApplication {
public static void main(String[] args) {SpringApplication.run(Mybatisplus04GeneratorApplication.class, args);
}}步骤四创建代码生成类 public class CodeGenerator {
public static void main(String[] args) {//1.获取代码生成器的对象AutoGenerator autoGenerator new AutoGenerator();//设置数据库相关配置DataSourceConfig dataSource new DataSourceConfig();dataSource.setDriverName(com.mysql.cj.jdbc.Driver);dataSource.setUrl(jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezoneUTC);dataSource.setUsername(root);dataSource.setPassword(YOURPASSWORD);autoGenerator.setDataSource(dataSource);//设置全局配置GlobalConfig globalConfig new GlobalConfig();globalConfig.setOutputDir(System.getProperty(user.dir)/项目名/src/main/java); //设置代码生成位置globalConfig.setOpen(false); //设置生成完毕后是否打开生成代码所在的目录globalConfig.setAuthor(heima); //设置作者globalConfig.setFileOverride(true); //设置是否覆盖原始生成的文件globalConfig.setMapperName(%sDao); //设置数据层接口名%s为占位符指代模块名称globalConfig.setIdType(IdType.ASSIGN_ID); //设置Id生成策略autoGenerator.setGlobalConfig(globalConfig);//设置包名相关配置PackageConfig packageInfo new PackageConfig();packageInfo.setParent(com.aaa); //设置生成的包名与代码所在位置不冲突二者叠加组成完整路径packageInfo.setEntity(domain); //设置实体类包名packageInfo.setMapper(dao); //设置数据层包名autoGenerator.setPackageInfo(packageInfo);//策略设置StrategyConfig strategyConfig new StrategyConfig();strategyConfig.setInclude(tb_user); //设置当前参与生成的表名参数为可变参数strategyConfig.setTablePrefix(tb_); //设置数据库表的前缀名称模块名 数据库表名 - 前缀名 例如 User tb_user - tb_strategyConfig.setRestControllerStyle(true); //设置是否启用Rest风格strategyConfig.setVersionFieldName(version); //设置乐观锁字段名strategyConfig.setLogicDeleteFieldName(deleted); //设置逻辑删除字段名strategyConfig.setEntityLombokModel(true); //设置是否启用lombokautoGenerator.setStrategy(strategyConfig);//2.执行生成操作autoGenerator.execute();}
}对于代码生成器中的代码内容我们可以直接从官方文档中获取代码进行修改
步骤五运行程序 运行成功后会在当前项目中生成很多代码代码包含controller,servicemapper和entity等
至此代码生成器就已经完成工作我们能快速根据数据库表来创建对应的类简化我们的代码开发。
初期还是不建议直接使用代码生成器还是多自己手写几遍比较好
5.2 MP中Service的CRUD
回顾我们之前业务层代码的编写编写接口和对应的实现类:
回顾我们之前业务层代码的编写编写接口和对应的实现类:
public interface UserService{}Service
public class UserServiceImpl implements UserService{}接口和实现类有了以后需要在接口和实现类中声明方法
public interface UserService{public ListUser findAll();
}Service
public class UserServiceImpl implements UserService{Autowiredprivate UserDao userDao;public ListUser findAll(){return userDao.selectList(null);}
}MP看到上面的代码以后就说这些方法也是比较固定和通用的那我来帮你抽取下所以MP提供了一个Service接口和实现类分别是:IService和ServiceImpl,后者是对前者的一个具体实现。
以后我们自己写的Service就可以进行如下修改:
public interface UserService extends IServiceUser{}Service
public class UserServiceImpl extends ServiceImplUserDao, User implements UserService{}修改以后的好处是MP已经帮我们把业务层的一些基础的增删改查都已经实现了可以直接进行使用。
编写测试类进行测试:
SpringBootTest
class Mybatisplus04GeneratorApplicationTests {private IUserService userService;Testvoid testFindAll() {ListUser list userService.list();System.out.println(list);}
}六、复习
之前如果要写动态SQL查询需要用XML配置文件用whereif标签来自动去除and连接词啥的。
select idselectByCondition resultMapbrandResultMapselect *from tb_brandwhereif testbrand.brandName ! null and brand.brandName ! and brand_name like #{brand.brandName}/ifif testbrand.companyName ! null and brand.companyName ! and company_name like #{brand.companyName}/ifif testbrand.status ! nulland status #{brand.status}/if/wherelimit #{begin} , #{size}
/select学完MyBatisPlus之后我们可以不用XML配置文件就用MP也能写动态SQL用Wrapper类。
针对图书类别和名称做的一个动态SQL就长这个样子
Override
public ListBook getByCondition(String type,String name) {LambdaQueryWrapperBook lqw new LambdaQueryWrapper();lqw.like(!(type null || .equals(type)), Book::getType, type).like(!(name null || .equals(name)),Book::getName, name);return bookDao.selectList(lqw);
}MP里的and不用显示声明而且还可以很简单的帮我们完成模糊查询当判断条件为false时则不会进行SQL语句的拼接。而且也不需要创建文件写配置
不过复杂的SQL语句还是要用XML写的用MP写的话可读性不