diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java index f97f5d7c..4bce68f3 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilder.java @@ -1,7 +1,6 @@ package org.hswebframework.ezorm.rdb.operator.builder.fragments.insert; import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.hswebframework.ezorm.core.RuntimeDefaultValue; @@ -77,10 +76,10 @@ public SqlRequest build(InsertOperatorParameter parameter) { //忽略null的列 if (ignoreNullColumn) { List values = parameter.getValues().get(0); - if (index >= values.size() - || values.get(index) instanceof NullValue - //为空并且没有默认值 - || (values.get(index) == null && !(columnMetadata.getDefaultValue() instanceof RuntimeDefaultValue))) { + Object value = index >= values.size() ? null : values.get(index); + //为空并且没有默认值 + if ((index >= values.size() || value == null || value instanceof NullValue) + && !(columnMetadata.getDefaultValue() instanceof RuntimeDefaultValue)) { index++; continue; } @@ -120,19 +119,21 @@ public SqlRequest build(InsertOperatorParameter parameter) { int idx = primaryIndex.get(0); if (idx < vSize) { Object idValue = values.get(idx); - if (idValue != null && !duplicatePrimary.add(idValue)) { + if (idValue != null + && !(idValue instanceof NullValue) + && !duplicatePrimary.add(idValue)) { continue; } } } // 唯一索引? else if (indexSize >= 1) { - Set dis = Sets.newHashSetWithExpectedSize(indexSize); + List dis = new ArrayList<>(indexSize); boolean allKeyPresent = true; for (Integer i : primaryIndex) { if (i < vSize) { Object value = values.get(i); - if (value != null) { + if (value != null && !(value instanceof NullValue)) { dis.add(value); } else { allKeyPresent = false; diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/DefaultSaveOrUpdateOperator.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/DefaultSaveOrUpdateOperator.java index eb1e96bc..248d0321 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/DefaultSaveOrUpdateOperator.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/DefaultSaveOrUpdateOperator.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.executor.NullValue; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; @@ -72,6 +73,7 @@ public SaveResultOperator execute(UpsertOperatorParameter parameter) { } protected Upsert createUpsert(UpsertOperatorParameter parameter) { + parameter = UpsertOperatorParameters.ensureRuntimeDefaultPrimaryKey(parameter, table); Map mapping = parameter.getColumns().stream() .collect(Collectors.toMap(InsertColumn::getColumn, Function.identity())); InsertSqlBuilder insertSqlBuilder = table.findFeatureNow(InsertSqlBuilder.ID); @@ -97,8 +99,8 @@ protected Upsert createUpsert(UpsertOperatorParameter parameter) { int index = 0; for (UpsertColumn column : columns) { if (column.getColumn().equals(id.getColumn())) { - Object idValue = value.get(index); - if (idValue == null) {//ID未指定则新增 + Object idValue = value.size() > index ? value.get(index) : null; + if (idValue == null || idValue instanceof NullValue) {//ID未指定则新增 insertParameter.getValues().add(value); continue V; } diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/UpsertOperatorParameters.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/UpsertOperatorParameters.java new file mode 100644 index 00000000..0fa16d96 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/UpsertOperatorParameters.java @@ -0,0 +1,58 @@ +package org.hswebframework.ezorm.rdb.operator.dml.upsert; + +import org.hswebframework.ezorm.core.RuntimeDefaultValue; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; + +import java.util.ArrayList; +import java.util.List; + +public final class UpsertOperatorParameters { + + private UpsertOperatorParameters() { + } + + public static UpsertOperatorParameter ensureRuntimeDefaultPrimaryKey(UpsertOperatorParameter parameter, + RDBTableMetadata table) { + RDBColumnMetadata primaryKey = table + .getColumns() + .stream() + .filter(RDBColumnMetadata::isPrimaryKey) + .findFirst() + .orElse(null); + + return ensureRuntimeDefaultPrimaryKey(parameter, primaryKey); + } + + public static UpsertOperatorParameter ensureRuntimeDefaultPrimaryKey(UpsertOperatorParameter parameter, + RDBColumnMetadata primaryKey) { + if (primaryKey == null + || !(primaryKey.getDefaultValue() instanceof RuntimeDefaultValue) + || hasColumn(parameter, primaryKey)) { + return parameter; + } + + UpsertOperatorParameter copy = new UpsertOperatorParameter(); + copy.setDoNothingOnConflict(parameter.isDoNothingOnConflict()); + copy.getWhere().addAll(parameter.getWhere()); + copy.getColumns().addAll(parameter.getColumns()); + copy.getColumns().add(UpsertColumn.of(primaryKey.getName(), false)); + + for (List values : parameter.getValues()) { + List newValues = new ArrayList<>(values.size() + 1); + newValues.addAll(values); + newValues.add(null); + copy.getValues().add(newValues); + } + return copy; + } + + private static boolean hasColumn(UpsertOperatorParameter parameter, RDBColumnMetadata column) { + for (UpsertColumn upsertColumn : parameter.getColumns()) { + if (column.equalsNameOrAlias(upsertColumn.getColumn())) { + return true; + } + } + return false; + } +} diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mssql/SqlServerBatchUpsertOperator.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mssql/SqlServerBatchUpsertOperator.java index 5a88d447..e3c0ee82 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mssql/SqlServerBatchUpsertOperator.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mssql/SqlServerBatchUpsertOperator.java @@ -23,7 +23,9 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuples; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,15 +41,12 @@ public class SqlServerBatchUpsertOperator implements SaveOrUpdateOperator { private RDBColumnMetadata idColumn; - private final SaveOrUpdateOperator fallback; - public SqlServerBatchUpsertOperator(RDBTableMetadata table) { this.table = table; this.builder = new UpsertBatchInsertSqlBuilder(table); this.idColumn = table.getColumns() .stream().filter(RDBColumnMetadata::isPrimaryKey) .findFirst().orElse(null); - this.fallback = new DefaultSaveOrUpdateOperator(table); } @Override @@ -61,11 +60,139 @@ public SaveResultOperator execute(org.hswebframework.ezorm.rdb.operator.dml.upse .orElse(null); if (this.idColumn == null) { - return fallback.execute(parameter); + InsertOperatorParameter insertParameter = createInsertParameter(parameter, -1); + insertParameter.setValues(parameter.getValues()); + return new InsertResultOperatorImpl(() -> createInsertSql(insertParameter)); } } - return new SaveResultOperatorImpl(() -> builder.build(new UpsertOperatorParameter(parameter))); + UpsertParameterSplit split = splitParameter(parameter); + + if (split.upsertParameter.getValues().isEmpty()) { + return new InsertResultOperatorImpl(() -> createInsertSql(split.insertParameter)); + } + if (split.insertParameter.getValues().isEmpty()) { + return new SaveResultOperatorImpl(() -> builder.build(new UpsertOperatorParameter(split.upsertParameter))); + } + + return new InsertAndUpsertResultOperatorImpl( + () -> createInsertSql(split.insertParameter), + () -> builder.build(new UpsertOperatorParameter(split.upsertParameter))); + } + + private UpsertParameterSplit splitParameter(org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertOperatorParameter parameter) { + int idIndex = indexOfIdColumn(parameter.getColumns()); + org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertOperatorParameter upsertParameter = + new org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertOperatorParameter(); + upsertParameter.setColumns(new LinkedHashSet<>(parameter.getColumns())); + upsertParameter.setWhere(parameter.getWhere()); + upsertParameter.setDoNothingOnConflict(parameter.isDoNothingOnConflict()); + + InsertOperatorParameter insertParameter = createInsertParameter(parameter, idIndex); + + for (List values : parameter.getValues()) { + if (hasIdValue(values, idIndex)) { + upsertParameter.getValues().add(values); + } else { + insertParameter.getValues().add(createInsertValues(values, idIndex)); + } + } + return new UpsertParameterSplit(insertParameter, upsertParameter); + } + + private int indexOfIdColumn(Set columns) { + if (idColumn == null) { + return -1; + } + int index = 0; + for (UpsertColumn column : columns) { + if (idColumn.equalsNameOrAlias(column.getColumn())) { + return index; + } + index++; + } + return -1; + } + + private boolean hasIdValue(List values, int idIndex) { + return idIndex >= 0 + && values.size() > idIndex + && values.get(idIndex) != null + && !(values.get(idIndex) instanceof NullValue); + } + + private InsertOperatorParameter createInsertParameter( + org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertOperatorParameter parameter, + int idIndex) { + InsertOperatorParameter insertParameter = new InsertOperatorParameter(); + boolean keepRuntimeDefaultId = useRuntimeDefaultId(); + if (idIndex < 0 && keepRuntimeDefaultId) { + insertParameter.getColumns().add(InsertColumn.of(idColumn.getName())); + } + int index = 0; + for (UpsertColumn column : parameter.getColumns()) { + if (index++ == idIndex && !keepRuntimeDefaultId) { + continue; + } + insertParameter.getColumns().add(column); + } + return insertParameter; + } + + private List createInsertValues(List values, int idIndex) { + if (!useRuntimeDefaultId()) { + return removeValue(values, idIndex); + } + if (idIndex >= 0) { + List newValues = new ArrayList<>(Math.max(values.size(), idIndex + 1)); + newValues.addAll(values); + while (newValues.size() <= idIndex) { + newValues.add(null); + } + if (newValues.get(idIndex) == null || newValues.get(idIndex) instanceof NullValue) { + newValues.set(idIndex, createRuntimeDefaultId()); + } + return newValues; + } + List newValues = new ArrayList<>(values.size() + 1); + newValues.add(createRuntimeDefaultId()); + newValues.addAll(values); + return newValues; + } + + private boolean useRuntimeDefaultId() { + return idColumn != null && idColumn.getDefaultValue() instanceof RuntimeDefaultValue; + } + + private Object createRuntimeDefaultId() { + return ((RuntimeDefaultValue) idColumn.getDefaultValue()).get(); + } + + private List removeValue(List values, int idIndex) { + if (idIndex < 0 || values.size() <= idIndex) { + return values; + } + List newValues = new ArrayList<>(values.size() - 1); + for (int i = 0; i < values.size(); i++) { + if (i != idIndex) { + newValues.add(values.get(i)); + } + } + return newValues; + } + + private SqlRequest createInsertSql(InsertOperatorParameter insertParameter) { + return table + .findFeatureNow(InsertSqlBuilder.ID) + .build(insertParameter); + } + + @AllArgsConstructor + private class UpsertParameterSplit { + + private InsertOperatorParameter insertParameter; + + private org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertOperatorParameter upsertParameter; } class UpsertOperatorParameter extends InsertOperatorParameter { @@ -107,6 +234,61 @@ public Mono reactive() { } } + @AllArgsConstructor + private class InsertResultOperatorImpl implements SaveResultOperator { + + Supplier sqlRequest; + + @Override + public SaveResult sync() { + return ExceptionUtils.translation(() -> { + SyncSqlExecutor sqlExecutor = table.findFeatureNow(SyncSqlExecutor.ID); + int inserted = sqlExecutor.update(sqlRequest.get()); + return SaveResult.of(inserted, 0); + }, table); + } + + @Override + public Mono reactive() { + return Mono + .fromSupplier(sqlRequest) + .as(table.findFeatureNow(ReactiveSqlExecutor.ID)::update) + .map(i -> SaveResult.of(i, 0)) + .as(ExceptionUtils.translation(table)); + } + } + + @AllArgsConstructor + private class InsertAndUpsertResultOperatorImpl implements SaveResultOperator { + + Supplier insertRequest; + + Supplier upsertRequest; + + @Override + public SaveResult sync() { + return ExceptionUtils.translation(() -> { + SyncSqlExecutor sqlExecutor = table.findFeatureNow(SyncSqlExecutor.ID); + int inserted = sqlExecutor.update(insertRequest.get()); + int updated = sqlExecutor.update(upsertRequest.get()); + return SaveResult.of(inserted, updated); + }, table); + } + + @Override + public Mono reactive() { + ReactiveSqlExecutor sqlExecutor = table.findFeatureNow(ReactiveSqlExecutor.ID); + return Mono + .fromSupplier(insertRequest) + .as(sqlExecutor::update) + .flatMap(inserted -> Mono + .fromSupplier(upsertRequest) + .as(sqlExecutor::update) + .map(updated -> SaveResult.of(inserted, updated))) + .as(ExceptionUtils.translation(table)); + } + } + private class UpsertBatchInsertSqlBuilder implements InsertSqlBuilder { private final RDBTableMetadata table; @@ -268,7 +450,7 @@ public SqlRequest build(InsertOperatorParameter parameter) { } } - if (update.isNotEmpty() || upsertParameter.doNoThingOnConflict) { + if (update.isNotEmpty() && !upsertParameter.doNoThingOnConflict) { fragments.addSql("when matched then update set"); fragments.addFragments(update); } diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mysql/MysqlBatchUpsertOperator.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mysql/MysqlBatchUpsertOperator.java index 611a4490..dc138d3c 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mysql/MysqlBatchUpsertOperator.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/mysql/MysqlBatchUpsertOperator.java @@ -73,11 +73,12 @@ protected boolean doFallback() { @Override public SaveResultOperator execute(UpsertOperatorParameter parameter) { + UpsertOperatorParameter upsertParameter = UpsertOperatorParameters.ensureRuntimeDefaultPrimaryKey(parameter, table); if (doFallback()) { - return fallback.execute(parameter); + return fallback.execute(upsertParameter); } return new MysqlSaveResultOperator(() -> builder - .build(new MysqlUpsertOperatorParameter(parameter)), parameter.getValues().size()); + .build(new MysqlUpsertOperatorParameter(upsertParameter)), upsertParameter.getValues().size()); } class MysqlUpsertOperatorParameter extends InsertOperatorParameter { diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/opengauss/OpengaussBatchUpsertOperator.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/opengauss/OpengaussBatchUpsertOperator.java index 65b3ed89..0735535e 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/opengauss/OpengaussBatchUpsertOperator.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/opengauss/OpengaussBatchUpsertOperator.java @@ -49,10 +49,11 @@ public OpengaussBatchUpsertOperator(RDBTableMetadata table) { @Override public SaveResultOperator execute(UpsertOperatorParameter parameter) { + UpsertOperatorParameter upsertParameter = UpsertOperatorParameters.ensureRuntimeDefaultPrimaryKey(parameter, table); if (getOrCreateOnConflict().isEmpty()) { - return fallback.execute(parameter); + return fallback.execute(upsertParameter); } - return new OpengaussSaveResultOperator(() -> builder.build(new OpengaussUpsertOperatorParameter(parameter))); + return new OpengaussSaveResultOperator(() -> builder.build(new OpengaussUpsertOperatorParameter(upsertParameter))); } SqlFragments getOrCreateOnConflict() { diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertOperator.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertOperator.java index ca9a3a5e..502a04c9 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertOperator.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertOperator.java @@ -27,7 +27,9 @@ import reactor.util.function.Tuples; import java.sql.JDBCType; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -42,14 +44,11 @@ public class OracleBatchUpsertOperator implements SaveOrUpdateOperator { private RDBColumnMetadata idColumn; - private final SaveOrUpdateOperator fallback; - public OracleBatchUpsertOperator(RDBTableMetadata table) { this.table = table; this.idColumn = table.getColumns() .stream().filter(RDBColumnMetadata::isPrimaryKey) .findFirst().orElse(null); - this.fallback = new DefaultSaveOrUpdateOperator(table); this.builder = new OracleUpsertBatchInsertSqlBuilder(table); } @@ -64,11 +63,138 @@ public SaveResultOperator execute(UpsertOperatorParameter parameter) { .orElse(null); if (this.idColumn == null) { - return fallback.execute(parameter); + InsertOperatorParameter insertParameter = createInsertParameter(parameter, -1); + insertParameter.setValues(parameter.getValues()); + return new InsertResultOperatorImpl(() -> createInsertSql(insertParameter)); + } + } + + UpsertParameterSplit split = splitParameter(parameter); + + if (split.upsertParameter.getValues().isEmpty()) { + return new InsertResultOperatorImpl(() -> createInsertSql(split.insertParameter)); + } + if (split.insertParameter.getValues().isEmpty()) { + return new OracleSaveResultOperator(() -> builder.build(new OracleUpsertOperatorParameter(split.upsertParameter))); + } + + return new InsertAndUpsertResultOperatorImpl( + () -> createInsertSql(split.insertParameter), + () -> builder.build(new OracleUpsertOperatorParameter(split.upsertParameter))); + } + + private UpsertParameterSplit splitParameter(UpsertOperatorParameter parameter) { + int idIndex = indexOfIdColumn(parameter.getColumns()); + UpsertOperatorParameter upsertParameter = new UpsertOperatorParameter(); + upsertParameter.setColumns(new LinkedHashSet<>(parameter.getColumns())); + upsertParameter.setWhere(parameter.getWhere()); + upsertParameter.setDoNothingOnConflict(parameter.isDoNothingOnConflict()); + + InsertOperatorParameter insertParameter = createInsertParameter(parameter, idIndex); + + for (List values : parameter.getValues()) { + if (hasIdValue(values, idIndex)) { + upsertParameter.getValues().add(values); + } else { + insertParameter.getValues().add(createInsertValues(values, idIndex)); + } + } + return new UpsertParameterSplit(insertParameter, upsertParameter); + } + + private int indexOfIdColumn(Set columns) { + if (idColumn == null) { + return -1; + } + int index = 0; + for (UpsertColumn column : columns) { + if (idColumn.equalsNameOrAlias(column.getColumn())) { + return index; + } + index++; + } + return -1; + } + + private boolean hasIdValue(List values, int idIndex) { + return idIndex >= 0 + && values.size() > idIndex + && values.get(idIndex) != null + && !(values.get(idIndex) instanceof NullValue); + } + + private InsertOperatorParameter createInsertParameter( + UpsertOperatorParameter parameter, + int idIndex) { + InsertOperatorParameter insertParameter = new InsertOperatorParameter(); + boolean keepRuntimeDefaultId = useRuntimeDefaultId(); + if (idIndex < 0 && keepRuntimeDefaultId) { + insertParameter.getColumns().add(InsertColumn.of(idColumn.getName())); + } + int index = 0; + for (UpsertColumn column : parameter.getColumns()) { + if (index++ == idIndex && !keepRuntimeDefaultId) { + continue; } + insertParameter.getColumns().add(column); } + return insertParameter; + } + + private List createInsertValues(List values, int idIndex) { + if (!useRuntimeDefaultId()) { + return removeValue(values, idIndex); + } + if (idIndex >= 0) { + List newValues = new ArrayList<>(Math.max(values.size(), idIndex + 1)); + newValues.addAll(values); + while (newValues.size() <= idIndex) { + newValues.add(null); + } + if (newValues.get(idIndex) == null || newValues.get(idIndex) instanceof NullValue) { + newValues.set(idIndex, createRuntimeDefaultId()); + } + return newValues; + } + List newValues = new ArrayList<>(values.size() + 1); + newValues.add(createRuntimeDefaultId()); + newValues.addAll(values); + return newValues; + } + + private boolean useRuntimeDefaultId() { + return idColumn != null && idColumn.getDefaultValue() instanceof RuntimeDefaultValue; + } + + private Object createRuntimeDefaultId() { + return ((RuntimeDefaultValue) idColumn.getDefaultValue()).get(); + } + + private List removeValue(List values, int idIndex) { + if (idIndex < 0 || values.size() <= idIndex) { + return values; + } + List newValues = new ArrayList<>(values.size() - 1); + for (int i = 0; i < values.size(); i++) { + if (i != idIndex) { + newValues.add(values.get(i)); + } + } + return newValues; + } + + private SqlRequest createInsertSql(InsertOperatorParameter insertParameter) { + return table + .findFeatureNow(InsertSqlBuilder.ID) + .build(insertParameter); + } + + @AllArgsConstructor + private class UpsertParameterSplit { - return new PostgresqlSaveResultOperator(() -> builder.build(new OracleUpsertOperatorParameter(parameter))); + private InsertOperatorParameter insertParameter; + + private UpsertOperatorParameter upsertParameter; } class OracleUpsertOperatorParameter extends InsertOperatorParameter { @@ -87,7 +213,7 @@ public OracleUpsertOperatorParameter(UpsertOperatorParameter parameter) { } @AllArgsConstructor - private class PostgresqlSaveResultOperator implements SaveResultOperator { + private class OracleSaveResultOperator implements SaveResultOperator { Supplier sqlRequest; @@ -110,6 +236,61 @@ public Mono reactive() { } } + @AllArgsConstructor + private class InsertResultOperatorImpl implements SaveResultOperator { + + Supplier sqlRequest; + + @Override + public SaveResult sync() { + return ExceptionUtils.translation(() -> { + SyncSqlExecutor sqlExecutor = table.findFeatureNow(SyncSqlExecutor.ID); + int inserted = sqlExecutor.update(sqlRequest.get()); + return SaveResult.of(inserted, 0); + }, table); + } + + @Override + public Mono reactive() { + return Mono + .fromSupplier(sqlRequest) + .as(table.findFeatureNow(ReactiveSqlExecutor.ID)::update) + .map(i -> SaveResult.of(i, 0)) + .as(ExceptionUtils.translation(table)); + } + } + + @AllArgsConstructor + private class InsertAndUpsertResultOperatorImpl implements SaveResultOperator { + + Supplier insertRequest; + + Supplier upsertRequest; + + @Override + public SaveResult sync() { + return ExceptionUtils.translation(() -> { + SyncSqlExecutor sqlExecutor = table.findFeatureNow(SyncSqlExecutor.ID); + int inserted = sqlExecutor.update(insertRequest.get()); + int updated = sqlExecutor.update(upsertRequest.get()); + return SaveResult.of(inserted, updated); + }, table); + } + + @Override + public Mono reactive() { + ReactiveSqlExecutor sqlExecutor = table.findFeatureNow(ReactiveSqlExecutor.ID); + return Mono + .fromSupplier(insertRequest) + .as(sqlExecutor::update) + .flatMap(inserted -> Mono + .fromSupplier(upsertRequest) + .as(sqlExecutor::update) + .map(updated -> SaveResult.of(inserted, updated))) + .as(ExceptionUtils.translation(table)); + } + } + static SqlFragments UNION_ALL = SqlFragments.of("union all "), L_SELECT = SqlFragments.of("(select"), FROM_DUAL_R = SqlFragments.of("from dual) "); @@ -285,7 +466,7 @@ public SqlRequest build(InsertOperatorParameter parameter) { } } - if (update.isNotEmpty() || upsertParameter.doNoThingOnConflict) { + if (update.isNotEmpty() && !upsertParameter.doNoThingOnConflict) { fragments.addSql("when matched then update set"); fragments.addFragments(update); } diff --git a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBatchUpsertOperator.java b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBatchUpsertOperator.java index 323f8553..85808ba0 100644 --- a/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBatchUpsertOperator.java +++ b/hsweb-easy-orm-rdb/src/main/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBatchUpsertOperator.java @@ -43,10 +43,11 @@ public PostgresqlBatchUpsertOperator(RDBTableMetadata table) { @Override public SaveResultOperator execute(UpsertOperatorParameter parameter) { + UpsertOperatorParameter upsertParameter = UpsertOperatorParameters.ensureRuntimeDefaultPrimaryKey(parameter, table); if (getOrCreateOnConflict().isEmpty()) { - return fallback.execute(parameter); + return fallback.execute(upsertParameter); } - return new PostgresqlSaveResultOperator(() -> builder.build(new PostgresqlUpsertOperatorParameter(parameter))); + return new PostgresqlSaveResultOperator(() -> builder.build(new PostgresqlUpsertOperatorParameter(upsertParameter))); } SqlFragments getOrCreateOnConflict() { diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilderTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilderTest.java index 495a55c6..189f755e 100644 --- a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilderTest.java +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/builder/fragments/insert/BatchInsertSqlBuilderTest.java @@ -1,8 +1,11 @@ package org.hswebframework.ezorm.rdb.operator.builder.fragments.insert; import org.hswebframework.ezorm.core.RuntimeDefaultValue; +import org.hswebframework.ezorm.rdb.executor.NullValue; import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; import org.hswebframework.ezorm.rdb.operator.builder.MetadataHelper; import org.hswebframework.ezorm.rdb.operator.dml.insert.InsertColumn; import org.hswebframework.ezorm.rdb.operator.dml.insert.InsertOperatorParameter; @@ -11,6 +14,7 @@ import org.junit.Test; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; public class BatchInsertSqlBuilderTest { @@ -43,6 +47,43 @@ public void testDefaultValue() { Assert.assertArrayEquals(request.getParameters(), new Object[]{"runtime_id"}); } + @Test + public void testNullValuePrimaryKeyUsesRuntimeDefaultForEachRow() { + RDBTableMetadata table = schema.getTable("test").orElseThrow(NullPointerException::new); + RDBColumnMetadata id = table.getColumn("id").orElseThrow(NullPointerException::new); + AtomicInteger idSequence = new AtomicInteger(); + id.setPrimaryKey(true); + id.setDefaultValue((RuntimeDefaultValue) () -> "runtime_id_" + idSequence.incrementAndGet()); + + InsertOperatorParameter insert = new InsertOperatorParameter(); + insert.getColumns().add(InsertColumn.of("id")); + insert.getColumns().add(InsertColumn.of("name")); + insert.getValues().add(Arrays.asList(NullValue.of(id.getType()), "test1")); + insert.getValues().add(Arrays.asList(NullValue.of(id.getType()), "test2")); + + SqlRequest request = builder.build(insert); + Assert.assertArrayEquals( + new Object[]{"runtime_id_1", "test1", "runtime_id_2", "test2"}, + request.getParameters()); + } + + @Test + public void testCompositePrimaryKeyDeduplicateByOrderedValues() { + RDBTableMetadata table = schema.getTable("test").orElseThrow(NullPointerException::new); + table.getColumn("id").orElseThrow(NullPointerException::new).setPrimaryKey(true); + table.getColumn("name").orElseThrow(NullPointerException::new).setPrimaryKey(true); + + InsertOperatorParameter insert = new InsertOperatorParameter(); + insert.getColumns().add(InsertColumn.of("id")); + insert.getColumns().add(InsertColumn.of("name")); + insert.getValues().add(Arrays.asList("a", "b")); + insert.getValues().add(Arrays.asList("b", "a")); + insert.getValues().add(Arrays.asList("a", "b")); + + SqlRequest request = builder.build(insert); + Assert.assertArrayEquals(new Object[]{"a", "b", "b", "a"}, request.getParameters()); + } + @Test public void test() { InsertOperatorParameter insert = new InsertOperatorParameter(); @@ -65,4 +106,4 @@ public void test() { System.out.println(request); } -} \ No newline at end of file +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/UpsertOperatorParametersTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/UpsertOperatorParametersTest.java new file mode 100644 index 00000000..35195f53 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/operator/dml/upsert/UpsertOperatorParametersTest.java @@ -0,0 +1,64 @@ +package org.hswebframework.ezorm.rdb.operator.dml.upsert; + +import org.hswebframework.ezorm.core.DefaultValue; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class UpsertOperatorParametersTest { + + @Test + public void testAppendRuntimeDefaultPrimaryKeyWhenMissing() { + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("test")); + + UpsertOperatorParameter result = UpsertOperatorParameters + .ensureRuntimeDefaultPrimaryKey(parameter, primaryKey("id", "id")); + + Assert.assertNotSame(parameter, result); + Assert.assertEquals(2, result.getColumns().size()); + Assert.assertEquals(2, result.getValues().get(0).size()); + } + + @Test + public void testDoNotAppendWhenPrimaryKeyNameMatchesIgnoreCase() { + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("ID", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("fixed-id", "test")); + + UpsertOperatorParameter result = UpsertOperatorParameters + .ensureRuntimeDefaultPrimaryKey(parameter, primaryKey("id", "id")); + + Assert.assertSame(parameter, result); + Assert.assertEquals(2, result.getColumns().size()); + Assert.assertEquals(2, result.getValues().get(0).size()); + } + + @Test + public void testDoNotAppendWhenPrimaryKeyAliasMatchesIgnoreCase() { + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("ID_ALIAS", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("fixed-id", "test")); + + UpsertOperatorParameter result = UpsertOperatorParameters + .ensureRuntimeDefaultPrimaryKey(parameter, primaryKey("id", "id_alias")); + + Assert.assertSame(parameter, result); + Assert.assertEquals(2, result.getColumns().size()); + Assert.assertEquals(2, result.getValues().get(0).size()); + } + + private static RDBColumnMetadata primaryKey(String name, String alias) { + RDBColumnMetadata column = new RDBColumnMetadata(); + column.setName(name); + column.setAlias(alias); + column.setPrimaryKey(true); + column.setDefaultValue(DefaultValue.runtime("generated-id")); + return column; + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/AbstractBatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/AbstractBatchUpsertIntegrationTest.java new file mode 100644 index 00000000..7824c7ad --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/AbstractBatchUpsertIntegrationTest.java @@ -0,0 +1,217 @@ +package org.hswebframework.ezorm.rdb.supports; + +import org.hswebframework.ezorm.core.RuntimeDefaultValue; +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.NullValue; +import org.hswebframework.ezorm.rdb.executor.SqlRequests; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers; +import org.hswebframework.ezorm.rdb.metadata.JdbcDataType; +import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.DefaultDatabaseOperator; +import org.junit.Assert; +import org.junit.Test; + +import java.sql.JDBCType; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers.lowerCase; + +public abstract class AbstractBatchUpsertIntegrationTest { + + protected abstract RDBSchemaMetadata getSchema(); + + protected abstract Dialect getDialect(); + + protected abstract SyncSqlExecutor getSqlExecutor(); + + @Test + public void testUpsertWithoutPrimaryKeyColumnUsesRuntimeDefaultOnRealDatabase() { + IntegrationContext context = newContext(); + AtomicInteger idSequence = new AtomicInteger(); + + try { + createTable(context, () -> "generated-" + idSequence.incrementAndGet()); + + context.operator + .dml() + .upsert(context.tableName) + .columns("name") + .values("test1") + .values("test2") + .execute() + .sync(); + + Map> rows = rowsByName(context); + Assert.assertEquals(2, rows.size()); + Assert.assertEquals("generated-1", rows.get("test1").get("id")); + Assert.assertEquals("generated-2", rows.get("test2").get("id")); + } finally { + dropTable(context); + } + } + + @Test + public void testNullPrimaryKeyValuesUseRuntimeDefaultOnRealDatabase() { + IntegrationContext context = newContext(); + AtomicInteger idSequence = new AtomicInteger(); + + try { + createTable(context, () -> "generated-" + idSequence.incrementAndGet()); + RDBTableMetadata table = context.database + .getCurrentSchema() + .getTable(context.tableName, false) + .orElseThrow(NullPointerException::new); + + context.operator + .dml() + .upsert(context.tableName) + .columns("id", "name") + .values(null, "null-id") + .values(NullValue.of(JdbcDataType.of(JDBCType.VARCHAR, String.class)), "typed-null-id") + .execute() + .sync(); + + Map> rows = rowsByName(context); + Assert.assertEquals(2, rows.size()); + Assert.assertEquals("generated-1", rows.get("null-id").get("id")); + Assert.assertEquals("generated-2", rows.get("typed-null-id").get("id")); + Assert.assertTrue(table.getColumn("id").isPresent()); + } finally { + dropTable(context); + } + } + + @Test + public void testSingleTypedNullPrimaryKeyUsesRuntimeDefaultOnRealDatabase() { + IntegrationContext context = newContext(); + AtomicInteger idSequence = new AtomicInteger(); + + try { + createTable(context, () -> "generated-" + idSequence.incrementAndGet()); + + context.operator + .dml() + .upsert(context.tableName) + .columns("id", "name") + .values(NullValue.of(JdbcDataType.of(JDBCType.VARCHAR, String.class)), "single-typed-null-id") + .execute() + .sync(); + + Map> rows = rowsByName(context); + Assert.assertEquals(1, rows.size()); + Assert.assertEquals("generated-1", rows.get("single-typed-null-id").get("id")); + } finally { + dropTable(context); + } + } + + @Test + public void testMixedPrimaryKeyValuesInsertAndUpdateOnRealDatabase() { + IntegrationContext context = newContext(); + AtomicInteger idSequence = new AtomicInteger(); + + try { + createTable(context, () -> "generated-" + idSequence.incrementAndGet()); + + context.operator + .dml() + .upsert(context.tableName) + .columns("id", "name") + .values("fixed-id", "first") + .execute() + .sync(); + + context.operator + .dml() + .upsert(context.tableName) + .columns("id", "name") + .values("fixed-id", "updated") + .values(null, "generated") + .execute() + .sync(); + + Map> rows = rowsByName(context); + Assert.assertEquals(2, rows.size()); + Assert.assertEquals("fixed-id", rows.get("updated").get("id")); + Assert.assertEquals("generated-1", rows.get("generated").get("id")); + } finally { + dropTable(context); + } + } + + private IntegrationContext newContext() { + RDBDatabaseMetadata database = new RDBDatabaseMetadata(getDialect()); + RDBSchemaMetadata schema = getSchema(); + database.addFeature(getSqlExecutor()); + database.addSchema(schema); + database.setCurrentSchema(schema); + + IntegrationContext context = new IntegrationContext(); + context.database = database; + context.operator = DefaultDatabaseOperator.of(database); + context.tableName = "UP_IT_" + Long.toString(System.nanoTime(), 36).toUpperCase(); + return context; + } + + private void createTable(IntegrationContext context, RuntimeDefaultValue idGenerator) { + context.operator + .ddl() + .createOrAlter(context.tableName) + .addColumn("id") + .defaultValueRuntime(idGenerator) + .primaryKey() + .varchar(32) + .commit() + .addColumn("name") + .varchar(64) + .commit() + .commit() + .sync(); + } + + private Map> rowsByName(IntegrationContext context) { + List> rows = context.operator + .dml() + .query(context.tableName) + .select("id", "name") + .fetch(lowerCase(ResultWrappers.mapList())) + .sync(); + + return rows + .stream() + .collect(Collectors.toMap( + row -> String.valueOf(row.get("name")), + row -> new HashMap<>(row) + )); + } + + private void dropTable(IntegrationContext context) { + try { + RDBTableMetadata table = context.database + .getCurrentSchema() + .getTable(context.tableName, false) + .orElse(null); + String tableName = table == null ? context.tableName : table.getFullName(); + context.operator + .sql() + .sync() + .execute(SqlRequests.of("drop table " + tableName)); + } catch (Throwable ignore) { + } + } + + private static class IntegrationContext { + private RDBDatabaseMetadata database; + private DatabaseOperator operator; + private String tableName; + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/h2/H2BatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/h2/H2BatchUpsertIntegrationTest.java new file mode 100644 index 00000000..eab4367b --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/h2/H2BatchUpsertIntegrationTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.ezorm.rdb.supports.h2; + +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.supports.AbstractBatchUpsertIntegrationTest; + +public class H2BatchUpsertIntegrationTest extends AbstractBatchUpsertIntegrationTest { + + @Override + protected RDBSchemaMetadata getSchema() { + return new H2SchemaMetadata("PUBLIC"); + } + + @Override + protected Dialect getDialect() { + return Dialect.H2; + } + + @Override + protected SyncSqlExecutor getSqlExecutor() { + return new TestSyncSqlExecutor(new H2ConnectionProvider()); + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mssql/MSSQLBatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mssql/MSSQLBatchUpsertIntegrationTest.java new file mode 100644 index 00000000..37121b2e --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mssql/MSSQLBatchUpsertIntegrationTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.ezorm.rdb.supports.mssql; + +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.supports.AbstractBatchUpsertIntegrationTest; + +public class MSSQLBatchUpsertIntegrationTest extends AbstractBatchUpsertIntegrationTest { + + @Override + protected RDBSchemaMetadata getSchema() { + return new SqlServerSchemaMetadata("dbo"); + } + + @Override + protected Dialect getDialect() { + return Dialect.MSSQL; + } + + @Override + protected SyncSqlExecutor getSqlExecutor() { + return new TestSyncSqlExecutor(new MSSQLConnectionProvider()); + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mssql/SqlServerBatchUpsertOperatorTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mssql/SqlServerBatchUpsertOperatorTest.java new file mode 100644 index 00000000..b2b0adcd --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mssql/SqlServerBatchUpsertOperatorTest.java @@ -0,0 +1,383 @@ +package org.hswebframework.ezorm.rdb.supports.mssql; + +import org.hswebframework.ezorm.core.DefaultValue; +import org.hswebframework.ezorm.core.RuntimeDefaultValue; +import org.hswebframework.ezorm.rdb.executor.NullValue; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper; +import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; +import org.hswebframework.ezorm.rdb.metadata.JdbcDataType; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.operator.dml.upsert.SaveOrUpdateOperator; +import org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertColumn; +import org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertOperatorParameter; +import org.junit.Assert; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.sql.JDBCType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class SqlServerBatchUpsertOperatorTest { + + @Test + public void testUpsertWithoutPrimaryKeyColumnFallbackToBatchInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + table.addColumn(column("id", true)); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("test1")); + parameter.getValues().add(Arrays.asList("test2")); + + SaveResult result = table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertEquals(2, result.getAdded()); + Assert.assertEquals(0, result.getUpdated()); + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().toLowerCase().startsWith("insert into")); + Assert.assertFalse(sqlExecutor.sqlRequest.getSql().toLowerCase().contains("merge into")); + Assert.assertFalse(sqlExecutor.sqlRequest.getSql().contains("[id]")); + Assert.assertEquals(2, sqlExecutor.sqlRequest.getParameters().length); + } + + @Test + public void testUpsertWithoutPrimaryKeyColumnUsesRuntimeDefaultWhenFallbackToInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("test")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("[id]")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequest.getParameters()) + .contains("generated-id")); + } + + @Test + public void testNullPrimaryKeyValueUsesRuntimeDefaultWhenFallbackToInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("[id]")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequest.getParameters()) + .contains("generated-id")); + } + + @Test + public void testNullValuePrimaryKeyUsesRuntimeDefaultWhenFallbackToInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList(NullValue.of(id.getType()), "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("[id]")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequest.getParameters()) + .contains("generated-id")); + } + + @Test + public void testBatchInsertWithoutPrimaryKeyColumnGeneratesRuntimeDefaultForEachRow() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + AtomicInteger idSequence = new AtomicInteger(); + id.setDefaultValue((RuntimeDefaultValue) () -> "generated-" + idSequence.incrementAndGet()); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("test1")); + parameter.getValues().add(Arrays.asList("test2")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + List parameters = Arrays.asList(sqlExecutor.sqlRequest.getParameters()); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("[id]")); + Assert.assertEquals(4, parameters.size()); + Assert.assertTrue(parameters.contains("generated-1")); + Assert.assertTrue(parameters.contains("generated-2")); + } + + @Test + public void testMixedPrimaryKeyValuesWithRuntimeDefaultSplitToInsertAndMerge() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("1", "with-id")); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertEquals(2, sqlExecutor.sqlRequests.size()); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().toLowerCase().startsWith("insert into")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().contains("[id]")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequests.get(0).getParameters()) + .contains("generated-id")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(1).getSql().toLowerCase().contains("merge into")); + } + + @Test + public void testReactiveMixedPrimaryKeyValuesWithRuntimeDefaultSplitToInsertAndMerge() { + CapturingReactiveSqlExecutor sqlExecutor = new CapturingReactiveSqlExecutor(); + RDBTableMetadata table = newTable(new CapturingSyncSqlExecutor(), sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("1", "with-id")); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .reactive() + .block(); + + Assert.assertEquals(2, sqlExecutor.sqlRequests.size()); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().toLowerCase().startsWith("insert into")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().contains("[id]")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequests.get(0).getParameters()) + .contains("generated-id")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(1).getSql().toLowerCase().contains("merge into")); + } + + @Test + public void testPrimaryKeyColumnMatchIgnoreCaseByAlias() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + table.addColumn(column("ID", "id", true)); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("1", "test")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().toLowerCase().contains("merge into")); + } + + @Test + public void testDoNothingOnConflictWithoutUpdateColumns() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + table.addColumn(column("id", true)); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.setDoNothingOnConflict(true); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", true)); + parameter.getValues().add(Arrays.asList("1", "test")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().toLowerCase().contains("merge into")); + Assert.assertFalse(sqlExecutor.sqlRequest.getSql().toLowerCase().contains("when matched then update set")); + } + + @Test + public void testMixedPrimaryKeyValuesSplitToInsertAndMerge() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + table.addColumn(column("id", true)); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("1", "with-id")); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + SaveResult result = table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertEquals(1, result.getAdded()); + Assert.assertEquals(2, result.getUpdated()); + Assert.assertEquals(2, sqlExecutor.sqlRequests.size()); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().toLowerCase().startsWith("insert into")); + Assert.assertFalse(sqlExecutor.sqlRequests.get(0).getSql().contains("[id]")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(1).getSql().toLowerCase().contains("merge into")); + } + + private static RDBTableMetadata newTable(CapturingSyncSqlExecutor sqlExecutor) { + return newTable(sqlExecutor, null); + } + + private static RDBTableMetadata newTable( + CapturingSyncSqlExecutor sqlExecutor, + CapturingReactiveSqlExecutor reactiveSqlExecutor) { + RDBDatabaseMetadata database = new RDBDatabaseMetadata(Dialect.MSSQL); + SqlServerSchemaMetadata schema = new SqlServerSchemaMetadata("dbo"); + + database.addFeature(sqlExecutor); + if (reactiveSqlExecutor != null) { + database.addFeature(reactiveSqlExecutor); + } + database.addSchema(schema); + database.setCurrentSchema(schema); + + return schema.newTable("upsert_test"); + } + + private static RDBColumnMetadata column(String name, boolean primaryKey) { + return column(name, name, primaryKey); + } + + private static RDBColumnMetadata column(String name, String alias, boolean primaryKey) { + RDBColumnMetadata column = new RDBColumnMetadata(); + column.setName(name); + column.setAlias(alias); + column.setPrimaryKey(primaryKey); + column.setLength(32); + column.setType(JdbcDataType.of(JDBCType.VARCHAR, String.class)); + return column; + } + + private static class CapturingSyncSqlExecutor implements SyncSqlExecutor { + + private SqlRequest sqlRequest; + + private final List sqlRequests = new ArrayList<>(); + + @Override + public int update(SqlRequest request) { + this.sqlRequest = request; + this.sqlRequests.add(request); + return request.getParameters().length; + } + + @Override + public void execute(SqlRequest request) { + this.sqlRequest = request; + } + + @Override + public R select(SqlRequest request, ResultWrapper wrapper) { + throw new UnsupportedOperationException(); + } + } + + private static class CapturingReactiveSqlExecutor implements ReactiveSqlExecutor { + + private SqlRequest sqlRequest; + + private final List sqlRequests = new ArrayList<>(); + + @Override + public Mono update(Publisher request) { + return Flux + .from(request) + .map(this::captureUpdate) + .reduce(0, Integer::sum); + } + + private int captureUpdate(SqlRequest request) { + this.sqlRequest = request; + this.sqlRequests.add(request); + return request.getParameters().length; + } + + @Override + public Mono execute(Publisher request) { + return Flux + .from(request) + .doOnNext(sqlRequest -> this.sqlRequest = sqlRequest) + .then(); + } + + @Override + public Flux select(Publisher request, ResultWrapper wrapper) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mysql/Mysql57BatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mysql/Mysql57BatchUpsertIntegrationTest.java new file mode 100644 index 00000000..6f402bfb --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mysql/Mysql57BatchUpsertIntegrationTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.ezorm.rdb.supports.mysql; + +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.supports.AbstractBatchUpsertIntegrationTest; + +public class Mysql57BatchUpsertIntegrationTest extends AbstractBatchUpsertIntegrationTest { + + @Override + protected RDBSchemaMetadata getSchema() { + return new MysqlSchemaMetadata("ezorm"); + } + + @Override + protected Dialect getDialect() { + return Dialect.MYSQL; + } + + @Override + protected SyncSqlExecutor getSqlExecutor() { + return new TestSyncSqlExecutor(new Mysql57ConnectionProvider()); + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mysql/Mysql8BatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mysql/Mysql8BatchUpsertIntegrationTest.java new file mode 100644 index 00000000..a90865ad --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/mysql/Mysql8BatchUpsertIntegrationTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.ezorm.rdb.supports.mysql; + +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.supports.AbstractBatchUpsertIntegrationTest; + +public class Mysql8BatchUpsertIntegrationTest extends AbstractBatchUpsertIntegrationTest { + + @Override + protected RDBSchemaMetadata getSchema() { + return new MysqlSchemaMetadata("ezorm"); + } + + @Override + protected Dialect getDialect() { + return Dialect.MYSQL; + } + + @Override + protected SyncSqlExecutor getSqlExecutor() { + return new TestSyncSqlExecutor(new Mysql8ConnectionProvider()); + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/opengauss/OpengaussBatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/opengauss/OpengaussBatchUpsertIntegrationTest.java new file mode 100644 index 00000000..45ddfff1 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/opengauss/OpengaussBatchUpsertIntegrationTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.ezorm.rdb.supports.opengauss; + +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.supports.AbstractBatchUpsertIntegrationTest; + +public class OpengaussBatchUpsertIntegrationTest extends AbstractBatchUpsertIntegrationTest { + + @Override + protected RDBSchemaMetadata getSchema() { + return new OpengaussSchemaMetadata("gaussdb"); + } + + @Override + protected Dialect getDialect() { + return new OpengaussDialect(); + } + + @Override + protected SyncSqlExecutor getSqlExecutor() { + return new TestSyncSqlExecutor(new OpengaussConnectionProvider()); + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertIntegrationTest.java new file mode 100644 index 00000000..fddb4a12 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertIntegrationTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.ezorm.rdb.supports.oracle; + +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.supports.AbstractBatchUpsertIntegrationTest; + +public class OracleBatchUpsertIntegrationTest extends AbstractBatchUpsertIntegrationTest { + + @Override + protected RDBSchemaMetadata getSchema() { + return new OracleSchemaMetadata("SYSTEM"); + } + + @Override + protected Dialect getDialect() { + return Dialect.ORACLE; + } + + @Override + protected SyncSqlExecutor getSqlExecutor() { + return new TestSyncSqlExecutor(new OracleConnectionProvider()); + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertOperatorTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertOperatorTest.java new file mode 100644 index 00000000..c99ee3ce --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/oracle/OracleBatchUpsertOperatorTest.java @@ -0,0 +1,358 @@ +package org.hswebframework.ezorm.rdb.supports.oracle; + +import org.hswebframework.ezorm.core.DefaultValue; +import org.hswebframework.ezorm.core.RuntimeDefaultValue; +import org.hswebframework.ezorm.rdb.executor.NullValue; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrapper; +import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; +import org.hswebframework.ezorm.rdb.metadata.JdbcDataType; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBDatabaseMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.operator.dml.upsert.SaveOrUpdateOperator; +import org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertColumn; +import org.hswebframework.ezorm.rdb.operator.dml.upsert.UpsertOperatorParameter; +import org.junit.Assert; +import org.junit.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.sql.JDBCType; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class OracleBatchUpsertOperatorTest { + + @Test + public void testUpsertWithoutPrimaryKeyColumnFallbackToBatchInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + table.addColumn(column("id", true)); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("test1")); + parameter.getValues().add(Arrays.asList("test2")); + + SaveResult result = table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertEquals(2, result.getAdded()); + Assert.assertEquals(0, result.getUpdated()); + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().toLowerCase().startsWith("insert all")); + Assert.assertFalse(sqlExecutor.sqlRequest.getSql().toLowerCase().contains("merge into")); + Assert.assertFalse(sqlExecutor.sqlRequest.getSql().contains("\"ID\"")); + Assert.assertEquals(2, sqlExecutor.sqlRequest.getParameters().length); + } + + @Test + public void testUpsertWithoutPrimaryKeyColumnUsesRuntimeDefaultWhenFallbackToInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("test")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("\"ID\"")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequest.getParameters()) + .contains("generated-id")); + } + + @Test + public void testNullPrimaryKeyValueUsesRuntimeDefaultWhenFallbackToInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("\"ID\"")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequest.getParameters()) + .contains("generated-id")); + } + + @Test + public void testNullValuePrimaryKeyUsesRuntimeDefaultWhenFallbackToInsert() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList(NullValue.of(id.getType()), "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("\"ID\"")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequest.getParameters()) + .contains("generated-id")); + } + + @Test + public void testBatchInsertWithoutPrimaryKeyColumnGeneratesRuntimeDefaultForEachRow() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + AtomicInteger idSequence = new AtomicInteger(); + id.setDefaultValue((RuntimeDefaultValue) () -> "generated-" + idSequence.incrementAndGet()); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("test1")); + parameter.getValues().add(Arrays.asList("test2")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + List parameters = Arrays.asList(sqlExecutor.sqlRequest.getParameters()); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().contains("\"ID\"")); + Assert.assertEquals(4, parameters.size()); + Assert.assertTrue(parameters.contains("generated-1")); + Assert.assertTrue(parameters.contains("generated-2")); + } + + @Test + public void testMixedPrimaryKeyValuesWithRuntimeDefaultSplitToInsertAndMerge() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("1", "with-id")); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertEquals(2, sqlExecutor.sqlRequests.size()); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().toLowerCase().startsWith("insert")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().contains("\"ID\"")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequests.get(0).getParameters()) + .contains("generated-id")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(1).getSql().toLowerCase().contains("merge into")); + } + + @Test + public void testReactiveMixedPrimaryKeyValuesWithRuntimeDefaultSplitToInsertAndMerge() { + CapturingReactiveSqlExecutor sqlExecutor = new CapturingReactiveSqlExecutor(); + RDBTableMetadata table = newTable(new CapturingSyncSqlExecutor(), sqlExecutor); + RDBColumnMetadata id = column("id", true); + id.setDefaultValue(DefaultValue.runtime("generated-id")); + table.addColumn(id); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("1", "with-id")); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .reactive() + .block(); + + Assert.assertEquals(2, sqlExecutor.sqlRequests.size()); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().toLowerCase().startsWith("insert")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().contains("\"ID\"")); + Assert.assertTrue(Arrays + .asList(sqlExecutor.sqlRequests.get(0).getParameters()) + .contains("generated-id")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(1).getSql().toLowerCase().contains("merge into")); + } + + @Test + public void testDoNothingOnConflictWithoutUpdateColumns() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + table.addColumn(column("id", true)); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.setDoNothingOnConflict(true); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", true)); + parameter.getValues().add(Arrays.asList("1", "test")); + + table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertNotNull(sqlExecutor.sqlRequest); + Assert.assertTrue(sqlExecutor.sqlRequest.getSql().toLowerCase().contains("merge into")); + Assert.assertFalse(sqlExecutor.sqlRequest.getSql().toLowerCase().contains("when matched then update set")); + } + + @Test + public void testMixedPrimaryKeyValuesSplitToInsertAndMerge() { + CapturingSyncSqlExecutor sqlExecutor = new CapturingSyncSqlExecutor(); + RDBTableMetadata table = newTable(sqlExecutor); + table.addColumn(column("id", true)); + table.addColumn(column("name", false)); + + UpsertOperatorParameter parameter = new UpsertOperatorParameter(); + parameter.getColumns().add(UpsertColumn.of("id", false)); + parameter.getColumns().add(UpsertColumn.of("name", false)); + parameter.getValues().add(Arrays.asList("1", "with-id")); + parameter.getValues().add(Arrays.asList(null, "without-id")); + + SaveResult result = table + .findFeatureNow(SaveOrUpdateOperator.ID) + .execute(parameter) + .sync(); + + Assert.assertEquals(1, result.getAdded()); + Assert.assertEquals(2, result.getUpdated()); + Assert.assertEquals(2, sqlExecutor.sqlRequests.size()); + Assert.assertTrue(sqlExecutor.sqlRequests.get(0).getSql().toLowerCase().startsWith("insert")); + Assert.assertFalse(sqlExecutor.sqlRequests.get(0).getSql().contains("\"ID\"")); + Assert.assertTrue(sqlExecutor.sqlRequests.get(1).getSql().toLowerCase().contains("merge into")); + } + + private static RDBTableMetadata newTable(CapturingSyncSqlExecutor sqlExecutor) { + return newTable(sqlExecutor, null); + } + + private static RDBTableMetadata newTable( + CapturingSyncSqlExecutor sqlExecutor, + CapturingReactiveSqlExecutor reactiveSqlExecutor) { + RDBDatabaseMetadata database = new RDBDatabaseMetadata(Dialect.ORACLE); + OracleSchemaMetadata schema = new OracleSchemaMetadata("PUBLIC"); + + database.addFeature(sqlExecutor); + if (reactiveSqlExecutor != null) { + database.addFeature(reactiveSqlExecutor); + } + database.addSchema(schema); + database.setCurrentSchema(schema); + + return schema.newTable("upsert_test"); + } + + private static RDBColumnMetadata column(String name, boolean primaryKey) { + RDBColumnMetadata column = new RDBColumnMetadata(); + column.setName(name); + column.setAlias(name); + column.setPrimaryKey(primaryKey); + column.setLength(32); + column.setType(JdbcDataType.of(JDBCType.VARCHAR, String.class)); + return column; + } + + private static class CapturingSyncSqlExecutor implements SyncSqlExecutor { + + private SqlRequest sqlRequest; + + private final List sqlRequests = new ArrayList<>(); + + @Override + public int update(SqlRequest request) { + this.sqlRequest = request; + this.sqlRequests.add(request); + return request.getParameters().length; + } + + @Override + public void execute(SqlRequest request) { + this.sqlRequest = request; + } + + @Override + public R select(SqlRequest request, ResultWrapper wrapper) { + throw new UnsupportedOperationException(); + } + } + + private static class CapturingReactiveSqlExecutor implements ReactiveSqlExecutor { + + private SqlRequest sqlRequest; + + private final List sqlRequests = new ArrayList<>(); + + @Override + public Mono update(Publisher request) { + return Flux + .from(request) + .map(this::captureUpdate) + .reduce(0, Integer::sum); + } + + private int captureUpdate(SqlRequest request) { + this.sqlRequest = request; + this.sqlRequests.add(request); + return request.getParameters().length; + } + + @Override + public Mono execute(Publisher request) { + return Flux + .from(request) + .doOnNext(sqlRequest -> this.sqlRequest = sqlRequest) + .then(); + } + + @Override + public Flux select(Publisher request, ResultWrapper wrapper) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBatchUpsertIntegrationTest.java b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBatchUpsertIntegrationTest.java new file mode 100644 index 00000000..011db143 --- /dev/null +++ b/hsweb-easy-orm-rdb/src/test/java/org/hswebframework/ezorm/rdb/supports/postgres/PostgresqlBatchUpsertIntegrationTest.java @@ -0,0 +1,25 @@ +package org.hswebframework.ezorm.rdb.supports.postgres; + +import org.hswebframework.ezorm.rdb.TestSyncSqlExecutor; +import org.hswebframework.ezorm.rdb.executor.SyncSqlExecutor; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.dialect.Dialect; +import org.hswebframework.ezorm.rdb.supports.AbstractBatchUpsertIntegrationTest; + +public class PostgresqlBatchUpsertIntegrationTest extends AbstractBatchUpsertIntegrationTest { + + @Override + protected RDBSchemaMetadata getSchema() { + return new PostgresqlSchemaMetadata("public"); + } + + @Override + protected Dialect getDialect() { + return Dialect.POSTGRES; + } + + @Override + protected SyncSqlExecutor getSqlExecutor() { + return new TestSyncSqlExecutor(new PostgresqlConnectionProvider()); + } +}