MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。mybatis-plus官方文档
Mybatis-Plus原理:扫描实体类Entity,通过反射机制分析出需要操作的表名、字段名(实体类类名 >> 表名,实体类属性名 >> 字段名),生成相应的CRUD及其他常用的sql语句,注入到mybatis容器中。
MyBatis-Plus中的基本CRUD在内置的BaseMapper中都已得到了实现,我们可以直接使用,接口如下:
public interface BaseMapper<T> extends Mapper<T> {
// 插入一条记录
int insert(T entity);
// 根据主键删除
int deleteById(Serializable id);
// 根据自定义条件删除
int deleteByMap(@Param("cm") Map<String, Object> columnMap);
...
}
需要使用时只需要继承该接口,即可获得BaseMapper所有封装好的CRUD等方法。注意BaseMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
MyBatis-Plus中有一个接口 IService和其实现类 ServiceImpl,封装了常见的业务层逻辑。封装方法介绍
通用 IService接口,进一步封装 CRUD 采用 get 查询单行、remove 删除、list 查询集合、page 分页,前缀命名方式区分 Mapper 层避免混淆。
使用时流程如下:注意实现类继承的ServiceImpl
// UserService继承IService模板提供的基础功能
public interface UserService extends IService<User> {
}
// 实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}
@TableName可以用来设置当前实体类映射的表名。详细介绍
当数据库所有表有统一的前缀 t_ 时,可以通过在配置文件中设置全局的表名前缀进行处理。
mybatis-plus:
configuration:
# 开启sql日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 设置实体类映射数据库表,表名的统一前缀
table-prefix: t_
Mybatis-Plus默认会将id作为主键,当映射表不存在id字段或者实体类中不存在id属性时,会报错。
@TableId会将当前属性对应的字段作为主键。详细介绍
@TableId中有两个属性:value、type。
属性 | 类型 | 必须指定 | 默认值 | 描述 |
---|---|---|---|---|
value | String | 否 | " " | 主键字段名 |
type | Enum | 否 | IdType.NONE (无状态) | 指定主键类型 |
当实体类主键属性名(id)和数据库表所映射的字段名(u_id)不一致时,可以通过value属性设置。
@Data
@TableName("t_user")
public class User {
// 指定映射t_user表中的u_id主键字段
@TableId("u_id")
private Integer id;
private String name;
}
默认生成主键的策略:IdType.NONE,未设置类型。修改生成策略用type属性,注意确保映射数据库表主键字段也要设置为自增。
@Data
@TableName("t_user")
public class User {
// 指定映射t_user表中的u_id主键字段,设置生成的id策略为自增。
@TableId(value = "u_id",type = IdType.AUTO )
private Integer id;
private String name;
}
全局统一配置主键生成策略
mybatis-plus:
global-config:
db-config:
# 设置实体类映射数据库表,表名的统一前缀
table-prefix: t_
// 设置全局主键生成策略为:自增
id-type: auto
@TableField可以指定(非主键)普通属性对应的字段名。详细介绍
注意:@TableField中的exist属性可以指定是否为数据库表字段。
@TableLogic表示逻辑删除,常用于数据恢复。需要指定映射表中存在 is_deleted字段,类型Int,默认值为0(0表示未删除,1表示已经逻辑删除)。当加上该注解后,所有的删除操作就变成了修改操作,且所有的查询操作都会加上逻辑是否删除的判断。
@TableLogic(value= " 原值 ",delval= " 改值 " )
@Data
@TableName("t_user")
public class User {
// 指定映射t_user表中的u_id主键字段,设置生成的id策略为自增。
@TableId(value = "u_id",type = IdType.AUTO )
private Integer id;
private String name;
// 表示逻辑删除字段
@TableLogic(value="0",delval="1")
private Integer isDeleted;
}
AbstractWrapper的方法详解
查询姓李、年龄在20到30之间、邮箱不为空的用户信息。注意:条件构造器方法中的column指的是数据库表的字段名,且可以使用链式结构,默认条件之间用 and 连接。
@Autowired
private UserMapper userMapper;
@Test
void test01() {
// 查询姓李、年龄在20到30之间、邮箱不为空的用户信息。
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.likeRight("u_name","李")
.between("age",20,30)
.isNotNull("email");
List<User> userList = userMapper.selectList(queryWrapper);
for (User user : userList) {
System.out.println(user);
}
}
查询用户信息,按照年龄的降序排序,若年龄相同,则按照id升序排序。
@Autowired
private UserMapper userMapper;
@Test
void test02(){
// 查询用户信息,按照年龄的降序排序,若年龄相同,则按照id升序排序。
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("age")
.orderByAsc("id");
for (User user : userMapper.selectList(queryWrapper)) {
System.out.println(user);
}
}
删除邮箱为空的用户信息。
@Autowired
private UserMapper userMapper;
@Test
void test03(){
// 删除邮箱为空的用户信息。
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.isNotNull("email");
int delete = userMapper.delete(queryWrapper);
System.out.println(delete);
}
将(年龄大于20并且名字中包含“小”字的)或者邮箱为null的用户信息修改。
@Autowired
private UserMapper userMapper;
@Test
void test04(){
// 将年龄大于20并且名字中包含“小”字的或者邮箱为null的用户信息修改。
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.gt("age",20)
.like("u_name","小")
// 默认and连接,这里需要改为or
.or()
.isNotNull("email");
User user = new User();
user.setName("李白");
user.setEmail("[email protected]");
// 第一个参数用来标明需要修改的内容,第二个参数用来标明参与修改的条件。
int update = userMapper.update(user, queryWrapper);
System.out.println(update);
}
将(年龄大于20或者邮箱为空)并且名字中含有“小”的用户信息修改。注意优先级问题,可以上一题类比。
@Autowired
private UserMapper userMapper;
@Test
void test05(){
// 将(年龄大于20或者邮箱为空)并且名字中含有“小”的用户信息修改。
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("u_name","小")
// lambda中的条件会优先执行。
.and(i->i.gt("age",20).or().isNotNull("email"));
User user = new User();
user.setName("李白");
user.setEmail("[email protected]");
int update = userMapper.update(user, queryWrapper);
System.out.println(update);
}
自定义需要查询的字段,并封装Map。
@Autowired
private UserMapper userMapper;
@Test
void test06(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 只需要查询u_name、age、email字段
queryWrapper.select("u_name", "age", "email");
// 通过selectMaps可以封装查询的条件。
List<Map<String, Object>> list = userMapper.selectMaps(queryWrapper);
for (Map<String, Object> selectMap : list) {
System.out.println(selectMap);
}
}
子查询语句。完整sql:select * from t_user where uid in (select uid from t_user where uid <= 100)
@Test
void test07(){
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 子查询语句
queryWrapper.inSql("uid","select uid from t_user where uid <= 100");
List<User> userList = userMapper.selectList(queryWrapper);
for (User user : userList) {
System.out.println(user);
}
}
condition条件用例:
可以避免使用 if 语句判断,简单的 if 条件可以直接写在 queryWrapper 属性中。
查出名字带有“a”,年龄在ageBegin和ageEnd区间的用户信息。
@Test
void test09(){
String username = "a";
Integer ageBegin = null;
Integer ageEnd = 30;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like(StringUtils.isNotBlank(username),"u_name","a")
.ge(ageBegin != null,"age",ageBegin)
.le(ageEnd != null,"age",ageEnd);
List<User> userList = userMapper.selectList(queryWrapper);
for (User user : userList) {
System.out.println(user);
}
}
将(年龄大于20或者邮箱为空)并且名字中含有“小”的用户信息修改,注意与queryWrapper类比。
@Test
void test08(){
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.like("u_name","小")
.and(i->i.gt("age",20).isNotNull("email"));
UpdateWrapper<User> set = updateWrapper.set("u_name", "李白")
.set("email", "[email protected]");
System.out.println(set);
}
只更新一个属性,把名字为小军的用户年龄更新为18,其他的属性保持不变。
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("name","小军").set("age",18);
userMapper.update(null,updateWrapper);
LambdaQueryWrapper和QueryWrapper的区别在与:
首先在结构关系上,LambdaQueryWrapper和LambdaUpdateWrapper继承自AbstractLambdaWrapper,而QueryWrapper和UpdateWrapper继承自AbstractWrapper。
其次在调用方法传参中,QueryWrapper和UpdateWrapper需要传入的是数据表中的字段名(column),是string类型。而LambdaQueryWrapper和LambdaUpdateWrapper则通过Lambda表达式的形式指定实体类中的属性名,是一个函数式的接口。这样设计的初衷在于使用Lambda表达式可以避免字段名写错或者对应不上数据库表的情况。
查出名字带有“a”,年龄在ageBegin和ageEnd区间的用户信息,使用LambdaQueryWrapper。
@Test
// 查出名字带有“a”,年龄在ageBegin和ageEnd区间的用户信息,使用LambdaQueryWrapper。
void test10(){
String username = "a";
Integer ageBegin = null;
Integer ageEnd = 30;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isBlank(username),User::getName,username)
.ge(ageBegin != null,User::getAge,ageBegin)
.le(ageEnd != null,User::getAge,ageEnd);
for (User user : userMapper.selectList(queryWrapper)) {
System.out.println(user);
}
}
将(年龄大于20或者邮箱为空)并且名字中含有“小”的用户信息修改,使用LambdaUpdateWrapper。
@Test
void test11(){
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.like(User::getName,"小")
.and(i->i.gt(User::getAge,20).isNotNull(User::getEmail));
LambdaUpdateWrapper<User> set = updateWrapper.set(User::getName, "李白")
.set(User::getEmail, "[email protected]");
System.out.println(set);
}
@Configuration
@MapperScan("scan.your.mapper.package")
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
@Test
public void testPage(){
//设置分页参数
Page<User> page = new Page<>(1, 5);
userMapper.selectPage(page, null);
//获取分页数据
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:"+page.getCurrent());
System.out.println("每页显示的条数:"+page.getSize());
System.out.println("总记录数:"+page.getTotal());
System.out.println("总页数:"+page.getPages());
System.out.println("是否有上一页:"+page.hasPrevious());
System.out.println("是否有下一页:"+page.hasNext());
}
测试结果:
User(id=1, name=Jone, age=18, email=test1@baomidou.com, isDeleted=0) User(id=2,
name=Jack, age=20, email=test2@baomidou.com, isDeleted=0) User(id=3, name=Tom,
age=28, email=test3@baomidou.com, isDeleted=0) User(id=4, name=Sandy, age=21,
email=test4@baomidou.com, isDeleted=0) User(id=5, name=Billie, age=24, email=test5@ba
omidou.com, isDeleted=0)
当前页:1
每页显示的条数:5
总记录数:17
总页数:4
是否有上一页:false
是否有下一页:true
UserMapper中定义接口方法
/**
* 根据年龄查询用户列表,分页显示
* @param page 分页对象,xml中可以从里面进行取值,传递参数 Page 即自动分页,必须放在第一位
* @param age 年龄 * @return
**/
Page<User> selectPageVo(@Param("page") Page<User> page, @Param("age") Integer age);
UserMapper.xml中编写SQL
<!--SQL片段,记录基础字段-->
<sql id="BaseColumns">id,username,age,email</sql>
<!--IPage<User> selectPageVo(Page<User> page, Integer age);-->
<select id="selectPageVo" resultType="User">
SELECT <include refid="BaseColumns"></include> FROM t_user WHERE age > # {age}
</select>
测试
@Test
public void testSelectPageVo(){
//设置分页参数
Page<User> page = new Page<>(1, 5);
userMapper.selectPageVo(page, 20);
//获取分页数据
List<User> list = page.getRecords();
list.forEach(System.out::println);
System.out.println("当前页:"+page.getCurrent());
System.out.println("每页显示的条数:"+page.getSize());
System.out.println("总记录数:"+page.getTotal());
System.out.println("总页数:"+page.getPages());
System.out.println("是否有上一页:"+page.hasPrevious());
System.out.println("是否有下一页:"+page.hasNext());
}
结果
User(id=3, name=Tom, age=28, email=test3@baomidou.com, isDeleted=null) User(id=4,
name=Sandy, age=21, email=test4@baomidou.com, isDeleted=null) User(id=5, name=Billie,
age=24, email=test5@baomidou.com, isDeleted=null) User(id=8, name=ybc1, age=21,
email=null, isDeleted=null) User(id=9, name=ybc2, age=22, email=null, isDeleted=null)
当前页:1
每页显示的条数:5
总记录数:12
总页数:3
是否有上一页:false
是否有下一页:true
一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。
此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王也在操作,取出商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就完全被小王的覆盖了。
现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1万多。
上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过了,则重新取出的被修改后的价格,150元,这样他会将120元存入数据库。
如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元。
乐观锁实现流程主要是通过在数据库中添加version字段,每次取出记录时都会获取当前的version,更新时version+1,如果where语句中的版本号不对,则更新失败。
1、首先在实体类上添加注解。
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;
}
2、在配置类中添加乐观锁配置
@Configuration
@MapperScan("scan.your.mapper.package")
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
表中的有些字段值是固定的,例如性别(男或女),此时我们可以使用MyBatis-Plus的通用枚举来实现。
1、@EnumValue 标识存储到数据库中的属性值。
package com.atguigu.mp.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;
}
}
2、配置扫描通用枚举。
mybatis-plus:
configuration:
# 配置MyBatis日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 配置MyBatis-Plus操作表的默认前缀
table-prefix: t_
# 配置MyBatis-Plus的主键策略
id-type: auto
# 配置扫描通用枚举
type-enums-package: com.hupei.mybatisplus.enums
3、测试。
@Test
public void testSexEnum(){
User user = new User();
user.setName("Enum");
user.setAge(20);
//设置性别信息为枚举项,会将@EnumValue注解所标识的属性值存储到数据库
user.setSex(SexEnum.MALE);
//INSERT INTO t_user ( username, age, sex ) VALUES ( ?, ?, ? )
//Parameters: Enum(String), 20(Integer), 1(Integer)
userMapper.insert(user);
}