MyBatis本身就支持插件,插件就是一个拦截器,可以在SQL执行的任意过程进行拦截。
MyBatis-Plus的分页插件也是对pagehelper的简单包装。
MyBatis-Plus使用分页时需要配置分页拦截器,如果在Application上已经配置了@MapperScan这里就不再需要配置了,只需配置一个地方即可。
@Configuration @MapperScan("com.example.mybatisplus.mapper") public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } @Test public void testSelect() { // current: 表示当前页 // size: 表示每页要显示的条数 // isSearchCount: 是否执行查询总数量的SQL语句,默认是true,可以设置false关闭 Page<User> page = new Page<>(2, 5, true); QueryWrapper<User> qw = new QueryWrapper<>(); qw.eq("status", 0); // 注意:分页时会执行两条sql,一个是查询总数量一个是查询分页的数据 // SELECT COUNT(1) FROM tbl_user WHERE (status = ?) // SELECT id,username,name,age,...,version FROM tbl_user WHERE (status = ?) LIMIT ?,? Page<User> userPage = userMapper.selectPage(page, qw); List<User> records = userPage.getRecords(); long total = userPage.getTotal(); long current = userPage.getCurrent(); }分页插件也可以在自定义SQL中使用。
public interface UserMapper extends BaseMapper<User> { IPage<User> selectUserPage(Page<User> page, @Param(Constants.WRAPPER) QueryWrapper<User> wrapper); } <?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.example.mybatisplus.mapper.UserMapper"> <select id="selectUserPage" resultType="com.example.mybatisplus.entity.User"> SELECT * FROM tbl_user ${ew.customSqlSegment} </select> </mapper> @Test void testMyBatisPlus() { QueryWrapper<User> queryWrapper = new QueryWrapper(); queryWrapper.gt("id", 1); Page<User> page = new Page<>(2, 3); IPage<User> iPage = userMapper.selectUserPage(page, queryWrapper); List<User> records = iPage.getRecords(); long total = iPage.getTotal(); System.out.println(records); }Mapper方法不传Page参数,只要参数中有分页数量和当前页参数,MyBatis-Plus也会进行分页。
@Data public class UserDto extends Page<User> { private Long id; } public interface UserMapper extends MyBaseMapper<User> { IPage<User> selectUserPage(UserDto userDto); } <select id="selectUserPage" resultType="com.example.mybatisplus.entity.User"> SELECT * FROM tbl_user </select> @Test void testMyBatisPlus() { UserDto userDto = new UserDto(); userDto.setSize(2); userDto.setCurrent(2); userDto.setId(1L); IPage<User> iPage = userMapper.selectUserPage(userDto); List<User> records = iPage.getRecords(); long total = iPage.getTotal(); System.out.println(records); }这种方法有点不太明白iPage的数据类型为什么是UserDto类型,我不是写的返回IPage< User > 的这吗?还没搞懂。
注意:Spring Cloud 中使用Feign 远程调用Controller方法返回值类型是不支持IPage接口类型的,解决方案可以自己定义序列化方式,或者返回Object类型,最好使用Page类。
feign.codec.DecodeException: Type definition error: [simple type, class com.baomidou.mybatisplus.core.metadata.IPage]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.baomidou.mybatisplus.core.metadata.IPage` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information at [Source: (PushbackInputStream); line: 1, column: 1]动态表名的应用场景一般是数据比较大,需要将数据分散到多个表中,比如日志表,例如一个月一张表,动态表名一般表中的字段都是完全一样的,一般表名的前缀都是一样的(例如tbl_log_),后面可以跟个日期等字段,在操作表时必须先知道后缀,然后拼接成完整的表名。
动态表名其实就是一个轻量级的分表方式,像第三方中间件(Sharding-JDBC、Mycat) 都是优秀的分库分表中间件。当不需要或者不允许引入第三方中间件时可以使用动态表名来实现分表。
动态表名就是使用拦截器帮你把完整的表名给拼接起来,在拦截器中需要配置要拦截的表及表名后缀值。
@Configuration @MapperScan("com.example.mybatisplus.mapper") public class MyBatisPlusConfig { public static ThreadLocal<String> threadLocal = new ThreadLocal<>(); @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor(); dynamicTableNameInnerInterceptor.setTableNameHandlerMap(new HashMap<String, TableNameHandler>(2){{ put("tbl_user", (sql, tableName) -> { // 表名动态获取 String month = threadLocal.get(); return tableName + "_" + month; }); }}); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor); return interceptor; } } @Test void testMyBatisPlus() { // SELECT * FROM tbl_user_202010 WHERE (id = 1) MyBatisPlusConfig.threadLocal.set("202010"); LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery().eq(User::getId, 1); userMapper.selectList(wrapper); }BlockAttackInnerInterceptor:是一个对SQL执行分析的插件,可以阻断全表更新、删除的操作。该插件仅用于开发环境,不适用于生成环境,因为会影响性能。
@Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 执行分析插件 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } } @Test public void testUpdateAll() { User user = new User(); user.setAge(30); // MybatisPlusException: Prohibition of table update operation userMapper.update(user, null); // MybatisPlusException: Prohibition of full table deletion userMapper.delete(null); }性能分析插件,用于输出每条SQL语句和执行时间,可以设置最大执行时间,超过时间会抛异常,该插件也是只适用开发环境,不适用生产环境。
乐观锁插件:当要更新一条记录的时候,希望这条记录没有被别人更新,乐观锁一般用于读多写少的情况。 乐观锁实现方式:
取出记录时,获取当前version更新时,带上这个version条件执行更新时设置version为一个新值, update xxx set version = oldVersion + 1 where version = oldVersion如果version不对,就更新失败特别说明:
支持的数据类型只有:int, Integer, long, Long, Date, Timestamp, LocalDateTime整数类型下newVersion = oldVersion + 1newVersion会回写到entity中仅支持updateById(id) 与 update(entity, wrapper) 方法在update(entity, wrapper)方法下,wrapper不能复用(也就是必须使用一个全新的没有被使用的wrapper) @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { // 乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }使用@Version注解标注乐观锁字段
public class User implements Serializable { /** * 版本号 */ @Version private Long version; } @Test public void testVersion() { User user = userMapper.selectById(1L); User updateUser = new User(); updateUser.setId(1L); updateUser.setAge(30); updateUser.setVersion(user.getVersion()); int affectRows = userMapper.updateById(user); if(affectRows <= 0) { // 并发情况下,修改失败 } }使用@TableLogic注解标记逻辑删除字段。
public class User implements Serializable { @TableLogic private Integer deleted; }application.yml 配置逻辑删除对应的值,logic-delete-value默认为1,logic-not-delete-value默认为0
mybatis-plus: global-config: db-config: logic-delete-value: 1 logic-not-delete-value: 0逻辑删除会将delete操作转为update操作,同时对于所有查询和更新操作也都增加了deleted=0的条件
@Test public void testDelete() { // UPDATE tbl_user SET deleted=1 WHERE id=? AND deleted=0 userMapper.deleteById(100L); // SELECT * FROM tbl_user WHERE deleted=0 userMapper.selectList(null); // UPDATE tbl_user SET age=? WHERE deleted=0 AND (id = ?) LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate().set(User::getAge, 20).eq(User::getId, 1); userMapper.update(null, wrapper); }注意:在自定义SQL中条件是不会自动加入deleted=0的条件,如果需要自能自己手动写这个条件。
枚举实现IEnum< T > 接口,重写toString(), 当查询数据时会调用toString()方法作枚举字段的返回值,枚举也可以用在条件构造器中使用。
public enum GenderEnum implements IEnum<Integer> { MAN(1, "男"), WOMAN(2, "女") ; private int value; private String desc; GenderEnum(int value, String desc) { this.value = value; this.desc = desc; } @Override public Integer getValue() { return this.value; } @Override public String toString() { return this.desc; } } public class User implements Serializable { private GenderEnum gender; } mybatis-plus.type-enums-package=com.example.mybatisplus.enums public void testEnum() { User user = new User(); user.setUsername("gaozhanlong3"); user.setPassword("123456"); user.setName("高占龙"); user.setGender(GenderEnum.MAN); userMapper.insert(user); User user1 = userMapper.selectById(1L); System.out.println(user1); QueryWrapper<User> queryWrapper = new QueryWrapper(); queryWrapper.eq("gender", GenderEnum.WOMAN); userMapper.selectList(queryWrapper); }插入或者更新时自动自动插入或者修改某些字段的值,如 create_id, create_time, update_id, update_time。
自动填充的应用场景:自动填充动态的值,如create_time、update_time 都可以通过设置数据库默认值来实现,而create_id和update_id这些值是动态的不确定的。
在属性上使用@TableField(fill=FieldFill.) 可以在insert、update、insert_update时进行填充。
public class User implements Serializable { @TableField(fill = FieldFill.INSERT) private Long createId; }MetaObjectHandler:设置自动填充的值。
@Component public class MyMetaObjectHandler implements MetaObjectHandler { /** * 插入数据时填充 * @param metaObject */ @Override public void insertFill(MetaObject metaObject) { // 判断要填充的字段值是否为空,如果不为空就不做填充 Object createId = getFieldValByName("create_id", metaObject); if (createId == null) { setFieldValByName("create_id", 1L, metaObject); } } /** * 更新数据时填充 * @param metaObject */ @Override public void updateFill(MetaObject metaObject) { } } @Test public void testInsert() { User user = new User(); user.setUsername("gaozhanlong2"); user.setPassword("123456"); user.setName("高占龙"); // 返回受影响的行数 // INSERT INTO tbl_user ( id, username, password, name ) VALUES ( ?, ?, ?, ? ) int affectRows = userMapper.insert(user); // 插入后主键id会赋值给实体 Long userId = user.getId(); System.out.println("affectRows=" + affectRows + ", userId=" + userId); }SQL注入器就是在BaseMapper中增加自定义的方法。
public interface MyBaseMapper<T> extends BaseMapper<T> { List<T> findAll(); } public class FindAll extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { String sql = "select * from " + tableInfo.getTableName(); SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); return this.addSelectMappedStatementForTable(mapperClass, "findAll", sqlSource, tableInfo); } } public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { // 获取原来BaseMapper中注入的方法 List<AbstractMethod> list = super.getMethodList(mapperClass); list.add(new FindAll()); return list; } } @Configuration public class MyBatisPlusConfig { @Bean public MySqlInjector mySqlInjector() { return new MySqlInjector(); } }集成自己的BaseMapper
public interface UserMapper extends MyBaseMapper<User>{ } @Test public void testMyFindAll() { // select * from tbl_user List<User> all = userMapper.findAll(); System.out.println(all); }注意:性能分析插件不建议在生产中使用。
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>application.yml中需要修改driver-class-name和url。
spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mysql://localhost:3306/mybatisplus?useUnicode=true&characterEncoding=utf8在resources目录下新建spy.properties
#3.2.1以上使用 modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory #3.2.1以下使用或者不配置 #modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql #appender=com.p6spy.engine.spy.appender.Slf4JLogger # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 实际驱动可多个 #driverlist=org.h2.Driver # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2执行SQL会在控制台打印SQL语句(已经替换过参数)和执行时间,如果不想打印到控制想打印到文件就在spy.properties中配置logfile=xxx.log
@Test void testMyBatisPlus() { LambdaUpdateWrapper<User> wrapper = Wrappers.<User>lambdaUpdate().set(User::getAge, 20).eq(User::getId, 1); userMapper.selectList(wrapper); }选装件就是MyBatis-Plus提供好的自定义方法,只是这个自定义方法并没有加入到BaseMapper中。
InsertBatchSomeColumn: 批量插入时指定插入的列或者排查插入的列, InsertBatchSomeColumn支持一个谓词用来过滤字段。注意:该选装件作者只在mysql中做过测试,其它数据库不保证正常,所以选装件还不成熟,不建议使用。
LogicDeleteByIdWithFill:逻辑删除时并同时修改某些字段,例如记录删除的人,删除时间等。
AlwaysUpdateSomeColumnById: 根据id更新字段时只更新指定的字段。根据id更新固定的那几个字段(但是不包含逻辑删除)
public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> list = super.getMethodList(mapperClass); list.add(new FindAll()); // 添加InsertBatchSomeColumn list.add(new InsertBatchSomeColumn(t -> !t.isLogicDelete())); return list; } } public interface MyBaseMapper<T> extends BaseMapper<T> { List<T> findAll(); int insertBatchSomeColumn(List<T> list); }LogicDeleteByIdWithFill: 逻辑删除填充功能的前提是必须先支持逻辑删除。
public class MySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { // 获取原来BaseMapper中注入的方法 List<AbstractMethod> list = super.getMethodList(mapperClass); list.add(new LogicDeleteByIdWithFill()); return list; } } public interface MyBaseMapper<T> extends BaseMapper<T> { int deleteByIdWithFill(T entity); } @TableName("tbl_user") public class User implements Serializable { /** * 是否删除(0: 未删除,1:已删除) */ @TableLogic private Integer deleted; /** * 修改人id */ @TableField(fill = FieldFill.UPDATE) private Long updateId; /** * 修改时间 */ @TableField(fill = FieldFill.UPDATE) private Date updateTime; } @Test void testMyBatisPlus() { User user = new User(); user.setId(2L); user.setUpdateId(1L); // UPDATE tbl_user SET update_id=?,update_time=?,deleted=1 WHERE id=2 AND deleted=0 int rows = userMapper.deleteByIdWithFill(user); System.out.println(rows); }