diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 3d41122081..0a006abb26 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -4705,9 +4705,9 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun } /** - * Create a {@link MutationOperation} for an "upsert". + * Create a {@link MutationOperation} for a updating an optional table */ - public MutationOperation createUpsertOperation( + public MutationOperation createOptionalTableUpdateOperation( EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 7ad5d96798..f8264c188f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -872,11 +872,11 @@ public class H2Dialect extends Dialect { } @Override - public MutationOperation createUpsertOperation( + public MutationOperation createOptionalTableUpdateOperation( EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) { final H2SqlAstTranslator translator = new H2SqlAstTranslator<>( factory, optionalTableUpdate ); - return translator.visitUpsert( optionalTableUpdate ); + return translator.createMergeOperation( optionalTableUpdate ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java index 682206ea73..953a288343 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java @@ -14,7 +14,6 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; -import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteContainer; @@ -35,11 +34,7 @@ import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.exec.spi.JdbcOperation; -import org.hibernate.sql.model.MutationOperation; -import org.hibernate.sql.model.ast.ColumnValueBinding; -import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.sql.model.internal.TableInsertStandard; -import org.hibernate.sql.model.jdbc.UpsertOperation; import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty; @@ -48,7 +43,7 @@ import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpt * * @author Christian Beikov */ -public class H2SqlAstTranslator extends AbstractSqlAstTranslator { +public class H2SqlAstTranslator extends SqlAstTranslatorWithMerge { private boolean renderAsArray; @@ -311,148 +306,4 @@ public class H2SqlAstTranslator extends AbstractSqlAstT // Introduction of PERCENT support https://github.com/h2database/h2database/commit/f45913302e5f6ad149155a73763c0c59d8205849 return getDialect().getVersion().isSameOrAfter( 1, 4, 198 ); } - - public MutationOperation visitUpsert(OptionalTableUpdate optionalTableUpdate) { - // template: - // - // merge into [table] as t - // using values([bindings]) as s ([column-names]) - // on t.[key] = s.[key] - // when not matched - // then insert ... - // when matched - // and s.[columns] is null - // then delete - // when matched - // then update set ... - - renderMergeInto( optionalTableUpdate ); - appendSql( " " ); - renderMergeUsing( optionalTableUpdate ); - appendSql( " " ); - renderMergeOn( optionalTableUpdate ); - appendSql( " " ); - renderMergeInsert( optionalTableUpdate ); - appendSql( " " ); - renderMergeDelete( optionalTableUpdate ); - appendSql( " " ); - renderMergeUpdate( optionalTableUpdate ); - - return new UpsertOperation( - optionalTableUpdate.getMutatingTable().getTableMapping(), - optionalTableUpdate.getMutationTarget(), - getSql(), - getParameterBinders() - ); - } - - private void renderMergeInto(OptionalTableUpdate optionalTableUpdate) { - appendSql( "merge into " ); - appendSql( optionalTableUpdate.getMutatingTable().getTableName() ); - appendSql( " as t" ); - } - - private void renderMergeUsing(OptionalTableUpdate optionalTableUpdate) { - final List valueBindings = optionalTableUpdate.getValueBindings(); - final List keyBindings = optionalTableUpdate.getKeyBindings(); - - final StringBuilder columnList = new StringBuilder(); - - appendSql( "using values (" ); - - for ( int i = 0; i < keyBindings.size(); i++ ) { - final ColumnValueBinding keyBinding = keyBindings.get( i ); - if ( i > 0 ) { - appendSql( ", " ); - columnList.append( ", " ); - } - columnList.append( keyBinding.getColumnReference().getColumnExpression() ); - renderCasted( keyBinding.getValueExpression() ); - } - for ( int i = 0; i < valueBindings.size(); i++ ) { - appendSql( ", " ); - columnList.append( ", " ); - final ColumnValueBinding valueBinding = valueBindings.get( i ); - columnList.append( valueBinding.getColumnReference().getColumnExpression() ); - renderCasted( valueBinding.getValueExpression() ); - } - - appendSql( ") as s(" ); - appendSql( columnList.toString() ); - appendSql( ")" ); - } - - private void renderMergeOn(OptionalTableUpdate optionalTableUpdate) { - appendSql( "on " ); - - // todo : optimistic locks? - - final List keyBindings = optionalTableUpdate.getKeyBindings(); - for ( int i = 0; i < keyBindings.size(); i++ ) { - final ColumnValueBinding keyBinding = keyBindings.get( i ); - if ( i > 0 ) { - appendSql( " and " ); - } - keyBinding.getColumnReference().appendReadExpression( this, "t" ); - appendSql( "=" ); - keyBinding.getColumnReference().appendReadExpression( this, "s" ); - } - } - - private void renderMergeInsert(OptionalTableUpdate optionalTableUpdate) { - final List valueBindings = optionalTableUpdate.getValueBindings(); - final List keyBindings = optionalTableUpdate.getKeyBindings(); - - final StringBuilder valuesList = new StringBuilder(); - - appendSql( "when not matched then insert (" ); - for ( int i = 0; i < keyBindings.size(); i++ ) { - if ( i > 0 ) { - appendSql( ", " ); - valuesList.append( ", " ); - } - final ColumnValueBinding keyBinding = keyBindings.get( i ); - appendSql( keyBinding.getColumnReference().getColumnExpression() ); - keyBinding.getColumnReference().appendReadExpression( "s", valuesList::append ); - } - for ( int i = 0; i < valueBindings.size(); i++ ) { - appendSql( ", " ); - valuesList.append( ", " ); - final ColumnValueBinding valueBinding = valueBindings.get( i ); - appendSql( valueBinding.getColumnReference().getColumnExpression() ); - valueBinding.getColumnReference().appendReadExpression( "s", valuesList::append ); - } - - appendSql( ") values (" ); - appendSql( valuesList.toString() ); - appendSql( ")" ); - } - - private void renderMergeDelete(OptionalTableUpdate optionalTableUpdate) { - final List valueBindings = optionalTableUpdate.getValueBindings(); - - appendSql( " when matched " ); - for ( int i = 0; i < valueBindings.size(); i++ ) { - final ColumnValueBinding binding = valueBindings.get( i ); - appendSql( " and " ); - binding.getColumnReference().appendReadExpression( this, "s" ); - appendSql( " is null" ); - } - appendSql( " then delete" ); - } - - private void renderMergeUpdate(OptionalTableUpdate optionalTableUpdate) { - final List valueBindings = optionalTableUpdate.getValueBindings(); - - appendSql( " when matched then update set " ); - for ( int i = 0; i < valueBindings.size(); i++ ) { - final ColumnValueBinding binding = valueBindings.get( i ); - if ( i > 0 ) { - appendSql( ", " ); - } - binding.getColumnReference().appendColumnForWrite( this, "t" ); - appendSql( "=" ); - binding.getColumnReference().appendColumnForWrite( this, "s" ); - } - } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index fc8932b401..43713a0be4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -13,6 +13,7 @@ import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.util.Calendar; import java.util.Date; +import java.util.List; import java.util.TimeZone; import org.hibernate.LockMode; @@ -48,6 +49,7 @@ import org.hibernate.exception.LockTimeoutException; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.mapping.Column; +import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; @@ -61,6 +63,8 @@ import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.tool.schema.internal.StandardSequenceExporter; import org.hibernate.tool.schema.spi.Exporter; import org.hibernate.type.BasicType; @@ -75,8 +79,6 @@ import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; -import java.util.List; - import jakarta.persistence.TemporalType; import static org.hibernate.query.sqm.TemporalUnit.NANOSECOND; @@ -1076,4 +1078,14 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { // public String getEnableConstraintStatement(String tableName, String name) { // return "alter table " + tableName + " with check check constraint " + name; // } + + + @Override + public MutationOperation createOptionalTableUpdateOperation( + EntityMutationTarget mutationTarget, + OptionalTableUpdate optionalTableUpdate, + SessionFactoryImplementor factory) { + final SQLServerSqlAstTranslator translator = new SQLServerSqlAstTranslator<>( factory, optionalTableUpdate ); + return translator.createMergeOperation( optionalTableUpdate ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java index ba6849eced..6f819d4794 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java @@ -16,7 +16,6 @@ import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; -import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; @@ -36,6 +35,7 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.select.SortSpecification; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.internal.OptionalTableUpdate; import org.hibernate.type.SqlTypes; /** @@ -43,7 +43,7 @@ import org.hibernate.type.SqlTypes; * * @author Christian Beikov */ -public class SQLServerSqlAstTranslator extends AbstractSqlAstTranslator { +public class SQLServerSqlAstTranslator extends SqlAstTranslatorWithMerge { private static final String UNION_ALL = " union all "; @@ -440,4 +440,9 @@ public class SQLServerSqlAstTranslator extends Abstract TOP_ONLY, EMULATED; } + + protected void renderMergeStatement(OptionalTableUpdate optionalTableUpdate) { + super.renderMergeStatement( optionalTableUpdate ); + appendSql( ";" ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SqlAstTranslatorWithMerge.java b/hibernate-core/src/main/java/org/hibernate/dialect/SqlAstTranslatorWithMerge.java new file mode 100644 index 0000000000..ee34fe0864 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SqlAstTranslatorWithMerge.java @@ -0,0 +1,211 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.dialect; + +import java.util.List; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.sql.model.MutationOperation; +import org.hibernate.sql.model.ast.ColumnValueBinding; +import org.hibernate.sql.model.internal.OptionalTableUpdate; +import org.hibernate.sql.model.jdbc.MergeOperation; + +/** + * Base SqlAstTranslator for translators which support a full insert/update/delete MERGE statement + * + * @author Steve Ebersole + */ +public abstract class SqlAstTranslatorWithMerge extends AbstractSqlAstTranslator { + public SqlAstTranslatorWithMerge(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + } + + /** + * Create the MutationOperation for performing a MERGE + */ + public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableUpdate) { + renderMergeStatement( optionalTableUpdate ); + + return new MergeOperation( + optionalTableUpdate.getMutatingTable().getTableMapping(), + optionalTableUpdate.getMutationTarget(), + getSql(), + getParameterBinders() + ); + } + + protected void renderMergeStatement(OptionalTableUpdate optionalTableUpdate) { + // template: + // + // merge into [table] as t + // using values([bindings]) as s ([column-names]) + // on t.[key] = s.[key] + // when not matched + // then insert ... + // when matched + // and s.[columns] is null + // then delete + // when matched + // then update set ... + + renderMergeInto( optionalTableUpdate ); + appendSql( " " ); + renderMergeUsing( optionalTableUpdate ); + appendSql( " " ); + renderMergeOn( optionalTableUpdate ); + appendSql( " " ); + renderMergeInsert( optionalTableUpdate ); + appendSql( " " ); + renderMergeDelete( optionalTableUpdate ); + appendSql( " " ); + renderMergeUpdate( optionalTableUpdate ); + } + + protected void renderMergeInto(OptionalTableUpdate optionalTableUpdate) { + appendSql( "merge into " ); + appendSql( optionalTableUpdate.getMutatingTable().getTableName() ); + renderMergeTargetAlias(); + } + + protected void renderMergeTargetAlias() { + appendSql( " as t" ); + } + + protected void renderMergeUsing(OptionalTableUpdate optionalTableUpdate) { + appendSql( "using " ); + + renderMergeSource( optionalTableUpdate ); + } + + protected boolean wrapMergeSourceExpression() { + return true; + } + + private void renderMergeSource(OptionalTableUpdate optionalTableUpdate) { + if ( wrapMergeSourceExpression() ) { + appendSql( " (" ); + } + + final List valueBindings = optionalTableUpdate.getValueBindings(); + final List keyBindings = optionalTableUpdate.getKeyBindings(); + + final StringBuilder columnList = new StringBuilder(); + + appendSql( " values (" ); + + for ( int i = 0; i < keyBindings.size(); i++ ) { + final ColumnValueBinding keyBinding = keyBindings.get( i ); + if ( i > 0 ) { + appendSql( ", " ); + columnList.append( ", " ); + } + columnList.append( keyBinding.getColumnReference().getColumnExpression() ); + renderCasted( keyBinding.getValueExpression() ); + } + for ( int i = 0; i < valueBindings.size(); i++ ) { + appendSql( ", " ); + columnList.append( ", " ); + final ColumnValueBinding valueBinding = valueBindings.get( i ); + columnList.append( valueBinding.getColumnReference().getColumnExpression() ); + renderCasted( valueBinding.getValueExpression() ); + } + + appendSql( ") " ); + + if ( wrapMergeSourceExpression() ) { + appendSql( ") " ); + } + + renderMergeSourceAlias(); + + appendSql( "(" ); + appendSql( columnList.toString() ); + appendSql( ")" ); + } + + protected void renderMergeSourceAlias() { + appendSql( " as s" ); + } + + protected void renderMergeOn(OptionalTableUpdate optionalTableUpdate) { + appendSql( "on (" ); + + final List keyBindings = optionalTableUpdate.getKeyBindings(); + for ( int i = 0; i < keyBindings.size(); i++ ) { + final ColumnValueBinding keyBinding = keyBindings.get( i ); + if ( i > 0 ) { + appendSql( " and " ); + } + keyBinding.getColumnReference().appendReadExpression( this, "t" ); + appendSql( "=" ); + keyBinding.getColumnReference().appendReadExpression( this, "s" ); + } + // todo : optimistic locks? + + appendSql( ")" ); + } + + protected void renderMergeInsert(OptionalTableUpdate optionalTableUpdate) { + final List valueBindings = optionalTableUpdate.getValueBindings(); + final List keyBindings = optionalTableUpdate.getKeyBindings(); + + final StringBuilder valuesList = new StringBuilder(); + + appendSql( "when not matched then insert (" ); + for ( int i = 0; i < keyBindings.size(); i++ ) { + if ( i > 0 ) { + appendSql( ", " ); + valuesList.append( ", " ); + } + final ColumnValueBinding keyBinding = keyBindings.get( i ); + appendSql( keyBinding.getColumnReference().getColumnExpression() ); + keyBinding.getColumnReference().appendReadExpression( "s", valuesList::append ); + } + for ( int i = 0; i < valueBindings.size(); i++ ) { + appendSql( ", " ); + valuesList.append( ", " ); + final ColumnValueBinding valueBinding = valueBindings.get( i ); + appendSql( valueBinding.getColumnReference().getColumnExpression() ); + valueBinding.getColumnReference().appendReadExpression( "s", valuesList::append ); + } + + appendSql( ") values (" ); + appendSql( valuesList.toString() ); + appendSql( ")" ); + } + + protected void renderMergeDelete(OptionalTableUpdate optionalTableUpdate) { + final List valueBindings = optionalTableUpdate.getValueBindings(); + + appendSql( " when matched " ); + for ( int i = 0; i < valueBindings.size(); i++ ) { + final ColumnValueBinding binding = valueBindings.get( i ); + appendSql( " and " ); + binding.getColumnReference().appendReadExpression( this, "s" ); + appendSql( " is null" ); + } + appendSql( " then delete" ); + } + + protected void renderMergeUpdate(OptionalTableUpdate optionalTableUpdate) { + final List valueBindings = optionalTableUpdate.getValueBindings(); + + appendSql( " when matched then update set " ); + for ( int i = 0; i < valueBindings.size(); i++ ) { + final ColumnValueBinding binding = valueBindings.get( i ); + if ( i > 0 ) { + appendSql( ", " ); + } + binding.getColumnReference().appendColumnForWrite( this, "t" ); + appendSql( "=" ); + binding.getColumnReference().appendColumnForWrite( this, "s" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java b/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java index 3f51e17e74..2f6fca6656 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/internal/OptionalTableUpdate.java @@ -131,7 +131,7 @@ public class OptionalTableUpdate // Fallback to the optional table mutation operation because we have to execute user specified SQL return new OptionalTableUpdateOperation( getMutationTarget(), this, factory ); } - return factory.getJdbcServices().getDialect().createUpsertOperation( + return factory.getJdbcServices().getDialect().createOptionalTableUpdateOperation( getMutationTarget(), this, factory diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/UpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/MergeOperation.java similarity index 86% rename from hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/UpsertOperation.java rename to hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/MergeOperation.java index cc820dd314..a583d5aee7 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/UpsertOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/MergeOperation.java @@ -15,12 +15,12 @@ import org.hibernate.sql.model.MutationType; import org.hibernate.sql.model.TableMapping; /** - * JdbcMutation implementation for MERGE/UPSERT handling + * JdbcMutation implementation for MERGE handling * * @author Steve Ebersole */ -public class UpsertOperation extends AbstractJdbcMutation { - public UpsertOperation( +public class MergeOperation extends AbstractJdbcMutation { + public MergeOperation( TableMapping tableDetails, MutationTarget mutationTarget, String sql, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/write/UpsertTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/write/OptionalTableUpdateTests.java similarity index 96% rename from hibernate-core/src/test/java/org/hibernate/orm/test/write/UpsertTests.java rename to hibernate-core/src/test/java/org/hibernate/orm/test/write/OptionalTableUpdateTests.java index 07bc2fab14..9ebbbe98cb 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/write/UpsertTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/write/OptionalTableUpdateTests.java @@ -24,9 +24,9 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Steve Ebersole */ -@DomainModel(annotatedClasses = UpsertTests.TheEntity.class) +@DomainModel(annotatedClasses = OptionalTableUpdateTests.TheEntity.class) @SessionFactory -public class UpsertTests { +public class OptionalTableUpdateTests { @Test void testUpsertInsert(SessionFactoryScope scope) { scope.inTransaction( (session) -> {