如果你用过通用Mapper,那上手MyBatis Plus会很轻松
视频地址:www.bilibili.com/video/av532…
1 快速入门
1.1 快速开始
- 导入Maven依赖
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.2</version>
</dependency>
- 定义Mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> {
}
- 使用
@Test
void saveUser() {
User user = new User();
user.setId(5L);
...
userMapper.insert(user);
}
@Test
void getById() {
User user = userMapper.selectById(5L);
}
@Test
void getByIds() {
List<User> users = userMapper.selectBatchIds(List.of(1L, 2L));
}
@Test
void updateById() {
User user = new User();
user.setId(5L);
user.setBalance(600);
userMapper.updateById(user);
}
@Test
void deleteById() {
userMapper.deleteById(5L);
}
如果在启动类上加了@MapperScan,Mapper接口上就不用加@Mapper
不加@MapperScan,就需要加@Mapper
1.2 常见注解
MyBatisPlus通过扫描实体类,并基于反射获取实体类信息作为数据库表信息
在没有使用注解的情况下,会默认使用如下规则映射到数据表上:
- 类名驼峰转下划线作为表名
- 名为id的字段作为主键
- 变量名驼峰转下划线作为表的字段名
- 根据变量类型推断数据库字段类型
@TableName
表名注解,标识实体类对应的表
@TableId
实体类主键字段
@TableField
字段注解(非主键)
1.3 常见配置
MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。例如
mybatis-plus:
type-aliases-package: com,itheima,mp,domain.po # 别名描包
mapper-locations: "classpatht:/mapper/**/*,xml" # Mopper.xml文件地址,默认值
global-config:
db-config:
id-type: auto # id为自增长
update-strategy: not_null # 更新策略: 只更新非空字段
configuration:
map-underscore-to-camel-case: true # 是否开下划线和驼的映射
cache-enabled: false # 是否开启二级缓存
2 核心功能
2.1 条件构造器
MyBatisPlus支持各种复杂的where条件,可以满足日常开发的所有需求
按条件查询:
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.select("id", "username", "info", "balance")
.like("username", "o")
.ge("balance", 1000);
List<User> users = userMapper.selectList(wrapper);
按条件更新:
User user = new User();
user.setBalance(2000);
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.eq("username", "jack"));
List<User> users = userMapper.update(user, wrapper);
特殊Set更新:
UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
.setSql("balance = balance - 200")
.in("id", List.of(1L, 2L, 4L));
userMapper.update(null, wrapper);
使用Lambda作为字段名:
Querywrapper<User> wrapper = new QueryWrapper<>();
wrapper.Lambda()
.select(User::getId, User::getusername, User::getInfo, User::getBalance)
.like(User::getusername, "o")
.ge(User::getBalance, 1000);
List<User> users = userMapper.selectList(wrapper);
2.2 自定义SQL
可以使用Wrapper构建复杂查询,并作为参数传递给XML中作为SQL的条件使用,然后自定义剩下的SQL。
- 基于Wrapper构建Where语句
// 1.构建查询条件 where name like "%o%”AND balance >= 1000
QueryWrapper<User> wrapper = new QueryWrapper<User>()
.Like("username" ,"o")
.ge("balance",1000);
// 2.查询数据
List<User> users = userMapper.findByCustom(wrapper);
- 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
List<User> findByCustom(@Param("ew") QueryWrapper<User> wrapper);
- 自定义SQL,并使用Wrapper条件
<select id="findByCustom" resultType="user">
SELECT * FROM user S{ew.customSqlSegment}
</select>
如果查询条件对应的表在SQL中有别名,可以通过
${ew.customSqlSegment("a")}添加别名
案例:将id在指定范围的用户(例如1、2、4)的余额扣减指定值,不允许在service层出现SQL。
传统SQL写法:
<select id="updateBalanceByIds" resultType="com.itheima.mp.domain.po.User">
UPDATE user
SET balance = balance - #{amount}
WHERE id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#{id}
</foreach>
</select>
Wrapper写法:
List<Long> ids = List.of(1L, 2L, 4L);
// 1.定义条件
Querywrapper<User>wrapper = new Querywrapper<User>()
.in("id", ids);
// 2.执行更新
userMapper.updateBalanceByWrapper(1000, wrapper);
public interface UserMapper extends BaseMapper<User> {
@Update("UPDATE user SET balance = balance - #(amount) ${ew.customSqlSegment}")
void updateBalanceByWrapper(@Param("amount") int amount, @Paren("ew") Querywrapper<User> wrapper);
案例:查询出所有收货地址在北京的并且用户id在指定范围的用户(例如1、2、4)的用户
传统SQL写法:
<select id="queryUserByIdAndAddr" resultType="com,itheima,mp,domain.po,User">
SELECT *
FROM user u
INNER JOIN address a ON u.id = a.user_id
WHERE u.id
<foreach collection="ids" separator="," item="id" open="IN (" close=")">
#[id}
</foreach>
AND a.city = #{city}
</select>
Wrapper写法:
List<Long> ids = List.of(1L, 2L, 4L);
Strng city = "北京";
// 1.定义条件
Querywrapper<User>wrapper = new Querywrapper<User>()
.in("u.id", ids)
.eq("a.city", city);
// 2.执行更新
userMapper.updateBalanceByWrapper(1000, wrapper);
<select id="queryUsersByWrapper" resultType="com.itheima.mp.domain.po.User">
SELECT U.*
FROM user u
INNER JOIN address a on u.id = a.user_id
${ew.customSqlSegment}
</select>
2.3 IService
MyBatisPlus提供了一个IService接口,包含了常用CRUD、批量CRUD、分页操作,并提供了一个默认实现类。
为了满足编译检查,并能增加自定义方法的同时不用实现这些默认方法:
-
自定义Service接口继承自IService
public UserService extends IService<User> {} -
自定义Service实现先继承ServiceImpl,再实现自定义Service接口:
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { }
批量新增
userService.saveBatch(list);
在MySQL中,必须在JDBC连接字符串中添加&rewriteBatchedStatements=true,MySQL内部才会自动将批量提交的sql重写为insert多值插入再执行。(3.1.13以后的mysql连接驱动都支持该配置)
当然也可以使用BaseMapper中的InsertBatchSomeColumn批量插入(不走executeBatch批量提交sql,所以不受上述参数影响)。
Lambda查询
需求:基于IService中的lambdaQuery()实现一个方法,满足下列需求:
-
查询名字为Rose的用户
User user = userService.lambdaQuery() .eq(User::getUsername, "Rose") .one(); -
查询名字中包含o的用户
List<User> list = userService.lambdaQuery() .like(User::getUsername, "o") .list() -
统计名字中包含o的用户的数量
Long count = userService.lambdaQuery() .like(User::getUsername, "o") .count(); -
定义一个方法,接收参数为username、status、minBalance、maxBalance,参数可以为空
- 如果username参数不为空,则采用模糊查询
- 如果status参数不为空,则采用精确匹配
- 如果minBalance参数不为空,则余额必须大于minBalance
- 如果maxBalance参数不为空,则余额必须小于maxBalance
@Test void testQueryUsers() { List<User> list = queryUsers("o", 1, null, null); list.forEach(System.out::println); } public List<User> queryUsers(String username, Integer status, Long min, Long max){ return userService.lambdaQuery() .like(username != null, ser::getUsername, username) .eq(status != null, User::getstatus, status) .gt(min != null, User::getBalance, min) .lt(max != null, User::getBalance, max) .list(); }
Lambda更新
需求:基于IService中的lambdaUpdate()方法实现一个更新方法,满足下列需求:
- 参数为balance、id、username
- id或username至少一个不为空,根据id或username精确匹配用户
- 将匹配到的用户余额修改为balance
- 如果balance为0,则将用户status修改为冻结状态(2)
public void updateBalance(Long balance, Long id, String username) {
// update user set balance = ? status = 2 WHERE id = ? AND username = ?
userService.lambdaUpdate()
.set(User::getBalance, balance)
.set(balance == 0, User::getStatus, 2)
.eq(id != null, User::getId, id)
.eq( username != null, User::getUsername, username)
.update();
}
2.4 静态工具
MyBatisPlus提供了Db工具类,提供了与IService中相同的所有静态方法,区别在于需要额外传入一个实体类Class。可以避免出现Service循环依赖注入的情况。
List<User> user = Db.lambdaQuery(User.class)
.like(username != null, ser::getUsername, username)
.eq(status != null, User::getstatus, status)
.gt(min != null, User::getBalance, min)
.lt(max != null, User::getBalance, max)
.list();
3 拓展功能
3.1 代码生成
安装Idea插件MyBatisPlus
Idea菜单会多出一个Other菜单
- Config Database:配置数据库连接
- Code Generator:配置生成类
3.2 逻辑删除
MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。
在application.yml中全局配置
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
Logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
3.3 枚举处理器
实体类字段类型可以设置为枚举类
public class User {
...
private UserStatus status;
}
枚举类中,要在需要写入数据库的值字段上添加@EnumValue注解
@Getter
public enum UserStatus {
NORMAL(1, "正常"),FREEZE(2, "陈结");
@EnumValue
private final int value;
private final String desc;
UserStatus(int value, String desc) {
this.value = value;
this.desc = desc;
}
}
在application.yml配置中加上MybatisEnumTypeHandler
注意:从 3.5.2 开始无需配置
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
将枚举值传给前端的方法(转换枚举类成字符串还是枚举值):baomidou.com/pages/8390a…
3.4 JSON处理器
实体类字段类型可以设置为另一个,需要:
- 在字段上增加
@TableField(typeHandler = JacksonTypeHandler.class)注解指定Json处理器。 - 在实体类上,将
@TableName的属性autoResultMap指定为true(否则无法映射其他类的查询结果)
@TableName(value = "user", autoResultMap = true)
public class User {
...
@TableField(typeHandler = JacksonTypeHandler.class)
private UserInfo info;
}
3.5 配置加密
MyBatisPlus从3.3.2版本开始提供了一个基于AES算法的加密工具,帮助我们对配置中的敏感信息做加密处理。
@Test
void contextLoads() {
// 1.生成16位随机 AES 秘钥
String randomKey = AES.generateRandomKey();
System.out.printn("randomKey = " + randomKey);
// 2.利用密钥对用户名加密
String username = AES.encrypt("root", randomKey);
System.out.println("username = " + username);
// 3.利用密钥对密码加密
String password = AES,encrypt("MySQL123",randomKey);
System.out.println("password = " + password);
}
在application.yaml文件中使用刚刚生成的秘钥,代替明文的用户名和密码
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mp
driver-class-name: com.mysql.cj.jdbc.Driver
username: mpw:QWWVnk10al3258x5rVhaeQ==
password: mpw:EUFmeH3cNAzdRGdOQcabWg==
在项目启动的时候,添加AES的秘钥,这样MyBatisPlus就可以解密数据了
// Jar包启动参数,arguments
--mpw.key=d1104d7c3b616fob
单元测试时,可以通过@SpringBootTest来指定秘钥
@SpringBootTest(args = "--mpw.key=6234633a66fb399f")
class UserServiceTest {
@Autowired
UserService userService;
}
Idea中可以通过启动配置中的Program arguments来配置
4 插件功能
4.1 分页查询
MyBatisPlus基于MyBatis的Interceptor实现了一个基础拦截器,并在内部保存了MyBatisPlus的内置拦截器的集合
| 序号 | 拦截器 | 描述 |
|---|---|---|
| 1 | TenantLineInnerInterceptor | 多租户插件 |
| 2 | DynamicTableNameInnerInterceptor | 动态表名插件 |
| 3 | PaginationInnerInterceptor | 分页插件 |
| 4 | OptimisticLockerInnerInterceptor | 乐观锁插件 |
| 5 | IllegalSQLInnerInterceptor | SQL性能规范插件,检测并拦截垃圾SQL |
| 6 | BlockAttackInnerInterceptor | 防止全表更新和删除的插件 |
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件
@Configuration
public class MybatisConfig [
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
// 1初始化核心插件
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 2,添加分页插件
PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
pageInterceptor.setMaxLimit(1000L); // 设置分页上限
interceptor.addInnerInterceptor(pageInterceptor);
return interceptor;
}
}
-
setMaxLimit:设置分页上限
-
setOverflow:如果查到最后一页,则自动回到第一页
接着,就可以使用分页的API了
@Test
void testPageQuery() {
// 1.查询
int pageNo = 1, pageSize = 5;
// 1.1.分页参数
Page<User> page = Page.of(pageNo, pageSize);
// 1.2.排序参数,通过OrderItem来指定
page.addOrder(new OrderItem("balance", false));
// 1.3.分页查询
Page<User> p = userService.page(page);
// 2.总条数
System.out.println("total = " + p.getTotal());
// 3.总页数
System.out.println("pages = " + .getPages());
// 4.分页数据
List<User> records = p.getRecords();
records.forEach(System.out::println);
}
查询结果的Page和查询参数的Page是同一个对象