Update the bulk section of the batching documentation chapter for the new mutation strategies. Also implement the missing InlineUpdateHandler

This commit is contained in:
Christian Beikov 2022-03-30 17:17:19 +02:00
parent 111fe26ccc
commit 4334b46376
30 changed files with 809 additions and 406 deletions

View File

@ -551,8 +551,8 @@ therefore reusing its execution plan.
==== Multi-table bulk HQL operations ==== Multi-table bulk HQL operations
`*hibernate.hql.bulk_id_strategy*` (e.g. A fully-qualified class name, an instance, or a `Class` object reference):: `*hibernate.query.mutation_strategy*` (e.g. A fully-qualified class name, an instance, or a `Class` object reference)::
Provide a custom https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/hql/spi/id/MultiTableBulkIdStrategy.html[`org.hibernate.hql.spi.id.MultiTableBulkIdStrategy`] implementation for handling multi-table bulk HQL operations. Provide a custom https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/query/sqm/mutation/spi/SqmMultiTableMutationStrategy.html[`org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy`] implementation for handling multi-table bulk HQL operations.
`*hibernate.hql.bulk_id_strategy.global_temporary.drop_tables*` (e.g. `true` or `false` (default value)):: `*hibernate.hql.bulk_id_strategy.global_temporary.drop_tables*` (e.g. `true` or `false` (default value))::
For databases that don't support local tables, but just global ones, this configuration property allows you to DROP the global tables used for multi-table bulk HQL operations when the `SessionFactory` or the `EntityManagerFactory` is closed. For databases that don't support local tables, but just global ones, this configuration property allows you to DROP the global tables used for multi-table bulk HQL operations when the `SessionFactory` or the `EntityManagerFactory` is closed.

View File

@ -1,7 +1,7 @@
[[batch]] [[batch]]
== Batching == Batching
:sourcedir: ../../../../../test/java/org/hibernate/userguide/batch :sourcedir: ../../../../../test/java/org/hibernate/userguide/batch
:bulkid-sourcedir: ../../../../../../../hibernate-core/src/test_legacy/java/org/hibernate/test/bulkid :bulkid-sourcedir: ../../../../../../../hibernate-core/src/test/java/org/hibernate/orm/test/bulkid
:extrasdir: extras :extrasdir: extras
[[batch-jdbcbatch]] [[batch-jdbcbatch]]
@ -165,7 +165,7 @@ Both the Hibernate native Query Language and JPQL (Java Persistence Query Langua
[[batch-bulk-hql-update-delete-example]] [[batch-bulk-hql-update-delete-example]]
.Pseudo-syntax for UPDATE and DELETE statements using HQL .Pseudo-syntax for UPDATE and DELETE statements using HQL
==== ====
[source, JAVA, indent=0] [source, SQL, indent=0]
---- ----
UPDATE FROM EntityName e WHERE e.name = ? UPDATE FROM EntityName e WHERE e.name = ?
@ -250,24 +250,32 @@ In the example of joined-subclass, a `DELETE` against one of the subclasses may
==== HQL syntax for INSERT ==== HQL syntax for INSERT
.Pseudo-syntax for INSERT statements .Pseudo-syntax for INSERT-SELECT statements
==== ====
[source, JAVA, indent=0] [source, SQL, indent=0]
---- ----
INSERT INTO EntityName INSERT INTO EntityName
properties_list properties_list
SELECT properties_list SELECT select_list
FROM ... FROM ...
---- ----
==== ====
Only the `INSERT INTO ... SELECT ...` form is supported. Alternatively one can also declare individual values
You cannot specify explicit values to insert.
.Pseudo-syntax for INSERT-VALUES statements
====
[source, SQL, indent=0]
----
INSERT INTO EntityName
properties_list
VALUES values_list
----
====
The `properties_list` is analogous to the column specification in the `SQL` `INSERT` statement. The `properties_list` is analogous to the column specification in the `SQL` `INSERT` statement.
For entities involved in mapped inheritance, you can only use properties directly defined on that given class-level in the `properties_list`. Note that `INSERT` statements are inherently non-polymorphic, so it is not possible to use an `EntityName`
Superclass properties are not allowed and subclass properties are irrelevant. which is abstract or refer to subclass properties.
In other words, `INSERT` statements are inherently non-polymorphic.
The SELECT statement can be any valid HQL select query, but the return types must match the types expected by the INSERT. The SELECT statement can be any valid HQL select query, but the return types must match the types expected by the INSERT.
Hibernate verifies the return types during query compilation, instead of expecting the database to check it. Hibernate verifies the return types during query compilation, instead of expecting the database to check it.
@ -282,7 +290,7 @@ Otherwise, Hibernate throws an exception during parsing. Available in-database
For properties mapped as either version or timestamp, the insert statement gives you two options. For properties mapped as either version or timestamp, the insert statement gives you two options.
You can either specify the property in the properties_list, in which case its value is taken from the corresponding select expressions or omit it from the properties_list, You can either specify the property in the properties_list, in which case its value is taken from the corresponding select expressions or omit it from the properties_list,
in which case the seed value defined by the org.hibernate.type.VersionType is used. in which case the seed value defined by the `org.hibernate.type.descriptor.java.VersionJavaType` is used.
[[batch-bulk-hql-insert-example]] [[batch-bulk-hql-insert-example]]
.HQL INSERT statement .HQL INSERT statement
@ -296,10 +304,19 @@ include::{sourcedir}/BatchTest.java[tags=batch-bulk-hql-insert-example]
This section is only a brief overview of HQL. For more information, see <<chapters/query/hql/QueryLanguage.adoc#query-language,Hibernate Query Language>>. This section is only a brief overview of HQL. For more information, see <<chapters/query/hql/QueryLanguage.adoc#query-language,Hibernate Query Language>>.
[[batch-bulk-hql-strategies]] [[batch-bulk-hql-strategies]]
==== Bulk-id strategies ==== Bulk mutation strategies
This article is about the https://hibernate.atlassian.net/browse/HHH-11262[HHH-11262] JIRA issue which now allows the bulk-id When a bulk mutation involves multiple tables, Hibernate has to issue individual DML statements to the respective tables.
strategies to work even when you cannot create temporary tables. Since the mutation itself could have an effect on the conditions used in the statement, it's generally not possible
to simply execute parts of the DML statement against the respective tables. Instead, Hibernate has to temporarily remember
which rows will be affected, and execute the DML statements based on these rows.
Usually, Hibernate will make use of local or global temporary tables to remember the primary keys of the rows.
For some databases, currently only PostgreSQL and DB2, a more advanced strategy (`CteMutationStrategy`) is used,
which makes use of DML in CTE support to execute the whole operation in one SQL statement.
The chosen strategy, unless overridden through the `hibernate.query.mutation_strategy` setting, is based on the
Dialect support through `org.hibernate.dialect.Dialect.getFallbackSqmMutationStrategy`.
[[batch-bulk-hql-strategies-class-diagram]] [[batch-bulk-hql-strategies-class-diagram]]
===== Class diagram ===== Class diagram
@ -311,22 +328,22 @@ image:images/domain/bulkid/temp_table_class_diagram.png[Entity class diagram]
The `Person` entity is the base class of this entity inheritance model, and is mapped as follows: The `Person` entity is the base class of this entity inheritance model, and is mapped as follows:
[[batch-bulk-hql-temp-table-base-class-example]] [[batch-bulk-hql-temp-table-base-class-example]]
.Bulk-id base class entity .Bulk mutation base class entity
==== ====
[source, JAVA, indent=0] [source, JAVA, indent=0]
---- ----
include::{bulkid-sourcedir}/AbstractBulkCompositeIdTest.java[tags=batch-bulk-hql-temp-table-base-class-example] include::{bulkid-sourcedir}/AbstractMutationStrategyCompositeIdTest.java[tags=batch-bulk-hql-temp-table-base-class-example]
---- ----
==== ====
Both the `Doctor` and `Engineer` entity classes extend the `Person` base class: Both the `Doctor` and `Engineer` entity classes extend the `Person` base class:
[[batch-bulk-hql-temp-table-sub-classes-example]] [[batch-bulk-hql-temp-table-sub-classes-example]]
.Bulk-id subclass entities .Bulk mutation subclass entities
==== ====
[source, JAVA, indent=0] [source, JAVA, indent=0]
---- ----
include::{bulkid-sourcedir}/AbstractBulkCompositeIdTest.java[tags=batch-bulk-hql-temp-table-sub-classes-example] include::{bulkid-sourcedir}/AbstractMutationStrategyIdTest.java[tags=batch-bulk-hql-temp-table-sub-classes-example]
---- ----
==== ====
@ -336,11 +353,11 @@ include::{bulkid-sourcedir}/AbstractBulkCompositeIdTest.java[tags=batch-bulk-hql
Now, when you try to execute a bulk entity delete query: Now, when you try to execute a bulk entity delete query:
[[batch-bulk-hql-temp-table-delete-query-example]] [[batch-bulk-hql-temp-table-delete-query-example]]
.Bulk-id delete query example .Bulk mutation delete query example
==== ====
[source, JAVA, indent=0] [source, JAVA, indent=0]
---- ----
include::{bulkid-sourcedir}/AbstractBulkCompositeIdTest.java[tags=batch-bulk-hql-temp-table-delete-query-example] include::{bulkid-sourcedir}/AbstractMutationStrategyCompositeIdTest.java[tags=batch-bulk-hql-temp-table-delete-query-example]
---- ----
[source, SQL, indent=0] [source, SQL, indent=0]
@ -349,31 +366,21 @@ include::{extrasdir}/batch-bulk-hql-temp-table-delete-query-example.sql[]
---- ----
==== ====
`HT_Person` is a temporary table that Hibernate creates to hold all the entity identifiers that are to be updated or deleted by the bulk id operation. `HT_Person` is a temporary table that Hibernate creates to hold all the entity identifiers that are to be updated or deleted by the bulk operation.
The temporary table can be either global or local, depending on the underlying database capabilities. The temporary table can be either global or local, depending on the underlying database capabilities.
[[batch-bulk-hql-strategies-non-temporary-table]] [[batch-bulk-hql-strategies-non-temporary-table]]
===== Non-temporary table bulk-id strategies ===== Non-temporary table bulk mutation strategies
As the https://hibernate.atlassian.net/browse/HHH-11262[HHH-11262] issue describes, there are use cases when the application developer cannot use temporary tables because When the temporary table strategy can not be used because the database user lacks privilege to create temporary tables,
the database user lacks this privilege. the `InlineMutationStrategy` must be used.
In this case, we defined several options which you can choose depending on your database capabilities:
- `InlineIdsInClauseBulkIdStrategy`
- `InlineIdsSubSelectValueListBulkIdStrategy`
- `InlineIdsOrClauseBulkIdStrategy`
- `CteValuesListBulkIdStrategy`
[[batch-bulk-hql-strategies-InlineIdsInClauseBulkIdStrategy]]
====== `InlineIdsInClauseBulkIdStrategy`
To use this strategy, you need to configure the following configuration property: To use this strategy, you need to configure the following configuration property:
[source,xml] [source,xml]
---- ----
<property name="hibernate.hql.bulk_id_strategy" <property name="hibernate.query.mutation_strategy"
value="org.hibernate.hql.spi.id.inline.InlineIdsInClauseBulkIdStrategy" value="org.hibernate.query.sqm.mutation.internal.inline.InlineMutationStrategy"
/> />
---- ----
@ -390,110 +397,3 @@ include::{extrasdir}/batch-bulk-hql-InlineIdsInClauseBulkIdStrategy-delete-query
So, the entity identifiers are selected first and used for each particular update or delete statement. So, the entity identifiers are selected first and used for each particular update or delete statement.
[TIP]
====
The IN clause row value expression has long been supported by Oracle, PostgreSQL, and nowadays by MySQL 5.7.
However, SQL Server 2014 does not support it, so you'll have to use a different strategy.
====
[[batch-bulk-hql-strategies-InlineIdsSubSelectValueListBulkIdStrategy]]
====== `InlineIdsSubSelectValueListBulkIdStrategy`
To use this strategy, you need to configure the following configuration property:
[source,xml]
----
<property name="hibernate.hql.bulk_id_strategy"
value="org.hibernate.hql.spi.id.inline.InlineIdsSubSelectValueListBulkIdStrategy"
/>
----
Now, when running the previous test case, Hibernate generates the following SQL statements:
[[batch-bulk-hql-InlineIdsSubSelectValueListBulkIdStrategy-delete-query-example]]
.`InlineIdsSubSelectValueListBulkIdStrategy` delete entity query example
====
[source, SQL, indent=0]
----
include::{extrasdir}/batch-bulk-hql-InlineIdsSubSelectValueListBulkIdStrategy-delete-query-example.sql[]
----
====
[TIP]
====
The underlying database must support the `VALUES` list clause, like PostgreSQL or SQL Server 2008.
However, this strategy requires the IN-clause row value expression for composite identifiers, and for this reason, you can only use the `InlineIdsSubSelectValueListBulkIdStrategy` strategy with PostgreSQL.
====
[[batch-bulk-hql-strategies-InlineIdsOrClauseBulkIdStrategy]]
====== `InlineIdsOrClauseBulkIdStrategy`
To use this strategy, you need to configure the following configuration property:
[source,xml]
----
<property name="hibernate.hql.bulk_id_strategy"
value="org.hibernate.hql.spi.id.inline.InlineIdsOrClauseBulkIdStrategy"
/>
----
Now, when running the previous test case, Hibernate generates the following SQL statements:
[[batch-bulk-hql-InlineIdsOrClauseBulkIdStrategy-delete-query-example]]
.`InlineIdsOrClauseBulkIdStrategy` delete entity query example
====
[source, SQL, indent=0]
----
include::{extrasdir}/batch-bulk-hql-InlineIdsOrClauseBulkIdStrategy-delete-query-example.sql[]
----
====
[TIP]
====
The `InlineIdsOrClauseBulkIdStrategy` strategy has the advantage of being supported by all the major relational database systems (e.g. Oracle, SQL Server, MySQL, and PostgreSQL).
====
[[batch-bulk-hql-strategies-CteValuesListBulkIdStrategy]]
====== `CteValuesListBulkIdStrategy`
To use this strategy, you need to configure the following configuration property:
[source,xml]
----
<property name="hibernate.hql.bulk_id_strategy"
value="org.hibernate.hql.spi.id.inline.CteValuesListBulkIdStrategy"
/>
----
Now, when running the previous test case, Hibernate generates the following SQL statements:
[[batch-bulk-hql-CteValuesListBulkIdStrategy-delete-query-example]]
.`CteValuesListBulkIdStrategy` delete entity query example
====
[source, SQL, indent=0]
----
include::{extrasdir}/batch-bulk-hql-CteValuesListBulkIdStrategy-delete-query-example.sql[]
----
====
[TIP]
====
The underlying database must support CTE (Common Table Expressions) that can be referenced from non-query statements as well. For instance, PostgreSQL supports this feature since version 9.1 and SQL Server offers support for it since version 2005.
The underlying database must also support the VALUES list clause, like PostgreSQL or SQL Server 2008.
However, this strategy requires the IN-clause row value expression for composite identifiers, so you can only use this strategy with PostgreSQL.
====
If you can use temporary tables, that's probably the best choice.
However, if you are not allowed to create temporary tables, you must pick one of these four strategies that works with your underlying database.
Before making up your mind, you should benchmark which one works best for your current workload.
For instance, https://blog.2ndquadrant.com/postgresql-ctes-are-optimization-fences/[CTE are optimization fences in PostgreSQL], so make sure you measure before making a decision.
If you're using Oracle or MySQL 5.7, you can choose either `InlineIdsOrClauseBulkIdStrategy` or `InlineIdsInClauseBulkIdStrategy`.
For older version of MySQL, then you can only use `InlineIdsOrClauseBulkIdStrategy`.
If you're using SQL Server, `InlineIdsOrClauseBulkIdStrategy` is the only option for you.
If you're using PostgreSQL, then you have the luxury of choosing any of these four strategies.

View File

@ -29,6 +29,7 @@ import org.hibernate.query.sqm.internal.SqmUtil;
import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess;
import org.hibernate.query.sqm.sql.internal.SqlAstQueryPartProcessingStateImpl; import org.hibernate.query.sqm.sql.internal.SqlAstQueryPartProcessingStateImpl;
import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement;
import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameter;
import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslator;
@ -47,6 +48,7 @@ import org.hibernate.sql.results.graph.DomainResult;
import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.sql.results.graph.basic.BasicResult;
import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.sql.results.internal.SqlSelectionImpl;
import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.sql.results.spi.ListResultsConsumer;
import org.hibernate.sql.results.spi.RowTransformer;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@ -190,7 +192,7 @@ public class MatchingIdSelectionHelper {
* or UPDATE SQM query * or UPDATE SQM query
*/ */
public static List<Object> selectMatchingIds( public static List<Object> selectMatchingIds(
SqmDeleteOrUpdateStatement sqmMutationStatement, SqmDeleteOrUpdateStatement<?> sqmMutationStatement,
DomainParameterXref domainParameterXref, DomainParameterXref domainParameterXref,
DomainQueryExecutionContext executionContext) { DomainQueryExecutionContext executionContext) {
final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); final SessionFactoryImplementor factory = executionContext.getSession().getFactory();
@ -236,46 +238,50 @@ public class MatchingIdSelectionHelper {
factory factory
); );
sqmConverter.getProcessingStateStack().push( if ( sqmMutationStatement instanceof SqmDeleteStatement<?> ) {
new SqlAstQueryPartProcessingStateImpl( // For delete statements we also want to collect FK values to execute collection table cleanups
matchingIdSelection.getQuerySpec(),
sqmConverter.getCurrentProcessingState(),
sqmConverter.getSqlAstCreationState(),
sqmConverter.getCurrentClauseStack()::getCurrent,
true
)
);
entityDescriptor.visitSubTypeAttributeMappings(
attribute -> {
if ( attribute instanceof PluralAttributeMapping ) {
final PluralAttributeMapping pluralAttribute = (PluralAttributeMapping) attribute;
if ( pluralAttribute.getSeparateCollectionTable() != null ) { sqmConverter.getProcessingStateStack().push(
// Ensure that the FK target columns are available new SqlAstQueryPartProcessingStateImpl(
final boolean useFkTarget = !( pluralAttribute.getKeyDescriptor() matchingIdSelection.getQuerySpec(),
.getTargetPart() instanceof EntityIdentifierMapping ); sqmConverter.getCurrentProcessingState(),
if ( useFkTarget ) { sqmConverter.getSqlAstCreationState(),
final TableGroup mutatingTableGroup = sqmConverter.getMutatingTableGroup(); sqmConverter.getCurrentClauseStack()::getCurrent,
pluralAttribute.getKeyDescriptor().getTargetPart().applySqlSelections( true
mutatingTableGroup.getNavigablePath(), )
mutatingTableGroup, );
sqmConverter, entityDescriptor.visitSubTypeAttributeMappings(
(selection, jdbcMapping) -> { attribute -> {
matchingIdSelection.getDomainResultDescriptors().add( if ( attribute instanceof PluralAttributeMapping ) {
new BasicResult<>( final PluralAttributeMapping pluralAttribute = (PluralAttributeMapping) attribute;
selection.getValuesArrayPosition(),
null, if ( pluralAttribute.getSeparateCollectionTable() != null ) {
jdbcMapping.getJavaTypeDescriptor() // Ensure that the FK target columns are available
) final boolean useFkTarget = !( pluralAttribute.getKeyDescriptor()
); .getTargetPart() instanceof EntityIdentifierMapping );
} if ( useFkTarget ) {
); final TableGroup mutatingTableGroup = sqmConverter.getMutatingTableGroup();
pluralAttribute.getKeyDescriptor().getTargetPart().applySqlSelections(
mutatingTableGroup.getNavigablePath(),
mutatingTableGroup,
sqmConverter,
(selection, jdbcMapping) -> {
matchingIdSelection.getDomainResultDescriptors().add(
new BasicResult<>(
selection.getValuesArrayPosition(),
null,
jdbcMapping.getJavaTypeDescriptor()
)
);
}
);
}
} }
} }
} }
} );
); sqmConverter.getProcessingStateStack().pop();
sqmConverter.getProcessingStateStack().pop(); }
final JdbcServices jdbcServices = factory.getJdbcServices(); final JdbcServices jdbcServices = factory.getJdbcServices();
final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment();
@ -318,11 +324,18 @@ public class MatchingIdSelectionHelper {
); );
lockOptions.setLockMode( lockMode ); lockOptions.setLockMode( lockMode );
final RowTransformer<Object> rowTransformer;
if ( matchingIdSelection.getDomainResultDescriptors().size() == 1 ) {
rowTransformer = row -> row[0];
}
else {
rowTransformer = row -> row;
}
return jdbcServices.getJdbcSelectExecutor().list( return jdbcServices.getJdbcSelectExecutor().list(
idSelectJdbcOperation, idSelectJdbcOperation,
jdbcParameterBindings, jdbcParameterBindings,
SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ), SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ),
row -> row[0], rowTransformer,
ListResultsConsumer.UniqueSemantic.FILTER ListResultsConsumer.UniqueSemantic.FILTER
); );
} }

View File

@ -4,7 +4,7 @@
* License: GNU Lesser General Public License (LGPL), version 2.1 or later * 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 * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html
*/ */
package org.hibernate.query.sqm.mutation.internal.temptable; package org.hibernate.query.sqm.mutation.internal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -17,17 +17,17 @@ import org.hibernate.sql.ast.tree.expression.SqlTuple;
/** /**
* @author Steve Ebersole * @author Steve Ebersole
*/ */
class TableKeyExpressionCollector { public class TableKeyExpressionCollector {
private final EntityMappingType entityMappingType;
TableKeyExpressionCollector(EntityMappingType entityMappingType) { private final EntityMappingType entityMappingType;
private Expression firstColumnExpression;
private List<Expression> collectedColumnExpressions;
public TableKeyExpressionCollector(EntityMappingType entityMappingType) {
this.entityMappingType = entityMappingType; this.entityMappingType = entityMappingType;
} }
Expression firstColumnExpression; public void apply(ColumnReference columnReference) {
List<Expression> collectedColumnExpressions;
void apply(ColumnReference columnReference) {
if ( firstColumnExpression == null ) { if ( firstColumnExpression == null ) {
firstColumnExpression = columnReference; firstColumnExpression = columnReference;
} }
@ -41,7 +41,7 @@ class TableKeyExpressionCollector {
} }
} }
Expression buildKeyExpression() { public Expression buildKeyExpression() {
if ( collectedColumnExpressions == null ) { if ( collectedColumnExpressions == null ) {
return firstColumnExpression; return firstColumnExpression;
} }

View File

@ -36,7 +36,7 @@ import org.hibernate.sql.ast.tree.select.SelectStatement;
import org.hibernate.sql.results.graph.basic.BasicResult; import org.hibernate.sql.results.graph.basic.BasicResult;
/** /**
* Bulk-id delete handler that uses CTE and VALUES lists. * Bulk mutation delete handler that uses CTE and VALUES lists.
* *
* @author Christian Beikov * @author Christian Beikov
*/ */

View File

@ -43,7 +43,7 @@ import org.hibernate.sql.exec.spi.StatementCreatorHelper;
*/ */
public class InlineDeleteHandler implements DeleteHandler { public class InlineDeleteHandler implements DeleteHandler {
private final MatchingIdRestrictionProducer matchingIdsPredicateProducer; private final MatchingIdRestrictionProducer matchingIdsPredicateProducer;
private final SqmDeleteStatement sqmDeleteStatement; private final SqmDeleteStatement<?> sqmDeleteStatement;
private final DomainParameterXref domainParameterXref; private final DomainParameterXref domainParameterXref;
private final DomainQueryExecutionContext executionContext; private final DomainQueryExecutionContext executionContext;
@ -54,7 +54,7 @@ public class InlineDeleteHandler implements DeleteHandler {
protected InlineDeleteHandler( protected InlineDeleteHandler(
MatchingIdRestrictionProducer matchingIdsPredicateProducer, MatchingIdRestrictionProducer matchingIdsPredicateProducer,
SqmDeleteStatement sqmDeleteStatement, SqmDeleteStatement<?> sqmDeleteStatement,
DomainParameterXref domainParameterXref, DomainParameterXref domainParameterXref,
DomainQueryExecutionContext context) { DomainQueryExecutionContext context) {
this.sqmDeleteStatement = sqmDeleteStatement; this.sqmDeleteStatement = sqmDeleteStatement;

View File

@ -26,17 +26,17 @@ import org.hibernate.query.sqm.tree.update.SqmUpdateStatement;
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class InlineMutationStrategy implements SqmMultiTableMutationStrategy { public class InlineMutationStrategy implements SqmMultiTableMutationStrategy {
private final Function<SqmDeleteOrUpdateStatement,MatchingIdRestrictionProducer> matchingIdsStrategy; private final Function<SqmDeleteOrUpdateStatement<?>,MatchingIdRestrictionProducer> matchingIdsStrategy;
public InlineMutationStrategy(Dialect dialect) { public InlineMutationStrategy(Dialect dialect) {
this( determinePredicateProducer( dialect ) ); this( determinePredicateProducer( dialect ) );
} }
private static Function<SqmDeleteOrUpdateStatement,MatchingIdRestrictionProducer> determinePredicateProducer(Dialect dialect) { private static Function<SqmDeleteOrUpdateStatement<?>,MatchingIdRestrictionProducer> determinePredicateProducer(Dialect dialect) {
return statement -> new InPredicateRestrictionProducer(); return statement -> new InPredicateRestrictionProducer();
} }
public InlineMutationStrategy(Function<SqmDeleteOrUpdateStatement,MatchingIdRestrictionProducer> matchingIdsStrategy) { public InlineMutationStrategy(Function<SqmDeleteOrUpdateStatement<?>,MatchingIdRestrictionProducer> matchingIdsStrategy) {
this.matchingIdsStrategy = matchingIdsStrategy; this.matchingIdsStrategy = matchingIdsStrategy;
} }

View File

@ -6,21 +6,85 @@
*/ */
package org.hibernate.query.sqm.mutation.internal.inline; package org.hibernate.query.sqm.mutation.internal.inline;
import org.hibernate.NotYetImplementedFor6Exception; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.metamodel.MappingMetamodel;
import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping;
import org.hibernate.metamodel.mapping.BasicValuedMapping;
import org.hibernate.metamodel.mapping.BasicValuedModelPart;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.metamodel.mapping.SelectableConsumer;
import org.hibernate.persister.entity.AbstractEntityPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.query.SemanticException;
import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.spi.DomainQueryExecutionContext;
import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.DomainParameterXref;
import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter;
import org.hibernate.query.sqm.internal.SqmUtil;
import org.hibernate.query.sqm.mutation.internal.MatchingIdSelectionHelper;
import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter;
import org.hibernate.query.sqm.mutation.internal.UpdateHandler; import org.hibernate.query.sqm.mutation.internal.UpdateHandler;
import org.hibernate.query.sqm.mutation.internal.TableKeyExpressionCollector;
import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess;
import org.hibernate.query.sqm.tree.expression.SqmParameter;
import org.hibernate.query.sqm.tree.predicate.SqmWhereClause;
import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement;
import org.hibernate.spi.NavigablePath;
import org.hibernate.sql.ast.SqlAstJoinType;
import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.spi.SqlAliasBaseImpl;
import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.ast.tree.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.JdbcParameter;
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
import org.hibernate.sql.ast.tree.expression.SqlTuple;
import org.hibernate.sql.ast.tree.from.NamedTableReference;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.TableGroupJoin;
import org.hibernate.sql.ast.tree.from.TableReference;
import org.hibernate.sql.ast.tree.from.TableReferenceJoin;
import org.hibernate.sql.ast.tree.from.UnionTableReference;
import org.hibernate.sql.ast.tree.from.ValuesTableGroup;
import org.hibernate.sql.ast.tree.insert.InsertStatement;
import org.hibernate.sql.ast.tree.insert.Values;
import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate;
import org.hibernate.sql.ast.tree.predicate.InListPredicate;
import org.hibernate.sql.ast.tree.predicate.NullnessPredicate;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.ast.tree.predicate.PredicateCollector;
import org.hibernate.sql.ast.tree.select.QuerySpec;
import org.hibernate.sql.ast.tree.select.SelectClause;
import org.hibernate.sql.ast.tree.update.Assignment;
import org.hibernate.sql.ast.tree.update.UpdateStatement;
import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.ExecutionContext;
import org.hibernate.sql.exec.spi.JdbcInsert;
import org.hibernate.sql.exec.spi.JdbcMutationExecutor; import org.hibernate.sql.exec.spi.JdbcMutationExecutor;
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.hibernate.sql.exec.spi.JdbcUpdate;
import org.hibernate.sql.results.internal.SqlSelectionImpl;
/** /**
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public class InlineUpdateHandler implements UpdateHandler { public class InlineUpdateHandler implements UpdateHandler {
private final SqmUpdateStatement sqmUpdate; private final SqmUpdateStatement<?> sqmUpdate;
private final DomainParameterXref domainParameterXref; private final DomainParameterXref domainParameterXref;
private final MatchingIdRestrictionProducer matchingIdsPredicateProducer; private final MatchingIdRestrictionProducer matchingIdsPredicateProducer;
@ -32,7 +96,7 @@ public class InlineUpdateHandler implements UpdateHandler {
public InlineUpdateHandler( public InlineUpdateHandler(
MatchingIdRestrictionProducer matchingIdsPredicateProducer, MatchingIdRestrictionProducer matchingIdsPredicateProducer,
SqmUpdateStatement sqmUpdate, SqmUpdateStatement<?> sqmUpdate,
DomainParameterXref domainParameterXref, DomainParameterXref domainParameterXref,
DomainQueryExecutionContext context) { DomainQueryExecutionContext context) {
this.matchingIdsPredicateProducer = matchingIdsPredicateProducer; this.matchingIdsPredicateProducer = matchingIdsPredicateProducer;
@ -48,6 +112,527 @@ public class InlineUpdateHandler implements UpdateHandler {
@Override @Override
public int execute(DomainQueryExecutionContext executionContext) { public int execute(DomainQueryExecutionContext executionContext) {
throw new NotYetImplementedFor6Exception( getClass() ); final List<Object> ids = MatchingIdSelectionHelper.selectMatchingIds(
sqmUpdate,
domainParameterXref,
executionContext
);
if ( ids == null || ids.isEmpty() ) {
return 0;
}
domainParameterXref.clearExpansions();
final MappingMetamodel domainModel = sessionFactory.getRuntimeMetamodels().getMappingMetamodel();
final String mutatingEntityName = sqmUpdate.getTarget().getModel().getHibernateEntityName();
final EntityPersister entityDescriptor = domainModel.getEntityDescriptor( mutatingEntityName );
final String rootEntityName = entityDescriptor.getEntityPersister().getRootEntityName();
final EntityPersister rootEntityDescriptor = domainModel.getEntityDescriptor( rootEntityName );
final String hierarchyRootTableName = ( (Joinable) rootEntityDescriptor ).getTableName();
final List<Expression> inListExpressions = new ArrayList<>( ids.size() );
final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping();
if ( identifierMapping instanceof BasicValuedModelPart ) {
final BasicValuedModelPart basicValuedModelPart = (BasicValuedModelPart) identifierMapping;
for ( int i = 0; i < ids.size(); i++ ) {
inListExpressions.add( new QueryLiteral<>( ids.get( i ), basicValuedModelPart ) );
}
}
else {
final int jdbcTypeCount = identifierMapping.getJdbcTypeCount();
for ( int i = 0; i < ids.size(); i++ ) {
final Object[] id = (Object[]) ids.get( i );
final List<Expression> tupleElements = new ArrayList<>( jdbcTypeCount );
inListExpressions.add( new SqlTuple( tupleElements, identifierMapping ) );
identifierMapping.forEachJdbcType(
(index, jdbcMapping) -> {
tupleElements.add(
new QueryLiteral<>( id[index], (BasicValuedMapping) jdbcMapping )
);
}
);
}
}
final MultiTableSqmMutationConverter converterDelegate = new MultiTableSqmMutationConverter(
entityDescriptor,
sqmUpdate,
sqmUpdate.getTarget(),
domainParameterXref,
executionContext.getQueryOptions(),
executionContext.getSession().getLoadQueryInfluencers(),
executionContext.getQueryParameterBindings(),
sessionFactory
);
final TableGroup updatingTableGroup = converterDelegate.getMutatingTableGroup();
final TableReference hierarchyRootTableReference = updatingTableGroup.resolveTableReference(
updatingTableGroup.getNavigablePath(),
hierarchyRootTableName
);
assert hierarchyRootTableReference != null;
final Map<SqmParameter<?>, List<List<JdbcParameter>>> parameterResolutions;
if ( domainParameterXref.getSqmParameterCount() == 0 ) {
parameterResolutions = Collections.emptyMap();
}
else {
parameterResolutions = new IdentityHashMap<>();
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// visit the set-clause using our special converter, collecting
// information about the assignments
final List<Assignment> assignments = new ArrayList<>();
final Map<SqmParameter<?>, MappingModelExpressible<?>> paramTypeResolutions = new LinkedHashMap<>();
converterDelegate.visitSetClause(
sqmUpdate.getSetClause(),
assignments::add,
(sqmParameter, mappingType, jdbcParameters) -> {
parameterResolutions.computeIfAbsent(
sqmParameter,
k -> new ArrayList<>( 1 )
).add( jdbcParameters );
paramTypeResolutions.put( sqmParameter, mappingType );
}
);
converterDelegate.addVersionedAssignment( assignments::add, sqmUpdate );
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// visit the where-clause using our special converter, collecting information
// about the restrictions
final Predicate providedPredicate;
final SqmWhereClause whereClause = sqmUpdate.getWhereClause();
if ( whereClause == null || whereClause.getPredicate() == null ) {
providedPredicate = null;
}
else {
providedPredicate = converterDelegate.visitWhereClause(
whereClause,
columnReference -> {},
(sqmParameter, mappingType, jdbcParameters) -> {
parameterResolutions.computeIfAbsent(
sqmParameter,
k -> new ArrayList<>( 1 )
).add( jdbcParameters );
paramTypeResolutions.put( sqmParameter, mappingType );
}
);
assert providedPredicate != null;
}
final PredicateCollector predicateCollector = new PredicateCollector( providedPredicate );
entityDescriptor.applyBaseRestrictions(
predicateCollector::applyPredicate,
updatingTableGroup,
true,
executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(),
null,
converterDelegate
);
converterDelegate.pruneTableGroupJoins();
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// cross-reference the TableReference by alias. The TableGroup already
// cross-references it by name, bu the ColumnReference only has the alias
final Map<String, TableReference> tableReferenceByAlias = CollectionHelper.mapOfSize( updatingTableGroup.getTableReferenceJoins().size() + 1 );
collectTableReference( updatingTableGroup.getPrimaryTableReference(), tableReferenceByAlias::put );
for ( int i = 0; i < updatingTableGroup.getTableReferenceJoins().size(); i++ ) {
collectTableReference( updatingTableGroup.getTableReferenceJoins().get( i ), tableReferenceByAlias::put );
}
final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings(
executionContext.getQueryParameterBindings(),
domainParameterXref,
SqmUtil.generateJdbcParamsXref(
domainParameterXref,
() -> parameterResolutions
),
sessionFactory.getRuntimeMetamodels().getMappingMetamodel(),
navigablePath -> updatingTableGroup,
new SqmParameterMappingModelResolutionAccess() {
@Override
@SuppressWarnings("unchecked")
public <T> MappingModelExpressible<T> getResolvedMappingModelType(SqmParameter<T> parameter) {
return (MappingModelExpressible<T>) paramTypeResolutions.get( parameter );
}
},
executionContext.getSession()
);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// segment the assignments by table-reference
final Map<TableReference, List<Assignment>> assignmentsByTable = new HashMap<>();
for ( int i = 0; i < assignments.size(); i++ ) {
final Assignment assignment = assignments.get( i );
final List<ColumnReference> assignmentColumnRefs = assignment.getAssignable().getColumnReferences();
TableReference assignmentTableReference = null;
for ( int c = 0; c < assignmentColumnRefs.size(); c++ ) {
final ColumnReference columnReference = assignmentColumnRefs.get( c );
final TableReference tableReference = resolveTableReference(
columnReference,
tableReferenceByAlias
);
if ( assignmentTableReference != null && assignmentTableReference != tableReference ) {
throw new SemanticException( "Assignment referred to columns from multiple tables: " + assignment.getAssignable() );
}
assignmentTableReference = tableReference;
}
List<Assignment> assignmentsForTable = assignmentsByTable.get( assignmentTableReference );
if ( assignmentsForTable == null ) {
assignmentsForTable = new ArrayList<>();
assignmentsByTable.put( assignmentTableReference, assignmentsForTable );
}
assignmentsForTable.add( assignment );
}
final int rows = ids.size();
final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext );
entityDescriptor.visitConstraintOrderedTables(
(tableExpression, tableKeyColumnVisitationSupplier) -> updateTable(
tableExpression,
tableKeyColumnVisitationSupplier,
entityDescriptor,
updatingTableGroup,
assignmentsByTable,
inListExpressions,
rows,
jdbcParameterBindings,
executionContextAdapter
)
);
return rows;
}
private void updateTable(
String tableExpression,
Supplier<Consumer<SelectableConsumer>> tableKeyColumnVisitationSupplier,
EntityPersister entityDescriptor,
TableGroup updatingTableGroup,
Map<TableReference, List<Assignment>> assignmentsByTable,
List<Expression> inListExpressions,
int expectedUpdateCount,
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext) {
final TableReference updatingTableReference = updatingTableGroup.getTableReference(
updatingTableGroup.getNavigablePath(),
tableExpression,
true,
true
);
final List<Assignment> assignments = assignmentsByTable.get( updatingTableReference );
if ( assignments == null || assignments.isEmpty() ) {
// no assignments for this table - skip it
return;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// create the in-subquery predicate to restrict the updates to just
// matching ids
final TableKeyExpressionCollector keyColumnCollector = new TableKeyExpressionCollector( entityDescriptor );
tableKeyColumnVisitationSupplier.get().accept(
(columnIndex, selection) -> {
assert selection.getContainingTableExpression().equals( tableExpression );
keyColumnCollector.apply(
new ColumnReference(
(String) null,
selection,
sessionFactory
)
);
}
);
final Expression keyExpression = keyColumnCollector.buildKeyExpression();
final InListPredicate idListPredicate = new InListPredicate(
keyExpression,
inListExpressions
);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Create the SQL AST and convert it into a JdbcOperation
final NamedTableReference dmlTableReference = resolveUnionTableReference( updatingTableReference, tableExpression );
final UpdateStatement sqlAst = new UpdateStatement(
dmlTableReference,
assignments,
idListPredicate
);
final JdbcServices jdbcServices = sessionFactory.getJdbcServices();
final JdbcUpdate jdbcUpdate = jdbcServices.getJdbcEnvironment()
.getSqlAstTranslatorFactory()
.buildUpdateTranslator( sessionFactory, sqlAst )
.translate( jdbcParameterBindings, executionContext.getQueryOptions() );
final int updateCount = jdbcServices.getJdbcMutationExecutor().execute(
jdbcUpdate,
jdbcParameterBindings,
sql -> executionContext.getSession()
.getJdbcCoordinator()
.getStatementPreparer()
.prepareStatement( sql ),
(integer, preparedStatement) -> {
},
executionContext
);
if ( updateCount == expectedUpdateCount ) {
// We are done when the update count matches
return;
}
// Otherwise we have to check if the table is nullable, and if so, insert into that table
final AbstractEntityPersister entityPersister = (AbstractEntityPersister) entityDescriptor.getEntityPersister();
boolean isNullable = false;
for (int i = 0; i < entityPersister.getTableSpan(); i++) {
if ( tableExpression.equals( entityPersister.getTableName( i ) ) && entityPersister.isNullableTable( i ) ) {
isNullable = true;
break;
}
}
if ( isNullable ) {
// Copy the subquery contents into a root query
final QuerySpec querySpec = new QuerySpec( true );
final NavigablePath valuesPath = new NavigablePath( "id" );
final List<Values> valuesList = new ArrayList<>( inListExpressions.size() );
for ( Expression inListExpression : inListExpressions ) {
if ( inListExpression instanceof SqlTuple ) {
//noinspection unchecked
valuesList.add( new Values( (List<Expression>) ( (SqlTuple) inListExpression ).getExpressions() ) );
}
else {
valuesList.add( new Values( Collections.singletonList( inListExpression ) ) );
}
}
final TableGroup rootTableGroup = entityDescriptor.createRootTableGroup(
true,
updatingTableGroup.getNavigablePath(),
updatingTableGroup.getSourceAlias(),
() -> predicate -> {},
new SqlAliasBaseImpl( updatingTableGroup.getGroupAlias() ),
null,
null,
sessionFactory
);
final List<String> columnNames;
final Predicate joinPredicate;
if ( keyExpression instanceof SqlTuple ) {
final List<? extends Expression> expressions = ( (SqlTuple) keyExpression ).getExpressions();
final List<Expression> lhs = new ArrayList<>( expressions.size() );
final List<Expression> rhs = new ArrayList<>( expressions.size() );
columnNames = new ArrayList<>( expressions.size() );
entityDescriptor.getIdentifierMapping().forEachSelectable(
(i, selectableMapping) -> {
final Expression expression = expressions.get( i );
final ColumnReference columnReference = expression.getColumnReference();
final ColumnReference valuesColumnReference = new ColumnReference(
valuesPath.getLocalName(),
columnReference.getColumnExpression(),
false,
null,
null,
columnReference.getJdbcMapping(),
null
);
columnNames.add( columnReference.getColumnExpression() );
lhs.add( valuesColumnReference );
rhs.add(
new ColumnReference(
rootTableGroup.getPrimaryTableReference(),
selectableMapping.getSelectionExpression(),
false,
null,
null,
columnReference.getJdbcMapping(),
null
)
);
querySpec.getSelectClause().addSqlSelection(
new SqlSelectionImpl( 1, 0, valuesColumnReference )
);
}
);
joinPredicate = new ComparisonPredicate(
new SqlTuple( lhs, entityDescriptor.getIdentifierMapping() ),
ComparisonOperator.EQUAL,
new SqlTuple( rhs, entityDescriptor.getIdentifierMapping() )
);
}
else {
final ColumnReference columnReference = keyExpression.getColumnReference();
final ColumnReference valuesColumnReference = new ColumnReference(
valuesPath.getLocalName(),
columnReference.getColumnExpression(),
false,
null,
null,
columnReference.getJdbcMapping(),
null
);
columnNames = Collections.singletonList( columnReference.getColumnExpression() );
joinPredicate = new ComparisonPredicate(
valuesColumnReference,
ComparisonOperator.EQUAL,
new ColumnReference(
rootTableGroup.getPrimaryTableReference(),
( (BasicEntityIdentifierMapping) entityDescriptor.getIdentifierMapping() ).getSelectionExpression(),
false,
null,
null,
columnReference.getJdbcMapping(),
null
)
);
querySpec.getSelectClause().addSqlSelection(
new SqlSelectionImpl( 1, 0, valuesColumnReference )
);
}
final ValuesTableGroup valuesTableGroup = new ValuesTableGroup(
valuesPath,
null,
valuesList,
valuesPath.getLocalName(),
columnNames,
true,
sessionFactory
);
valuesTableGroup.addNestedTableGroupJoin(
new TableGroupJoin(
rootTableGroup.getNavigablePath(),
SqlAstJoinType.LEFT,
rootTableGroup,
joinPredicate
)
);
querySpec.getFromClause().addRoot( valuesTableGroup );
// Only when the target row does not exist
querySpec.applyPredicate(
new NullnessPredicate(
new ColumnReference(
rootTableGroup.resolveTableReference( tableExpression ),
columnNames.get( 0 ),
entityDescriptor.getIdentifierMapping().getJdbcMappings().get( 0 ),
null
)
)
);
// Collect the target column references from the key expressions
final List<ColumnReference> targetColumnReferences = new ArrayList<>();
if ( keyExpression instanceof SqlTuple ) {
//noinspection unchecked
targetColumnReferences.addAll( (Collection<? extends ColumnReference>) ( (SqlTuple) keyExpression ).getExpressions() );
}
else {
targetColumnReferences.add( (ColumnReference) keyExpression );
}
// And transform assignments to target column references and selections
for ( Assignment assignment : assignments ) {
targetColumnReferences.addAll( assignment.getAssignable().getColumnReferences() );
querySpec.getSelectClause().addSqlSelection(
new SqlSelectionImpl(
0,
-1,
assignment.getAssignedValue()
)
);
}
final InsertStatement insertSqlAst = new InsertStatement(
dmlTableReference
);
insertSqlAst.addTargetColumnReferences( targetColumnReferences.toArray( new ColumnReference[0] ) );
insertSqlAst.setSourceSelectStatement( querySpec );
final JdbcInsert jdbcInsert = jdbcServices.getJdbcEnvironment()
.getSqlAstTranslatorFactory()
.buildInsertTranslator( sessionFactory, insertSqlAst )
.translate( jdbcParameterBindings, executionContext.getQueryOptions() );
final int insertCount = jdbcServices.getJdbcMutationExecutor().execute(
jdbcInsert,
jdbcParameterBindings,
sql -> executionContext.getSession()
.getJdbcCoordinator()
.getStatementPreparer()
.prepareStatement( sql ),
(integer, preparedStatement) -> {
},
executionContext
);
assert insertCount + updateCount == expectedUpdateCount;
}
}
private Expression asExpression(SelectClause selectClause) {
final List<SqlSelection> sqlSelections = selectClause.getSqlSelections();
if ( sqlSelections.size() == 1 ) {
return sqlSelections.get( 0 ).getExpression();
}
final List<Expression> expressions = new ArrayList<>( sqlSelections.size() );
for ( SqlSelection sqlSelection : sqlSelections ) {
expressions.add( sqlSelection.getExpression() );
}
return new SqlTuple( expressions, null );
}
private void collectTableReference(
TableReference tableReference,
BiConsumer<String, TableReference> consumer) {
consumer.accept( tableReference.getIdentificationVariable(), tableReference );
}
private void collectTableReference(
TableReferenceJoin tableReferenceJoin,
BiConsumer<String, TableReference> consumer) {
collectTableReference( tableReferenceJoin.getJoinedTableReference(), consumer );
}
private TableReference resolveTableReference(
ColumnReference columnReference,
Map<String, TableReference> tableReferenceByAlias) {
final TableReference tableReferenceByQualifier = tableReferenceByAlias.get( columnReference.getQualifier() );
if ( tableReferenceByQualifier != null ) {
return tableReferenceByQualifier;
}
throw new SemanticException( "Assignment referred to column of a joined association: " + columnReference );
}
private NamedTableReference resolveUnionTableReference(
TableReference tableReference,
String tableExpression) {
if ( tableReference instanceof UnionTableReference ) {
return new NamedTableReference(
tableExpression,
tableReference.getIdentificationVariable(),
tableReference.isOptional(),
sessionFactory
);
}
return (NamedTableReference) tableReference;
} }
} }

View File

@ -39,6 +39,7 @@ import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter;
import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.internal.SqmUtil;
import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter;
import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper; import org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper;
import org.hibernate.query.sqm.mutation.internal.TableKeyExpressionCollector;
import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess;
import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameter;

View File

@ -31,6 +31,7 @@ import org.hibernate.query.sqm.ComparisonOperator;
import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.DomainParameterXref;
import org.hibernate.query.sqm.internal.SqmUtil; import org.hibernate.query.sqm.internal.SqmUtil;
import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter;
import org.hibernate.query.sqm.mutation.internal.TableKeyExpressionCollector;
import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess;
import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameter;
import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement;

View File

@ -12,7 +12,15 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
public class Values { public class Values {
private final List<Expression> expressions = new ArrayList<>(); private final List<Expression> expressions;
public Values() {
this.expressions = new ArrayList<>();
}
public Values(List<Expression> expressions) {
this.expressions = expressions;
}
public List<Expression> getExpressions() { public List<Expression> getExpressions() {
return expressions; return expressions;

View File

@ -1,4 +1,4 @@
package org.hibernate.test.bulkid; package org.hibernate.orm.test.bulkid;
import java.io.Serializable; import java.io.Serializable;
import java.util.Objects; import java.util.Objects;
@ -9,7 +9,7 @@ import jakarta.persistence.InheritanceType;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration; import org.hibernate.cfg.Configuration;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Before; import org.junit.Before;
@ -21,7 +21,7 @@ import static org.junit.Assert.assertEquals;
/** /**
* @author Vlad Mihalcea * @author Vlad Mihalcea
*/ */
public abstract class AbstractBulkCompositeIdTest extends BaseCoreFunctionalTestCase { public abstract class AbstractMutationStrategyCompositeIdTest extends BaseCoreFunctionalTestCase {
@Override @Override
protected Class<?>[] getAnnotatedClasses() { protected Class<?>[] getAnnotatedClasses() {
@ -35,13 +35,13 @@ public abstract class AbstractBulkCompositeIdTest extends BaseCoreFunctionalTest
@Override @Override
protected void configure(Configuration configuration) { protected void configure(Configuration configuration) {
super.configure( configuration ); super.configure( configuration );
Class<? extends MultiTableBulkIdStrategy> strategyClass = getMultiTableBulkIdStrategyClass(); Class<? extends SqmMultiTableMutationStrategy> strategyClass = getMultiTableBulkIdStrategyClass();
if ( strategyClass != null ) { if ( strategyClass != null ) {
configuration.setProperty( AvailableSettings.HQL_BULK_ID_STRATEGY, strategyClass.getName() ); configuration.setProperty( AvailableSettings.QUERY_MULTI_TABLE_MUTATION_STRATEGY, strategyClass.getName() );
} }
} }
protected abstract Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass(); protected abstract Class<? extends SqmMultiTableMutationStrategy> getMultiTableBulkIdStrategyClass();
@Override @Override
protected boolean isCleanupTestDataRequired() { protected boolean isCleanupTestDataRequired() {

View File

@ -1,4 +1,4 @@
package org.hibernate.test.bulkid; package org.hibernate.orm.test.bulkid;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@ -8,7 +8,7 @@ import jakarta.persistence.InheritanceType;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration; import org.hibernate.cfg.Configuration;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Before; import org.junit.Before;
@ -20,7 +20,7 @@ import static org.junit.Assert.assertEquals;
/** /**
* @author Vlad Mihalcea * @author Vlad Mihalcea
*/ */
public abstract class AbstractBulkIdTest extends BaseCoreFunctionalTestCase { public abstract class AbstractMutationStrategyIdTest extends BaseCoreFunctionalTestCase {
@Override @Override
protected Class<?>[] getAnnotatedClasses() { protected Class<?>[] getAnnotatedClasses() {
@ -31,15 +31,20 @@ public abstract class AbstractBulkIdTest extends BaseCoreFunctionalTestCase {
}; };
} }
@Override
@Override @Override
protected void configure(Configuration configuration) { protected void configure(Configuration configuration) {
super.configure( configuration ); super.configure( configuration );
configuration.setProperty( AvailableSettings.HQL_BULK_ID_STRATEGY, getMultiTableBulkIdStrategyClass().getName() ); final Class<? extends SqmMultiTableMutationStrategy> multiTableBulkIdStrategyClass = getMultiTableBulkIdStrategyClass();
if ( multiTableBulkIdStrategyClass != null ) {
configuration.setProperty(
AvailableSettings.QUERY_MULTI_TABLE_MUTATION_STRATEGY,
multiTableBulkIdStrategyClass.getName()
);
}
} }
protected abstract Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass(); protected abstract Class<? extends SqmMultiTableMutationStrategy> getMultiTableBulkIdStrategyClass();
@Override @Override
protected boolean isCleanupTestDataRequired() { protected boolean isCleanupTestDataRequired() {

View File

@ -0,0 +1,14 @@
package org.hibernate.orm.test.bulkid;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
/**
* @author Vlad Mihalcea
*/
public class DefaultMutationStrategyCompositeIdTest extends AbstractMutationStrategyCompositeIdTest {
@Override
protected Class<? extends SqmMultiTableMutationStrategy> getMultiTableBulkIdStrategyClass() {
return null;
}
}

View File

@ -0,0 +1,14 @@
package org.hibernate.orm.test.bulkid;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
/**
* @author Vlad Mihalcea
*/
public class DefaultMutationStrategyIdTest extends AbstractMutationStrategyIdTest {
@Override
protected Class<? extends SqmMultiTableMutationStrategy> getMultiTableBulkIdStrategyClass() {
return null;
}
}

View File

@ -1,4 +1,4 @@
package org.hibernate.test.bulkid; package org.hibernate.orm.test.bulkid;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.Arrays; import java.util.Arrays;
@ -13,17 +13,13 @@ import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType; import jakarta.persistence.TemporalType;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; import org.hibernate.query.sqm.mutation.internal.inline.InlineMutationStrategy;
import org.hibernate.hql.spi.id.inline.InlineIdsOrClauseBulkIdStrategy;
import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase;
import org.hibernate.testing.TestForIssue; import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -31,8 +27,7 @@ import static org.junit.Assert.assertEquals;
* @author Vlad Mihalcea * @author Vlad Mihalcea
*/ */
@TestForIssue( jiraKey = "HHH-12561" ) @TestForIssue( jiraKey = "HHH-12561" )
public class GlobalQuotedIdentifiersBulkIdTest public class GlobalQuotedIdentifiersBulkIdTest extends BaseEntityManagerFunctionalTestCase {
extends BaseEntityManagerFunctionalTestCase {
@Override @Override
protected Class<?>[] getAnnotatedClasses() { protected Class<?>[] getAnnotatedClasses() {
@ -46,7 +41,7 @@ public class GlobalQuotedIdentifiersBulkIdTest
@Override @Override
protected void addConfigOptions(Map options) { protected void addConfigOptions(Map options) {
options.put( AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, Boolean.TRUE ); options.put( AvailableSettings.GLOBALLY_QUOTED_IDENTIFIERS, Boolean.TRUE );
options.put( AvailableSettings.QUERY_MULTI_TABLE_MUTATION_STRATEGY, InlineIdsOrClauseBulkIdStrategy.class.getName() ); options.put( AvailableSettings.QUERY_MULTI_TABLE_MUTATION_STRATEGY, InlineMutationStrategy.class.getName() );
} }
@Before @Before

View File

@ -0,0 +1,15 @@
package org.hibernate.orm.test.bulkid;
import org.hibernate.query.sqm.mutation.internal.inline.InlineMutationStrategy;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
/**
* @author Vlad Mihalcea
*/
public class InlineMutationStrategyCompositeIdTest extends AbstractMutationStrategyCompositeIdTest {
@Override
protected Class<? extends SqmMultiTableMutationStrategy> getMultiTableBulkIdStrategyClass() {
return InlineMutationStrategy.class;
}
}

View File

@ -0,0 +1,15 @@
package org.hibernate.orm.test.bulkid;
import org.hibernate.query.sqm.mutation.internal.inline.InlineMutationStrategy;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
/**
* @author Vlad Mihalcea
*/
public class InlineMutationStrategyIdTest extends AbstractMutationStrategyIdTest {
@Override
protected Class<? extends SqmMultiTableMutationStrategy> getMultiTableBulkIdStrategyClass() {
return InlineMutationStrategy.class;
}
}

View File

@ -0,0 +1,15 @@
package org.hibernate.orm.test.bulkid;
/**
* Special test that tries to update 1100 rows. Oracle only supports up to 1000 parameters per in-predicate,
* so we want to test if this scenario works.
*
* @author Vlad Mihalcea
*/
public class OracleInlineMutationStrategyIdTest extends InlineMutationStrategyIdTest {
@Override
protected int entityCount() {
return 1100;
}
}

View File

@ -1,19 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.cte.CteValuesListBulkIdStrategy;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
/**
* @author Vlad Mihalcea
*/
@RequiresDialectFeature(DialectChecks.SupportNonQueryValuesListWithCTE.class)
public class CteValuesListBulkCompositeIdTest extends AbstractBulkCompositeIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return CteValuesListBulkIdStrategy.class;
}
}

View File

@ -1,19 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.cte.CteValuesListBulkIdStrategy;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
/**
* @author Vlad Mihalcea
*/
@RequiresDialectFeature(DialectChecks.SupportNonQueryValuesListWithCTE.class)
public class CteValuesListBulkIdTest extends AbstractBulkIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return CteValuesListBulkIdStrategy.class;
}
}

View File

@ -1,21 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.global.GlobalTemporaryTableBulkIdStrategy;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
/**
* @author Vlad Mihalcea
*/
@RequiresDialectFeature(DialectChecks.SupportsGlobalTemporaryTables.class)
public class GlobalTemporaryTableBulkCompositeIdTest extends AbstractBulkCompositeIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
// Since we only allow dialects that support global temporary tables, we avoid overriding the strategy
// This is important because otherwise we would loose id table configurations that are made in the dialects
return null;
}
}

View File

@ -1,20 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.inline.InlineIdsInClauseBulkIdStrategy;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
/**
* @author Vlad Mihalcea
*/
@RequiresDialectFeature(DialectChecks.SupportRowValueConstructorSyntaxInInList.class)
public class InlineIdsInClauseBulkCompositeIdTest extends
AbstractBulkCompositeIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return InlineIdsInClauseBulkIdStrategy.class;
}
}

View File

@ -1,19 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.inline.InlineIdsInClauseBulkIdStrategy;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
/**
* @author Vlad Mihalcea
*/
@RequiresDialectFeature(DialectChecks.SupportRowValueConstructorSyntaxInInList.class)
public class InlineIdsInClauseBulkIdTest extends AbstractBulkIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return InlineIdsInClauseBulkIdStrategy.class;
}
}

View File

@ -1,13 +0,0 @@
package org.hibernate.test.bulkid;
/**
* @author Vlad Mihalcea
*/
public class InlineIdsOrClauseBulkCompositeIdTest extends
AbstractBulkCompositeIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return InlineIdsOrClauseBulkIdStrategy.class;
}
}

View File

@ -1,15 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.inline.InlineIdsOrClauseBulkIdStrategy;
/**
* @author Vlad Mihalcea
*/
public class InlineIdsOrClauseBulkIdTest extends AbstractBulkIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return InlineIdsOrClauseBulkIdStrategy.class;
}
}

View File

@ -1,19 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.inline.InlineIdsSubSelectValueListBulkIdStrategy;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
/**
* @author Vlad Mihalcea
*/
@RequiresDialectFeature(DialectChecks.SupportValuesListAndRowValueConstructorSyntaxInInList.class)
public class InlineIdsSubSelectValuesListBulkCompositeIdTest extends AbstractBulkCompositeIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return InlineIdsSubSelectValueListBulkIdStrategy.class;
}
}

View File

@ -1,19 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy;
import org.hibernate.hql.spi.id.inline.InlineIdsSubSelectValueListBulkIdStrategy;
import org.hibernate.testing.DialectChecks;
import org.hibernate.testing.RequiresDialectFeature;
/**
* @author Vlad Mihalcea
*/
@RequiresDialectFeature(DialectChecks.SupportValuesListAndRowValueConstructorSyntaxInInList.class)
public class InlineIdsSubSelectValuesListBulkIdTest extends AbstractBulkIdTest {
@Override
protected Class<? extends MultiTableBulkIdStrategy> getMultiTableBulkIdStrategyClass() {
return InlineIdsSubSelectValueListBulkIdStrategy.class;
}
}

View File

@ -1,17 +0,0 @@
package org.hibernate.test.bulkid;
import org.hibernate.dialect.Oracle8iDialect;
import org.hibernate.testing.RequiresDialect;
/**
* @author Vlad Mihalcea
*/
@RequiresDialect(Oracle8iDialect.class)
public class OracleInlineIdsInClauseBulkIdTest extends InlineIdsInClauseBulkIdTest {
@Override
protected int entityCount() {
return 1100;
}
}

View File

@ -82,6 +82,9 @@ containing the various individual DML statements that would normally be executed
This allows to run SQM DML statements in a single JDBC operation that does not move any data between the database and the application, This allows to run SQM DML statements in a single JDBC operation that does not move any data between the database and the application,
which should provide a significant boost for statements that involve many rows. which should provide a significant boost for statements that involve many rows.
Note that the configuration property `hibernate.hql.bulk_id_strategy` was changed to `hibernate.query.mutation_strategy`
which will now refer to classes or objects implementing `org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy`.
[[identifier-object]] [[identifier-object]]
== Identifier as Object == Identifier as Object