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}