一次查询如果结果返回太多(1万或更多),往往会导致系统性能下降,有时更会内存不足,影响系统稳定性,故需要做限制。
1.经分析最后决定,应限制一次查询返回的最大结果数量不应该超出1万,对于一次返回结果大于限制的时候应该抛出异常,而不应该截取(limit 10000)最大结果(结果需求不匹配)。
2.利用mybatis拦截器技术,统一拦截sql,并真对大结果的查询先做一次count查询。
public class PreCheckBigQueryInnerInterceptor implements InnerInterceptor {}
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { // 解析sql Statement stmt = CCJSqlParserUtil.parse(boundSql.getSql()); if (stmt instanceof Select) { PlainSelect selectStmt = (PlainSelect) ((Select) stmt).getSelectBody(); if (Objects.nonNull(selectStmt.getLimit())) { //包含limit查询 return true; } for (SelectItem selectItem : selectStmt.getSelectItems()) { //计数查询 count(); SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; if (selectExpressionItem.getExpression() instanceof Function) { //包含function查询 return true; } } Long aLong = doQueryCount(executor, ms, parameter, rowBounds, resultHandler, boundSql); if (aLong == 0L) { return false; } if (aLong > 20) { throw new RuntimeException("单个查询结果大于20条!!!"); } } return true; }
private Long doQueryCount(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { MappedStatement countMs = buildAutoCountMappedStatement(ms); String countSqlStr = autoCountSql(true, boundSql.getSql()); PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql); BoundSql countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mpBoundSql.parameterMappings(), parameter); PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters()); CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql); Object result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql).get(0); System.out.println(result); return (result == null ? 0L : Long.parseLong(result.toString())); }
<mapper namespace="com.example.UserMapper"> <select id="selectAllUsers" resultType="com.example.User"> SELECT * FROM user </select> </mapper>
注意:必须重新构造,不能直接使用入参中的ms
String.format("SELECT COUNT(1) FROM (%s) TOTAL", originalSql);
@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));//分页插件(Mybatis-plus) interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());//防止全表更新(Mybatis-plus) interceptor.addInnerInterceptor(new PreCheckBigQueryInnerInterceptor());//防止全表查询(自定义插件) return interceptor; }
public class MybatisPlusInterceptor implements Interceptor { @Setter private List<InnerInterceptor> interceptors = new ArrayList<>(); }
他是基于mybatis的Interceptor接口做的拦截器,上文中我们 注册拦截器PreCheckBigQueryInnerInterceptor的拦截器其实添加到MybatisPlusInterceptor.interceptors集合中。
public Object intercept(Invocation invocation) throws Throwable { ...... for (InnerInterceptor query : interceptors) { if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) { return Collections.emptyList(); } query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql); } ...... return invocation.proceed(); }
2.1 willDoQuery先于beforeQuery方法,且一定会执行