fix: toMany join性能优化

@RelationOneToMany和@RelationManyToMany的实现存在严重的性能问题。内存join算法的复杂度高达n^2
测试sql脚本init中设置编码格式避免中文乱码
This commit is contained in:
SWQXDBA 2024-02-28 14:02:28 +08:00
parent 83c3afe542
commit c61ff5865c
27 changed files with 197 additions and 126 deletions

View File

@ -26,9 +26,13 @@ import java.util.*;
class ToManyRelation<SelfEntity> extends AbstractRelation<SelfEntity> {
protected String mapKeyField;
protected FieldWrapper mapKeyFieldWrapper;
protected String orderBy;
protected long limit = 0;
protected String selfValueSplitBy;
@ -43,6 +47,14 @@ class ToManyRelation<SelfEntity> extends AbstractRelation<SelfEntity> {
);
}
public static Class<? extends Map> getMapWrapType(Class<?> type) {
if (ClassUtil.canInstance(type.getModifiers())) {
return (Class<? extends Map>) type;
}
return HashMap.class;
}
/**
* 构建查询目标对象的 QueryWrapper
*
@ -71,7 +83,6 @@ class ToManyRelation<SelfEntity> extends AbstractRelation<SelfEntity> {
return super.buildQueryWrapper(targetValues);
}
@Override
public void customizeQueryWrapper(QueryWrapper queryWrapper) {
if (StringUtil.isNotBlank(orderBy)) {
@ -86,67 +97,131 @@ class ToManyRelation<SelfEntity> extends AbstractRelation<SelfEntity> {
@SuppressWarnings("rawtypes")
@Override
public void join(List<SelfEntity> selfEntities, List<?> targetObjectList, List<Row> mappingRows) {
selfEntities.forEach(selfEntity -> {
Object selfValue = selfFieldWrapper.get(selfEntity);
if (selfValue != null) {
selfValue = selfValue.toString();
Set<String> targetMappingValues = new HashSet<>();
//目标表关联字段->目标表对象
Map<String, List<Object>> leftFieldToRightTableMap = new HashMap<>(targetObjectList.size());
for (Object targetObject : targetObjectList) {
Object targetJoinFieldValue = targetFieldWrapper.get(targetObject);
if (targetJoinFieldValue != null) {
leftFieldToRightTableMap.computeIfAbsent(targetJoinFieldValue.toString(), k -> new ArrayList<>()).add(targetObject);
}
}
//通过中间表
if (mappingRows != null) {
//当使用中间表时需要重新映射关联关系
Map<String, List<Object>> temp = new HashMap<>(selfEntities.size());
for (Row mappingRow : mappingRows) {
if (selfValue.equals(String.valueOf(mappingRow.getIgnoreCase(joinSelfColumn)))) {
Object joinValue = mappingRow.getIgnoreCase(joinTargetColumn);
if (joinValue != null) {
targetMappingValues.add(joinValue.toString());
Object midTableJoinSelfValue = mappingRow.getIgnoreCase(joinSelfColumn);
if (midTableJoinSelfValue == null) {
continue;
}
Object midTableJoinTargetValue = mappingRow.getIgnoreCase(joinTargetColumn);
if (midTableJoinTargetValue == null) {
continue;
}
List<Object> targetObjects = leftFieldToRightTableMap.get(midTableJoinTargetValue.toString());
if (targetObjects == null) {
continue;
}
temp.computeIfAbsent(midTableJoinSelfValue.toString(), k -> new ArrayList<>(targetObjects.size())).addAll(targetObjects);
}
leftFieldToRightTableMap = temp;
}
//关联集合的类型
Class<?> fieldType = relationFieldWrapper.getFieldType();
Class<?> wrapType;
if (Map.class.isAssignableFrom(fieldType)) {
wrapType = getMapWrapType(fieldType);
} else {
if (StringUtil.isNotBlank(selfValueSplitBy)) {
wrapType = MapperUtil.getCollectionWrapType(fieldType);
}
for (SelfEntity selfEntity : selfEntities) {
if (selfEntity == null) {
continue;
}
Object selfValue = selfFieldWrapper.get(selfEntity);
if (selfValue == null) {
continue;
}
selfValue = selfValue.toString();
Set<String> targetMappingValues;//只有当splitBy不为空时才会有多个值
boolean splitMode = StringUtil.isNotBlank(selfValueSplitBy);
if (splitMode) {
String[] splitValues = ((String) selfValue).split(selfValueSplitBy);
targetMappingValues.addAll(Arrays.asList(splitValues));
targetMappingValues = new LinkedHashSet<>(Arrays.asList(splitValues));
} else {
targetMappingValues = new HashSet<>(1);
targetMappingValues.add((String) selfValue);
}
}
if (targetMappingValues.isEmpty()) {
return;
}
Class<?> fieldType = relationFieldWrapper.getFieldType();
//map
if (Map.class.isAssignableFrom(fieldType)) {
Class<?> wrapType = getMapWrapType(fieldType);
Map map = (Map) ClassUtil.newInstance(wrapType);
for (Object targetObject : targetObjectList) {
Object targetValue = targetFieldWrapper.get(targetObject);
if (targetValue != null && targetMappingValues.contains(targetValue.toString())) {
Set<Object> validateCountSet = new HashSet<>(targetMappingValues.size());
for (String targetMappingValue : targetMappingValues) {
List<Object> targetObjects = leftFieldToRightTableMap.get(targetMappingValue);
//如果非真实外键约束 可能没有对应的对象
if (targetObjects == null) {
continue;
}
for (Object targetObject : targetObjects) {
Object keyValue = mapKeyFieldWrapper.get(targetObject);
Object needKeyValue = ConvertUtil.convert(keyValue, relationFieldWrapper.getKeyType());
if (validateCountSet.contains(needKeyValue) ) {
//当字段类型为Map时一个key对应的value只能有一个
throw FlexExceptions.wrap("When fieldType is Map, the target entity can only be one,\n" +
" current entity type is : " + selfEntity + "\n" +
" relation field name is : " + relationField.getName() + "\n" +
" target entity is : " + targetObjects);
}
validateCountSet.add(needKeyValue);
map.put(needKeyValue, targetObject);
}
}
if (!map.isEmpty()) {
relationFieldWrapper.set(map, selfEntity);
}
}
//集合
else {
Class<?> wrapType = MapperUtil.getCollectionWrapType(fieldType);
Collection collection = (Collection) ClassUtil.newInstance(wrapType);
for (Object targetObject : targetObjectList) {
Object targetValue = targetFieldWrapper.get(targetObject);
if (targetValue != null && targetMappingValues.contains(targetValue.toString())) {
if (onlyQueryValueField) {
//仅绑定某个字段
collection.add(FieldWrapper.of(targetObject.getClass(), valueField).get(targetObject));
} else {
collection.add(targetObject);
Object first = targetObjectList.iterator().next();
//将getter方法用单独的变量存储 FieldWrapper.of虽然有缓存 但每次调用至少有一个HashMap的get开销
FieldWrapper fieldValueFieldWrapper = FieldWrapper.of(first.getClass(), valueField);
for (String targetMappingValue : targetMappingValues) {
List<Object> targetObjects = leftFieldToRightTableMap.get(targetMappingValue);
if (targetObjects == null) {
continue;
}
for (Object targetObject : targetObjects) {
//仅绑定某个字段
collection.add(fieldValueFieldWrapper.get(targetObject));
}
}
} else {
for (String targetMappingValue : targetMappingValues) {
List<Object> targetObjects = leftFieldToRightTableMap.get(targetMappingValue);
if (targetObjects == null) {
continue;
}
collection.addAll(targetObjects);
}
}
relationFieldWrapper.set(collection, selfEntity);
}
}
});
}
public void setMapKeyField(String mapKeyField) {
@ -160,12 +235,4 @@ class ToManyRelation<SelfEntity> extends AbstractRelation<SelfEntity> {
}
}
public static Class<? extends Map> getMapWrapType(Class<?> type) {
if (ClassUtil.canInstance(type.getModifiers())) {
return (Class<? extends Map>) type;
}
return HashMap.class;
}
}

View File

@ -45,7 +45,7 @@ public class ArticleTester {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -43,7 +43,7 @@ public class BatchTester {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap.getInstance()

View File

@ -38,7 +38,7 @@ public class CursorTestStarter {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -49,7 +49,7 @@ public class DbTest {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -48,7 +48,7 @@ public class DbTest273 {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema_273.sql")
.addScript("data273.sql")
.addScript("data273.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -29,7 +29,7 @@ public class DbTestStarter {
public static void main(String[] args) {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("schema.sql").setScriptEncoding("UTF-8")
// .addScript("data.sql")
.build();

View File

@ -40,7 +40,7 @@ public class EntityTestStarter {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -32,7 +32,7 @@ public class InsertWithPkTestStarter {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -32,7 +32,7 @@ public class JoinTestStarter {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -33,7 +33,7 @@ public class JoinTester {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -41,7 +41,7 @@ public class JoinWithDeleteColumnTestStarter {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -33,7 +33,7 @@ public class MapperProxyCacheTestStarter {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -30,7 +30,7 @@ public class MaskManagerTest {
.setType(EmbeddedDatabaseType.H2)
.setName("db1")
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap.getInstance()

View File

@ -38,7 +38,7 @@ public class MultiDataSourceTester {
.setType(EmbeddedDatabaseType.H2)
.setName("db1")
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
HikariDataSource dataSource2 = new HikariDataSource();

View File

@ -28,6 +28,7 @@ public class MultiThreadsTest {
.setName("db1")
.addScript("schema.sql")
.addScript("data.sql")
.setScriptEncoding("UTF-8")
.build();
HikariDataSource dataSource2 = new HikariDataSource();

View File

@ -36,7 +36,7 @@ public class UpdateWrapperTest {
DataSource dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -42,7 +42,7 @@ public class Account6Test implements WithAssertions {
this.dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("none_key_schema.sql")
.addScript("none_key_data.sql")
.addScript("none_key_data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap()

View File

@ -44,7 +44,7 @@ public class Account7Test implements WithAssertions {
this.dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("generate_key_schema.sql")
.addScript("generate_key_data.sql")
.addScript("generate_key_data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap()

View File

@ -48,7 +48,7 @@ public class AccountInsertWithArrayAttrTest implements WithAssertions {
this.dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema05.sql")
.addScript("data05.sql")
.addScript("data05.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap()

View File

@ -67,7 +67,7 @@ public class AccountNativeTest implements WithAssertions {
this.dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("auto_increment_key_schema.sql")
.addScript("auto_increment_key_data.sql")
.addScript("auto_increment_key_data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap()

View File

@ -59,7 +59,7 @@ public class DbChainTest implements WithAssertions {
this.database = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("auto_increment_key_schema.sql")
.addScript("auto_increment_key_data.sql")
.addScript("auto_increment_key_data.sql").setScriptEncoding("UTF-8")
.build();
// Environment environment = new Environment(ENVIRONMENT_ID, new JdbcTransactionFactory(), this.database);

View File

@ -47,7 +47,7 @@ public class ListenerTest implements WithAssertions {
public void init() {
dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("auto_increment_key_schema.sql")
.addScript("auto_increment_key_schema.sql").setScriptEncoding("UTF-8")
.build();
// 注册全局监听器
FlexGlobalConfig defaultConfig = FlexGlobalConfig.getDefaultConfig();

View File

@ -70,7 +70,7 @@ public class RelationsTest implements WithAssertions {
dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("relation/onetoone/schema.sql")
.addScript("relation/onetoone/data.sql")
.addScript("relation/onetoone/data.sql").setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = MybatisFlexBootstrap.getInstance()

View File

@ -57,6 +57,7 @@ public class RowTest implements WithAssertions {
.setType(EmbeddedDatabaseType.H2)
.addScript("schema_row.sql")
.addScript("data_row.sql")
.setScriptEncoding("UTF-8")
.build();
new MybatisFlexBootstrap().setDataSource(DATA_SOURCE_KEY, dataSource)

View File

@ -42,11 +42,12 @@ import static com.mybatisflex.test.table.ArticleTableDef.ARTICLE;
public class UpdateChainTest implements WithAssertions {
private AccountMapper accountMapper;
private EmbeddedDatabase dataSource;
private static final String DATA_SOURCE_KEY = "ds2";
private AccountMapper accountMapper;
private EmbeddedDatabase dataSource;
@BeforeClass
public static void enableAudit() {
AuditManager.setAuditEnable(true);
@ -59,6 +60,7 @@ public class UpdateChainTest implements WithAssertions {
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.setScriptEncoding("UTF-8")
.build();
MybatisFlexBootstrap bootstrap = new MybatisFlexBootstrap()

View File

@ -41,7 +41,7 @@ public class AppConfig implements ApplicationListener<ContextRefreshedEvent> {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.addScript("data.sql").setScriptEncoding("UTF-8")
.build();
}