HHH-16103 - MERGE for optional table updates on SQL Server

This commit is contained in:
Steve Ebersole 2023-01-25 21:44:37 -06:00
parent 5525b8d9b7
commit e27dc5bc47
9 changed files with 243 additions and 164 deletions

View File

@ -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) {

View File

@ -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 );
}
}

View File

@ -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<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
public class H2SqlAstTranslator<T extends JdbcOperation> extends SqlAstTranslatorWithMerge<T> {
private boolean renderAsArray;
@ -311,148 +306,4 @@ public class H2SqlAstTranslator<T extends JdbcOperation> 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<ColumnValueBinding> valueBindings = optionalTableUpdate.getValueBindings();
final List<ColumnValueBinding> 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<ColumnValueBinding> 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<ColumnValueBinding> valueBindings = optionalTableUpdate.getValueBindings();
final List<ColumnValueBinding> 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<ColumnValueBinding> 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<ColumnValueBinding> 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" );
}
}
}

View File

@ -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<JdbcOperation> translator = new SQLServerSqlAstTranslator<>( factory, optionalTableUpdate );
return translator.createMergeOperation( optionalTableUpdate );
}
}

View File

@ -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<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
public class SQLServerSqlAstTranslator<T extends JdbcOperation> extends SqlAstTranslatorWithMerge<T> {
private static final String UNION_ALL = " union all ";
@ -440,4 +440,9 @@ public class SQLServerSqlAstTranslator<T extends JdbcOperation> extends Abstract
TOP_ONLY,
EMULATED;
}
protected void renderMergeStatement(OptionalTableUpdate optionalTableUpdate) {
super.renderMergeStatement( optionalTableUpdate );
appendSql( ";" );
}
}

View File

@ -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<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
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<ColumnValueBinding> valueBindings = optionalTableUpdate.getValueBindings();
final List<ColumnValueBinding> 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<ColumnValueBinding> 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<ColumnValueBinding> valueBindings = optionalTableUpdate.getValueBindings();
final List<ColumnValueBinding> 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<ColumnValueBinding> 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<ColumnValueBinding> 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" );
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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) -> {