MyBatis-Plus 轻松掌握🐱🏍:
官方图标是一个 魂斗罗
表示:Mybatis 和 Plus就像兄弟一样,相辅相成👍
MyBatis-Plus 简称 MP, 是一个MyBatis 的增强工具 官方网站
在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
家喻户晓
,ORM 对象关系映射的,半自动化,持久层
的框架
MyBatis 的开发代码...
特性:
MySQL
Oracle
DB2
HSQL
本人使用的数据库,.sql
文件
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_name` varchar(20) NOT NULL COMMENT '用户名',
`password` varchar(20) NOT NULL COMMENT '密码',
`name` varchar(30) DEFAULT NULL COMMENT '姓名',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`address` varchar(100) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
insert into `user`(`id`,`user_name`,`password`,`name`,`age`,`address`) values
(1,'ruiwen','123','瑞文',12,'诺克萨斯'),
(2,'gailun','1332','盖伦',13,'德玛西亚'),
(3,'timu','123','提姆',22,'约德尔'),
(4,'daji','1222','亚索',221,'艾欧尼亚');
pom.xml
<!-- SpringBoot 的父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>
<!-- Maven 依赖 -->
<dependencies>
<!-- SpringBoot starter依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot test测试程序依赖; -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok 依赖,为了方便快速加载实体类~ -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 使用Mybatis-plus 依赖;
加载了Mybatis-plus 依赖,就可以不需要Mybatis 依赖了, plus依赖中默认集成了 Mybatis依赖;
-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!-- mysql的驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
这里不过多的解释SpringBoot 了, 需要学习的小伙伴可以借鉴:Java_慈祥学习笔记
SpringBootRun.Java
//SpringBoot 启动类注解~
@SpringBootApplication
public class SpringBootRun {
public static void main(String[] args) {
SpringApplication.run(SpringBootRun.class);
}
}
com.wsm.entity 包下的实体类:User.Java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String userName;
private String password;
private String name;
private Integer age;
private String address;
}
com.wsm.mapper 包下的 xxxMapper 接口
面向接口编程
提供每一个实体类对应的接口,与对应的Mapper.xml 映射文件进行 sql
的实现;
MyBatis-plus
对其进行了封装
Mapper接口,extends继承 BaseMapper<T>
并通过 <T> 泛型
指定对应的实体类… 使 xxxMapper接口, 拥有BaseMapper的所有方法();
MyBatis-plus 对其中的方法都有其对应的实现映射,所以,只需要 继承BaseMapper<T>
就实现了大量的常用方法, 这就是MP的简单强大之处,省去开发者的大量重复工作
MP 提供了大量的方法, 各种的 CRUD
UserMapper.Java
@Mapper
//@Mapper 注解,使当前的Mapper 接口,被Spring进行管理,不然需要在,启动类上声明 @MapperScan("com.wsm.mapper") 类扫描指定包下,mapper接口文件;
public interface UserMapper extends BaseMapper<User> {
//Mapper 接口 extends集成 BaseMapper<T> 泛型对应的实体类;
// Ctrl+右击, 进入BaseMapper 中可以看到, MP 默认给对应实体类提供好的实现方法();
// 增删改查... 即各种的, 重载 CRUD 的操作;
//如果,BaseMapper<T> 中,没有提供的,后面还可以在,该 xxxMapper 文件中, 自定义自己需要的方法();
}
application.yml
的语法结构,比较清晰明了
spring:
# 配置SpringBoot 连接的数据源,注意 这里的Mysql连接 用户名 密码 要根据自己的实际情况来~
datasource:
url: jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: ok
driver-class-name: com.mysql.cj.jdbc.Driver
🆗,一切准备就绪运行SpringBoot 启动类:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.5.0)
启动成功, 准备Maven 测试类,进行测试程序:
test模块,就Maven包下的专门用来测试程序的模块
com.wsm.MPTest.Java
测试程序,第一个MP程序:查询所有的User表
/* MP的测试类; **/
@SpringBootTest
//@SpringBootTest JUnit等其他测试框架结合起来,提供了便捷高效的测试手段.
//使用@SpringBootTest后,Spring将加载所有被管理的bean,基本等同于启动了整个服务,此时便可以开始功能测试. 需要引入: spring-boot-starter-test 依赖;
public class MPTest {
@Autowired
private UserMapper userMapper;
@Test
public void testSelectList(){
System.out.println("测试 selectList(null); 查询全部!");
// selectList(queryWrapper); 参数需要是一个 查询添加Wrapper; 不设置,则无条件查询全部!
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
}
运行🏃♂️:
MyBatis-Plus是如此的简单,BaseMapper<T>接口类
中,已经默认集成了很多单表的 CRUD
的操作,我们只需要去调用即可 轻松的完成:增删改查
MP 具有强大的注解
/ yml的全局配置
使框架具有更加强大的功能!
@TableName
默认情况下MP操作的表名就是实体类的类名,但是如果表名和类名不一致就需要我们自己设置映射规则
@TableName注解
进行,Java实体 与 数据库之间的相互映射
**注解在类上,指定类和数据库表的映射关系 **
如果,实体类 类名——>转换 小写后——> 和数据库映射表相同,可以不指定该注解;
MP 默认就是这样映射的;
这里介绍一下,验证思路:
同样的库中,重新创建一个 除表名外,表结构相同的,数据不同的表
通过 @TableName
来进行切换执行查看数据;
上面的 @TableName
可以设置,数据库与实体的 表名进行映射
一般一个项目表名的前缀都是统一风格的,这个时候如果一个个设置就太麻烦了。我们可以通过配置来设置全局的表名前缀
例如:
如果一个项目中所有的表名相比于类名都是多了个前缀: tb_
这可以使用如下方式配置:
# MP 参数设置:
mybatis-plus:
global-config:
db-config:
# 设置数据库映射 实体时候添加的 前缀;
table-prefix: tb_
这样,MP 在通过实体映射 数据库表的时候,会在前面自动添加 tb_
,可以大量节省开发者的工作…(只限于,存在规律的表;
验证思路:
在数据库中,创建一个 表名前缀 tb_ ,表结构相同的,数据不同的表
执行查看运行数据!
@TableId
注解在实体类的某一字段上,表示这个字段对应数据库表的主键
id
时候,无需使用改注解进行指定, MP会自定进行关联;
且默认使用的是 雪花算法
❄
@TableId注解的
value属性进行关联~
type属性:主键策略
IdType
主键生成策略的值,是一个枚举类型,全都定义在 idType枚举类中
,取值如下:
AUTO 自增
默认采用 雪花算法
NONE 默认
默认采用雪花算法
要注意数据库主键列的 长度要11 位,不然数据库新增列不够长报错!长度太小!
Java 的字段要是 Long 长整型
INPUT 手动输入
插入操作生成SQL语句时,主键这一列的值会是null
ASSIGN_ID 手动+默认
ASSIGN_UUID 手动+uuid
uuid 是一个带有字母的字符串,数据库的字段需要是 varchar 字符类型,长度40
Java字段要是String字符类型;
首先,雪花算法 和 uuid 都是为了保证,在分布式环境下,保证数据库表中,主键唯一!
雪花算法:
分布式环境下,有序且唯一的全局id
时间戳 + 机器id + 毫秒序列号
, 一般常用的有了分布式保证数据唯一且自增,有的公司第一个数据使用 雪花算法
,后面的数据采用auto 自增
1478677587608748035
1478677587608748036
...
uuid
String uuid = UUID.randomUUID().toString();
5c6aeee6-00f1-45b1-aafa-d615a18217aa
mybatis-plus:
global-config:
db-config:
# 设置全局 主键生成策略;
id-type: auto
@TableFieid
与@TableName 类型
注解在某一字段上,指定Java实体类的字段和数据库表的列的映射关系
表列/实体字段的 驼峰映射
即:数据库中的 user_name
字段,会自动与 Java实体的 userName
进行映射匹配
数据库列/实体字段
可以通过 @TableFieid
进行关联,注解声明在要匹配的字段名上
value
属性指定表的列名
fill
属性指定,字段为空时会进行自动填充的值
exist
属性,设置之后表示该,实体属性,不和任何数据库列匹配 CRUD的Sql 会忽略这个字段~
exist
也可以通过其它方式来完成,如使用 static
transient
关键字的属性,不过不是很合理;
如果需要打印MP操作对应的SQL语句等,可以配置日志输出:
mybatis-plus:
configuration:
# 设置MP 打印SQL 语句日志;
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
MP 封装了一些最基础的CRUD方法,Mapper接口
只需要继承 BaseMapper<T>
,MP 在程序运行时候,会自动给 Mapper接口,完成对应的实现~
Service CRUD接口
还有 Wrapper条件构造器 方便组装SQL where条件
insert(T entity)
插入一条记录
deleteById(Serializable id)
根据主键id删除一条记录
delete(Wrapper<T> wrapper)
根据条件构造器wrapper进行删除
selectById(Serializable id)
根据主键id进行查找
selectBatchIds(Collection idList)
根据主键id进行批量查找
selectList(Wrapper<T> wrapper)
根据条件构造器wrapper
进行查询
selectMaps(Wrapper<T> wrapper)
根据 wrapper 条件,查询记录,将查询结果封装为一个Map,Map的key为结果的列,value为值
update(T entity, Wrapper<T> wrapper)
根据条件构造器wrapper
进行更新
updateById(T entity)
传入对象类型,必须给主键列赋值,修改非主键列的字段…
通过 BaseMapper的 insert(); 方法, 传入一个对象, 对其进行新增入库;
test模块:com.wsm.MPTest.Java
// insert(); 新增用户
@Test
public void testInsert(){
User user = new User();
user.setName("wsm");
user.setAge(540);
user.setPassword("qwer");
user.setUserName("wsm");
user.setAddress("皮尔及沃特");
// 讲创建的对象,新增入库,并返回影响行数;
int insert = userMapper.insert(user);
// 判断影响行数,是否新增成功!
if(insert >0)
System.out.println("新增成功");
else
System.out.println("新增失败");
}
test模块:com.wsm.MPTest.Java
// deleteById(); 根据id 删除数据;
@Test
public void testDelByid(){
// deleteById(Serializable id);
// Java 数值类型 继承了 Number抽象类 实现了 Serializable序列化接口,所以传入一个 Serializable对象;
int del = userMapper.deleteById(1);
// 判断影响行数,是否删除成功!
if(del >0)
System.out.println("删除成功");
else
System.out.println("删除失败");
}
// deleteBatchIds(Collection); 根据 id 集合,批量删除数据!
@Test
public void testDelBat(){
List<Integer> ids = new ArrayList<>();
ids.add(2);
ids.add(3);
ids.add(4);
// 批量删除 2 3 4 主键列的数据;
int del = userMapper.deleteBatchIds(ids);
// 判断影响行数,是否删除成功!
if(del >0)
System.out.println("批量删除成功");
else
System.out.println("批量删除失败");
}
// deleteByMap(Map); 根据传入Map 指定的K作为列名和V作为列值进行等值匹配查找;
@Test
public void testDelByMap(){
HashMap<String, Object> map = new HashMap<>();
map.put("name","wsm");
map.put("age",540);
// 删除 name = wsm age = 540 的数据;
int del = userMapper.deleteByMap(map);
// 判断影响行数,是否删除成功!
if(del >0)
System.out.println("删除匹配的数据");
else
System.out.println("未删除数据");
}
test模块:com.wsm.MPTest.Java
// updateByid(T) 根据传入对象,id属性来修改对应数据的对应字段值...
@Test
public void updByid(){
User user = new User();
user.setId(2L);
user.setUserName("wsm");
// 修改数据库 id=2 的数据,user_name 值为wsm 其它属性未赋值,数据库不会改动~
int upd = userMapper.updateById(user);
// 判断影响行数,是否修改成功!
if(upd >0)
System.out.println("数据修改成功!");
else
System.out.println("数据修改失败!");
}
我们在实际操作数据库的时候会涉及到很多的条件,MP为我们提供了一个功能强大的条件构造器 Wrapper
Wrapper 是一个 抽象类,
其子类 AbstractWrapper抽象类 中提供了很多用于构造Where条件的方法
AbstractWrapper
的子类QueryWrapper
则额外提供了用于针对Select语法的select
方法。可以用来设置查询哪些列;
AbstractWrapper
的子类UpdateWrapper
则额外提供了用于针对SET语法的set
方法。可以用来设置对哪些列进行更新;
所有条件构造器的方法中();
boolean类型
的参数,condition
可以,用来决定该条件是否加入最后生成的WHERE语句中举例:
// 假设name变量是一个外部传入的参数
String name = "";
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.like(StringUtils.hasText(name), "name", name);
// 仅当 StringUtils.hasText(name) 为 true 时, 会拼接这个like语句到WHERE中
// 其实就是对下面代码的简化
if (StringUtils.hasText(name)) {
wrapper.like("name", name);
}
// StringUitils.hasText(""); 里面的值为 null 、"" 、 " ",那么返回值为false...
// 即:上面的表示,name 不存在则不加该条件, 当然判断是否成立的方式有很多: Lambda函数式接口...
eq 等于 =
eq("name", "老王") ---> name = '老王'
ne 不等于 <>
ne("name", "老王")---> name <> '老王'
gt 大于 >
gt("age", 18) ---> age > 18
ge 大于等于≥
ge("age", 18) ---> age >= 18
lt 小于<
lt("age", 18) ---> age < 18
le 小于等于≤
le("age", 18) ---> age <= 18
between 相当于SQL中的BETWEEN AND
between("age", 18, 30) ---> age between 18 and 30 (18 ≤ age ≥ 30)
notBetween 相当于between 取反
notBetween("age", 18, 30) ---> age not between 18 and 30
like 模糊匹配
like("name", "王") ---> name like '%王%'
notLike 模糊匹配取反
notLike("name", "王") ---> name not like '%王%'
likeRight 模糊匹配右半边
likeRight("name", "王") ---> name like '王%'
likeLeft 模糊匹配左半边
likeLeft("name", "王") ---> name like '%王'
isNull 判断字段为空的匹配
isNull("name") ---> name is null
isNotNull 字段不为空的匹配
isNotNull("name") ---> name is not null
and 嵌套
and(i -> i.eq("name", "李白").ne("status", "活着")) ---> and (name = '李白' and status <> '活着')
or 拼接
eq("id",1).or().eq("name","老王") ---> id = 1 or name = '老王'
in
in("age",{1,2,3})--->age in (1,2,3)
groupBy 分组
groupBy("id", "name")--->group by id,name
orderByAsc 正排序
orderByAsc("id", "name")--->order by id ASC,name ASC
orderByDesc 倒排
orderByDesc("id", "name")--->order by id DESC,name DESC
**更多请参考官方 👉 **
SQL语句如下:
SELECT
id,user_name,PASSWORD,NAME,age,address
FROM
tb_user
WHERE
age > 18 or address like 'tb_德玛%'
Wrapper写法如下:
/** AbstractWrapper 使用 **/
@Test
public void testAbsWra(){
QueryWrapper query = new QueryWrapper();
query.gt("age", 18);
query.or();
query.likeRight("address", "tb_德玛");
// 查询传入 wrapper 条件构造器,查询: 年龄大于18 或 地址是 tb_德玛 开头的;
List<User> users = userMapper.selectList(query);
users.forEach(System.out::println);
}
QueryWrapper的 select 可以设置要查询的列
select(String... sqlSelect)
方法的指定要查询的列名
SQL语句如下:
SELECT id,`name`
FROM tb_user
WHERE age > 18
Wrapper写法如下:
// select(String... sqlSelect) 方法的指定要查询的列名
public void testQueryWra1(){
// 创建QueryWrapper<T> 实例;
QueryWrapper<User> query = new QueryWrapper<>();
// .select('列1','列2',...); 指定要查询的列, QueryWrapper支持链式编程...
query.select("id","name").gt("age", 18);
// 传入QueryWrapper 开始查询~
List<User> users = userMapper.selectList(query);
users.forEach(System.out::println);
}
select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
SQL语句如下:
SELECT id,user_name,`name`,age,address FROM tb_user WHERE age > 18
Wrapper写法如下:
// select(Class<T> entityClass, Predicate<TableFieldInfo> predicate)
@Test
public void testQueryWra2(){
// 创建QueryWrapper<T> 实例;
QueryWrapper<User> query = new QueryWrapper<>();
// .select(类对象,Predicate); 内部类实现;
// query.select(User.class, new Predicate<TableFieldInfo>() {
// @Override
// public boolean test(TableFieldInfo tableFieldInfo) {
// return !"password".equals(tableFieldInfo.getColumn());
// }
// }).gt("age", 18);
// .select(类对象,Predicate); 支持使用Lambda表达式实现;
// Predicate中的test(); 方法返回 boolean类型, 程序会循环对每个字段进行比较,为 true 的才会查询该列~
query.select(User.class,u-> !"password".equals(u.getColumn())).gt("age", 18);
// 传入QueryWrapper 开始查询~
List<User> users = userMapper.selectList(query);
users.forEach(System.out::println);
}
我们前面在使用update方法时需要创建一个实体类对象传入,用来指定要更新的列及对应的值
sql语句如下:
-- 小于等于 18 岁的用户,都更新为 540;
UPDATE tb_user SET age = 540 WHERE age <= 18
Wrapper写法如下:
/** UpdateWrapper使用 **/
@Test
public void tetsUpdWra(){
UpdateWrapper<User> updwra = new UpdateWrapper<>();
// 小于等于 18 岁的用户,都更新为 540
updwra.le("age",1).set("age","540");
// 传入UpdateWrapper 开始查询~
int update = userMapper.update(null, updwra);
// 判断影响行数,是否修改成功!
if(update >0)
System.out.println("数据修改成功!");
else
System.out.println("数据修改失败!");
}
我们前面在使用条件构造器时列名都是用字符串的形式去指定,这种方式无法在编译期确定列名的合法性 无法更加准确的保证列匹配正确;
MP提供了一个Lambda条件构造器可以让我们直接以实体类的方法引用的形式来指定列名
SQL语句如下:
SELECT id,user_name,`name`,age,address FROM tb_user WHERE age > 18
Wrapper写法如下:
/** LambdaQueryWrapper使用 **/
// Lamdba 表达式实现的
@Test
public void testQueryWralmd(){
LambdaQueryWrapper<User> lam = new LambdaQueryWrapper<>();
// gt(User::getAge,18); 使用方法引用的形式,对参数进行绑定,避免了编译期不确定数据库列,而造成的失误~
lam.select(User.class,u-> !"password".equals(u.getColumn())).gt(User::getAge,18);
// 传入QueryWrapper 开始查询~
List<User> users = userMapper.selectList(lam);
users.forEach(System.out::println);
}
虽然MP为我们提供了很多常用的方法,并且也提供了条件构造器
其实本质上是没有太大变化的,还是正常的:定义Mapper接口
创建对应的映射文件
映射文件中编写sql
调用测试
com.wsm.mapper 包下的 xxxMapper 接口
@Mapper
//@Mapper 注解,使当前的Mapper 接口,被Spring进行管理,不然需要在,启动类上声明 @MapperScan("com.wsm.mapper") 类扫描指定包下,mapper接口文件;
public interface UserMapper extends BaseMapper<User> {
//Mapper 接口 extends集成 BaseMapper<T> 泛型对应的实体类;
// Ctrl+右击, 进入BaseMapper 中可以看到, MP 默认给对应实体类提供好的实现方法();
// 增删改查... 即各种的, 重载 CRUD 的操作;
//如果,BaseMapper<T> 中,没有提供的,后面还可以在,该 xxxMapper 文件中, 自定义自己需要的方法();
/** 自定义方法 **/
// 根据id查询对象;
User findMyUser(Long id);
}
为了方便管理在:
resources 资源目录下创建 mapper 文件夹,中创建 对应的sql映射文件 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!-- 指定映射的mapper -->
<mapper namespace="com.wsm.mapper.UserMapper">
<!-- 指定sql 对应的方法名,返回的结果集类型; -->
<select id="findMyUser" resultType="com.wsm.entity.User">
select * from user where id = #{id}
</select>
</mapper>
mybatis-plus:
# 设置扫描的sql映射文件,加载至环境中;
mapper-locations: classpath*:/mapper/**/*.xml
test模块下:com.wsm.MPTest.Java
/** 测试自定义方法查询结果 **/
@Test
public void testfindMyUser(){
User myUser = userMapper.findMyUser(1L);
System.out.println(myUser);
}
运行,ok 可以查询到数据!
MyBatis-plus 归根结底底层也还是 Mybatis 所以,按照正常 Mybatis 写法来对MP进行扩展Mybatis写法,没有任何影响
我们在使用上述方式自定义方法时, 如果也希望我们的自定义方法能像MP自带方法一样使用条件构造器来进行条件构造的话只需要使用如下方式即可
添加Warpper类型的参数,并且要注意给其指定参数名 UserMapper.Java
@Mapper
//@Mapper 注解,使当前的Mapper 接口,被Spring进行管理,不然需要在,启动类上声明 @MapperScan("com.wsm.mapper") 类扫描指定包下,mapper接口文件;
public interface UserMapper extends BaseMapper<User> {
//Mapper 接口 extends集成 BaseMapper<T> 泛型对应的实体类;
// Ctrl+右击, 进入BaseMapper 中可以看到, MP 默认给对应实体类提供好的实现方法();
// 增删改查... 即各种的, 重载 CRUD 的操作;
//如果,BaseMapper<T> 中,没有提供的,后面还可以在,该 xxxMapper 文件中, 自定义自己需要的方法();
/** 自定义方法 **/
// 根据id查询对象;
User findMyUser(Long id);
// 自定义方法,使用MP 的Wrapper
List<User> findUsers(@Param(Constants.WRAPPER) Wrapper<User> wrapper); //wrapper 是MP包下的依赖~
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!-- 指定映射的mapper -->
<mapper namespace="com.wsm.mapper.UserMapper">
<!-- 指定sql 对应的方法名,返回的结果集类型; -->
<select id="findMyUser" resultType="com.wsm.entity.User">
select * from user where id = #{id}
</select>
<!-- 指定sql 对应的方法名,返回的结果集类型; -->
<select id="findUsers" resultType="com.wsm.entity.User">
select * from user ${ew.customSqlSegment}
</select>
</mapper>
test模块下:com.wsm.MPTest.Java
/** 自定义方法查询 +MP的 wrapper 条件构造 **/
@Test
public void testfindusers(){
// 定义QueryWrapper 构造器,查询 age 大于 18 的数据;
QueryWrapper<User> query = new QueryWrapper<>();
query.gt("age", 18);
// 调用 自定义的方法(wrapper 参数);
List<User> users = userMapper.findUsers(query);
users.forEach(System.out::println);
}
运行,ok 可以查询到数据!
分页查询是一个使用非常频繁的功能,通常实现方式:
当前页int
每页行int
总记录数int
总页数int
每页的数据集合List
总记录数
,给Page对象的属性赋值,同时根据分页算法
总记录数%每页行==0?总记录数/每页行:总记录数/每页行+1 得总页数赋值
总记录 整除 每页行 不整除+1 得总页数;
使用 limit x,y 关键字:获取查询结果的 第x行 往下 y个记录数;
当前端,请求后端传入:分页条件
查询第几页x
每页几行y
后台Java 会根据:x=(x-1)乘y 得到limit 起始行,0开始
y
并将 x y传入sql 中执行 limit x,y
返回分页的结果集存入 Page类对象的每页的数据集合List
MP 为了方便外面操作,页对分页进行了封装, 我们不需要关注太多就可以完成分页操作!
MP 使用拦截器进行分页处理,所以创建一个 util工具包来, 存放分页配置类
MPPageConfig.Java
/** MP 分页,配置类 */
// @Configuration 将类加载至Spring容器中去;
@Configuration
public class MPPageConfig {
/**
* 3.4.0之前的版本
* @return
*/
// @Bean
// public PaginationInterceptor paginationInterceptor(){
// return new PaginationInterceptor();
// }
/**
* 3.4.0之后版本
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
/** selectPage(); Mapper 单表分页 */
@Test
public void testPage(){
// 定义分页对象
Page<User> userPage = new Page<>();
// 设置分页参数
// 每页行数
userPage.setSize(2);
// 当前查询的第几页
userPage.setCurrent(1L); //后台会自动根据 每页行 第几页 计算拼接limit sql;
// 执行分页查询 selectPage(page,wrapper); 两个参数: 分页对象,条件构造器;
userMapper.selectPage(userPage, null);
// 返回的分页对象,就算传入的对象 userPage
System.out.println("获取当前页的数据");
System.out.println(userPage.getRecords());
System.out.println("获取总记录数");
System.out.println(userPage.getTotal());
System.out.println("当前页码");
System.out.println(userPage.getCurrent());
}
定义需要的数据库sql
CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`price` int(11) DEFAULT NULL COMMENT '价格',
`remark` varchar(100) DEFAULT NULL COMMENT '备注',
`user_id` int(11) DEFAULT NULL COMMENT '用户id',
`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',
`create_time` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`version` int(11) DEFAULT '1' COMMENT '版本',
`del_flag` int(1) DEFAULT '0' COMMENT '逻辑删除标识,0-未删除,1-已删除',
`create_by` varchar(100) DEFAULT NULL COMMENT '创建人',
`update_by` varchar(100) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
/*Data for the table `orders` */
insert into `orders`
(`id`,`price`,`remark`,`user_id`,`update_time`,`create_time`,`version`,`del_flag`,`create_by`,`update_by`) values
(1,2000,'无',2,'2021-08-24 21:02:43','2021-08-24 21:02:46',1,0,NULL,NULL),
(2,3000,'无',3,'2021-08-24 21:03:32','2021-08-24 21:03:35',1,0,NULL,NULL),
(3,4000,'无',2,'2021-08-24 21:03:39','2021-08-24 21:03:41',1,0,NULL,NULL);
com.wsm.entity 包下的实体类:Orders.Java
@Data
@NoArgsConstructor
@AllArgsConstructor
// 因为设置了全局的表前缀 tb_ 为了方便操作,@TableName 指定表;
@TableName("orders")
public class Orders {
// Orders 表属性:
private Long id;
private Integer price;
private String remark;
private Integer userId;
private LocalDateTime updateTime;
private LocalDateTime createTime;
private Integer version;
private Integer delFlag;
// 多表查询,User表扩展属性;
// MP 默认的CRUD 不对该属性进行sql映射,自定义Mapper 可以通过同名/取别名 自动映射;
@TableField(exist = false)
public String userName;
}
OrdersMapper.Java
接口OrdersMapper.Java
Page<T>
并且返回类型也是 Page<T>
@Mapper
public interface OrdersMapper extends BaseMapper<Orders> {
// 自定义多表分页方法
public Page<Orders> selPageOrdUs(Page<Orders> page);
}
OrderMapper.xml
映射文件OrdersMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!-- 指定映射的mapper -->
<mapper namespace="com.wsm.mapper.OrdersMapper">
<!-- MP多表分页 -->
<select id="selPageOrdUs" resultType="com.wsm.entity.Orders">
SELECT
o.*,u.`user_name` as userName
FROM orders o
INNER JOIN tb_user u
ON u.id = o.user_id
</select>
</mapper>
MPTest.Java
/** selectPage(); Mapper 多表分页 */
//定义 OrdersMapper 对象;
@Autowired
OrdersMapper ordersMapper;
@Test
public void testselPageOrdUs(){
// 定义MP 分页对象,并设置分页属性
Page<Orders> ordersPage = new Page<>();
// 每页行,当前页
ordersPage.setSize(2);
ordersPage.setCurrent(1);
// 进行查询,返回分页对象, 因为: 引用类型的实参改变形参会受影响~所以, ordersPage == ordersIPage
Page<Orders> ordersIPage = ordersMapper.selPageOrdUs(ordersPage);
System.out.println("ordersPage == ordersIPage是否相等:"+(ordersPage == ordersIPage));
// 返回的结果
System.out.println("获取总记录数");
System.out.println(ordersIPage.getTotal());
System.out.println("获取当前页的数据");
System.out.println(ordersIPage.getRecords());
}
ordersPage == ordersIPage是否相等:true
获取总记录数
3
获取当前页的数据
[Orders(id=1, price=2000, remark=无, userId=2, updateTime=2021-08-24T21:02:43, createTime=2021-08-24T21:02:46, version=1, delFlag=0, userName=gailun), Orders(id=3, price=4000, remark=无, userId=2, updateTime=2021-08-24T21:03:39, createTime=2021-08-24T21:03:41, version=1, delFlag=0, userName=gailun)]
MP 也为我们提供了Service 层的接口来完成 CRUD的操作:
Why?🤯,多次一举?最开始学习的时候,我也很疑惑,对呀为啥呢?
因为:为了方便开发程序现在程序大部分都是 三层架构``Dao持久
Service业务
Controller 控制
而,很多时候Servcie又很简单只能调用了Mapper的方法();
Controller控制层就可以完成开发,开发者不需要在写简单的Service代码
SaveOrUpdate(T entity)
更新记录T 如果不存在,插入一条记录;
saveOrUpdate(T entity, Wrapper<T> updateWrapper);
根据条件修改一条数据, 如果没有匹配则删除
saveOrUpdateBatch(Collection<T> entityList);
批量修改插入
Service 对 Mapper 多了更多的组合批量操作
算是, 节省了开发者的工作量; 官方🐱🏍
在使用MP Service 的 CRUD 之前还是需要确保,Mapper 继承 BaseMapper<T>
UserService.Java
extends继承 IService<操作的实体类T>
IService类中,定义了Service的很多批量CRUD方法~// Service接口 继承MP的 IService<T> T泛型,要CRUD对应映射的实体;
public interface UserService extends IService<User> { }
UserServiceImpl.Java
// Spring注解,表示改类是一个 Service 业务逻辑类,并交给Spring容器管理;
@Service
// ServiceImpl 是 Service 的实现
// 继承MP 的ServiceImpl<对应的Mapper,映射表的实体类>
// 因为是 Service的实现, 实现对应的接口 implement Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
MPTest.Java
/** Service 的CRUD **/
// 从Spring容器中获取Service对象;
@Autowired
UserService userService;
// 只获取匹配的第一条数据;
@Test
public void testGetOne(){
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery();
wrapper.gt(User::getAge, 28);
User one = userService.getOne(wrapper, false); // 第二参数指定为false,使得在查到了多行记录时,不抛出异常,而返回第一条记录
System.out.println(one);
}
就是正常的引用 Mapper
UserService.Java
// Service接口 继承MP的 IService<T> T泛型,要CRUD对应映射的实体;
public interface UserService extends IService<User> {
//自定义Service 实现:
List<User> selall();
}
UserServiceImpl.Java
// Spring注解,表示改类是一个 Service 业务逻辑类,并交给Spring容器管理;
@Service
// ServiceImpl 是 Service 的实现
// 继承MP 的ServiceImpl<对应的Mapper,映射表的实体类>
// 因为是 Service的实现, 实现对应的接口 implement Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
// 创建UserMapper 对象实例;
@Autowired
UserMapper userMapper;
@Override
public List<User> selall() {
// 直接调用Mapper 的查询全部~ 当然Service业务逻辑层,可以写很多更加复杂的操作...
return userMapper.selectList(null);
}
}
MP提供了一个代码生成器,可以让我们一键生成实体类,Mapper接口,Service,Controller等全套代码
实体 mapper service controller
啥啥都不需要自己写了! 实在是太牛逼了!<!-- Mybatsi-Plus 代码生成器依赖: -->
<!--mybatisplus代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!--模板引擎-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
</dependency>
一般情况下,可以将这个文件放在 项目Util 包下,作为一个工具类使用:
代码生成的地址...
创建完成之后,将需要的东西拖到项目中也可以GeneratorTest.Java
public class GeneratorTest {
@Test
public void generate() {
AutoGenerator generator = new AutoGenerator();
// 全局配置
GlobalConfig config = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
// 设置输出到的目录: 可以更改为任何路径 D盘 C盘...
// config.setOutputDir("D:/MP");
config.setOutputDir(projectPath + "/src/main/java");
// 生成的作者名
config.setAuthor("wsm");
// 生成结束后是否打开文件夹
config.setOpen(false);
// 全局配置添加到 generator 上
generator.setGlobalConfig(config);
// 数据源配置: 配置自己的数据库要生成的数据库 用户/密码
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/mybatis_plus?characterEncoding=utf-8&serverTimezone=UTC");
dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
dataSourceConfig.setUsername("root");
dataSourceConfig.setPassword("ok");
// 数据源配置添加到 generator
generator.setDataSource(dataSourceConfig);
// 包配置, 生成的代码放在哪个包下
PackageConfig packageConfig = new PackageConfig();
packageConfig.setParent("com.wsm");
// 包配置添加到 generator
generator.setPackageInfo(packageConfig);
// 策略配置
StrategyConfig strategyConfig = new StrategyConfig();
// 下划线驼峰命名转换
strategyConfig.setNaming(NamingStrategy.underline_to_camel);
strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);
// 开启lombok,生成的实体类上面就会又 lombok注解;
strategyConfig.setEntityLombokModel(true);
// 开启RestController
strategyConfig.setRestControllerStyle(true);
generator.setStrategy(strategyConfig);
generator.setTemplateEngine(new FreemarkerTemplateEngine());
// 开始生成
generator.execute();
}
}
生成的路径:D:/MP
在实际项目中表不仅仅会有开发中需要的功能字段有时候还会需要很多的附属字段:
更新时间
创建时间
创建人
更新人
逻辑删除列
乐观锁Version
备用1
备用2
…
自动填充
来完成对这些数据的操作
@TableFieId注解
在对应字段上增加注解,@TableFieId
的 fill属性
来设置字段的自动填充; Orders为例子
fill
属性:是一个枚举FieldFill
DEFAULT
默认值无任何处理 INSERT
新增触发 UPDATE
修改时触发 INSERT_UPDATE
新增或修改时触发
MetaObjectHandler.Java
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
// 新增时候触发,并设置新增时候对应数据要赋的值
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", LocalDateTime.now(), metaObject);
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
// 修改时候触发,并设置修改时候对应数据要赋的值
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
}
/** MP 自动填充: **/
@Test
public void testinsertOrd(){
// 创建新增对象
Orders orders = new Orders();
orders.setPrice(1000);
orders.setRemark("无");
orders.setUserId(2);
// 执行新增
int insert = ordersMapper.insert(orders);
if(insert>0)
System.out.println("新增成功");
else
System.out.println("新增失败");
}
新增成功, 查看数据库结果集!
我们深处大数据时代,一般企业的数据都是不允许真实删除的,这样后面找都不好找
逻辑删除
,用来判断数据是否删除,一般只有两个值:0|1通常逻辑删除
只需要在 yaml
配置一下即可
注意:
3.3.0版本之前还需要在对应的字段上加上@TableLogic
注解
mybatis-plus:
global-config:
db-config:
logic-delete-field: delFlag # 全局逻辑删除的实体字段名, 3.3.0配置后可以不添加注解,之前的还需要添加注解 @Tablelogic)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
/** MP 逻辑删除 **/
@Test
public void delOrd(){
// 配置了逻辑删除之后,直接调用MP 的删除方法就是进行逻辑删除了! 注意: 自定义sql的操作还需要自己完成注意!
int i = ordersMapper.deleteById(1);
if(i>0)
System.out.println("逻辑删除成功");
else
System.out.println("逻辑删除失败");
}
🔒 在程序开发中大家应该都很了解吧,为了避免多线程情况下数据紊乱需要对数据进行加锁
典型的冲突
乐观锁:
“version”
字段来实现,当读取数据时,将version字段的值一同读出,`数据每更新一次,对此version值加一
Update set version=version+1 where version = version
每次更新前都要判断传入的 版本 是否是现在最新版本!
A B 同时要更新数据 1 都获取了version版本 1
A 先更新:Update set version=version+1 where version = 1
version就是1 所以更新成功!
B 在更新:Update set version=version+1 where version = 1
version已经被A +1 所以version 是2 where 2=1
不成立 B更新失败
悲观锁:
同一时间只能,允许一个人来修改这条数据!
总结:
MP 为了方便操作就对次进行了封装处理,更加方便的进行了操作;
@Configuration
public class MybatisPlusConfig {
/**
* 旧版
*/
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
/**
* 新版
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}
}
目前项目中直接运行会报错,因为 MybatisPlusInterceptor
会在很多地方使用到:MP分页
MP乐观锁
...
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
定义在对应的 MybatisPlusInterceptor
@Version
Orders.Java
省略其它未更改代码;
// 添加乐观锁的注解,使用MP 的方式实现乐观锁 保证数据安全;
// version字段,类型只支持int,long,Date,Timestamp,LocalDateTime
@Version
private Integer version;
注意:
在更新前我们一定要先查询到version设置到实体类上再进行更新才能生效
传入的对象一定要携带 Version列有值
updateById(id)
与update(entity, wrapper)
方法 wrapper
不能复用!会出现重复参数使用;
MPTest.Java
/** MP 乐观锁 */
// 操作前要去抱必须获取到数据最新的 version: 先查询在修改:
@Test
public void testupdVer(){
// 先查询:
Orders orders = ordersMapper.selectById(1);
// 设置更新列
orders.setPrice(123);
System.out.println(orders);
// 修改: 乐观锁插件仅支持`updateById(id)`与`update(entity, wrapper)`方法
int i = ordersMapper.updateById(orders);
if(i>0)
System.out.println("修改成功,version+1");
else
System.out.println("修改失败");
}
cmd 控制台输出:
查询
==> Preparing: SELECT id,price,remark,user_id,update_time,create_time,version,del_flag,create_by,update_by FROM orders WHERE id=? AND del_flag=0
==> Parameters: 1(Integer)
<== Columns: id, price, remark, user_id, update_time, create_time, version, del_flag, create_by, update_by
<== Row: 1, 2000, 无, 2, 2021-08-24 21:02:43, 2021-08-24 21:02:46, 1, 0, null, null
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@772caabe]
Orders(id=1, price=123, remark=无, userId=2, updateTime=2021-08-24T21:02:43, createTime=2021-08-24T21:02:46, version=1, delFlag=0, createBy=null, updateBy=null, userName=null)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@51da32e5] was not registered for synchronization because synchronization is not active
修改
JDBC Connection [HikariProxyConnection@2069678360 wrapping com.mysql.cj.jdbc.ConnectionImpl@4538856f] will not be managed by Spring
==> Preparing: UPDATE orders SET price=?, remark=?, user_id=?, update_time=?, create_time=?, version=?, update_by=? WHERE id=? AND version=? AND del_flag=0
==> Parameters: 123(Integer), 无(String), 2(Integer), 2022-01-07T00:54:39.578(LocalDateTime), 2021-08-24T21:02:46(LocalDateTime), 2(Integer), www(String), 1(Long), 1(Integer)
<== Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@51da32e5]
修改成功,version+1
🆗,MP 的学习就到这里了, 感谢点赞👍
本人对每个案例Demo 都进行了, 本地Git管理,并在 wlog.md
中有更详细的使用说明:
网盘链接:https://pan.baidu.com/s/1fwm9GO4vRvnJGFSEEj2Xuw 提取码:2540