diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/BaseMapper.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/BaseMapper.java index 0860ee05..5eaa4a89 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/BaseMapper.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/BaseMapper.java @@ -652,75 +652,26 @@ public interface BaseMapper { default Page paginateAs(Page page, QueryWrapper queryWrapper, Class asType, Consumer>... consumers) { List selectColumns = CPI.getSelectColumns(queryWrapper); List orderBys = CPI.getOrderBys(queryWrapper); - List joins = CPI.getJoins(queryWrapper); - boolean removedJoins = true; // 只有 totalRow 小于 0 的时候才会去查询总量 // 这样方便用户做总数缓存,而非每次都要去查询总量 // 一般的分页场景中,只有第一页的时候有必要去查询总量,第二页以后是不需要的 if (page.getTotalRow() < 0) { - - //移除 select - CPI.setSelectColumns(queryWrapper, Collections.singletonList(count().as("total"))); - - //移除 OrderBy - if (CollectionUtil.isNotEmpty(orderBys)) { - CPI.setOrderBys(queryWrapper, null); - } - - - //移除 left join - if (joins != null && !joins.isEmpty()) { - for (Join join : joins) { - if (!Join.TYPE_LEFT.equals(CPI.getJoinType(join))) { - removedJoins = false; - break; - } - } - } else { - removedJoins = false; - } - - if (removedJoins) { - List joinTables = new ArrayList<>(); - joins.forEach(join -> { - QueryTable joinQueryTable = CPI.getJoinQueryTable(join); - if (joinQueryTable != null && StringUtil.isNotBlank(joinQueryTable.getName())) { - joinTables.add(joinQueryTable.getName()); - } - }); - - QueryCondition where = CPI.getWhereQueryCondition(queryWrapper); - if (CPI.containsTable(where, CollectionUtil.toArrayString(joinTables))) { - removedJoins = false; - } - } - - if (removedJoins) { - CPI.setJoins(queryWrapper, null); - } - - + queryWrapper = MapperUtil.optimizeCountQueryWrapper(queryWrapper); long count = selectCountByQuery(queryWrapper); page.setTotalRow(count); } - if (page.getTotalRow() == 0 || page.getPageNumber() > page.getTotalPage()) { + if (page.isEmpty()) { return page; } //重置 selectColumns CPI.setSelectColumns(queryWrapper, selectColumns); - //重置 orderBys - if (CollectionUtil.isNotEmpty(orderBys)) { - CPI.setOrderBys(queryWrapper, orderBys); - } - + CPI.setOrderBys(queryWrapper, orderBys); //重置 join - if (removedJoins) { - CPI.setJoins(queryWrapper, joins); - } + CPI.setJoins(queryWrapper, joins); int offset = page.getPageSize() * (page.getPageNumber() - 1); queryWrapper.limit(offset, page.getPageSize()); @@ -734,6 +685,11 @@ public interface BaseMapper { MapperUtil.queryFields(this, records, consumers); page.setRecords(records); } + + // 将之前设置的 limit 清除掉 + CPI.setLimitRows(queryWrapper, null); + CPI.setLimitOffset(queryWrapper, null); + return page; } diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/paginate/Page.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/paginate/Page.java index cc041183..f89f7b7e 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/paginate/Page.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/paginate/Page.java @@ -1,17 +1,17 @@ -/** - * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. +/* + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.mybatisflex.core.paginate; @@ -128,6 +128,14 @@ public class Page implements Serializable { } } + public boolean isEmpty() { + return getTotalRow() == 0 || getPageNumber() > getTotalPage(); + } + + public boolean hasNext() { + return getTotalPage() != 0 && getPageNumber() < getTotalPage(); + } + public Page map(Function mapper) { Page newPage = new Page<>(); diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/QueryWrapper.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/QueryWrapper.java index 28c60e56..8c581d32 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/QueryWrapper.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/QueryWrapper.java @@ -1,17 +1,17 @@ -/** - * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. +/* + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.mybatisflex.core.query; @@ -33,6 +33,11 @@ public class QueryWrapper extends BaseQueryWrapper { } + public QueryWrapper select() { + return this; + } + + public QueryWrapper select(String... columns) { for (String column : columns) { addSelectColumn(new StringQueryColumn(column)); diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/SelectQueryTable.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/SelectQueryTable.java index 5ca6fa9a..b10306ac 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/SelectQueryTable.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/query/SelectQueryTable.java @@ -1,17 +1,17 @@ -/** - * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. +/* + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.mybatisflex.core.query; @@ -51,7 +51,7 @@ public class SelectQueryTable extends QueryTable { if (StringUtil.isNotBlank(alias)) { return "(" + sql + ") AS " + dialect.wrap(alias); } else { - return sql; + return "(" + sql + ")"; } } } diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapper.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapper.java index b1cee652..fc8bb921 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapper.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/row/RowMapper.java @@ -1,17 +1,17 @@ -/** - * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. +/* + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package com.mybatisflex.core.row; @@ -21,11 +21,14 @@ import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.provider.RowSqlProvider; import com.mybatisflex.core.query.*; import com.mybatisflex.core.util.CollectionUtil; +import com.mybatisflex.core.util.MapperUtil; import com.mybatisflex.core.util.StringUtil; import org.apache.ibatis.annotations.*; import org.apache.ibatis.exceptions.TooManyResultsException; -import java.util.*; +import java.util.Collection; +import java.util.List; +import java.util.Map; import static com.mybatisflex.core.query.QueryMethods.count; @@ -435,78 +438,37 @@ public interface RowMapper { List orderBys = CPI.getOrderBys(queryWrapper); List joins = CPI.getJoins(queryWrapper); - boolean removedJoins = true; // 只有 totalRow 小于 0 的时候才会去查询总量 // 这样方便用户做总数缓存,而非每次都要去查询总量 // 一般的分页场景中,只有第一页的时候有必要去查询总量,第二页以后是不需要的 if (page.getTotalRow() < 0) { - - //移除 seelct - CPI.setSelectColumns(queryWrapper, Collections.singletonList(count().as("total"))); - - //移除 OrderBy - if (CollectionUtil.isNotEmpty(orderBys)) { - CPI.setOrderBys(queryWrapper, null); - } - - //移除 left join - if (joins != null && !joins.isEmpty()) { - for (Join join : joins) { - if (!Join.TYPE_LEFT.equals(CPI.getJoinType(join))) { - removedJoins = false; - break; - } - } - } else { - removedJoins = false; - } - - if (removedJoins) { - List joinTables = new ArrayList<>(); - joins.forEach(join -> { - QueryTable joinQueryTable = CPI.getJoinQueryTable(join); - if (joinQueryTable != null && StringUtil.isNotBlank(joinQueryTable.getName())) { - joinTables.add(joinQueryTable.getName()); - } - }); - - QueryCondition where = CPI.getWhereQueryCondition(queryWrapper); - if (CPI.containsTable(where, CollectionUtil.toArrayString(joinTables))) { - removedJoins = false; - } - } - - if (removedJoins) { - CPI.setJoins(queryWrapper, null); - } - + queryWrapper = MapperUtil.optimizeCountQueryWrapper(queryWrapper); long count = selectCountByQuery(schema,tableName, queryWrapper); page.setTotalRow(count); } - if (page.getTotalRow() == 0 || page.getPageNumber() > page.getTotalPage()) { + if (page.isEmpty()) { return page; } //重置 selectColumns CPI.setSelectColumns(queryWrapper, selectColumns); - //重置 orderBys - if (CollectionUtil.isNotEmpty(orderBys)) { - CPI.setOrderBys(queryWrapper, orderBys); - } - + CPI.setOrderBys(queryWrapper, orderBys); //重置 join - if (removedJoins) { - CPI.setJoins(queryWrapper, joins); - } + CPI.setJoins(queryWrapper, joins); int offset = page.getPageSize() * (page.getPageNumber() - 1); queryWrapper.limit(offset, page.getPageSize()); List records = selectListByQuery(schema,tableName, queryWrapper); page.setRecords(records); + + // 将之前设置的 limit 清除掉 + CPI.setLimitRows(queryWrapper, null); + CPI.setLimitOffset(queryWrapper, null); + return page; } diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ClassUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ClassUtil.java index a6c0f90a..a4151175 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ClassUtil.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/ClassUtil.java @@ -105,6 +105,10 @@ public class ClassUtil { || clazz == double[].class; } + public static boolean canInstance(int mod) { + return !Modifier.isAbstract(mod) || !Modifier.isInterface(mod); + } + public static T newInstance(Class clazz) { try { diff --git a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/MapperUtil.java b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/MapperUtil.java index af2d8e25..271d4a62 100644 --- a/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/MapperUtil.java +++ b/mybatis-flex-core/src/main/java/com/mybatisflex/core/util/MapperUtil.java @@ -18,19 +18,112 @@ package com.mybatisflex.core.util; import com.mybatisflex.core.BaseMapper; import com.mybatisflex.core.field.FieldQuery; import com.mybatisflex.core.field.FieldQueryBuilder; -import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.query.*; import org.apache.ibatis.exceptions.TooManyResultsException; import org.apache.ibatis.session.defaults.DefaultSqlSession; import java.util.*; import java.util.function.Consumer; +import static com.mybatisflex.core.query.QueryMethods.count; + public class MapperUtil { private MapperUtil() { } + /** + *

原生的、未经过优化的 COUNT 查询。抛开效率问题不谈,只关注结果的准确性, + * 这个 COUNT 查询查出来的分页总数据是 100% 正确的,不接受任何反驳。 + * + *

为什么这么说,因为是用子查询实现的,生成的 SQL 如下: + * + *

+     * {@code
+     * SELECT COUNT(*) AS `total` FROM ( ...用户构建的 SQL 语句... );
+     * }
+     * 
+ * + *

不进行 SQL 优化的时候,返回的就是这样的 COUNT 查询语句。 + */ + public static QueryWrapper rawCountQueryWrapper(QueryWrapper queryWrapper) { + return QueryWrapper.create() + .select(count().as("total")) + .from(queryWrapper); + } + + /** + * 优化 COUNT 查询语句。 + */ + public static QueryWrapper optimizeCountQueryWrapper(QueryWrapper queryWrapper) { + List selectColumns = CPI.getSelectColumns(queryWrapper); + List groupByColumns = CPI.getGroupByColumns(queryWrapper); + // 如果有 distinct 语句或者 group by 语句则不优化 + // 这种一旦优化了就会造成 count 语句查询出来的值不对 + if (hasDistinct(selectColumns) || hasGroupBy(groupByColumns)) { + return rawCountQueryWrapper(queryWrapper); + } + // 判断能不能清除 join 语句 + if (canClearJoins(queryWrapper)) { + CPI.setJoins(queryWrapper, null); + } + // 最后将最后面的 order by 移除掉 + // 将 select 里面的列换成 COUNT(*) AS `total` 就好了 + CPI.setOrderBys(queryWrapper, null); + CPI.setSelectColumns(queryWrapper, Collections.singletonList(count().as("total"))); + return queryWrapper; + } + + private static boolean hasDistinct(List selectColumns) { + if (CollectionUtil.isEmpty(selectColumns)) { + return false; + } + for (QueryColumn selectColumn : selectColumns) { + if (selectColumn instanceof DistinctQueryColumn) { + return true; + } + } + return false; + } + + private static boolean hasGroupBy(List groupByColumns) { + return CollectionUtil.isNotEmpty(groupByColumns); + } + + private static boolean canClearJoins(QueryWrapper queryWrapper) { + List joins = CPI.getJoins(queryWrapper); + if (CollectionUtil.isEmpty(joins)) { + return false; + } + + // 只有全是 left join 语句才会清除 join + // 因为如果是 inner join 或 right join 往往都会放大记录数 + for (Join join : joins) { + if (!Join.TYPE_LEFT.equals(CPI.getJoinType(join))) { + return false; + } + } + + // 获取 join 语句中使用到的表名 + List joinTables = new ArrayList<>(); +// Map joinTables = new HashMap<>(); + joins.forEach(join -> { + QueryTable joinQueryTable = CPI.getJoinQueryTable(join); + if (joinQueryTable != null && StringUtil.isNotBlank(joinQueryTable.getName())) { + joinTables.add(joinQueryTable.getName()); + } + }); + + // 获取 where 语句中的条件 + QueryCondition where = CPI.getWhereQueryCondition(queryWrapper); + + // 最后判断一下 where 中是否用到了 join 的表 + return !CPI.containsTable(where, CollectionUtil.toArrayString(joinTables)); + } + + + @SuppressWarnings({"rawtypes", "unchecked"}) public static void queryFields(BaseMapper mapper, List list, Consumer>[] consumers) { if (CollectionUtil.isEmpty(list) || ArrayUtil.isEmpty(consumers) || consumers[0] == null) { return; @@ -69,6 +162,10 @@ public class MapperUtil { private static Class getWrapType(Class type) { + if (ClassUtil.canInstance(type.getModifiers())) { + return type; + } + if (List.class.isAssignableFrom(type)) { return ArrayList.class; } diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/test/java/com/mybatisflex/test/common/MapperUtilTest.java b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/test/java/com/mybatisflex/test/common/MapperUtilTest.java new file mode 100644 index 00000000..ea1f2705 --- /dev/null +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/test/java/com/mybatisflex/test/common/MapperUtilTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022-2023, Mybatis-Flex (fuhai999@gmail.com). + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mybatisflex.test.common; + +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.core.util.MapperUtil; +import org.junit.jupiter.api.Test; + +import static com.mybatisflex.test.model.table.RoleTableDef.ROLE; +import static com.mybatisflex.test.model.table.UserRoleTableDef.USER_ROLE; +import static com.mybatisflex.test.model.table.UserTableDef.USER; + +/** + * @author 王帅 + * @since 2023-06-09 + */ +class MapperUtilTest { + + @Test + void testOptimizeCountQueryWrapper() { + QueryWrapper queryWrapper = QueryWrapper.create() + .select(USER.USER_ID, USER.USER_NAME, ROLE.ALL_COLUMNS) + .from(USER.as("u")) + .leftJoin(USER_ROLE).as("ur").on(USER_ROLE.USER_ID.eq(USER.USER_ID)) + .leftJoin(ROLE).as("r").on(USER_ROLE.ROLE_ID.eq(ROLE.ROLE_ID)) + .where(USER.USER_ID.eq(3)) + .and(USER_ROLE.ROLE_ID.eq(6)) + .groupBy(ROLE.ROLE_ID); + System.out.println(queryWrapper.toSQL()); + System.out.println(MapperUtil.rawCountQueryWrapper(queryWrapper).toSQL()); + System.out.println(MapperUtil.optimizeCountQueryWrapper(queryWrapper).toSQL()); + } + +} \ No newline at end of file diff --git a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/test/java/com/mybatisflex/test/mapper/UserMapperTest.java b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/test/java/com/mybatisflex/test/mapper/UserMapperTest.java index 7a5487cd..2165ca0b 100644 --- a/mybatis-flex-test/mybatis-flex-spring-boot-test/src/test/java/com/mybatisflex/test/mapper/UserMapperTest.java +++ b/mybatis-flex-test/mybatis-flex-spring-boot-test/src/test/java/com/mybatisflex/test/mapper/UserMapperTest.java @@ -16,6 +16,7 @@ package com.mybatisflex.test.mapper; +import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.test.model.UserInfo; import com.mybatisflex.test.model.UserVO; @@ -39,6 +40,7 @@ import static com.mybatisflex.test.model.table.UserTableDef.USER; * @since 2023-06-07 */ @SpringBootTest +@SuppressWarnings("all") class UserMapperTest { @Autowired @@ -114,4 +116,21 @@ class UserMapperTest { userInfos.forEach(System.err::println); } + @Test + void testPage() { + QueryWrapper queryWrapper = QueryWrapper.create() + .select(USER.USER_ID, USER.USER_NAME, ROLE.ALL_COLUMNS) + .from(USER.as("u")) + .leftJoin(USER_ROLE).as("ur").on(USER_ROLE.USER_ID.eq(USER.USER_ID)) + .leftJoin(ROLE).as("r").on(USER_ROLE.ROLE_ID.eq(ROLE.ROLE_ID)); + System.err.println(queryWrapper.toSQL()); + Page page; + int pageNumber = 0; + do { + page = Page.of(++pageNumber, 1); + page = userMapper.paginateAs(page, queryWrapper, UserVO.class); + System.err.println(page); + } while (page.hasNext()); + } + } \ No newline at end of file