001/* 002 * Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.mybatisflex.core.util; 017 018import com.mybatisflex.core.BaseMapper; 019import com.mybatisflex.core.FlexGlobalConfig; 020import com.mybatisflex.core.constant.SqlConsts; 021import com.mybatisflex.core.dialect.DbType; 022import com.mybatisflex.core.dialect.DialectFactory; 023import com.mybatisflex.core.exception.FlexExceptions; 024import com.mybatisflex.core.field.FieldQuery; 025import com.mybatisflex.core.field.FieldQueryBuilder; 026import com.mybatisflex.core.field.FieldQueryManager; 027import com.mybatisflex.core.paginate.Page; 028import com.mybatisflex.core.query.CPI; 029import com.mybatisflex.core.query.DistinctQueryColumn; 030import com.mybatisflex.core.query.Join; 031import com.mybatisflex.core.query.QueryColumn; 032import com.mybatisflex.core.query.QueryCondition; 033import com.mybatisflex.core.query.QueryTable; 034import com.mybatisflex.core.query.QueryWrapper; 035import com.mybatisflex.core.relation.RelationManager; 036import com.mybatisflex.core.table.TableInfo; 037import com.mybatisflex.core.table.TableInfoFactory; 038import org.apache.ibatis.exceptions.TooManyResultsException; 039import org.apache.ibatis.session.defaults.DefaultSqlSession; 040 041import java.util.ArrayList; 042import java.util.Collection; 043import java.util.Collections; 044import java.util.HashMap; 045import java.util.HashSet; 046import java.util.List; 047import java.util.Map; 048import java.util.Set; 049import java.util.function.Consumer; 050 051import static com.mybatisflex.core.query.QueryMethods.count; 052 053public class MapperUtil { 054 055 private MapperUtil() { 056 } 057 058 059 /** 060 * <p>原生的、未经过优化的 COUNT 查询。抛开效率问题不谈,只关注结果的准确性, 061 * 这个 COUNT 查询查出来的分页总数据是 100% 正确的,不接受任何反驳。 062 * 063 * <p>为什么这么说,因为是用子查询实现的,生成的 SQL 如下: 064 * 065 * <p><pre> 066 * {@code 067 * SELECT COUNT(*) AS `total` FROM ( ...用户构建的 SQL 语句... ) AS `t`; 068 * } 069 * </pre> 070 * 071 * <p>不进行 SQL 优化的时候,返回的就是这样的 COUNT 查询语句。 072 */ 073 public static QueryWrapper rawCountQueryWrapper(QueryWrapper queryWrapper) { 074 return QueryWrapper.create() 075 .select(count().as("total")) 076 .from(queryWrapper).as("t"); 077 } 078 079 /** 080 * 优化 COUNT 查询语句。 081 */ 082 public static QueryWrapper optimizeCountQueryWrapper(QueryWrapper queryWrapper) { 083 // 对克隆对象进行操作,不影响原来的 QueryWrapper 对象 084 QueryWrapper clone = queryWrapper.clone(); 085 // 将最后面的 order by 移除掉 086 CPI.setOrderBys(clone, null); 087 // 获取查询列和分组列,用于判断是否进行优化 088 List<QueryColumn> selectColumns = CPI.getSelectColumns(clone); 089 List<QueryColumn> groupByColumns = CPI.getGroupByColumns(clone); 090 QueryCondition havingCondition = CPI.getHavingQueryCondition(clone); 091 // 如果有 distinct、group by、having 等语句则不优化 092 // 这种一旦优化了就会造成 count 语句查询出来的值不对 093 if (hasDistinct(selectColumns) || hasGroupBy(groupByColumns) || havingCondition != null) { 094 return rawCountQueryWrapper(clone); 095 } 096 // 判断能不能清除 join 语句 097 if (canClearJoins(clone)) { 098 CPI.setJoins(clone, null); 099 } 100 // 将 select 里面的列换成 COUNT(*) AS `total` 101 CPI.setSelectColumns(clone, Collections.singletonList(count().as("total"))); 102 return clone; 103 } 104 105 public static boolean hasDistinct(List<QueryColumn> selectColumns) { 106 if (CollectionUtil.isEmpty(selectColumns)) { 107 return false; 108 } 109 for (QueryColumn selectColumn : selectColumns) { 110 if (selectColumn instanceof DistinctQueryColumn) { 111 return true; 112 } 113 } 114 return false; 115 } 116 117 private static boolean hasGroupBy(List<QueryColumn> groupByColumns) { 118 return CollectionUtil.isNotEmpty(groupByColumns); 119 } 120 121 private static boolean canClearJoins(QueryWrapper queryWrapper) { 122 List<Join> joins = CPI.getJoins(queryWrapper); 123 if (CollectionUtil.isEmpty(joins)) { 124 return false; 125 } 126 127 // 只有全是 left join 语句才会清除 join 128 // 因为如果是 inner join 或 right join 往往都会放大记录数 129 for (Join join : joins) { 130 if (!SqlConsts.LEFT_JOIN.equals(CPI.getJoinType(join))) { 131 return false; 132 } 133 } 134 135 // 获取 join 语句中使用到的表名 136 List<String> joinTables = new ArrayList<>(); 137 joins.forEach(join -> { 138 QueryTable joinQueryTable = CPI.getJoinQueryTable(join); 139 if (joinQueryTable != null) { 140 String tableName = joinQueryTable.getName(); 141 if (StringUtil.isNotBlank(joinQueryTable.getAlias())) { 142 joinTables.add(tableName + "." + joinQueryTable.getAlias()); 143 } else { 144 joinTables.add(tableName); 145 } 146 } 147 }); 148 149 // 获取 where 语句中的条件 150 QueryCondition where = CPI.getWhereQueryCondition(queryWrapper); 151 152 // 最后判断一下 where 中是否用到了 join 的表 153 return !CPI.containsTable(where, CollectionUtil.toArrayString(joinTables)); 154 } 155 156 @SafeVarargs 157 public static <T, R> Page<R> doPaginate( 158 BaseMapper<T> mapper, 159 Page<R> page, 160 QueryWrapper queryWrapper, 161 Class<R> asType, 162 boolean withRelations, 163 Consumer<FieldQueryBuilder<R>>... consumers 164 ) { 165 Long limitRows = CPI.getLimitRows(queryWrapper); 166 Long limitOffset = CPI.getLimitOffset(queryWrapper); 167 try { 168 // 只有 totalRow 小于 0 的时候才会去查询总量 169 // 这样方便用户做总数缓存,而非每次都要去查询总量 170 // 一般的分页场景中,只有第一页的时候有必要去查询总量,第二页以后是不需要的 171 172 if (page.getTotalRow() < 0) { 173 174 QueryWrapper countQueryWrapper; 175 176 if (page.needOptimizeCountQuery()) { 177 countQueryWrapper = MapperUtil.optimizeCountQueryWrapper(queryWrapper); 178 } else { 179 countQueryWrapper = MapperUtil.rawCountQueryWrapper(queryWrapper); 180 } 181 182 // optimize: 在 count 之前先去掉 limit 参数,避免 count 查询错误 183 CPI.setLimitRows(countQueryWrapper, null); 184 CPI.setLimitOffset(countQueryWrapper, null); 185 186 page.setTotalRow(mapper.selectCountByQuery(countQueryWrapper)); 187 } 188 189 if (!page.hasRecords()) { 190 if (withRelations) { 191 RelationManager.clearConfigIfNecessary(); 192 } 193 return page; 194 } 195 196 queryWrapper.limit(page.offset(), page.getPageSize()); 197 198 List<R> records; 199 if (asType != null) { 200 records = mapper.selectListByQueryAs(queryWrapper, asType); 201 } else { 202 // noinspection unchecked 203 records = (List<R>) mapper.selectListByQuery(queryWrapper); 204 } 205 206 if (withRelations) { 207 queryRelations(mapper, records); 208 } 209 210 queryFields(mapper, records, consumers); 211 page.setRecords(records); 212 213 return page; 214 215 } finally { 216 // 将之前设置的 limit 清除掉 217 // 保险起见把重置代码放到 finally 代码块中 218 CPI.setLimitRows(queryWrapper, limitRows); 219 CPI.setLimitOffset(queryWrapper, limitOffset); 220 } 221 } 222 223 224 public static <R> void queryFields(BaseMapper<?> mapper, List<R> list, Consumer<FieldQueryBuilder<R>>[] consumers) { 225 if (CollectionUtil.isEmpty(list) || ArrayUtil.isEmpty(consumers) || consumers[0] == null) { 226 return; 227 } 228 229 Map<String, FieldQuery> fieldQueryMap = new HashMap<>(); 230 for (Consumer<FieldQueryBuilder<R>> consumer : consumers) { 231 FieldQueryBuilder<R> fieldQueryBuilder = new FieldQueryBuilder<>(); 232 consumer.accept(fieldQueryBuilder); 233 234 FieldQuery fieldQuery = fieldQueryBuilder.build(); 235 236 String className = fieldQuery.getEntityClass().getName(); 237 String fieldName = fieldQuery.getFieldName(); 238 String mapKey = className + '#' + fieldName; 239 240 fieldQueryMap.put(mapKey, fieldQuery); 241 } 242 243 FieldQueryManager.queryFields(mapper, list, fieldQueryMap); 244 } 245 246 247 public static <E> E queryRelations(BaseMapper<?> mapper, E entity) { 248 if (entity != null) { 249 queryRelations(mapper, Collections.singletonList(entity)); 250 } else { 251 RelationManager.clearConfigIfNecessary(); 252 } 253 return entity; 254 } 255 256 public static <E> List<E> queryRelations(BaseMapper<?> mapper, List<E> entities) { 257 RelationManager.queryRelations(mapper, entities); 258 return entities; 259 } 260 261 262 public static Class<? extends Collection> getCollectionWrapType(Class<?> type) { 263 if (ClassUtil.canInstance(type.getModifiers())) { 264 return (Class<? extends Collection>) type; 265 } 266 267 if (List.class.isAssignableFrom(type)) { 268 return ArrayList.class; 269 } 270 271 if (Set.class.isAssignableFrom(type)) { 272 return HashSet.class; 273 } 274 275 throw new IllegalStateException("Field query can not support type: " + type.getName()); 276 } 277 278 279 /** 280 * 搬运加改造 {@link DefaultSqlSession#selectOne(String, Object)} 281 */ 282 public static <T> T getSelectOneResult(List<T> list) { 283 if (list == null || list.isEmpty()) { 284 return null; 285 } 286 int size = list.size(); 287 if (size == 1) { 288 return list.get(0); 289 } 290 throw new TooManyResultsException( 291 "Expected one result (or null) to be returned by selectOne(), but found: " + size); 292 } 293 294 public static long getLongNumber(List<Object> objects) { 295 Object object = objects == null || objects.isEmpty() ? null : objects.get(0); 296 if (object == null) { 297 return 0; 298 } else if (object instanceof Number) { 299 return ((Number) object).longValue(); 300 } else { 301 throw FlexExceptions.wrap("selectCountByQuery error, can not get number value of result: \"" + object + "\""); 302 } 303 } 304 305 306 public static Map<String, Object> preparedParams(BaseMapper<?> baseMapper, Page<?> page, QueryWrapper queryWrapper, Map<String, Object> params) { 307 Map<String, Object> newParams = new HashMap<>(); 308 309 if (params != null) { 310 newParams.putAll(params); 311 } 312 313 newParams.put("pageOffset", page.offset()); 314 newParams.put("pageNumber", page.getPageNumber()); 315 newParams.put("pageSize", page.getPageSize()); 316 317 DbType dbType = DialectFactory.getHintDbType(); 318 newParams.put("dbType", dbType != null ? dbType : FlexGlobalConfig.getDefaultConfig().getDbType()); 319 320 if (queryWrapper != null) { 321 TableInfo tableInfo = TableInfoFactory.ofMapperClass(baseMapper.getClass()); 322 tableInfo.appendConditions(null, queryWrapper); 323 preparedQueryWrapper(newParams, queryWrapper); 324 } 325 326 return newParams; 327 } 328 329 330 private static void preparedQueryWrapper(Map<String, Object> params, QueryWrapper queryWrapper) { 331 String sql = DialectFactory.getDialect().buildNoSelectSql(queryWrapper); 332 StringBuilder sqlBuilder = new StringBuilder(); 333 char quote = 0; 334 int index = 0; 335 for (int i = 0; i < sql.length(); ++i) { 336 char ch = sql.charAt(i); 337 if (ch == '\'') { 338 if (quote == 0) { 339 quote = ch; 340 } else if (quote == '\'') { 341 quote = 0; 342 } 343 } else if (ch == '"') { 344 if (quote == 0) { 345 quote = ch; 346 } else if (quote == '"') { 347 quote = 0; 348 } 349 } 350 if (quote == 0 && ch == '?') { 351 sqlBuilder.append("#{qwParams_").append(index++).append("}"); 352 } else { 353 sqlBuilder.append(ch); 354 } 355 } 356 params.put("qwSql", sqlBuilder.toString()); 357 Object[] valueArray = CPI.getValueArray(queryWrapper); 358 for (int i = 0; i < valueArray.length; i++) { 359 params.put("qwParams_" + i, valueArray[i]); 360 } 361 } 362 363}