之前我在《通用Mapper》文章中呼吁大家去看MyBatis-Plus官方文档,不知道大家去看了没。我倒是认认真真看了一遍,也就花了3、40分钟而已。我的评价是,有通用Mapper使用经验的,会觉得写得还马马虎虎,但完全没有类似经验的人看来,没什么卵用。
如果大家觉得看文档有难度,可以参考以下视频:
黑马程序员MyBatis-Plus(简单明了,但对于Wrapper的介绍不够细致)
慕课网MyBatis-Plus入门教程(官网推荐)
慕课网MyBatis-Plus进阶(官网推荐)
有了通用Mapper的铺垫,跟着本文直接操作是最佳实践,所以我不打算写太多描述性的东西,都在代码里。
学习的切入点就是3个测试类,我花了很长时间写的:
- BaseMapperTest
- ServiceTest
- MapperXMLTest
按顺序学习,好好看注释。最后给了思维导图,画了很久的。
通用Mapper的问题
上一篇我们测试了通用Mapper,结果发现隐藏了很多坑:
- 原生接口批量操作支持不佳
- 非Selective方法会导致数据库默认值不生效(会传入null)
- 对于条件查询、条件更新、条件删除,当条件为null时可能导致全表操作
那MyBatis-Plus能解决这些问题吗?
MyBatis-Plus环境搭建
MyBatis-Plus由baomidou组织开源,和通用Mapper不是同一个作者。但不论是通用Mapper还是Mybatis-Plus,都是我们国人开发的。按官网的说法,MyBatis-Plus是在Spring容器启动时完成SQL的解析和注入,之后的调用几乎没什么性能损耗。
让我们开始吧。
最终结构:
准备一个空的SpringBoot项目。
SQL
DROP TABLE IF EXISTS `mp_user`; CREATE TABLE `mp_user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` varchar(50) DEFAULT '' COMMENT '姓名', `age` tinyint(3) unsigned DEFAULT NULL COMMENT '年龄', `user_type` tinyint(1) unsigned DEFAULT '1' COMMENT '用户类型 1-大帅哥 2-普通帅哥 3-小帅哥', `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', `deleted` tinyint(1) unsigned DEFAULT '0' COMMENT '是否删除', `version` int(11) unsigned DEFAULT '0' COMMENT '乐观锁版本号', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.bravo</groupId> <artifactId>mybatis-plus-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>mybatis-plus-demo</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
启动类
/** * 注意,Mybatis-Plus使用的是原生的@MapperScan,是org包下的,而通用Mapper使用的是tk包下的 * @author qiyu */ @MapperScan("com.bravo.demo.dao") @SpringBootApplication public class MybatisPlusDemoApplication { public static void main(String[] args) { SpringApplication.run(MybatisPlusDemoApplication.class, args); } }
application.yml
server: port: 7890 spring: datasource: url: jdbc:mysql:///test?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: root driver-class-name: com.mysql.jdbc.Driver logging: level: com.bravo.demo.dao: debug
POJO
/** * 通用Mapper中叫 @Table(name = "tk_user") * * @author qiyu */ @Data @TableName("mp_user") public class MpUserPojo { /** * MyBatis-Plus默认名为'id'的字段是主键 * 如果主键名不叫'id',而是'userId'之类的,必须通过 @TableId 标识 * 主键生成策略默认是无意义的long数值,可以指定@TableId的IdType属性为AUTO,随数据库自增 * 加上 @TableId(type = IdType.AUTO) */ private Long id; /** * 姓名 */ private String name; /** * 年龄 */ private Integer age; /** * 用户类型 */ private Integer userType; /** * 创建时间 */ private Date createTime; /** * 修改时间 */ private Date updateTime; /** * 是否删除,逻辑删除请用 @TableLogic */ private Boolean deleted; /** * 乐观锁版本号,需要乐观锁请用 @Version * 支持的字段类型: * long, * Long, * int, * Integer, * java.util.Date, * java.sql.Timestamp, * java.time.LocalDateTime */ private Integer version; }
Mapper接口
/** * 继承MyBatis-Plus提供的BaseMapper,提供了增删改查及分页方法,基本已经完全满足日常开发需求 * * @author qiyu */ public interface MpUserMapper extends BaseMapper<MpUserPojo> { }
Mapper.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 namespace="com.bravo.demo.dao.MpUserMapper"> <resultMap id="BaseResultMap" type="com.bravo.demo.pojo.MpUserPojo"> <!-- WARNING - @mbg.generated --> <id column="id" jdbcType="BIGINT" property="id"/> <result column="name" jdbcType="VARCHAR" property="name"/> <result column="age" jdbcType="TINYINT" property="age"/> <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> <result column="deleted" jdbcType="BIT" property="deleted"/> </resultMap> <select id="myGetById" resultType="com.bravo.demo.pojo.MpUserPojo"> SELECT name, age, user_type FROM mp_user WHERE id=#{id} </select> </mapper>
Service接口
/** * 继承MyBatis-Plus提供的IService接口 * * @author qiyu * @date 2020-09-13 16:38 */ public interface MpUserService extends IService<MpUserPojo> { }
Service实现类
/** * 继承MyBatis-Plus提供的ServiceImpl,定好Mapper和DO * 实现自己的Service接口 * <p> * 但不得不说,这种写法很恶心,语义上讲不通 * * @author qiyu * @date 2020-09-13 16:39 */ @Service public class MpUserServiceImpl extends ServiceImpl<MpUserMapper, MpUserPojo> implements MpUserService { }
MybaitsPlusConfig
/** * @author qiyu * @date 2020-09-13 14:00 */ @Configuration public class MybatisPlusConfig { /** * MyBatis-Plus插件配置 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }
测试案例
拷贝后在自己本地调试每个方法
如果数据脏了需要清表,请重新执行一遍最开始的SQL。
通过BaseMapper学习基本操作
/** * 执行的时候注意观察控制台打印的SQL * * 困惑一: * QueryWrapper和UpdateWrapper啥区别? * * 你可以把QueryWrapper等同于通用Mapper的Example,是用来构造条件的。 * 而UpdateWrapper几乎和QueryWrapper是一样的,只不过多了set()方法,我们可以通过set把要更新的值也一并设置进去。 * 比如你用UpdateWrapper.eq()等方法构造了一堆条件,如果不想额外构造entity,则可以直接set("name", "bravo") * 也就是说 UpdateWrapper ≈ QueryWrapper + 要设置的值 * * 困惑二: * LambdaQuery这种Lambda开头的Wrapper又是啥? * * QueryWrapper/UpdateWrapper默认是这样的wrapper.eq("user_type", 1),条件字段是数据库Column * 这样的坏处是容易写错,而且编译器没法校验。 * LambdaWrapper则支持POJO字段作为条件,比如wrapper.eq(User::getUserType, 1),可以编译期发现错误。 * * @author qiyu */ @SpringBootTest class BaseMapperTest { @Autowired private MpUserMapper userMapper; @Test void contextLoads() { } /** * 插入,默认类似于insertSelective()效果。相比通用Mapper,默认名字为id的字段为主键,不需要主键注解 * INSERT INTO mp_user ( id, name, age, user_type ) VALUES ( 1301805567879794689, '测试1', 18, 1 ); * 你会发现插入时已经有主键了,MyBatis-Plus默认会自动生成long数值,如需改变请修改MpUserPojo的id */ @Test public void testInsert() { MpUserPojo mpUserPojo = new MpUserPojo(); mpUserPojo.setName("测试1"); mpUserPojo.setAge(18); mpUserPojo.setUserType(1); // 插入 userMapper.insert(mpUserPojo); } @Test public void testSelect() { /** * 根据主键查询 * SELECT id,name,age,user_type,create_time,update_time,deleted,version * FROM mp_user * WHERE id=1; */ MpUserPojo mpUserPojo = userMapper.selectById(null); /** * 条件查询,如果查出多个则抛异常,个人觉得很鸡肋,不推荐使用 * SELECT id,name,age,user_type,create_time,update_time,deleted,version * FROM mp_user * WHERE (age = 18); */ // QueryWrapper<MpUserPojo> oneQuery = new QueryWrapper<>(); // oneQuery.eq("age", 18); // MpUserPojo one = userMapper.selectOne(oneQuery); /** * 条件查询,本质上和selectOne()一样,推荐 * SELECT id,name,age,user_type,create_time,update_time,deleted,version * FROM mp_user * WHERE (age = 18); */ QueryWrapper<MpUserPojo> listQuery = new QueryWrapper<>(); listQuery.eq("age", 18); List<MpUserPojo> list = userMapper.selectList(listQuery); /** * 条件查询 likeRight ==> like '测试%',还可以指定查询的字段,避免全列返回 * SELECT name,age * FROM mp_user * WHERE (name LIKE 'null%'); * 即使条件为null,也不会像通用Mapper一样忽略条件导致查询全部 */ QueryWrapper<MpUserPojo> queryWrapper = new QueryWrapper<>(); queryWrapper.select("name", "age").likeRight("name", null); List<MpUserPojo> mpUserPojoList2 = userMapper.selectList(queryWrapper); /** * 批量查询,如果ids==null 或 ids.size()==0,会报错,而不是像通用Mapper一样查询全部 * SELECT id,name,age,user_type,create_time,update_time,deleted,version * FROM mp_user * WHERE id IN ( ); * 其实你点进源码,会发现人家已经提醒了ids不能为空 */ // List<MpUserPojo> mpUserList = userMapper.selectBatchIds(new ArrayList<>()); /** * 分页查询,需要额外配置MyBatis-Plus提供的分页插件,{@link com.bravo.demo.config.MybatisPlusConfig} * * 会发送两条SQL: * * 1.查询总数量 * SELECT COUNT(1) * FROM mp_user * WHERE (name LIKE '测试1%'); * * 2.查询分页数据 * SELECT name,age * FROM mp_user * WHERE (name LIKE '测试1%') * LIMIT 1; * * list打印:[MpUserPojo(id=null, name=测试1, age=18, userType=null, createTime=null, updateTime=null, deleted=null, version=null)] */ // 分页 Page<MpUserPojo> page = new Page<>(); page.setPages(1); page.setSize(1); // 条件 QueryWrapper<MpUserPojo> pageQuery = new QueryWrapper<>(); pageQuery.select("name", "age").likeRight("name", "测试1"); Page<MpUserPojo> pageList = userMapper.selectPage(page, pageQuery); System.out.println(pageList.getRecords()); /** * 如果你仔细观察上面SQL,会发现虽然指定查询name、age,但实际上是用整个MpUserPojo对象接收的,其他都为null * 如果你觉得这样太难看,可以使用以下方法,让返回值是map。 * 然而个人觉得,没太大卵用,因为实际开发还是建议返回对象,而不是map * * SELECT name,age * FROM mp_user * WHERE (name LIKE '测试1%'); * * list打印:[{name=测试1, age=18}, {name=测试1, age=18}] */ List<Map<String, Object>> maps = userMapper.selectMaps(pageQuery); System.out.println(maps); /** * 和上面的一样,只不过这次是分页。我不是很喜欢这种。 * * 1.查询总数量 * SELECT COUNT(1) * FROM mp_user * WHERE (name LIKE '测试1%'); * * 2.查询分页数据 * SELECT name,age * FROM mp_user * WHERE (name LIKE '测试1%') * LIMIT 1; * * list打印:[{name=测试1, age=18}] */ Page<Map<String, Object>> mapPage = new Page<>(); mapPage.setPages(1); mapPage.setSize(1); Page<Map<String, Object>> mapPageList = userMapper.selectMapsPage(mapPage, pageQuery); System.out.println(mapPageList.getRecords()); /** * COUNT方法 * * SELECT COUNT( 1 ) * FROM mp_user * WHERE (name = '测试1'); */ QueryWrapper<MpUserPojo> countQuery = new QueryWrapper<>(); countQuery.eq("name", "测试1"); Integer count = userMapper.selectCount(countQuery); System.out.println(count); /** * 查询条件除了用QueryWrapper构造以外,还可以用Map,不过Map构造的条件都是等值条件,而queryWrapper可以使用like等 * 就用QueryWrapper即可,太多选择反而显得凌乱,不推荐 */ Map<String, Object> queryMap = new HashMap<>(); queryMap.put("name", "测试1"); List<MpUserPojo> result = userMapper.selectByMap(queryMap); System.out.println(result); } /** * 只更新设置的字段,类似于updateByPrimaryKeySelective() * INSERT INTO mp_user ( id, name, age, user_type ) VALUES ( 1, '测试1', 18, 1 ); * UPDATE mp_user SET name='修改后的值' WHERE id=1; */ @Test public void testUpdateById() { // 先插入 MpUserPojo mpUserPojo = new MpUserPojo(); mpUserPojo.setName("测试1"); mpUserPojo.setAge(18); mpUserPojo.setUserType(1); userMapper.insert(mpUserPojo); // 修改 MpUserPojo updatePojo = new MpUserPojo(); updatePojo.setId(mpUserPojo.getId()); updatePojo.setName("修改后的值"); userMapper.updateById(updatePojo); } /** * 即使传的条件为null也不会导致条件消失,能更有效防止条件失效导致的全表修改 * INSERT INTO mp_user ( id, name, age, user_type ) VALUES ( 1, '测试1', 18, 1 ); * UPDATE mp_user SET name='修改后的值' WHERE (user_type = null); */ @Test public void testUpdate() { // 先插入 MpUserPojo mpUserPojo = new MpUserPojo(); mpUserPojo.setName("测试2"); mpUserPojo.setAge(18); mpUserPojo.setUserType(1); userMapper.insert(mpUserPojo); // 修改 MpUserPojo updatePojo = new MpUserPojo(); updatePojo.setId(mpUserPojo.getId()); updatePojo.setName("修改后的值"); UpdateWrapper<MpUserPojo> updateWrapper = new UpdateWrapper<>(); // 即使条件为null也不会更新全表。但是!!!如果Wrapper没有设置任何条件,仍然会更新全表! // updateWrapper.eq("user_type", null); userMapper.update(updatePojo, updateWrapper); } /** * 默认都是物理删除,如果需要逻辑删除,请在deleted上加@TableLogic */ @Test public void testDelete() { /** * 根据主键删除(物理删除) * DELETE * FROM mp_user * WHERE id=1; */ userMapper.deleteById(1L); /** * 批量删除(物理删除),如果ids.size()==0,会报错,而不是像通用Mapper一样删除全部 * DELETE * FROM mp_user * WHERE id IN ( ); */ userMapper.deleteBatchIds(Arrays.asList(1L, 2L)); /** * 条件删除(物理删除) * DELETE * FROM mp_user * WHERE (age = null); * 即使条件为null,也不会像通用Mapper一样忽略条件导致删除全部(age=null是无效的,age is NULL才会生效) * 注意,如果你不给queryWrapper设置任何条件,仍然会删除全表!!!! */ QueryWrapper<MpUserPojo> queryWrapper = new QueryWrapper<>(); // queryWrapper.eq("age", null); userMapper.delete(queryWrapper); /** * map构造条件,不说了 */ // userMapper.deleteByMap() } /** * 乐观锁 * * 支持乐观锁分两步: * 1.配置乐观锁插件 {@link com.bravo.demo.config.MybatisPlusConfig} * 2.DO字段上加@Version * * UPDATE mp_user SET name='测试乐观锁', version=1 * WHERE id=1 AND version=0 AND deleted=0; */ @Test public void testOptimisticLock() { MpUserPojo mpUserPojo = new MpUserPojo(); mpUserPojo.setId(1L); mpUserPojo.setName("测试乐观锁"); // 假设这条记录是刚从数据库查出来的,version=0 mpUserPojo.setVersion(0); // 如果更新成功,version=version+1 userMapper.updateById(mpUserPojo); } }
Service对Mapper的封装
/** * 1.Service层更加易用,对Mapper做了一层封装 * * 2.Service层的命名方式和Mapper不同 * select --> get * insert --> save * delete --> remove * update --> update * * 3.支持了批量操作,而且可以指定一次操作多少个,比如 saveBatch(Collection<T> entityList, int batchSize); * * 4.对于增删改,返回值统一为boolean,而不是int(修改行数) * * 5.getOne(Wrapper, boolean)与BaseMapper的selectOne(Wrapper)不同,如果传false不会抛异常,有多个值则list.get(0) * T getOne(Wrapper<T> queryWrapper, boolean throwEx); * * 6.Service层链式调用更顺手 * * @author qiyu * @date 2020-09-13 16:41 */ @SpringBootTest public class ServiceTest { @Autowired private MpUserService userService; @Test public void testServiceSave() { // ---------- 增 ----------- // 插入 MpUserPojo userPojo = new MpUserPojo(); userPojo.setName("test Service"); userPojo.setAge(18); userService.save(userPojo); userPojo.setId(null); // 批量插入,即使不传入batchSize,默认也是1000条。比如实际由1w条,内部会按每次1000条批量插入 boolean save = userService.saveBatch(Collections.singletonList(userPojo), 1000); } /** * 特别注意,虽然Wrapper的条件设置为null不影响,但Wrapper本身不设置任何条件还是会触发全表更新 */ @Test public void testServiceUpdate() { // ---------- 改 LambdaWrapper和普通Wrapper的唯一区别是 一个用POJO的字段,一个用数据库的字段表示条件----------- // 根据id更新 MpUserPojo updateUser = new MpUserPojo(); updateUser.setId(1L); updateUser.setName("bravo2020"); userService.updateById(updateUser); // 条件更新 方式1 lambdaUpdate,用DO的字段 boolean update1 = userService.lambdaUpdate().eq(MpUserPojo::getName, "bravo").set(MpUserPojo::getAge, 23).update(); // 条件更新 方式2 普通update,用数据库字段 boolean update2 = userService.update().eq("name", "bravo").set("age", 18).update(); // 条件更新 方式3 传入LambdaUpdateWrapper,用DO的字段 boolean update3 = userService.update(new LambdaUpdateWrapper<MpUserPojo>().eq(MpUserPojo::getName, "bravo").set(MpUserPojo::getAge, 18)); // 条件更新 方式4 传入UpdateWrapper,用数据库字段 boolean update4 = userService.update(new UpdateWrapper<MpUserPojo>().eq("name", "bravo").set("age", 18)); // 条件更新 方式5 传入Entity表示更新的字段,QueryWrapper表示条件 boolean bravo5 = userService.update(updateUser, new QueryWrapper<MpUserPojo>().lambda().eq(MpUserPojo::getName, "bravo")); // 批量更新。如果要根据条件批量更新还是自己写吧,注意SQL。 boolean update = userService.updateBatchById(Collections.singletonList(updateUser), 1000); } @Test public void testServiceGet() { // ---------- 查 ----------- // 条件查询 方式1 lambdaQuery,用DO的字段 List<MpUserPojo> queryList1 = userService.lambdaQuery().eq(MpUserPojo::getName, "bravo") .select(MpUserPojo::getName, MpUserPojo::getAge) .list(); // 条件查询 方式2 普通Query,用数据库字段 List<MpUserPojo> queryList2 = userService.query().ge("age", 19).select("name", "age").list(); // 条件查询 方式3 传入LambdaQueryWrapper,用DO的字段 List<MpUserPojo> queryList3 = userService.list(new LambdaQueryWrapper<MpUserPojo>().eq(MpUserPojo::getName, "bravo")); // 条件查询 方式4 传入QueryWrapper,用数据库字段 List<MpUserPojo> queryList4 = userService.list(new QueryWrapper<MpUserPojo>().eq("name", "bravo")); // getOne条件查询,有个重载方法 getOne(Wrapper<T> queryWrapper, boolean throwEx); MpUserPojo getOne = userService.getOne(Wrappers.<MpUserPojo>lambdaQuery().eq(MpUserPojo::getName, "bravo")); // 批量查询 List<MpUserPojo> listBatch = userService.listByIds(Arrays.asList(1L, 2L, 3L)); // 分页 Page<MpUserPojo> page = new Page<>(); page.setPages(1); page.setSize(2); Page<MpUserPojo> pageList = userService.page(page, new QueryWrapper<MpUserPojo>().lambda().eq(MpUserPojo::getName, "bravo")); // count int count = userService.count(new LambdaQueryWrapper<MpUserPojo>().eq(MpUserPojo::getName, "bravo")); } /** * 特别注意,虽然Wrapper的条件设置为null不影响,但Wrapper本身不设置任何条件还是会触发全表删除 */ @Test public void testServiceRemove() { // ---------- 删 和通用Mapper不同的是,@TableLogic对批量删除也是起作用的 ----------- // 根据id删除 userService.removeById(1L); // 条件删除,特别注意,Wrapper不设置任何条件还是会触发全表删除 userService.remove(Wrappers.<MpUserPojo>lambdaQuery()); // 根据ids批量删除 UPDATE mp_user SET deleted=1 WHERE id IN ( 1 , 2 , 3 ) AND deleted=0; boolean remove = userService.removeByIds(Arrays.asList(1L, 2L, 3L)); } }
手写SQL与驼峰映射
/** * 测试手写SQL与接口方法是否会自动映射驼峰 * <p> * 在通用Mapper中,接口的驼峰和手写SQL的驼峰要分别开启,MyBatis-Plus是一起的 * * @author qiyu * @date 2020-09-13 18:16 */ @SpringBootTest public class MapperXMLTest { @Autowired private MpUserMapper userMapper; @Test public void create() { MpUserPojo mpUserPojo = new MpUserPojo(); mpUserPojo.setName("bravo"); mpUserPojo.setUserType(1); mpUserPojo.setAge(18); userMapper.insert(mpUserPojo); } @Test public void test() { // 手写SQL MpUserPojo userPojo = userMapper.myGetById(1305079897975795715L); System.out.println(userPojo); // 接口方法 MpUserPojo mpUserPojo = userMapper.selectById(1305079897975795715L); System.out.println(mpUserPojo); } }
全局设置
#关于MyBatis-Plus的设置 mybatis-plus: # 别名扫描路径。这样Mapper.xml中的每个语句的resultType就不用写全路径了(但我还是习惯写全路径,尤其类很多的时候,不看路径不知道谁是谁) type-aliases-package: com.bravo.demo.pojo # Mapper.xml扫描路径。然后在Mapper接口写上自定义方法并关联XML语句,即可实现手写SQL mapper-locations: classpath*:mapper/**/*.xml # MyBatis-Plus驼峰转换,配置后不论手写SQL还是接口方法,都能自动映射(默认on) configuration: map-underscore-to-camel-case: on # 全局设置:主键生成策略、逻辑删除字段 global-config: db-config: id-type: auto logic-delete-field: deleted
MyBatis-Plus VS 通用Mapper
- MyBatis-Plus默认是Selective的,避免插入null导致数据库默认值失效,实在想改也行,但不建议
- MyBatis-Plus默认会生成id,如果不需要可以自行配置,且默认"id"为主键,但可以通过@TableId修改
- MyBatis-Plus提供了Wrapper接口,使用更安全,能避免一部分条件空值导致的全表修改,但Wrapper为空还是会导致全表操作
- MyBatis-Plus和通用Mapper一样,默认是物理删除,但可配置为逻辑删除
- MyBatis-Plus条件构造可以使用数据库字段也可以用POJO属性(Lambda),通用Mapper使用的是POJO属性
- MyBatis-Plus自带分页功能,而通用Mapper需要额外引入PageHelper
- MyBatis-Plus做乐观锁很简单
- MyBatis-Plus还有一些插件能防止全表更新、删除
- MyBatis-Plus可以帮我们分批执行批量SQL,比如1次1000条(MyBatis默认批量操作一次最多发送1w+)
可以说,MyBatis-Plus几乎是完胜通用Mapper的...硬要说缺点,MyBatis-Plus比通用Mapper灵活意味着它更难掌握,前者上手难度大概是后者的2倍,尤其Wrapper那一块,api太多了。
还有其他插件以及枚举处理、自动填充,这里就不介绍了,大家看官方文档即可。
要相信自己,官方文档而已,还是中国人写的,稳得一批~
create_time/update_time默认值处理
大家应该注意到了,虽然MyBatis-Plus有自动填充的功能(去查文档),但我都习惯让数据库默认生成create_time、update_time、deleted。在我看来,这样的好处有:
- 防止忘了设置
- 简化代码
那么,对于代码中set和数据库默认设置,两者性能对比如何呢?
我做了个实验:循环插入10w条数据并测试耗时。
/** * 关于create_time,update_time默认值的生成性能 * <p> * 数据库自动生成 VS 代码里生成 * * 大家把Date类型改为LocalDateTime * * @author qiyu * @date 2020-09-13 20:48 */ @SpringBootTest public class DBGenerateDefaultValueTest { @Autowired private MpUserMapper userMapper; /** * 代码中生成,耗时:821630 */ @Test public void testCodeGenerate() { long start = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { MpUserPojo userPojo = new MpUserPojo(); userPojo.setName("bravo"); userPojo.setAge(18); userPojo.setUserType(1); userPojo.setCreateTime(LocalDateTime.now()); userPojo.setUpdateTime(LocalDateTime.now()); userPojo.setDeleted(false); userPojo.setVersion(0); userMapper.insert(userPojo); } System.out.println("耗时:" + (System.currentTimeMillis() - start)); } // ========== 为了公平性,记得清表 =========== /** * 数据库生成 耗时:812613 */ @Test public void testDbGenerate() { long start = System.currentTimeMillis(); for (int i = 0; i < 100000; i++) { MpUserPojo userPojo = new MpUserPojo(); userPojo.setName("bravo"); userPojo.setAge(18); userPojo.setUserType(1); userMapper.insert(userPojo); } System.out.println("耗时:" + (System.currentTimeMillis() - start)); } }
代码生成耗时:821秒
数据库生成耗时:812秒
两种方式相差不大。