优化分页

This commit is contained in:
yulichang 2024-09-09 14:09:26 +08:00
parent 064f5ccaa9
commit 69713b1c39
20 changed files with 1011 additions and 21 deletions

View File

@ -75,4 +75,7 @@ public interface IAdapter {
}
return typeHandler;
}
default void checkCollectionPage() {
}
}

View File

@ -3,6 +3,7 @@ package com.github.yulichang.adapter.v33x;
import com.baomidou.mybatisplus.core.MybatisPlusVersion;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.github.yulichang.adapter.base.IAdapter;
@ -87,4 +88,9 @@ public class Adapter33x implements IAdapter {
}
return typeHandler;
}
@Override
public void checkCollectionPage() {
throw ExceptionUtils.mpe("page by main need MP version 3.5.6+, current version: " + MybatisPlusVersion.getVersion());
}
}

View File

@ -3,6 +3,7 @@ package com.github.yulichang.adapter.v3431;
import com.baomidou.mybatisplus.core.MybatisPlusVersion;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.github.yulichang.adapter.base.IAdapter;
@ -60,4 +61,9 @@ public class Adapter3431 implements IAdapter {
}
return typeHandler;
}
@Override
public void checkCollectionPage() {
throw ExceptionUtils.mpe("page by main need MP version 3.5.6+, current version: " + MybatisPlusVersion.getVersion());
}
}

View File

@ -1,5 +1,7 @@
package com.github.yulichang.adapter.v355;
import com.baomidou.mybatisplus.core.MybatisPlusVersion;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.github.yulichang.adapter.base.IAdapter;
import com.github.yulichang.adapter.jsqlparser.v46.JSqlParserHelperV46;
import org.apache.ibatis.session.Configuration;
@ -29,4 +31,9 @@ public class Adapter355 implements IAdapter {
}
return typeHandler;
}
@Override
public void checkCollectionPage() {
throw ExceptionUtils.mpe("page by main need MP version 3.5.6+, current version: " + MybatisPlusVersion.getVersion());
}
}

View File

@ -34,7 +34,6 @@ public class MPJInterceptorConfig {
}
}
@SuppressWarnings("unchecked")
private void replaceInterceptorChain(List<SqlSessionFactory> sqlSessionFactoryList) {
if (CollectionUtils.isEmpty(sqlSessionFactoryList)) {
return;
@ -46,6 +45,7 @@ public class MPJInterceptorConfig {
InterceptorChain chain = (InterceptorChain) interceptorChain.get(factory.getConfiguration());
Field interceptors = InterceptorChain.class.getDeclaredField("interceptors");
interceptors.setAccessible(true);
@SuppressWarnings("unchecked")
List<Interceptor> list = (List<Interceptor>) interceptors.get(chain);
if (CollectionUtils.isEmpty(list)) {
interceptors.set(chain, new InterceptorList<>());

View File

@ -20,8 +20,10 @@ import com.github.yulichang.toolkit.TableHelper;
import com.github.yulichang.toolkit.TableMap;
import com.github.yulichang.toolkit.support.ColumnCache;
import com.github.yulichang.wrapper.interfaces.MFunction;
import com.github.yulichang.wrapper.segments.PageInfo;
import com.github.yulichang.wrapper.segments.SelectCache;
import lombok.Getter;
import lombok.Setter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@ -58,6 +60,12 @@ public abstract class AptAbstractWrapper<T, Children extends AptAbstractWrapper<
protected boolean resultMap = false;
@Getter
protected boolean resultMapCollection = false;
@Setter
protected PageInfo pageInfo;
@Getter
protected boolean pageByMain = false;
/**
* 表序号
*/
@ -134,6 +142,29 @@ public abstract class AptAbstractWrapper<T, Children extends AptAbstractWrapper<
initNeed();
}
/**
* 根据主表分页
*/
public Children pageByMain() {
this.pageByMain = true;
this.pageInfo = new PageInfo();
return typedThis;
}
/**
* 根据主表分页
*/
public Children pageByMain(MFunction<PageInfo.PageInfoBuilder> function) {
this.pageByMain = true;
PageInfo.PageInfoBuilder apply = function.apply(PageInfo.builder());
this.pageInfo = apply.build();
return typedThis;
}
public PageInfo getPageInfo() {
return pageInfo == null ? new PageInfo() : pageInfo;
}
/**
* 设置表别名
* 设置表别名注意sql注入问题
@ -405,6 +436,8 @@ public abstract class AptAbstractWrapper<T, Children extends AptAbstractWrapper<
this.alias = ConfigProperties.tableAlias;
this.resultMap = false;
this.resultMapCollection = false;
this.pageInfo = null;
this.pageByMain = false;
this.tableIndex = 1;
this.dynamicTableName = false;
this.tableFunc = null;

View File

@ -14,9 +14,12 @@ import com.github.yulichang.extension.kt.interfaces.QueryJoin;
import com.github.yulichang.toolkit.*;
import com.github.yulichang.toolkit.support.ColumnCache;
import com.github.yulichang.wrapper.enums.PrefixEnum;
import com.github.yulichang.wrapper.interfaces.MFunction;
import com.github.yulichang.wrapper.segments.PageInfo;
import com.github.yulichang.wrapper.segments.SelectCache;
import kotlin.reflect.KProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@ -57,6 +60,12 @@ public abstract class KtAbstractLambdaWrapper<T, Children extends KtAbstractLamb
protected boolean resultMap = false;
@Getter
protected boolean resultMapCollection = false;
@Setter
protected PageInfo pageInfo;
@Getter
protected boolean pageByMain = false;
/**
* 表序号
*/
@ -168,6 +177,29 @@ public abstract class KtAbstractLambdaWrapper<T, Children extends KtAbstractLamb
}
}
/**
* 根据主表分页
*/
public Children pageByMain() {
this.pageByMain = true;
this.pageInfo = new PageInfo();
return typedThis;
}
/**
* 根据主表分页
*/
public Children pageByMain(MFunction<PageInfo.PageInfoBuilder> function) {
this.pageByMain = true;
PageInfo.PageInfoBuilder apply = function.apply(PageInfo.builder());
this.pageInfo = apply.build();
return typedThis;
}
public PageInfo getPageInfo() {
return pageInfo == null ? new PageInfo() : pageInfo;
}
/**
* 设置表别名
* 设置表别名注意sql注入问题
@ -448,6 +480,8 @@ public abstract class KtAbstractLambdaWrapper<T, Children extends KtAbstractLamb
this.alias = ConfigProperties.tableAlias;
this.resultMap = false;
this.resultMapCollection = false;
this.pageInfo = null;
this.pageByMain = false;
this.tableIndex = 1;
this.dynamicTableName = false;
this.tableFunc = null;

View File

@ -1,15 +1,14 @@
package com.github.yulichang.interceptor;
import com.baomidou.mybatisplus.core.MybatisPlusVersion;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.github.yulichang.adapter.AdapterHelper;
import com.github.yulichang.adapter.base.tookit.VersionUtils;
import com.github.yulichang.config.ConfigProperties;
import com.github.yulichang.interfaces.MPJBaseJoin;
import com.github.yulichang.toolkit.ReflectionKit;
import com.github.yulichang.toolkit.*;
import com.github.yulichang.toolkit.support.FieldCache;
import com.github.yulichang.wrapper.interfaces.SelectWrapper;
@ -20,6 +19,8 @@ import com.github.yulichang.wrapper.segments.SelectLabel;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMap;
@ -50,6 +51,7 @@ public class MPJInterceptor implements Interceptor {
private static final Map<String, Val> MS_MAPPER_CACHE = new ConcurrentHashMap<>();
private static final Map<String, Val> RES_MAPPER_CACHE = new ConcurrentHashMap<>();
private static final Log log = LogFactory.getLog(MPJInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
@ -75,7 +77,7 @@ public class MPJInterceptor implements Interceptor {
}
}
if (Objects.nonNull(rt)) {
args[0] = getMappedStatement(ms, rt, ew);
args[0] = getMappedStatement(ms, rt, ew, map);
}
}
}
@ -88,7 +90,7 @@ public class MPJInterceptor implements Interceptor {
/**
* 获取MappedStatement
*/
public <E> MappedStatement getMappedStatement(MappedStatement ms, Class<?> resultType, Object ew) {
public <E> MappedStatement getMappedStatement(MappedStatement ms, Class<?> resultType, Object ew, Map<String, Object> map) {
if (ew instanceof SelectWrapper) {
SelectWrapper<E, ?> wrapper = (SelectWrapper<E, ?>) ew;
if (wrapper.getEntityClass() == null) {
@ -97,6 +99,19 @@ public class MPJInterceptor implements Interceptor {
if (wrapper.getSelectColumns().isEmpty() && wrapper.getEntityClass() != null) {
wrapper.selectAll();
}
if (wrapper.isResultMapCollection()) {
if (map.values().stream().anyMatch(a -> a instanceof IPage)) {
if (wrapper.isPageByMain()) {
AdapterHelper.getAdapter().checkCollectionPage();
IPage<?> page = ParameterUtils.findPage(map).orElse(null);
wrapper.getPageInfo().setInnerPage(page);
map.entrySet().removeIf(entry -> entry.getValue() instanceof IPage);
} else {
// 一对多分页问题警告
log.warn("select one to many and page query will result in errors in the total count statistics, please use xml.");
}
}
}
}
return buildMappedStatement(ms, resultType, ew);
}

View File

@ -0,0 +1,119 @@
package com.github.yulichang.interceptor.pagination;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.plugins.pagination.DialectModel;
import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect;
import lombok.Getter;
import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.*;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* @author yulichang
* @since 1.5.0
*/
public class DialectWrapper {
private final IDialect dialect;
@Getter
private final List<ParameterMapping> fullMappings = new ArrayList<>();
@Getter
private final List<ParameterMapping> pageMappings = new ArrayList<>();
@Getter
private String finallySql;
public DialectWrapper(IDialect dialect) {
this.dialect = dialect;
}
public void buildPaginationSql(String originalSql, List<ParameterMapping> mappings, long offset, long limit,
Consumer<DialectModel> consumers, MappedStatement ms, Object parameter) {
try {
// 解析sql
Select select = (Select) JsqlParserGlobal.parse(originalSql);
if (select instanceof SetOperationList) {
throw ExceptionUtils.mpe("不支持 union 对多分页");
}
//获取 ? 出现的次数
String sourceSql = select.toString();
int count = ParseHelper.countChar(sourceSql);
String formatSql;
if (count == mappings.size()) {
formatSql = ParseHelper.decode(sourceSql);
} else {
SqlSource sqlSource = ms.getSqlSource();
if (sqlSource instanceof DynamicSqlSource) {
formatSql = ParseHelper.getOriginalSql(parameter, (DynamicSqlSource) sqlSource);
} else {
throw ExceptionUtils.mpe("unknown type: " + sqlSource.getClass().getName());
}
}
//分页的sql
PlainSelect pageSql = (PlainSelect) JsqlParserGlobal.parse(formatSql);
List<Join> joins = pageSql.getJoins();
if (CollectionUtils.isNotEmpty(joins)) {
pageSql.setJoins(null);
}
pageSql.setSelectItems(Collections.singletonList(new SelectItem<>(new Column().withColumnName("*"))));
// 替换回 ? 同步mappings顺序
String repSql = pageSql.toString();
Map<Integer, ParameterMapping> sortMap = new HashMap<>();
repSql = ParseHelper.encode(mappings, count, repSql, sortMap);
this.pageMappings.addAll(sortMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).collect(Collectors.toList()));
DialectModel dialectModel = dialect.buildPaginationSql(repSql, offset, limit);
consumers.accept(dialectModel);
// 带分页方言的sql
String formatDialectSql = ParseHelper.decode(dialectModel.getDialectSql(), i -> "s" + i);
//完整的sql
PlainSelect fullSql = (PlainSelect) JsqlParserGlobal.parse(formatSql);
PlainSelect finalPlainSelect = new PlainSelect();
finalPlainSelect.setSelectItems(fullSql.getSelectItems());
finalPlainSelect.setDistinct(fullSql.getDistinct());
finalPlainSelect.setFromItem(
new Table()
.withName("(" + formatDialectSql + ")")
.withAlias(new Alias(fullSql.getFromItem().getAlias().getName(), false)));
finalPlainSelect.setJoins(fullSql.getJoins());
String finalSql = finalPlainSelect.toString();
Map<Integer, ParameterMapping> finalSortMap = new HashMap<>();
//page部分
for (int i = 0; i < count; i++) {
String repStr = ParseHelper.format.apply("s" + i);
int i1 = finalSql.indexOf(repStr);
if (i1 != -1) {
finalSql = finalSql.replace(repStr, "?");
finalSortMap.put(i1, this.pageMappings.get(i));
}
}
//其他部分
finalSql = ParseHelper.encode(mappings, count, finalSql, finalSortMap);
this.fullMappings.addAll(finalSortMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).collect(Collectors.toList()));
this.finallySql = finalSql;
} catch (Exception e) {
throw ExceptionUtils.mpe("not support this sql, please use xml. error sql: " + originalSql);
}
}
}

View File

@ -0,0 +1,252 @@
package com.github.yulichang.interceptor.pagination;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.dialects.IDialect;
import com.github.yulichang.wrapper.interfaces.SelectWrapper;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author yulichang
* @since 1.5.0
*/
public class PageInnerInterceptor extends PaginationInnerInterceptor {
private static final Log log = LogFactory.getLog(PageInnerInterceptor.class);
public PageInnerInterceptor(PaginationInnerInterceptor pagination) {
super.setOptimizeJoin(true);
super.setDbType(pagination.getDbType());
super.setDialect(pagination.getDialect());
super.setOverflow(pagination.isOverflow());
super.setMaxLimit(pagination.getMaxLimit());
}
/**
* 执行count
*/
@Override
public boolean willDoQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
//没有wrapper 或者 不是对多查询 不做处理
SelectWrapper<?, ?> wrapper = findMPJWrapper(parameter).orElse(null);
if (wrapper == null || !wrapper.isPageByMain()) {
return true;
}
// copy super
IPage<?> page = wrapper.getPageInfo().getInnerPage();
if (page == null || page.getSize() < 0 || !page.searchCount() || resultHandler != Executor.NO_RESULT_HANDLER) {
return true;
}
BoundSql countSql;
MappedStatement countMs = buildCountMappedStatement(ms, page.countId());
if (countMs != null) {
countSql = countMs.getBoundSql(parameter);
} else {
countMs = buildAutoCountMappedStatement(ms);
ArrayList<ParameterMapping> mappingArrayList = new ArrayList<>(boundSql.getParameterMappings());
String countSqlStr = autoCountSql(boundSql.getSql(), mappingArrayList, ms, parameter, wrapper);
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
countSql = new BoundSql(countMs.getConfiguration(), countSqlStr, mappingArrayList, parameter);
PluginUtils.setAdditionalParameter(countSql, mpBoundSql.additionalParameters());
}
CacheKey cacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countSql);
List<Object> result = executor.query(countMs, parameter, rowBounds, resultHandler, cacheKey, countSql);
long total = 0;
if (CollectionUtils.isNotEmpty(result)) {
// 个别数据库 count 没数据不会返回 0
Object o = result.get(0);
if (o != null) {
total = Long.parseLong(o.toString());
}
}
page.setTotal(total);
return continuePage(page);
}
/**
* 添加分页方言
*/
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
SelectWrapper<?, ?> wrapper = findMPJWrapper(parameter).orElse(null);
if (wrapper == null || !wrapper.isPageByMain()) {
return;
}
// copy super
IPage<?> page = wrapper.getPageInfo().getInnerPage();
if (null == page) {
return;
}
// 处理 orderBy 拼接
boolean addOrdered = false;
String buildSql = boundSql.getSql();
List<OrderItem> orders = page.orders();
if (CollectionUtils.isNotEmpty(orders)) {
addOrdered = true;
buildSql = this.concatOrderBy(buildSql, orders);
}
// size 小于 0 且不限制返回值则不构造分页sql
Long _limit = page.maxLimit() != null ? page.maxLimit() : maxLimit;
if (page.getSize() < 0 && null == _limit) {
if (addOrdered) {
PluginUtils.mpBoundSql(boundSql).sql(buildSql);
}
return;
}
handlerLimit(page, _limit);
DialectWrapper dialect = findMPJDialect(executor);
final Configuration configuration = ms.getConfiguration();
PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
Map<String, Object> additionalParameter = mpBoundSql.additionalParameters();
dialect.buildPaginationSql(buildSql, boundSql.getParameterMappings(), page.offset(), page.getSize(),
c -> c.consumers(dialect.getPageMappings(), configuration, additionalParameter),
ms, parameter);
mpBoundSql.sql(dialect.getFinallySql());
mpBoundSql.parameterMappings(dialect.getFullMappings());
}
@Override
public void setProperties(Properties properties) {
super.setProperties(properties);
}
public String autoCountSql(String sql, List<ParameterMapping> mappings, MappedStatement ms, Object parameter, SelectWrapper<?, ?> wrapper) {
try {
Select select = (Select) JsqlParserGlobal.parse(sql);
if (select instanceof SetOperationList) {
throw ExceptionUtils.mpe("不支持 union 对多分页");
}
//获取 ? 出现的次数
String sourceSql = select.toString();
int count = ParseHelper.countChar(sourceSql);
String formatSql;
if (count == mappings.size()) {
formatSql = ParseHelper.decode(sourceSql);
} else {
SqlSource sqlSource = ms.getSqlSource();
if (sqlSource instanceof DynamicSqlSource) {
formatSql = ParseHelper.getOriginalSql(parameter, (DynamicSqlSource) sqlSource);
} else {
log.error("unknown type: " + sqlSource.getClass().getName());
throw ExceptionUtils.mpe("not support this sql, please use xml. error sql: " + sql);
}
}
PlainSelect ps = (PlainSelect) JsqlParserGlobal.parse(formatSql);
// 优化 order by 在非分组情况下
List<OrderByElement> orderBy = ps.getOrderByElements();
if (CollectionUtils.isNotEmpty(orderBy)) {
boolean canClean = true;
for (OrderByElement order : orderBy) {
// order by 里带参数,不去除order by
Expression expression = order.getExpression();
if (!(expression instanceof Column) && expression.toString().contains(StringPool.QUESTION_MARK)) {
canClean = false;
break;
}
}
if (canClean) {
ps.setOrderByElements(null);
}
}
List<Join> joins = ps.getJoins();
if (CollectionUtils.isNotEmpty(joins)) {
ps.setJoins(null);
}
String repSql;
Distinct distinct = ps.getDistinct();
GroupByElement groupBy = ps.getGroupBy();
String mainAlias = Optional.of(ps.getFromItem().getAlias().getName()).orElse("");
// 包含 distinctgroupBy 不优化
if (null != distinct || null != groupBy) {
if (wrapper.getPageInfo().getCountSelectSql() != null) {
ps.setSelectItems(Collections.singletonList(new SelectItem<>(new Column().withColumnName(wrapper.getPageInfo().getCountSelectSql()))));
} else {
ps.getSelectItems().removeIf(s -> {
if (s.getExpression() instanceof Column) {
Column column = (Column) s.getExpression();
return !mainAlias.equals(column.getTable().toString());
}
return false;
});
}
repSql = lowLevelCountSql(ps.toString());
} else {
// 优化 SQL
ps.setSelectItems(COUNT_SELECT_ITEM);
repSql = ps.toString();
}
// 替换回 ? 同步mappings顺序
Map<Integer, ParameterMapping> sortMap = new HashMap<>();
repSql = ParseHelper.encode(mappings, count, repSql, sortMap);
mappings.clear();
mappings.addAll(sortMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).collect(Collectors.toList()));
return repSql;
} catch (JSQLParserException e) {
logger.error("optimize this sql to a count sql has exception, sql:\"" + sql + "\", exception:\n" + e.getCause());
} catch (Exception e) {
logger.error("optimize this sql to a count sql has error, sql:\"" + sql + "\", exception:\n" + e);
}
throw ExceptionUtils.mpe("not support this sql, please use xml. error sql: " + sql);
}
protected DialectWrapper findMPJDialect(Executor executor) {
IDialect dialect = super.findIDialect(executor);
return new DialectWrapper(dialect);
}
public static Optional<SelectWrapper<?, ?>> findMPJWrapper(Object parameterObject) {
if (parameterObject != null) {
if (parameterObject instanceof Map) {
Map<?, ?> parameterMap = (Map<?, ?>) parameterObject;
for (Map.Entry<?, ?> entry : parameterMap.entrySet()) {
if (entry.getValue() != null && entry.getValue() instanceof SelectWrapper) {
return Optional.of((SelectWrapper<?, ?>) entry.getValue());
}
}
} else if (parameterObject instanceof SelectWrapper) {
return Optional.of((SelectWrapper<?, ?>) parameterObject);
}
}
return Optional.empty();
}
}

View File

@ -0,0 +1,111 @@
package com.github.yulichang.interceptor.pagination;
import com.baomidou.mybatisplus.core.toolkit.Assert;
import com.github.yulichang.toolkit.ReflectionKit;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.scripting.xmltags.SqlNode;
import org.apache.ibatis.session.Configuration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
/**
* sql 分页解析工具类
*
* @author yulichang
* @since 1.5.0
*/
@SuppressWarnings("unused")
public final class ParseHelper {
private static final Map<SqlSource, SqlSourceWrapper> SQL_SOURCE_CACHE = new ConcurrentHashMap<>();
private static final String prefix = "MPJ_Param_i_";
private static final String suffix = "_MPJ_Param_i";
private static final char placeholder_char = '?';
private static final String placeholder_str = String.valueOf(placeholder_char);
public static final Function<Object, String> format = index -> prefix + index + suffix;
public static int countChar(String str) {
return countChar(str, placeholder_char);
}
public static int countChar(String str, String tag) {
return countChar(str, tag.charAt(0));
}
public static int countChar(String str, char tag) {
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == tag) {
count++;
}
}
return count;
}
public static String decode(String str) {
return decode(str, null);
}
public static String decode(String str, Function<Object, String> formatter) {
return decode(str, formatter, placeholder_char);
}
public static String decode(String str, Function<Object, String> formatter, char placeholder) {
StringBuilder sb = new StringBuilder();
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == placeholder) {
sb.append(format.apply(formatter == null ? count : formatter.apply(count)));
count++;
} else {
sb.append(str.charAt(i));
}
}
return sb.toString();
}
public static String decode(String str, Function<Object, String> formatter, String placeholder) {
StringBuilder sb = new StringBuilder();
String[] split = str.split(placeholder);
for (int i = 0; i < split.length; i++) {
sb.append(split[i]);
if (i < split.length - 1) {
sb.append(format.apply(formatter == null ? i : formatter.apply(i)));
}
}
return sb.toString();
}
public static String encode(List<ParameterMapping> mappings, int count, String repSql, Map<Integer, ParameterMapping> sortMap) {
return encode(mappings, count, repSql, sortMap, placeholder_str);
}
public static String encode(List<ParameterMapping> mappings, int count, String repSql, Map<Integer, ParameterMapping> sortMap, String placeholder) {
for (int i = 0; i < count; i++) {
String repStr = ParseHelper.format.apply(i);
int i1 = repSql.indexOf(repStr);
if (i1 != -1) {
repSql = repSql.replace(repStr, placeholder);
sortMap.put(i1, mappings.get(i));
}
}
return repSql;
}
public static String getOriginalSql(Object parameter, DynamicSqlSource sqlSource) {
Assert.notNull(sqlSource, "sqlSource must not be null");
SqlSourceWrapper sqlSourceWrapper = SQL_SOURCE_CACHE.computeIfAbsent(sqlSource, key -> {
Configuration configuration = ReflectionKit.getFieldValue(sqlSource, "configuration");
SqlNode sqlNode = ReflectionKit.getFieldValue(sqlSource, "rootSqlNode");
return new SqlSourceWrapper(configuration, sqlNode);
});
return ParseHelper.decode(sqlSourceWrapper.getSql(parameter), null, ParseHelper.format.apply(""));
}
}

View File

@ -0,0 +1,159 @@
package com.github.yulichang.interceptor.pagination;
import org.apache.ibatis.builder.BaseBuilder;
import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.ParameterExpression;
import org.apache.ibatis.builder.SqlSourceBuilder;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.parsing.GenericTokenParser;
import org.apache.ibatis.parsing.TokenHandler;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.scripting.xmltags.DynamicContext;
import org.apache.ibatis.scripting.xmltags.SqlNode;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author yulichang
* @since 1.5.0
*/
public class SqlSourceWrapper {
private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName";
private final Configuration configuration;
private final SqlNode rootSqlNode;
public SqlSourceWrapper(Configuration configuration, SqlNode rootSqlNode) {
this.configuration = configuration;
this.rootSqlNode = rootSqlNode;
}
public String getSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
return parse(context.getSql(), parameterType, context.getBindings());
}
public String parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType,
additionalParameters) {
@Override
public String handleToken(String content) {
super.handleToken(content);
return ParseHelper.format.apply("");
}
};
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
String sql;
if (configuration.isShrinkWhitespacesInSql()) {
sql = parser.parse(SqlSourceBuilder.removeExtraWhitespaces(originalSql));
} else {
sql = parser.parse(originalSql);
}
return sql;
}
/**
* copy {@link org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler}
*/
@SuppressWarnings("all")
private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler {
private final List<ParameterMapping> parameterMappings = new ArrayList<>();
private final Class<?> parameterType;
private final MetaObject metaParameters;
public ParameterMappingTokenHandler(Configuration configuration, Class<?> parameterType,
Map<String, Object> additionalParameters) {
super(configuration);
this.parameterType = parameterType;
this.metaParameters = configuration.newMetaObject(additionalParameters);
}
public List<ParameterMapping> getParameterMappings() {
return parameterMappings;
}
@Override
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
private ParameterMapping buildParameterMapping(String content) {
Map<String, String> propertiesMap = parseParameterMapping(content);
String property = propertiesMap.get("property");
Class<?> propertyType;
if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
propertyType = metaParameters.getGetterType(property);
} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {
propertyType = parameterType;
} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {
propertyType = java.sql.ResultSet.class;
} else if (property == null || Map.class.isAssignableFrom(parameterType)) {
propertyType = Object.class;
} else {
MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());
if (metaClass.hasGetter(property)) {
propertyType = metaClass.getGetterType(property);
} else {
propertyType = Object.class;
}
}
ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);
Class<?> javaType = propertyType;
String typeHandlerAlias = null;
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
String name = entry.getKey();
String value = entry.getValue();
if ("javaType".equals(name)) {
javaType = resolveClass(value);
builder.javaType(javaType);
} else if ("jdbcType".equals(name)) {
builder.jdbcType(resolveJdbcType(value));
} else if ("mode".equals(name)) {
builder.mode(resolveParameterMode(value));
} else if ("numericScale".equals(name)) {
builder.numericScale(Integer.valueOf(value));
} else if ("resultMap".equals(name)) {
builder.resultMapId(value);
} else if ("typeHandler".equals(name)) {
typeHandlerAlias = value;
} else if ("jdbcTypeName".equals(name)) {
builder.jdbcTypeName(value);
} else if ("property".equals(name)) {
// Do Nothing
} else if ("expression".equals(name)) {
throw new BuilderException("Expression based parameters are not supported yet");
} else {
throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content
+ "}. Valid properties are " + PARAMETER_PROPERTIES);
}
}
if (typeHandlerAlias != null) {
builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));
}
return builder.build();
}
private Map<String, String> parseParameterMapping(String content) {
try {
return new ParameterExpression(content);
} catch (BuilderException ex) {
throw ex;
} catch (Exception ex) {
throw new BuilderException("Parsing error was found in mapping #{" + content
+ "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex);
}
}
}
}

View File

@ -1,10 +1,19 @@
package com.github.yulichang.toolkit;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.github.yulichang.adapter.AdapterHelper;
import com.github.yulichang.interceptor.MPJInterceptor;
import com.github.yulichang.interceptor.pagination.PageInnerInterceptor;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.plugin.Interceptor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
/**
@ -17,6 +26,8 @@ import java.util.function.Predicate;
*/
public class InterceptorList<E extends Interceptor> extends ArrayList<E> {
private static final Log log = LogFactory.getLog(InterceptorList.class);
public InterceptorList() {
super();
}
@ -29,6 +40,41 @@ public class InterceptorList<E extends Interceptor> extends ArrayList<E> {
super.removeIf(predicate);
super.add(mpjInterceptor);
}
try {
AdapterHelper.getAdapter().checkCollectionPage();
} catch (Exception e) {
return;
}
if (this.stream().anyMatch(i -> i instanceof MybatisPlusInterceptor)) {
MybatisPlusInterceptor mybatisPlusInterceptor = super.stream().filter(i -> i instanceof MybatisPlusInterceptor)
.map(i -> (MybatisPlusInterceptor) i).findFirst().orElse(null);
if (mybatisPlusInterceptor != null) {
try {
Field field = MybatisPlusInterceptor.class.getDeclaredField("interceptors");
field.setAccessible(true);
@SuppressWarnings("unchecked")
List<InnerInterceptor> interceptors = (List<InnerInterceptor>) field.get(mybatisPlusInterceptor);
Integer index = null;
PaginationInnerInterceptor paginationInnerInterceptor = null;
if (interceptors.stream().noneMatch(i -> i instanceof PageInnerInterceptor)) {
for (int i = 0; i < interceptors.size(); i++) {
InnerInterceptor innerInterceptor = interceptors.get(i);
if (innerInterceptor instanceof PaginationInnerInterceptor) {
paginationInnerInterceptor = (PaginationInnerInterceptor) innerInterceptor;
index = i;
break;
}
}
}
if (index != null) {
interceptors.add(index + 1, new PageInnerInterceptor(paginationInnerInterceptor));
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
}
@Override

View File

@ -71,16 +71,13 @@ public final class ReflectionKit {
* @param fieldName 字段名称
* @return 属性值
*/
public static Object getFieldValue(Object entity, String fieldName) {
Class<?> cls = entity.getClass();
Map<String, Field> fieldMaps = getFieldMap(cls);
public static <T> T getFieldValue(Object entity, String fieldName) {
try {
Field field = fieldMaps.get(fieldName);
Assert.notNull(field, "Error: NoSuchField in %s for %s. Cause:", cls.getSimpleName(), fieldName);
Field field = entity.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(entity);
return (T) field.get(entity);
} catch (ReflectiveOperationException e) {
throw ExceptionUtils.mpe("Error: Cannot read field in %s. Cause:", e, cls.getSimpleName());
throw ExceptionUtils.mpe("Error: Cannot read field in %s. Cause:", e, entity.getClass().getSimpleName());
}
}

View File

@ -14,9 +14,12 @@ import com.github.yulichang.config.enums.LogicDelTypeEnum;
import com.github.yulichang.toolkit.*;
import com.github.yulichang.toolkit.support.ColumnCache;
import com.github.yulichang.wrapper.enums.PrefixEnum;
import com.github.yulichang.wrapper.interfaces.MFunction;
import com.github.yulichang.wrapper.interfaces.QueryJoin;
import com.github.yulichang.wrapper.segments.PageInfo;
import com.github.yulichang.wrapper.segments.SelectCache;
import lombok.Getter;
import lombok.Setter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
@ -57,6 +60,12 @@ public abstract class JoinAbstractLambdaWrapper<T, Children extends JoinAbstract
*/
@Getter
protected boolean resultMapCollection = false;
@Setter
protected PageInfo pageInfo;
@Getter
protected boolean pageByMain = false;
/**
* 表序号
*/
@ -168,6 +177,29 @@ public abstract class JoinAbstractLambdaWrapper<T, Children extends JoinAbstract
}
}
/**
* 根据主表分页
*/
public Children pageByMain() {
this.pageByMain = true;
this.pageInfo = new PageInfo();
return typedThis;
}
/**
* 根据主表分页
*/
public Children pageByMain(MFunction<PageInfo.PageInfoBuilder> function) {
this.pageByMain = true;
PageInfo.PageInfoBuilder apply = function.apply(PageInfo.builder());
this.pageInfo = apply.build();
return typedThis;
}
public PageInfo getPageInfo() {
return pageInfo == null ? new PageInfo() : pageInfo;
}
/**
* 设置表别名
* 设置表别名注意sql注入问题
@ -475,6 +507,8 @@ public abstract class JoinAbstractLambdaWrapper<T, Children extends JoinAbstract
this.alias = ConfigProperties.tableAlias;
this.resultMap = false;
this.resultMapCollection = false;
this.pageInfo = null;
this.pageByMain = false;
this.tableIndex = 1;
this.dynamicTableName = false;
this.tableFunc = null;

View File

@ -1,6 +1,7 @@
package com.github.yulichang.wrapper.interfaces;
import com.github.yulichang.wrapper.resultmap.Label;
import com.github.yulichang.wrapper.segments.PageInfo;
import com.github.yulichang.wrapper.segments.Select;
import java.util.List;
@ -26,4 +27,10 @@ public interface SelectWrapper<Entity, Children> {
String getFrom();
String getAlias();
boolean isResultMapCollection();
PageInfo getPageInfo();
boolean isPageByMain();
}

View File

@ -0,0 +1,50 @@
package com.github.yulichang.wrapper.segments;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author yulichang
* @since 1.5.0
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageInfo implements Serializable {
private IPage<?> innerPage;
private String countSelectSql;
public static PageInfoBuilder builder() {
return new PageInfoBuilder();
}
@SuppressWarnings("unused")
public static class PageInfoBuilder {
private IPage<?> innerPage;
private String countSelectSql;
PageInfoBuilder() {
}
public PageInfoBuilder innerPage(IPage<?> innerPage) {
this.innerPage = innerPage;
return this;
}
public PageInfoBuilder countSelectSql(String countSelectSql) {
this.countSelectSql = countSelectSql;
return this;
}
public PageInfo build() {
return new PageInfo(this.innerPage, this.countSelectSql);
}
}
}

View File

@ -51,8 +51,6 @@ public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor page = new PaginationInnerInterceptor();
page.setOptimizeJoin(false);
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
@ -64,7 +62,11 @@ public class MybatisPlusConfig {
return !tableName.startsWith("user_tenant");
}
}));
PaginationInnerInterceptor page = new PaginationInnerInterceptor();
page.setOptimizeJoin(false);
interceptor.addInnerInterceptor(page);
interceptor.addInnerInterceptor(new SqlInterceptor());
return interceptor;
}

View File

@ -15,8 +15,8 @@ mybatis-plus:
logic-delete-value: true
logic-not-delete-value: false
banner: true
# configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus-join:
# 打印 mybatis plus join banner
@ -26,9 +26,9 @@ mybatis-plus-join:
logic-del-type: where
logging:
level:
com.github.yulichang: debug
#logging:
# level:
# com.github.yulichang: debug
---
spring:

View File

@ -0,0 +1,109 @@
package com.github.yulichang.test.join.unit;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.test.join.dto.UserDTO;
import com.github.yulichang.test.join.entity.AddressDO;
import com.github.yulichang.test.join.entity.UserDO;
import com.github.yulichang.test.util.ThreadLocalUtils;
import com.github.yulichang.toolkit.JoinWrappers;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class PageByMainTest {
@Test
void pageByMain() {
ThreadLocalUtils.set("SELECT t.id, t.pid, t.`name`, t.`json`, t.sex, t.head_img, t.create_time, t.address_id, " +
"t.address_id2, t.del, t.create_by, t.update_by, t1.id AS joina_id, t1.user_id, t1.area_id, t1.tel, " +
"t1.address, t1.del AS joina_del FROM (SELECT * FROM `user` t WHERE t.del = false AND (t.id <= ?) LIMIT ?) t " +
"LEFT JOIN address t1 ON (t1.user_id = t.id AND t1.user_id >= ? AND t1.del = false)");
MPJLambdaWrapper<UserDO> wrapper = JoinWrappers.lambda(UserDO.class)
.selectAll()
.selectCollection(AddressDO.class, UserDTO::getAddressList)
.leftJoin(AddressDO.class, on -> on
.eq(AddressDO::getUserId, UserDO::getId)
.ge(AddressDO::getUserId, 0))
.le(UserDO::getId, 15)
.logicDelToOn()
.pageByMain();
Page<UserDTO> page = wrapper.page(new Page<>(1, 8), UserDTO.class);
page.getRecords().forEach(System.out::println);
assert page.getRecords().size() == 8;
}
@Test
void pageByMain1() {
ThreadLocalUtils.set("SELECT t.id, t.pid, t.`name`, t.`json`, t.sex, t.head_img, t.create_time, t.address_id, t.address_id2," +
" t.del, t.create_by, t.update_by, concat('?', t.id) AS sdafsdfsdfsd, t1.id AS joina_id, t1.user_id, " +
"t1.area_id, t1.tel, t1.address, t1.del AS joina_del FROM (SELECT * FROM `user` t WHERE t.del = false " +
"AND (t.id <= ?) LIMIT ?) t LEFT JOIN address t1 ON (t1.user_id = t.id AND t1.user_id >= ? AND t1.del = false)");
MPJLambdaWrapper<UserDO> wrapper = JoinWrappers.lambda(UserDO.class)
.selectAll()
.selectFunc(() -> "concat('?',%s)", UserDO::getId, "sdafsdfsdfsd")
.selectCollection(AddressDO.class, UserDTO::getAddressList)
.leftJoin(AddressDO.class, on -> on
.eq(AddressDO::getUserId, UserDO::getId)
.ge(AddressDO::getUserId, 0))
.le(UserDO::getId, 15)
.logicDelToOn()
.pageByMain();
Page<UserDTO> page = wrapper.page(new Page<>(1, 8), UserDTO.class);
page.getRecords().forEach(System.out::println);
assert page.getRecords().size() == 8;
}
@Test
void test11() {
ThreadLocalUtils.set("SELECT t.id, t.pid, t.`name`, t.`json`, t.sex, t.head_img, t.create_time, t.address_id, " +
"t.address_id2, t.del, t.create_by, t.update_by, concat('?', t.id) AS sdafsdfsdfsd, t1.id AS joina_id, " +
"t1.user_id, t1.area_id, t1.tel, t1.address, t1.del AS joina_del FROM (SELECT * FROM `user` t " +
"WHERE t.del = false AND (t.id <= ?) GROUP BY t.id ORDER BY t.id DESC LIMIT ?) t LEFT JOIN address t1 ON " +
"(t1.user_id = t.id AND t1.user_id >= ? AND t1.del = false)");
MPJLambdaWrapper<UserDO> wrapper = JoinWrappers.lambda(UserDO.class)
.selectAll()
.selectFunc(() -> "concat('?',%s)", UserDO::getId, "sdafsdfsdfsd")
.selectCollection(AddressDO.class, UserDTO::getAddressList)
.leftJoin(AddressDO.class, on -> on
.eq(AddressDO::getUserId, UserDO::getId)
.ge(AddressDO::getUserId, 0))
.le(UserDO::getId, 15)
.orderByDesc(UserDO::getId)
.groupBy(UserDO::getId)
.logicDelToOn()
.pageByMain(f -> f.countSelectSql("1"));
Page<UserDTO> page = wrapper.page(new Page<>(1, 8), UserDTO.class);
page.getRecords().forEach(System.out::println);
assert page.getRecords().size() == 8;
}
@Test
void test13() {
ThreadLocalUtils.set("SELECT t.id, t.pid, t.`name`, t.`json`, t.sex, t.head_img, t.create_time, t.address_id, " +
"t.address_id2, t.del, t.create_by, t.update_by, concat('?', t.id) AS sdafsdfsdfsd, t1.id AS joina_id, " +
"t1.user_id, t1.area_id, t1.tel, t1.address, t1.del AS joina_del FROM (SELECT * FROM `user` t " +
"WHERE t.del = false AND (t.id <= ?) GROUP BY t.id ORDER BY t.id DESC LIMIT ?) t LEFT JOIN address t1 ON " +
"(t1.user_id = t.id AND t1.user_id >= ? AND t1.del = false)");
MPJLambdaWrapper<UserDO> wrapper = JoinWrappers.lambda(UserDO.class)
.selectAll()
.selectFunc(() -> "concat('?',%s)", UserDO::getId, "sdafsdfsdfsd")
.selectCollection(AddressDO.class, UserDTO::getAddressList)
.leftJoin(AddressDO.class, on -> on
.eq(AddressDO::getUserId, UserDO::getId)
.ge(AddressDO::getUserId, 0))
.le(UserDO::getId, 15)
.orderByDesc(UserDO::getId)
.groupBy(UserDO::getId)
.logicDelToOn()
.pageByMain();
Page<UserDTO> page = wrapper.page(new Page<>(1, 8), UserDTO.class);
page.getRecords().forEach(System.out::println);
assert page.getRecords().size() == 8;
}
}