因个人工作原因,需要用到MybatisPlus(之前是用的Mybatis),所以开始学习MP(MybatisPlus)。这篇文章大部分是通过学习哔哩哔哩中的视频“狂神说Java_MyBatisPlus教程”,也添加了一些我个人工作中比较常用的功能,哔哩哔哩的原视频连接是https://www.bilibili.com/video/BV17E411N7KN?p=1
本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!
我个人的github链接(仅供参考): https://github.com/zhengtianliang/mybatisplus-kuang
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
package com.zheng.pojo;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author: ZhengTianLiang
* @date: 2020/12/2 20:42
* @desc:
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product extends Model<Product> {
private String id;
private String name;
private Integer age;
private String email;
}
package com.zheng.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.zheng.pojo.Product;
/**
* @author: ZhengTianLiang
* @date: 2020/12/2 20:50
* @desc:
*/
public interface ProductService extends IService<Product> {
}
serviceImpl继承ServiceImpl
package com.zheng.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zheng.mapper.ProductMapper;
import com.zheng.pojo.Product;
import com.zheng.service.ProductService;
import org.springframework.stereotype.Service;
/**
* @author: ZhengTianLiang
* @date: 2020/12/2 20:56
* @desc:
*/
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}
package com.zheng.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.zheng.pojo.Product;
/**
* @author: ZhengTianLiang
* @date: 2020/12/2 20:43
* @desc:
*/
public interface ProductMapper extends BaseMapper<Product> {
}
<?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 namespace="com.hollycrm.hollycloud.ics.knowledge.admin.mapper.ForbidWordsMapper">
<!-- 通用查询映射结果 -->
<resultMap id="forbidResultMap" type="com.hollycrm.hollycloud.ics.knowledge.admin.vo.ForbidWordsVO">
<id column="id" property="id"/>
<result column="forbidWords" property="ForbidWords"/>
<result column="en_code" property="enCode"/>
<result column="en_full_name" property="enFullName"/>
<result column="spell_code" property="spellCode"/>
</resultMap>
<select id="queryForbidListByCondition" resultMap="forbidResultMap">
SELECT
id,
forbid_words,
en_code,
en_full_name,
spell_code
FROM
tb_forbid_words
<where>
is_invalid = '1'
<if test="forbidWords != null and forbidWords != ''">
AND forbid_words LIKE CONCAT('%',#{forbidWords},'%')
</if>
</where>
ORDER BY create_time DESC
</select>
</mapper>
因为我公司有自己的代码生成模块,所以一直以来没用MP(MybatisPlus)自带的代码生成器。MP的主要功能有:代码生成器、条件构造器、分页插件、自定义ID、字段填充、逻辑删除、慢sql等,我着重介绍一些我比较常使用的功能。更多请参考官方文档:https://baomidou.com/guide/
自动填充是指,“创建时间”、“修改时间”这些字段的自动填充(是填充至实体的,而非数据库设置默认值)。
其中@TableField(fill = FieldFill.INSERT) 代表着这个属性在新增的时候作某种操作(由规则中配置的什么操作,就做什么操作); @TableField(fill = FieldFill.INSERT_UPDATE)代表新增、更新的时候,做某种操作,由配置的规则决定。
package com.zheng.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* @author: ZhengTianLiang
* @date: 2020/11/14 22:59
* @desc: 用户表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@TableId(value = "id",type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime modifyTime;
}
是配置具体的当作新增、更新动作的时候做的事情
package com.zheng.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Date;
/**
* @author: ZhengTianLiang
* @date: 2020/11/17 21:58
* @desc: 规定一下update的规则
*/
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
// 插入填充时的策略
@Override
public void insertFill(MetaObject metaObject) {
log.info("start insertFill ");
this.setFieldValByName("createTime", LocalDateTime.now(),metaObject);
this.setFieldValByName("modifyTime", LocalDateTime.now(),metaObject);
}
// 更新填充时的策略
@Override
public void updateFill(MetaObject metaObject) {
log.info("start updateFill ");
this.setFieldValByName("modifyTime",LocalDateTime.now(),metaObject);
}
}
正常来讲,数据库设计的时候,除了业务字段以外,一般都会有一些公共的字段:主键ID、创建时间、修改时间、状态、版本号。版本号这个字段主要是用于并发控制的:
具体做法一直是为每张表设置一个数据库版本字段(Version),每次更新操作都要比较这个Version,如果版本号不匹配则说明数据已被他人修改,提示用户重新加载数据。
实现步骤:
alter table tb_user add reversion int(11) default 0 comment '乐观锁' after name;
package com.zheng.pojo;
import com.baomidou.mybatisplus.annotation.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* @author: ZhengTianLiang
* @date: 2020/11/14 22:59
* @desc: 用户表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@TableId(value = "id",type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
// 乐观锁
@Version
private Integer reversion;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime modifyTime;
}
springboot的话,返回一个OptimisticLockerInterceptor 的bean对象
package com.zheng.config;
import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @author: ZhengTianLiang
* @date: 2020/11/17 22:17
* @desc: 这是测试乐观锁的时候用的
*/
@Component
@EnableTransactionManagement // 这个是默认开启的,可以不写
@MapperScan(value = "com.zheng.mapper") // 这个本来是写在启动类上的,我给挪到这个config里面了
// 注意,要想使用mybatisplus,这个mapperScan必不可少
public class MybatisPlusConfig {
// 返回一个乐观锁组件
@Bean
public OptimisticLockerInterceptor getOptimisticLockerInnerInterceptor(){
return new OptimisticLockerInterceptor();
}
}
注意,是先userMapper.selectById(),必须先定位到数据,
// 测试乐观锁
@Test
public void test03(){
User user = userMapper.selectById(7L);
// SELECT id,name,age,email,reversion,create_time,modify_time FROM user WHERE id=?
user.setName("lsdjd");
//UPDATE user SET name=?, age=?, reversion=?, modify_time=? WHERE id=? AND reversion=?
userMapper.updateById(user);
}
日常开发中,除非真的是一些脏数据,否则一般很少物理删除,几乎都是逻辑删除。
物理删除:是说我直接将这条数据从数据库里删除了
逻辑删除:是说我将这条数据设置成失效的状态,我查询的时候只查状态有效的数据
alter table tb_user add status int default 1 comment '状态,0无效,1有效' after reversion;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@TableId(value = "id",type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
private String email;
// 乐观锁
@Version
private Integer reversion;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime modifyTime;
// 状态,0无效,1有效
@TableLogic
private Integer status;
}
默认1是已删除,0是未删除。mybatis-plus.global-config.db-config.logic-delete-value属性。
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_kuang?serverTimezone=UTC&useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
#by zhengkai.blog.csdn.net
#mybatis-plus配置控制台打印完整带参数SQL语句
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-value: 0 # 已删除的,0(默认为1)
logic-not-delete-value: 1 # 1是有效的 未删除的(默认为0)
@Bean
public ISqlInjector sqlInjector(){
return new LogicSqlInjector();
}
boolean a = userService.removeById("10");
// 测试的sql语句
UPDATE user SET status=1 WHERE name = ? AND status=0
// 调用查询的时候,也会自动的加上delete = 0
SELECT id,name,age,email,reversion,create_time,modify_time,status FROM user WHERE status=1 LIMIT ?
//Spring boot方式
@Configuration
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
// 测试分页
@Test
public void test04(){
Page page = new Page<>(1, 5);
userMapper.selectPage(page,null);
page.getRecords().forEach(System.out::println);
System.out.println("总条数为:"+page.getTotal());
}
慢sql的作用就是监控sql的执行,可以指定一个超时时间,假设是5毫秒,那么全部的sql,只要执行超过5毫秒,就会报错,然后定位sql。通过执行计划(explain)看看原因。执行计划中有两点比较重要,一个是有没有走索引,走索引比全表扫描好;另一个是查询所涉及的行数,这个是越小越好。通过执行计划来定位问题,帮助我们优化sql,减少响应时间,提高用户体验度。
package com.zheng.config;
import com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @author: ZhengTianLiang
* @date: 2020/11/17 22:17
* @desc: 这是测试慢sql的时候用的
*/
@Component
@EnableTransactionManagement // 这个是默认开启的,可以不写
@MapperScan(value = "com.zheng.mapper") // 这个本来是写在启动类上的,我给挪到这个config里面了
// 注意,要想使用mybatisplus,这个mapperScan必不可少
public class MybatisPlusConfig {
/**
* 配置慢sql用的 bean对象
*/
@Bean
@Profile({"dev", "test"}) // 设置 是dev、test环境下才会开启,保证线上环境的效率
public PerformanceInterceptor getPerformanceIntercepter() {
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
performanceInterceptor.setMaxTime(100); // 设置sql执行的最大时间,超过了这个时间就会报错,单位是毫秒
performanceInterceptor.setFormat(true); // 设置sql的格式化
return performanceInterceptor;
}
}
@Test
public void test01(){
List users = userMapper.selectList(null);
users.forEach(System.out::println);
}
Time:26 ms - ID:com.zheng.mapper.UserMapper.selectList
Execute SQL:
SELECT
id,
name,
age,
email,
reversion,
create_time,
modify_time,
status
FROM
user
MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。
package com.zheng;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;
/**
* @author: ZhengTianLiang
* @date: 2020/11/23 21:35
* @desc: 测试MybatisPlis的代码生成器 测试类
*/
@SpringBootTest
public class GenerateCode {
public static void main(String[] args) {
// 需要构建一个代码生成器对象
AutoGenerator autoGenerator = new AutoGenerator();
// 配置策略
// 1、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
System.out.println("项目的路径是:"+projectPath);
gc.setOutputDir(projectPath+"src/main/java"); // 生成的代码的存放路径
gc.setAuthor("ZhengTianLiang_mybatis_plus_auto"); // 设置作者
gc.setOpen(false); // 是否自动打开文件夹
gc.setFileOverride(false); // 是否覆盖
gc.setServiceName("%sService"); // 去掉Service的I前缀
gc.setIdType(IdType.ID_WORKER); // 设置主键策略
gc.setDateType(DateType.ONLY_DATE); // 设置日期类型,(仅仅只是时间)
gc.setSwagger2(true); // 是否开启swagger的注解
autoGenerator.setGlobalConfig(gc); // 将全局配置set进代码生成器对象中去
// 2、设置数据源
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/mybatis_kuang?serverTimezone=UTC&useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8");
dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
dataSourceConfig.setUsername("root");
dataSourceConfig.setPassword("123456");
dataSourceConfig.setDbType(DbType.MYSQL); // 设置数据源 mysql数据库
autoGenerator.setDataSource(dataSourceConfig); // 将数据源set进代码生成器对象中去
// 3、包的位置
PackageConfig packageConfig = new PackageConfig();
packageConfig.setModuleName("blog"); // 生成的代码都在这个包下面
packageConfig.setParent("com.zheng"); // 设置包名
packageConfig.setEntity("entity"); // 设置实体类的包的名
packageConfig.setMapper("mapper"); // 设置mapper的名
packageConfig.setService("service"); // 设置service
packageConfig.setController("controller"); // 设置controller
autoGenerator.setPackageInfo(packageConfig); // 将包的设置set进代码生成器对象中去
// 策略配置
StrategyConfig strategyConfig = new StrategyConfig();
strategyConfig.setInclude("clas","product"); // 设置要生成的表名,可以多个,只生成这个对应的表
strategyConfig.setNaming(NamingStrategy.underline_to_camel); // 包的命名规则(下划线变驼峰)
strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel); // 列的命名规则(下划线变驼峰)
// strategyConfig.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategyConfig.setEntityLombokModel(true); // 实体上是否用lombok注解来简化代码
strategyConfig.setRestControllerStyle(true); // 开启restful的驼峰命名格式
// 设置逻辑删除的字段
strategyConfig.setLogicDeleteFieldName("deleted");
// 乐观锁的配置
strategyConfig.setVersionFieldName("version");
// 设置url的一个风格,可以不设置 localhost:8080/hello_id_2 比较清晰一些
strategyConfig.setControllerMappingHyphenStyle(true);
// 自动填充的配置
TableFill createTime = new TableFill("create_time", FieldFill.INSERT);// 创建时间,创建的时候自动填充
TableFill modifyTime = new TableFill("modify_time", FieldFill.INSERT_UPDATE); // 修改时间,创建和更新的时候自动填充
// 将上面的自动填充 set进策略配置中
List tableFills = new ArrayList<>();
tableFills.add(createTime);
tableFills.add(modifyTime);
strategyConfig.setTableFillList(tableFills);
autoGenerator.setStrategy(strategyConfig);
// 执行
autoGenerator.execute();
}
}
我个人的理解是,这个Wrapper有点类似于SpringDataJPA的criteriaBuilder,都是不用自己手写sql,通过方法来拼接出来的sql。
criteriaBuilder.eq、.like、.gt等等。
criteriaBuilder的例子:
public Page getGroupMemberCardList(Long groupId, String keyWord,
GroupMemberCardAccountType accountType, Integer page, Integer size) {
String sortType = "desc";
String sortField = "createTime";
// 生成排序分页规则
PageRequest pr = CareerUtil.getPage(sortType, sortField, page, size);
Page cardAccountPage = groupMemberCardAccountRepository.findAll(new Specification() {
@Nullable
@Override
public Predicate toPredicate(Root root, CriteriaQuery> criteriaQuery, CriteriaBuilder criteriaBuilder) {
List list = new ArrayList();
list.add(criteriaBuilder.equal(root.get("groupMemberCard").get("group").get("id").as(Long.class), groupId));
if (accountType != null) {
list.add(criteriaBuilder.equal(root.get("accountType").as(GroupMemberCardAccountType.class), accountType));
}
if (StringUtils.isNotEmpty(keyWord)) {
Predicate p1 = criteriaBuilder.like(root.get("groupMemberCard").get("memberName").as(String.class), "%" + keyWord + "%");
Predicate p2 = criteriaBuilder.like(root.get("groupMemberCard").get("cardNo").as(String.class), "%"+keyWord +"");
Predicate p3 = criteriaBuilder.like(root.get("groupMemberCard").get("user").get("telphone").as(String.class), "%" + keyWord + "%");
list.add(criteriaBuilder.or(p1,p2,p3));
}
Predicate[] p = new Predicate[list.size()];
return criteriaBuilder.and(list.toArray(p));
}
}, pr);
return cardAccountPage;
}
MybatisPlus的Wrapper也是一样:
// 查询列表
@Test
public void test01(){
// 查询name = Jone ,age 大于2的 email不为空的user对象
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.isNotNull("email")
.eq("name","Jone")
.gt("age",2);
//userMapper.selectList(wrapper); // SELECT id,name,age,email,reversion,create_time,modify_time,status FROM user WHERE status=1 AND email IS NOT NULL AND name = ? AND age > ?
userService.list(wrapper); // SELECT id,name,age,email,reversion,create_time,modify_time,status FROM user WHERE status=1 AND email IS NOT NULL AND name = ? AND age > ?
}
// 查询单个
@Test
public void test02(){
// 查询名字是Jone 的对象,
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("name","Jone");
// User one = userService.getOne(wrapper); // 即使结果集有多个,也不报错,但是只会展示第一个 SELECT id,name,age,email,reversion,create_time,modify_time,status FROM user WHERE status=1 AND name = ?
User one = userMapper.selectOne(wrapper); // 结果集是多个的话,报错 SELECT id,name,age,email,reversion,create_time,modify_time,status FROM user WHERE status=1 AND name = ?
System.out.println(one);
}
@Test
public void test03(){
// 查询年龄在20-30之间的用户
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.between("age",10,20);
//Integer integer = userMapper.selectCount(wrapper); // SELECT COUNT(1) FROM user WHERE status=1 AND age BETWEEN ? AND ?
int integer = userService.count(wrapper); // SELECT COUNT(1) FROM user WHERE status=1 AND age BETWEEN ? AND ?
System.out.println(integer);
}
// 模糊查询
@Test
public void test04(){
// 查询名字中不带e的,并且email是以a开头的 用户
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.notLike("name","e") // AND name NOT LIKE '%e%'
.likeLeft("email","a") // AND email LIKE '%a'
.likeRight("age","1"); // AND age LIKE '1%'
//List
@TableName(value = “user”) 用于数据库名和实体类不匹配的情况
@TableId(value=‘主键字段’,type=‘主键生成策略’)
@TableName(value=‘数据库表名’)
@TableField(value=‘数据库字段名’,exist = false,select = false)
exist=false 表示不是数据库字段
select = false 表示sql不查询这个字段
@Version 乐观锁
@EnumValue 通用枚举类注解,将数据库字段映射成实体类的枚举类型成员变量
@TableLogic 映射逻辑删除,注释掉这个@TableLogic,去执行mapper.deleteById()的话,就会物理删除
值得注意的是,在@TableId(type=‘主键生成策略’)假设你是MySql数据库,那么你可以选择 @TableId(value = “id”, type = IdType.AUTO);若你是Oracle数据库的话,需要换成@TableId(value = “id”, type = IdType.INPUT),切身踩坑,希望大家可以避免我的问题。
总的来说,有之前的ORM框架(Hibernate,Mybatis)的相关经验的话,还是比较容易上手的。无论是Hibernate、Mybatis、MybatisPlus、SpringDataJPA这些框架是哪种,但是底层肯定是对java的JDBC的封装,越来越好的框架是为了帮助我们节约开发时间成本,提高效率。