内容目录
1、Mybatis-plus的介绍
2、Mybatis-plus自定义插件开发
Mybatis-plus的介绍
简介
MyBatis-Plus (简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 强大的crud:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持lambda链式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错,方便数据库字段修改
- 内置代码生成器
- 内置分页插件
- 内置性能分析插件:下文中提及
- 内置全局拦截插件:下文中提及
SpringBoot整合引入
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath/> </parent>
<modelVersion>4.0.0</modelVersion> <artifactId>mybatis-plus-springboot</artifactId> <name>mybatis-plus-springboot</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.0.3.RELEASE</version> </dependency>
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.30</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.2</version> </dependency>
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.20</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.31</version> </dependency>
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
|
application.properties
1 2 3 4 5 6 7
| spring.datasource.driverClassName=com.mysql.jdbc.Driver spring.datasource.url= jdbc:mysql:///mybatis-study spring.datasource.username=root spring.datasource.password=123456 mybatis-plus.mapper-locations=classpath:mapper/system/repository/dao/**/*.xml mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
|
1 2 3 4 5 6 7
| @SpringBootApplication @MapperScan(basePackages = {"com.study.domain.*.dao"}) public class SpringbootClass { public static void main(String[] args) { SpringApplication.run(SpringbootClass.class, args); } }
|
内置代码生成器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
| public class MybatisGenerator {
public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); }
public static void main(String[] args) { AutoGenerator mpg = new AutoGenerator();
GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/mybatis-plus-springboot/src/main/java"); gc.setAuthor("24khandsome"); gc.setOpen(false); gc.setEnableCache(false); gc.setEntityName("%sPO"); gc.setMapperName("%sDao"); gc.setXmlName("%sDao"); gc.setServiceName("%sService"); gc.setServiceImplName("%sServiceImpl"); gc.setControllerName("%sController"); gc.setXmlName("%sDao"); mpg.setGlobalConfig(gc);
DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:mysql:///mybatis-study?useUnicode=true&useSSL=false&characterEncoding=utf8"); dsc.setDriverName("com.mysql.jdbc.Driver"); dsc.setUsername("root"); dsc.setPassword("123456"); mpg.setDataSource(dsc);
PackageConfig pc = new PackageConfig(); pc.setModuleName(scanner("模块名")); pc.setEntity("entity.po"); pc.setMapper("dao"); pc.setController("api"); pc.setParent("com.study.domain"); mpg.setPackageInfo(pc);
InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { } };
String templatePath = "/templates/mapper.xml.ftl";
List<FileOutConfig> focList = new ArrayList<>(); focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { return projectPath + "/mybatis-plus-springboot/src/main/resources/mapper/" + pc.getModuleName() + "/repository/dao/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } });
cfg.setFileOutConfigList(focList); mpg.setCfg(cfg);
TemplateConfig templateConfig = new TemplateConfig();
templateConfig.setXml(null); mpg.setTemplate(templateConfig); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setSuperEntityClass(BasePO.class); strategy.setSuperEntityColumns("create_time", "create_user", "update_time", "update_user", "version", "is_delete", "last_update_time"); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setTablePrefix(pc.getModuleName() + "_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); }
}
@Data public class BasePO {
@TableField(value = "create_user", fill = FieldFill.INSERT) private Long createUser; @TableField(value = "create_time", fill = FieldFill.INSERT) private Date createTime;
@TableField(value = "update_user", fill = FieldFill.UPDATE) private Long updateUser; @TableField(value = "update_time", fill = FieldFill.UPDATE) private Date updateTime;
@Version private Integer version;
@TableField("is_delete") @TableLogic private Integer isDelete;
public enum Delete { NORMAL(0, "正常"), DELETED(1, "删除"); @Getter private final Integer code; @Getter private final String name;
Delete(Integer code, String name) { this.code = code; this.name = name; }
public static Delete getDelete(Integer state) { for (Delete delete : values()) { if (delete.getCode().equals(state)) { return delete; } } return null; } } }
|
运行结果

自定义SQL语句模板(Sql 注入器)
参考:https://gitee.com/baomidou/mybatis-plus-samples/tree/master/mybatis-plus-sample-deluxe
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| 1.自定义方法 public class MyDeleteAllMethod extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { String sql; String sqlMethod = "<script>\nDELETE FROM %s \n</script>"; sql = String.format(sqlMethod, tableInfo.getTableName(), sqlLogicSet(tableInfo), sqlWhereEntityWrapper(true, tableInfo), sqlComment()); SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); return this.addDeleteMappedStatement(mapperClass, "deleteAll", sqlSource); } }
2.自定义SqlInjector,注册自定义方法 public class MyLogicSqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass) { List<AbstractMethod> list = super.getMethodList(mapperClass); list.add(new MyTruncateMethod()); return list; } }
3.把方法定义到BaseMapper public interface MyBaseMapper<T> extends BaseMapper<T> { int deleteAll(); }
4、mapper继承自己的basemapper public interface UserDao extends MyBaseMapper<UserPO> {
UserPO selectOneUserByName(String userName);
List<UserPO> findPageUser(Page<UserPO> page); }
5、覆盖全局SQL注入器配置 @Configuration public class MybatisPlusConfig { @Bean public ISqlInjector myLogicSqlInjector(){ return new MyLogicSqlInjector(); } } 6、使用 @Service public class UserServiceImpl extends ServiceImpl<UserDao, UserPO> implements UserService { @Resource private UserDao userDao; @Override public void deleteAll() { userDao.deleteAll(); } }
|
内置性能分析插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
| @Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})}) public class PerformanceInterceptor implements Interceptor { private static final String HIKARIPROXY_PREPARED_STATEMENT = "com.zaxxer.hikari.pool.HikariProxyPreparedStatement";
private final static SqlFormatter SQL_FORMATTER = new SqlFormatter();
public static String sqlFormat(String boundSql, boolean format) { if (format) { return SQL_FORMATTER.format(boundSql); } else { return boundSql.replaceAll("[\\s]+", " "); } }
@Getter @Setter private long maxTime = 0;
@Getter @Setter private boolean format = false;
@Override public Object intercept(Invocation invocation) throws Throwable { Statement statement; Object firstArg = invocation.getArgs()[0]; if (Proxy.isProxyClass(firstArg.getClass())) { statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement"); } else { statement = (Statement) firstArg; } try { statement = (Statement) SystemMetaObject.forObject(statement).getValue("stmt.statement"); } catch (Exception e) { }
String originalSql = null; String stmtClassName = statement.getClass().getName(); if (HIKARIPROXY_PREPARED_STATEMENT.equals(stmtClassName)) { try { Class<?> clazz = Class.forName(HIKARIPROXY_PREPARED_STATEMENT); Method druidGetSqlMethod = clazz.getMethod("getSql"); Object stmtSql = druidGetSqlMethod.invoke(statement); if (stmtSql != null && stmtSql instanceof String) { originalSql = (String) stmtSql; } } catch (Exception ignored) { } }
if (originalSql == null) { originalSql = statement.toString(); } originalSql = originalSql.replaceAll("[\\s]+", " "); int index = originalSql.indexOf(':'); if (index > 0) { originalSql = originalSql.substring(index + 1, originalSql.length()); } String sqlFormat = sqlFormat(originalSql, format); Object target = realTarget(invocation.getTarget()); MetaObject metaObject = SystemMetaObject.forObject(target); MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); Logger mybatisLogger = LoggerFactory.getLogger(this.getClass()); long start = System.currentTimeMillis(); Object result = null; try { result = invocation.proceed(); } catch (InvocationTargetException | IllegalAccessException e) { long timing = System.currentTimeMillis() - start; StringBuilder formatSql = new StringBuilder() .append("execute sql error, use time:").append(timing).append("\n") .append(sqlFormat); mybatisLogger.error(formatSql.toString()); throw e; } long timing = System.currentTimeMillis() - start;
StringBuilder formatSql = new StringBuilder().append("Execute sql use time:").append(timing) .append("\n") .append(" Execute SQL:").append(sqlFormat) .append("\n") .append(" result: ").append(result);
if (this.getMaxTime() >= 1 && timing > this.getMaxTime()) { mybatisLogger.error(formatSql.toString()); } else { mybatisLogger.debug(formatSql.toString()); }
return result; }
@Override public Object plugin(Object target) { if (target instanceof StatementHandler) { return Plugin.wrap(target, this); } return target; }
@Override public void setProperties(Properties prop) { String maxTime = prop.getProperty("maxTime"); String format = prop.getProperty("format"); if (!StringUtils.isEmptyOrWhitespaceOnly(maxTime)) { this.maxTime = Long.parseLong(maxTime); } if (!StringUtils.isEmptyOrWhitespaceOnly(format)) { this.format = Boolean.valueOf(format); } }
public static <T> T realTarget(Object target) { if (Proxy.isProxyClass(target.getClass())) { MetaObject metaObject = SystemMetaObject.forObject(target); return realTarget(metaObject.getValue("h.target")); } return (T) target; }
}
@Bean public PerformanceInterceptor performanceInterceptor(){ return new PerformanceInterceptor(); }
|
事务检查插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| @Bean public MybatisUpdateCheckInterceptor mybatisUpdateCheckInterceptor(DataSource dataSource) { return new MybatisUpdateCheckInterceptor(dataSource); } @Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})}) public class MybatisUpdateCheckInterceptor implements Interceptor {
private DataSource dataSource;
public MybatisUpdateCheckInterceptor(DataSource dataSource) { this.dataSource = dataSource; }
@Override public Object intercept(Invocation invocation) throws Throwable {
assertTransactional();
Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; SqlCommandType sqlCommandType = ms.getSqlCommandType();
Object param = args[1];
if (SqlCommandType.INSERT == sqlCommandType) { checkParam(param); Object proceed = invocation.proceed(); validateReturnGtZero(proceed); return proceed; } Object proceed = invocation.proceed(); return proceed; }
private void assertTransactional() { Connection connection = DataSourceUtils.getConnection(dataSource); if (!DataSourceUtils.isConnectionTransactional(connection, dataSource)) { throw new RuntimeException("mybatis can not update without transactional"); } }
private void checkParam(Object param) { if (null == param) { throw new RuntimeException("mybatis update can not insert null object"); } }
private void validateReturnGtZero(Object proceed) { if (!(proceed instanceof Number)) { throw new RuntimeException("mybatis update return is not a number"); }
Number result = (Number) proceed; if (0L == result.longValue()) { throw new RuntimeException("数据已被他人修改,请刷新重试"); } }
}
|
动态表名插件(since 3.4.0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); DynamicTableNameInnerInterceptor interceptor = new DynamicTableNameInnerInterceptor(); int b=(int)(Math.random()*3); String[] table = {"system_user","system_user_copy1","system_user_copy2"}; Map<String, TableNameHandler> tableNameHandlerMap = new HashMap<String,TableNameHandler>(1); tableNameHandlerMap.put("system_user",((sql,tablename) -> table[b])); interceptor.setTableNameHandlerMap(tableNameHandlerMap); mybatisPlusInterceptor.addInnerInterceptor(interceptor); return mybatisPlusInterceptor; }
|
多租户(约等于数据隔离、也可作为权限过滤)
多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!
启用多租户后所有执行的method的sql都会进行处理.
自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
public interface TenantHandler {
Expression getTenantId(boolean select);
String getTenantIdColumn();
boolean doTableFilter(String tableName); }
|
假设有如下场景:
查询某些表的数据需要根据这些表的某个字段进行数据隔离
于是
- 自定义TenantHandler
- 自定义MyTenantSqlParser
- 注册多租户组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
| public enum DataAccessTable { SYSTEM_USER("system_user", "用户表", "user_id"), ;
private static Map<String, DataAccessTable> TABLE_MAP = new HashMap<>(); static { for (DataAccessTable value : DataAccessTable.values()) { TABLE_MAP.put(value.getTableName(), value); } }
@Getter private final String tableName; @Getter private final String desc; @Getter private final String publisherColumn;
DataAccessTable(String tableName, String desc, String publisherColumn) { this.tableName = tableName; this.desc = desc; this.publisherColumn = publisherColumn; }
public static Set<String> getDataAccessTables(){ return TABLE_MAP.keySet(); }
public static String getDataPublisherColumn(String tableName){ DataAccessTable dataAccessTable = TABLE_MAP.get(tableName); return dataAccessTable == null ? "" : dataAccessTable.getPublisherColumn();
} } public interface MyTenantHandler extends TenantHandler {
String getPublishUserIdColumn(String tableName); } @Data public class MyTenantSqlParser extends TenantSqlParser{
private MyTenantHandler greatTenantHandler; @Override public TenantSqlParser setTenantHandler(TenantHandler tenantHandler) { greatTenantHandler = (MyTenantHandler) tenantHandler; return super.setTenantHandler(tenantHandler); }
@Override protected Expression processTableAlias4CustomizedTenantIdExpression(Expression expression, Table table) { Expression target; if (expression instanceof ValueListExpression) { InExpression inExpression = new InExpression(); inExpression.setLeftExpression(this.getTableAliasColumn(table, greatTenantHandler.getTenantIdColumn())); inExpression.setRightItemsList(((ValueListExpression) expression).getExpressionList()); target = inExpression; } else { EqualsTo equalsTo = new EqualsTo(); equalsTo.setLeftExpression(this.getTableAliasColumn(table, greatTenantHandler.getTenantIdColumn())); equalsTo.setRightExpression(expression); target = equalsTo; } String publishUserIdColumn = greatTenantHandler.getPublishUserIdColumn(table.getName()); if(StringUtils.isBlank(publishUserIdColumn)){ return target; } EqualsTo equalsTo = new EqualsTo(); equalsTo.setLeftExpression(this.getTableAliasColumn(table, publishUserIdColumn)); equalsTo.setRightExpression(new LongValue("8")); return new Parenthesis(new OrExpression(target, equalsTo)); }
protected Column getTableAliasColumn(Table table, String columnName) { StringBuilder column = new StringBuilder(); if (table.getAlias() != null) { column.append(table.getAlias().getName()).append(StringPool.DOT); } column.append(columnName); return new Column(column.toString()); } }
@Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor page = new PaginationInterceptor(); List<ISqlParser> sqlParserList = new ArrayList<>(); MyTenantSqlParser tenantSqlParser = new MyTenantSqlParser(); tenantSqlParser.setTenantHandler(new MyTenantHandler() { @Override public String getPublishUserIdColumn(String tableName) { return DataAccessTable.getDataPublisherColumn(tableName); }
@Override public Expression getTenantId(boolean select) { ValueListExpression expression = new ValueListExpression(); List<Expression> expressions = new ArrayList<>(); expressions.add(new StringValue("木子李"));
expression.setExpressionList(new ExpressionList(expressions)); return expression; }
@Override public String getTenantIdColumn() { return "username"; }
@Override public boolean doTableFilter(String tableName) { Set<String> tableNameSet = DataAccessTable.getDataAccessTables(); if (tableNameSet.contains(tableName)) { return false; } return true; } }); sqlParserList.add(tenantSqlParser); page.setSqlParserList(sqlParserList); page.setSqlParserFilter(metaObject ->{ MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement"); if(!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())){ return true; }
return false; }); return page; }
|
效果
