Handle insert-select for entities with generators that do not support bulk insertion

This commit is contained in:
Christian Beikov 2022-06-09 20:19:21 +02:00
parent 49e9696ced
commit ed1cea6ba1
5 changed files with 201 additions and 143 deletions

View File

@ -93,6 +93,7 @@ import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement;
import org.hibernate.query.sqm.tree.insert.SqmValues;
import org.hibernate.query.sqm.tree.select.SqmQueryPart;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.query.sqm.tree.select.SqmSelectableNode;
import org.hibernate.query.sqm.tree.select.SqmSelection;
import org.hibernate.query.sqm.tree.update.SqmAssignment;
import org.hibernate.query.sqm.tree.update.SqmUpdateStatement;
@ -371,10 +372,10 @@ public class QuerySqmImpl<R>
}
else {
final SqmInsertSelectStatement<R> statement = (SqmInsertSelectStatement<R>) sqmStatement;
final List<SqmSelection<?>> selections = statement.getSelectQueryPart()
final List<SqmSelectableNode<?>> selections = statement.getSelectQueryPart()
.getFirstQuerySpec()
.getSelectClause()
.getSelections();
.getSelectionItems();
verifyInsertTypesMatch( hqlString, insertionTargetPaths, selections );
statement.getSelectQueryPart().validateQueryStructureAndFetchOwners();
}

View File

@ -21,6 +21,7 @@ import org.hibernate.engine.FetchTiming;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.BulkInsertionCapableIdentifierGenerator;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.id.OptimizableGenerator;
import org.hibernate.id.PostInsertIdentifierGenerator;
@ -288,7 +289,10 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio
final IdentifierGenerator identifierGenerator = entityDescriptor.getEntityPersister().getIdentifierGenerator();
final List<Assignment> assignments = assignmentsByTable.get( updatingTableReference );
if ( ( assignments == null || assignments.isEmpty() ) && !( identifierGenerator instanceof PostInsertIdentifierGenerator ) ) {
if ( ( assignments == null || assignments.isEmpty() )
&& !( identifierGenerator instanceof PostInsertIdentifierGenerator )
&& ( !( identifierGenerator instanceof BulkInsertionCapableIdentifierGenerator )
|| ( (BulkInsertionCapableIdentifierGenerator) identifierGenerator ).supportsBulkInsertionIdentifierGeneration() ) ) {
throw new IllegalStateException( "There must be at least a single root table assignment" );
}
@ -312,24 +316,26 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio
querySpec.getFromClause().addRoot( temporaryTableGroup );
final InsertStatement insertStatement = new InsertStatement( dmlTableReference );
insertStatement.setSourceSelectStatement( querySpec );
for ( Assignment assignment : assignments ) {
insertStatement.addTargetColumnReferences( assignment.getAssignable().getColumnReferences() );
for ( ColumnReference columnReference : assignment.getAssignable().getColumnReferences() ) {
querySpec.getSelectClause().addSqlSelection(
new SqlSelectionImpl(
1,
0,
new ColumnReference(
updatingTableReference.getIdentificationVariable(),
columnReference.getColumnExpression(),
false,
null,
null,
columnReference.getJdbcMapping(),
sessionFactory
)
)
);
if ( assignments != null ) {
for ( Assignment assignment : assignments ) {
insertStatement.addTargetColumnReferences( assignment.getAssignable().getColumnReferences() );
for ( ColumnReference columnReference : assignment.getAssignable().getColumnReferences() ) {
querySpec.getSelectClause().addSqlSelection(
new SqlSelectionImpl(
1,
0,
new ColumnReference(
updatingTableReference.getIdentificationVariable(),
columnReference.getColumnExpression(),
false,
null,
null,
columnReference.getJdbcMapping(),
sessionFactory
)
)
);
}
}
}
final JdbcServices jdbcServices = sessionFactory.getJdbcServices();
@ -394,114 +400,113 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio
}
else {
entityTableToRootIdentity = null;
if ( identifierGenerator instanceof OptimizableGenerator ) {
final Optimizer optimizer = ( (OptimizableGenerator) identifierGenerator ).getOptimizer();
// If the generator uses an optimizer, we have to generate the identifiers for the new rows
// but only if the target paths don't already contain the id
if ( optimizer != null && optimizer.getIncrementSize() > 1 && insertStatement.getTargetColumnReferences()
.stream()
.noneMatch( c -> keyColumns[0].equals( c.getColumnExpression() ) ) ) {
final BasicEntityIdentifierMapping identifierMapping = (BasicEntityIdentifierMapping) entityDescriptor.getIdentifierMapping();
// if the target paths don't already contain the id, and we need identifier generation,
// then we load update rows from the temporary table with the generated identifiers,
// to then insert into the target tables in once statement
if ( needsIdentifierGeneration( identifierGenerator )
&& insertStatement.getTargetColumnReferences()
.stream()
.noneMatch( c -> keyColumns[0].equals( c.getColumnExpression() ) ) ) {
final BasicEntityIdentifierMapping identifierMapping = (BasicEntityIdentifierMapping) entityDescriptor.getIdentifierMapping();
final JdbcParameter rowNumber = new JdbcParameterImpl( identifierMapping.getJdbcMapping() );
final JdbcParameter rootIdentity = new JdbcParameterImpl( identifierMapping.getJdbcMapping() );
final List<Assignment> temporaryTableAssignments = new ArrayList<>( 1 );
final ColumnReference idColumnReference = new ColumnReference(
(String) null,
identifierMapping,
sessionFactory
);
temporaryTableAssignments.add(
new Assignment(
idColumnReference,
rootIdentity
)
);
final TemporaryTableColumn rowNumberColumn = entityTable.getColumns().get(
entityTable.getColumns().size() - 1
);
final UpdateStatement updateStatement = new UpdateStatement(
temporaryTableReference,
temporaryTableAssignments,
new ComparisonPredicate(
new ColumnReference(
(String) null,
rowNumberColumn.getColumnName(),
false,
null,
null,
rowNumberColumn.getJdbcMapping(),
sessionFactory
),
ComparisonOperator.EQUAL,
rowNumber
)
);
final JdbcUpdate jdbcUpdate = jdbcServices.getJdbcEnvironment()
.getSqlAstTranslatorFactory()
.buildUpdateTranslator( sessionFactory, updateStatement )
.translate( null, executionContext.getQueryOptions() );
final JdbcParameterBindings updateBindings = new JdbcParameterBindingsImpl( 2 );
for ( int i = 0; i < rows; i++ ) {
updateBindings.addBinding(
rowNumber,
new JdbcParameterBindingImpl(
final JdbcParameter rowNumber = new JdbcParameterImpl( identifierMapping.getJdbcMapping() );
final JdbcParameter rootIdentity = new JdbcParameterImpl( identifierMapping.getJdbcMapping() );
final List<Assignment> temporaryTableAssignments = new ArrayList<>( 1 );
final ColumnReference idColumnReference = new ColumnReference(
(String) null,
identifierMapping,
sessionFactory
);
temporaryTableAssignments.add(
new Assignment(
idColumnReference,
rootIdentity
)
);
final TemporaryTableColumn rowNumberColumn = entityTable.getColumns().get(
entityTable.getColumns().size() - 1
);
final UpdateStatement updateStatement = new UpdateStatement(
temporaryTableReference,
temporaryTableAssignments,
new ComparisonPredicate(
new ColumnReference(
(String) null,
rowNumberColumn.getColumnName(),
false,
null,
null,
rowNumberColumn.getJdbcMapping(),
i + 1
)
);
updateBindings.addBinding(
rootIdentity,
new JdbcParameterBindingImpl(
identifierMapping.getJdbcMapping(),
identifierGenerator.generate(
executionContext.getSession(),
null
)
)
);
jdbcServices.getJdbcMutationExecutor().execute(
jdbcUpdate,
updateBindings,
sql -> executionContext.getSession()
.getJdbcCoordinator()
.getStatementPreparer()
.prepareStatement( sql ),
(integer, preparedStatement) -> {
},
executionContext
);
}
sessionFactory
),
ComparisonOperator.EQUAL,
rowNumber
)
);
insertStatement.addTargetColumnReferences(
new ColumnReference(
(String) null,
keyColumns[0],
false,
null,
null,
identifierMapping.getJdbcMapping(),
sessionFactory
final JdbcUpdate jdbcUpdate = jdbcServices.getJdbcEnvironment()
.getSqlAstTranslatorFactory()
.buildUpdateTranslator( sessionFactory, updateStatement )
.translate( null, executionContext.getQueryOptions() );
final JdbcParameterBindings updateBindings = new JdbcParameterBindingsImpl( 2 );
for ( int i = 0; i < rows; i++ ) {
updateBindings.addBinding(
rowNumber,
new JdbcParameterBindingImpl(
rowNumberColumn.getJdbcMapping(),
i + 1
)
);
querySpec.getSelectClause().addSqlSelection(
new SqlSelectionImpl(
1,
0,
new ColumnReference(
updatingTableReference.getIdentificationVariable(),
idColumnReference.getColumnExpression(),
false,
null,
null,
idColumnReference.getJdbcMapping(),
sessionFactory
updateBindings.addBinding(
rootIdentity,
new JdbcParameterBindingImpl(
identifierMapping.getJdbcMapping(),
identifierGenerator.generate(
executionContext.getSession(),
null
)
)
);
jdbcServices.getJdbcMutationExecutor().execute(
jdbcUpdate,
updateBindings,
sql -> executionContext.getSession()
.getJdbcCoordinator()
.getStatementPreparer()
.prepareStatement( sql ),
(integer, preparedStatement) -> {
},
executionContext
);
}
insertStatement.addTargetColumnReferences(
new ColumnReference(
(String) null,
keyColumns[0],
false,
null,
null,
identifierMapping.getJdbcMapping(),
sessionFactory
)
);
querySpec.getSelectClause().addSqlSelection(
new SqlSelectionImpl(
1,
0,
new ColumnReference(
updatingTableReference.getIdentificationVariable(),
idColumnReference.getColumnExpression(),
false,
null,
null,
idColumnReference.getJdbcMapping(),
sessionFactory
)
)
);
}
}
@ -608,6 +613,17 @@ public class InsertExecutionDelegate implements TableBasedInsertHandler.Executio
}
}
private boolean needsIdentifierGeneration(IdentifierGenerator identifierGenerator) {
if ( !( identifierGenerator instanceof OptimizableGenerator ) ) {
return false;
}
// If the generator uses an optimizer or is not bulk insertion capable, we have to generate identifiers for the new rows,
// as that couldn't have been done through a SQL expression
final Optimizer optimizer = ( (OptimizableGenerator) identifierGenerator ).getOptimizer();
return optimizer != null && optimizer.getIncrementSize() > 1 || identifierGenerator instanceof BulkInsertionCapableIdentifierGenerator
&& !( (BulkInsertionCapableIdentifierGenerator) identifierGenerator ).supportsBulkInsertionIdentifierGeneration();
}
private void insertTable(
String tableExpression,
String[] keyColumns,

View File

@ -1259,18 +1259,48 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
identifierGenerator = null;
}
else if ( identifierGenerator != null ) {
// When we have an identifier generator, we somehow must list the identifier column in the insert statement.
final boolean addIdColumn;
if ( sqmStatement instanceof SqmInsertValuesStatement<?> ) {
// For an InsertValuesStatement, we can just list the column, as we can inject a parameter in the VALUES clause.
addIdColumn = true;
}
else if ( !( identifierGenerator instanceof BulkInsertionCapableIdentifierGenerator ) ) {
// For non-identity generators that don't implement BulkInsertionCapableIdentifierGenerator, there is nothing we can do
addIdColumn = false;
}
else {
// Same condition as in AdditionalInsertValues#applySelections
final Optimizer optimizer;
if ( identifierGenerator instanceof OptimizableGenerator
&& ( optimizer = ( (OptimizableGenerator) identifierGenerator ).getOptimizer() ) != null
&& optimizer.getIncrementSize() > 1
|| !( (BulkInsertionCapableIdentifierGenerator) identifierGenerator ).supportsBulkInsertionIdentifierGeneration() ) {
// If the dialect does not support window functions, we don't need the id column in the temporary table insert
// because we will make use of the special "rn_" column that is auto-incremented and serves as temporary identifier for a row,
// which is needed to control the generation of proper identifier values with the generator afterwards
addIdColumn = creationContext.getSessionFactory().getJdbcServices().getDialect().supportsWindowFunctions();
}
else {
// If the generator supports bulk insertion and the optimizer uses an increment size of 1,
// we can list the column, because we can emit a SQL expression.
addIdColumn = true;
}
}
identifierMapping = (BasicEntityIdentifierMapping) entityDescriptor.getIdentifierMapping();
final BasicValuedPathInterpretation<?> identifierPath = new BasicValuedPathInterpretation<>(
new ColumnReference(
rootTableGroup.resolveTableReference( identifierMapping.getContainingTableExpression() ),
identifierMapping,
getCreationContext().getSessionFactory()
),
rootTableGroup.getNavigablePath().append( identifierMapping.getPartName() ),
identifierMapping,
rootTableGroup
);
targetColumnReferenceConsumer.accept( identifierPath, identifierPath.getColumnReferences() );
if ( addIdColumn ) {
final BasicValuedPathInterpretation<?> identifierPath = new BasicValuedPathInterpretation<>(
new ColumnReference(
rootTableGroup.resolveTableReference( identifierMapping.getContainingTableExpression() ),
identifierMapping,
getCreationContext().getSessionFactory()
),
rootTableGroup.getNavigablePath().append( identifierMapping.getPartName() ),
identifierMapping,
rootTableGroup
);
targetColumnReferenceConsumer.accept( identifierPath, identifierPath.getColumnReferences() );
}
}
return new AdditionalInsertValues(
@ -1340,15 +1370,19 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
}
if ( identifierGenerator != null ) {
if ( identifierSelection == null ) {
if ( !( identifierGenerator instanceof BulkInsertionCapableIdentifierGenerator )
|| !( (BulkInsertionCapableIdentifierGenerator) identifierGenerator ).supportsBulkInsertionIdentifierGeneration() ) {
if ( !( identifierGenerator instanceof BulkInsertionCapableIdentifierGenerator ) ) {
throw new SemanticException(
"SQM INSERT-SELECT without bulk insertion capable identifier generator: " + identifierGenerator );
}
if ( identifierGenerator instanceof OptimizableGenerator ) {
final Optimizer optimizer = ( (OptimizableGenerator) identifierGenerator ).getOptimizer();
if ( optimizer != null && optimizer.getIncrementSize() > 1 ) {
if ( optimizer != null && optimizer.getIncrementSize() > 1
|| !( (BulkInsertionCapableIdentifierGenerator) identifierGenerator ).supportsBulkInsertionIdentifierGeneration() ) {
// This is a special case where we have a sequence with an optimizer
// or a table based identifier generator
if ( !sessionFactory.getJdbcServices().getDialect().supportsWindowFunctions() ) {
return false;
}
final BasicType<Integer> rowNumberType = sessionFactory.getTypeConfiguration()
.getBasicTypeForJavaType( Integer.class );
identifierSelection = new SqlSelectionImpl(

View File

@ -29,11 +29,11 @@ import org.hibernate.query.sqm.tree.select.SqmQuerySpec;
*/
@Incubating
public class SqmInsertSelectStatement<T> extends AbstractSqmInsertStatement<T> implements JpaCriteriaInsertSelect<T> {
private SqmQueryPart<T> selectQueryPart;
private SqmQueryPart<?> selectQueryPart;
public SqmInsertSelectStatement(SqmRoot<T> targetRoot, NodeBuilder nodeBuilder) {
super( targetRoot, SqmQuerySource.HQL, nodeBuilder );
this.selectQueryPart = new SqmQuerySpec<T>( nodeBuilder );
this.selectQueryPart = new SqmQuerySpec<>( nodeBuilder );
}
public SqmInsertSelectStatement(Class<T> targetEntity, NodeBuilder nodeBuilder) {
@ -58,7 +58,7 @@ public class SqmInsertSelectStatement<T> extends AbstractSqmInsertStatement<T> i
boolean withRecursiveCte,
SqmRoot<T> target,
List<SqmPath<?>> insertionTargetPaths,
SqmQueryPart<T> selectQueryPart) {
SqmQueryPart<?> selectQueryPart) {
super( builder, querySource, parameters, cteStatements, withRecursiveCte, target, insertionTargetPaths );
this.selectQueryPart = selectQueryPart;
}
@ -84,11 +84,11 @@ public class SqmInsertSelectStatement<T> extends AbstractSqmInsertStatement<T> i
);
}
public SqmQueryPart<T> getSelectQueryPart() {
public SqmQueryPart<?> getSelectQueryPart() {
return selectQueryPart;
}
public void setSelectQueryPart(SqmQueryPart<T> selectQueryPart) {
public void setSelectQueryPart(SqmQueryPart<?> selectQueryPart) {
this.selectQueryPart = selectQueryPart;
}

View File

@ -7,13 +7,13 @@
package org.hibernate.orm.test.query.criteria;
import org.hibernate.query.sqm.internal.SqmCriteriaNodeBuilder;
import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import jakarta.persistence.Basic;
@ -21,22 +21,25 @@ import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Tuple;
/**
* @author Steve Ebersole
*/
@DomainModel(annotatedClasses = InsertSelectTests.AnEntity.class)
@SessionFactory
@Disabled(value = "Disabled for now because this test fails on MySQL and Sybase (SequenceStyleGenerator is not bulk capable on those)")
public class InsertSelectTests {
@Test
public void simpleTest(SessionFactoryScope scope) {
scope.inTransaction( (session) -> {
session.persist( new AnEntity( "test" ) );
final SqmCriteriaNodeBuilder criteriaBuilder = (SqmCriteriaNodeBuilder) session.getCriteriaBuilder();
final SqmInsertSelectStatement<AnEntity> insertSelect = criteriaBuilder.createCriteriaInsertSelect( AnEntity.class );
final SqmSelectStatement<AnEntity> select = criteriaBuilder.createQuery( AnEntity.class );
final SqmSelectStatement<Tuple> select = criteriaBuilder.createQuery( Tuple.class );
insertSelect.addInsertTargetStateField( insertSelect.getTarget().get( "name" ) );
final SqmRoot<AnEntity> root = select.from( AnEntity.class );
select.multiselect( root.get( "name" ) );
insertSelect.setSelectQueryPart( select.getQuerySpec() );
select.from( AnEntity.class );
session.createMutationQuery( insertSelect ).executeUpdate();
} );
}
@ -55,6 +58,10 @@ public class InsertSelectTests {
// for use by Hibernate
}
public AnEntity(String name) {
this.name = name;
}
public AnEntity(Integer id, String name) {
this.id = id;
this.name = name;