HHH-16101 - MERGE for optional table updates on Oracle

This commit is contained in:
Steve Ebersole 2023-01-25 23:06:38 -06:00
parent ee8d80a8bd
commit 1d62d2d66e
7 changed files with 504 additions and 10 deletions

View File

@ -50,6 +50,7 @@ import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
import org.hibernate.internal.util.JdbcExceptionHelper;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
import org.hibernate.procedure.internal.StandardCallableStatementSupport;
import org.hibernate.procedure.spi.CallableStatementSupport;
import org.hibernate.query.SemanticException;
@ -70,6 +71,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.extract.internal.SequenceInformationExtractorOracleDatabaseImpl;
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
import org.hibernate.type.JavaObjectType;
@ -79,9 +82,9 @@ import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType;
import org.hibernate.type.descriptor.jdbc.AggregateJdbcType;
import org.hibernate.type.descriptor.jdbc.BlobJdbcType;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType;
import org.hibernate.type.descriptor.jdbc.NullJdbcType;
import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType;
import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType;
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl;
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
@ -1441,4 +1444,13 @@ public class OracleDialect extends Dialect {
public String rowId(String rowId) {
return "rowid";
}
@Override
public MutationOperation createOptionalTableUpdateOperation(
EntityMutationTarget mutationTarget,
OptionalTableUpdate optionalTableUpdate,
SessionFactoryImplementor factory) {
final OracleSqlAstTranslator<?> translator = new OracleSqlAstTranslator<>( factory, optionalTableUpdate );
return translator.createMergeOperation( optionalTableUpdate );
}
}

View File

@ -18,12 +18,11 @@ import org.hibernate.query.sqm.FetchClauseType;
import org.hibernate.query.sqm.FrameExclusion;
import org.hibernate.query.sqm.FrameKind;
import org.hibernate.sql.ast.Clause;
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.CteMaterialization;
import org.hibernate.sql.ast.tree.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression;
import org.hibernate.sql.ast.tree.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.FunctionExpression;
import org.hibernate.sql.ast.tree.expression.Literal;
@ -45,6 +44,8 @@ import org.hibernate.sql.ast.tree.select.SelectClause;
import org.hibernate.sql.ast.tree.select.SortSpecification;
import org.hibernate.sql.ast.tree.update.Assignment;
import org.hibernate.sql.exec.spi.JdbcOperation;
import org.hibernate.sql.model.ast.ColumnValueBinding;
import org.hibernate.sql.model.internal.OptionalTableUpdate;
import org.hibernate.type.SqlTypes;
/**
@ -52,7 +53,7 @@ import org.hibernate.type.SqlTypes;
*
* @author Christian Beikov
*/
public class OracleSqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
public class OracleSqlAstTranslator<T extends JdbcOperation> extends SqlAstTranslatorWithUpsert<T> {
public OracleSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) {
super( sessionFactory, statement );
@ -542,4 +543,42 @@ public class OracleSqlAstTranslator<T extends JdbcOperation> extends AbstractSql
public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) {
aggregateColumnWriteExpression.appendWriteExpression( this, this );
}
@Override
protected void renderMergeTargetAlias() {
appendSql( " t" );
}
@Override
protected void renderMergeSourceAlias() {
appendSql( " s" );
}
protected void renderMergeSource(OptionalTableUpdate optionalTableUpdate) {
final List<ColumnValueBinding> valueBindings = optionalTableUpdate.getValueBindings();
final List<ColumnValueBinding> keyBindings = optionalTableUpdate.getKeyBindings();
appendSql( "(select " );
for ( int i = 0; i < keyBindings.size(); i++ ) {
final ColumnValueBinding keyBinding = keyBindings.get( i );
if ( i > 0 ) {
appendSql( ", " );
}
renderCasted( keyBinding.getValueExpression() );
appendSql( " " );
appendSql( keyBinding.getColumnReference().getColumnExpression() );
}
for ( int i = 0; i < valueBindings.size(); i++ ) {
appendSql( ", " );
final ColumnValueBinding valueBinding = valueBindings.get( i );
renderCasted( valueBinding.getValueExpression() );
appendSql( " " );
appendSql( valueBinding.getColumnReference().getColumnExpression() );
}
appendSql( " from dual)" );
renderMergeSourceAlias();
}
}

View File

@ -18,7 +18,7 @@ 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
* Base for translators which support a full insert-or-update-or-delete (MERGE) command
*
* @author Steve Ebersole
*/
@ -53,7 +53,7 @@ public abstract class SqlAstTranslatorWithMerge<T extends JdbcOperation> extends
// and s.[columns] is null
// then delete
// when matched
// then update set ...
// then update ...
renderMergeInto( optionalTableUpdate );
appendSql( " " );

View File

@ -0,0 +1,202 @@
/*
* 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.persister.entity.mutation.EntityTableMapping;
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.DeleteOrUpsertOperation;
import org.hibernate.sql.model.jdbc.UpsertOperation;
/**
* Base SqlAstTranslator for translators which support an insert-or-update (UPSERT) command
*
* @author Steve Ebersole
*/
public class SqlAstTranslatorWithUpsert<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
protected SqlAstTranslatorWithUpsert(SessionFactoryImplementor sessionFactory, Statement statement) {
super( sessionFactory, statement );
}
/**
* Create the MutationOperation for performing the DELETE or UPSERT
*/
public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableUpdate) {
renderUpsertStatement( optionalTableUpdate );
final UpsertOperation upsertOperation = new UpsertOperation(
optionalTableUpdate.getMutatingTable().getTableMapping(),
optionalTableUpdate.getMutationTarget(),
getSql(),
getParameterBinders()
);
return new DeleteOrUpsertOperation(
optionalTableUpdate.getMutationTarget(),
(EntityTableMapping) optionalTableUpdate.getMutatingTable().getTableMapping(),
upsertOperation,
optionalTableUpdate
);
}
private void renderUpsertStatement(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
// then update ...
renderMergeInto( optionalTableUpdate );
appendSql( " " );
renderMergeUsing( optionalTableUpdate );
appendSql( " " );
renderMergeOn( optionalTableUpdate );
appendSql( " " );
renderMergeInsert( 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;
}
protected 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 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

@ -0,0 +1,202 @@
/*
* 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.sql.model.jdbc;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.hibernate.engine.jdbc.mutation.JdbcValueBindings;
import org.hibernate.engine.jdbc.mutation.ParameterUsage;
import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails;
import org.hibernate.engine.jdbc.mutation.internal.MutationQueryOptions;
import org.hibernate.engine.jdbc.mutation.internal.PreparedStatementGroupSingleTable;
import org.hibernate.engine.jdbc.mutation.spi.Binding;
import org.hibernate.engine.jdbc.mutation.spi.BindingGroup;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
import org.hibernate.persister.entity.mutation.EntityTableMapping;
import org.hibernate.persister.entity.mutation.UpdateValuesAnalysis;
import org.hibernate.sql.ast.SqlAstTranslator;
import org.hibernate.sql.model.MutationTarget;
import org.hibernate.sql.model.MutationType;
import org.hibernate.sql.model.SelfExecutingUpdateOperation;
import org.hibernate.sql.model.TableMapping;
import org.hibernate.sql.model.ValuesAnalysis;
import org.hibernate.sql.model.ast.ColumnValueParameter;
import org.hibernate.sql.model.internal.OptionalTableUpdate;
import org.hibernate.sql.model.internal.TableDeleteStandard;
import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER;
/**
* @author Steve Ebersole
*/
public class DeleteOrUpsertOperation implements SelfExecutingUpdateOperation {
private final EntityMutationTarget mutationTarget;
private final EntityTableMapping tableMapping;
private final UpsertOperation upsertOperation;
private final OptionalTableUpdate optionalTableUpdate;
public DeleteOrUpsertOperation(
EntityMutationTarget mutationTarget,
EntityTableMapping tableMapping,
UpsertOperation upsertOperation,
OptionalTableUpdate optionalTableUpdate) {
this.mutationTarget = mutationTarget;
this.tableMapping = tableMapping;
this.upsertOperation = upsertOperation;
this.optionalTableUpdate = optionalTableUpdate;
}
@Override
public MutationType getMutationType() {
return MutationType.UPDATE;
}
@Override
public MutationTarget<?> getMutationTarget() {
return mutationTarget;
}
@Override
public TableMapping getTableDetails() {
return tableMapping;
}
@Override
public JdbcValueDescriptor findValueDescriptor(String columnName, ParameterUsage usage) {
return upsertOperation.findValueDescriptor( columnName, usage );
}
@Override
public void performMutation(
JdbcValueBindings jdbcValueBindings,
ValuesAnalysis valuesAnalysis,
SharedSessionContractImplementor session) {
final UpdateValuesAnalysis analysis = (UpdateValuesAnalysis) valuesAnalysis;
if ( !analysis.getTablesWithNonNullValues().contains( tableMapping ) ) {
// all the new values are null - delete
performDelete( jdbcValueBindings, session );
}
else {
performUpsert( jdbcValueBindings, session );
}
}
private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) {
MODEL_MUTATION_LOGGER.tracef( "#performDelete(%s)", tableMapping.getTableName() );
final TableDeleteStandard upsertDeleteAst = new TableDeleteStandard(
optionalTableUpdate.getMutatingTable(),
mutationTarget,
"upsert delete",
optionalTableUpdate.getKeyBindings(),
Collections.emptyList(),
Collections.emptyList()
);
final SqlAstTranslator<JdbcDeleteMutation> translator = session
.getJdbcServices()
.getJdbcEnvironment()
.getSqlAstTranslatorFactory()
.buildModelMutationTranslator( upsertDeleteAst, session.getFactory() );
final JdbcDeleteMutation upsertDelete = translator.translate( null, MutationQueryOptions.INSTANCE );
final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertDelete, session );
final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( tableMapping.getTableName() );
final PreparedStatement upsertDeleteStatement = statementDetails.resolveStatement();
session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() );
bindDeleteKeyValues(
jdbcValueBindings,
optionalTableUpdate.getParameters(),
statementDetails,
session
);
final int rowCount = session.getJdbcCoordinator().getResultSetReturn()
.executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() );
MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() );
}
private void bindDeleteKeyValues(
JdbcValueBindings jdbcValueBindings,
List<ColumnValueParameter> parameters,
PreparedStatementDetails statementDetails,
SharedSessionContractImplementor session) {
final PreparedStatement statement = statementDetails.resolveStatement();
final BindingGroup bindingGroup = jdbcValueBindings.getBindingGroup( tableMapping.getTableName() );
final Set<Binding> bindings = bindingGroup.getBindings();
int jdbcBindingPosition = 1;
for ( Binding binding : bindings ) {
if ( binding.getValueDescriptor().getUsage() != ParameterUsage.RESTRICT ) {
continue;
}
bindKeyValue(
jdbcBindingPosition++,
binding,
binding.getValueDescriptor(),
statement,
statementDetails.getSqlString(),
tableMapping,
session
);
}
}
private static void bindKeyValue(
int jdbcPosition,
Binding binding,
JdbcValueDescriptor valueDescriptor,
PreparedStatement statement,
String sql,
EntityTableMapping tableMapping,
SharedSessionContractImplementor session) {
try {
binding.getValueBinder().bind( statement, binding.getValue(), jdbcPosition, session );
}
catch (SQLException e) {
throw session.getJdbcServices().getSqlExceptionHelper().convert(
e,
String.format(
Locale.ROOT,
"Unable to bind parameter for upsert insert : %s.%s",
tableMapping.getTableName(),
valueDescriptor.getColumnName()
),
sql
);
}
}
private void performUpsert(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) {
MODEL_MUTATION_LOGGER.tracef( "#performUpsert(%s)", tableMapping.getTableName() );
final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertOperation, session );
final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( tableMapping.getTableName() );
final PreparedStatement updateStatement = statementDetails.resolveStatement();
session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() );
jdbcValueBindings.beforeStatement( statementDetails );
final int rowCount = session.getJdbcCoordinator().getResultSetReturn()
.executeUpdate( updateStatement, statementDetails.getSqlString() );
MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() );
}
}

View File

@ -72,7 +72,7 @@ public class OptionalTableUpdateOperation implements SelfExecutingUpdateOperatio
private final List<ColumnValueBinding> optimisticLockBindings;
private final List<ColumnValueParameter> parameters;
private final List<JdbcValueDescriptorImpl> jdbcValueDescriptors;
private final List<JdbcValueDescriptor> jdbcValueDescriptors;
public OptionalTableUpdateOperation(
MutationTarget<?> mutationTarget,
@ -205,7 +205,7 @@ public class OptionalTableUpdateOperation implements SelfExecutingUpdateOperatio
bindings: for ( Binding binding : bindings ) {
// binding-position here is 1-based (JDBC)
final JdbcValueDescriptorImpl valueDescriptor = jdbcValueDescriptors.get( binding.getPosition() - 1 );
final JdbcValueDescriptor valueDescriptor = jdbcValueDescriptors.get( binding.getPosition() - 1 );
// key bindings would have a usage of RESTRICT relative to the UPDATE
if ( valueDescriptor.getUsage() != ParameterUsage.RESTRICT ) {
@ -224,6 +224,7 @@ public class OptionalTableUpdateOperation implements SelfExecutingUpdateOperatio
valueDescriptor,
statement,
jdbcDelete.getSqlString(),
tableMapping,
session
);
break;
@ -238,12 +239,13 @@ public class OptionalTableUpdateOperation implements SelfExecutingUpdateOperatio
}
}
private void bindKeyValue(
private static void bindKeyValue(
int jdbcPosition,
Binding binding,
JdbcValueDescriptorImpl valueDescriptor,
JdbcValueDescriptor valueDescriptor,
PreparedStatement statement,
String sql,
EntityTableMapping tableMapping,
SharedSessionContractImplementor session) {
try {
binding.getValueBinder().bind( statement, binding.getValue(), jdbcPosition, session );

View File

@ -0,0 +1,37 @@
/*
* 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.sql.model.jdbc;
import java.util.List;
import org.hibernate.jdbc.Expectations;
import org.hibernate.sql.exec.spi.JdbcParameterBinder;
import org.hibernate.sql.model.MutationTarget;
import org.hibernate.sql.model.MutationType;
import org.hibernate.sql.model.TableMapping;
/**
* JdbcMutation implementation for UPSERT handling
*
* @author Steve Ebersole
*/
public class UpsertOperation extends AbstractJdbcMutation {
public UpsertOperation(
TableMapping tableDetails,
MutationTarget<?> mutationTarget,
String sql,
List<? extends JdbcParameterBinder> parameterBinders) {
super( tableDetails, mutationTarget, sql, false, Expectations.NONE, parameterBinders );
}
@Override
public MutationType getMutationType() {
return MutationType.UPDATE;
}
}