diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index ab250b2d0d..5c955d4428 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -2865,6 +2865,24 @@ public abstract class Dialect implements ConversionContext { return true; } + /** + * Does this dialect/database support non-query statements (e.g. INSERT, UPDATE, DELETE) with CTE (Common Table Expressions)? + * + * @return {@code true} if non-query statements are supported with CTE + */ + public boolean supportsNonQueryWithCTE() { + return false; + } + + /** + * Does this dialect/database support VALUES list (e.g. VALUES (1), (2), (3) ) + * + * @return {@code true} if VALUES list are supported + */ + public boolean supportsValuesList() { + return false; + } + public boolean isLegacyLimitHandlerBehaviorEnabled() { return legacyLimitHandlerBehavior; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQL57InnoDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQL57InnoDBDialect.java index 412b9d3c09..a0210d06f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQL57InnoDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQL57InnoDBDialect.java @@ -65,4 +65,12 @@ public class MySQL57InnoDBDialect extends MySQL5InnoDBDialect { // from_unixtime(), timestamp() are functions that return TIMESTAMP that do not support a // fractional seconds precision argument (so there's no need to override them here): } + + /** + * @see MySQL 5.7 work log + * @return supports IN clause row value expressions + */ + public boolean supportsRowValueConstructorSyntaxInInList() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL82Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL82Dialect.java index 948fadf948..5199d26067 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL82Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL82Dialect.java @@ -56,4 +56,13 @@ public class PostgreSQL82Dialect extends PostgreSQL81Dialect { public String getDropSequenceString(String sequenceName) { return "drop sequence if exists " + sequenceName; } + + @Override + public boolean supportsValuesList() { + return true; + } + + public boolean supportsRowValueConstructorSyntaxInInList() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL91Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL91Dialect.java index e8af7e2d9a..3ff68522c2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL91Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQL91Dialect.java @@ -17,4 +17,9 @@ public class PostgreSQL91Dialect extends PostgreSQL9Dialect { public boolean supportsPartitionBy() { return true; } + + @Override + public boolean supportsNonQueryWithCTE() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2005Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2005Dialect.java index 54691d36d3..ecee9faf2e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2005Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2005Dialect.java @@ -105,4 +105,9 @@ public class SQLServer2005Dialect extends SQLServerDialect { } }; } + + @Override + public boolean supportsNonQueryWithCTE() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2008Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2008Dialect.java index ff375384c6..52f8232828 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2008Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2008Dialect.java @@ -56,4 +56,9 @@ public class SQLServer2008Dialect extends SQLServer2005Dialect { return orderByElement.toString(); } + + @Override + public boolean supportsValuesList() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractIdsBulkIdHandler.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractIdsBulkIdHandler.java new file mode 100644 index 0000000000..166d647a9e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractIdsBulkIdHandler.java @@ -0,0 +1,106 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.JDBCException; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.QueryParameters; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.tree.AbstractRestrictableStatement; +import org.hibernate.hql.internal.ast.tree.FromElement; +import org.hibernate.param.ParameterSpecification; +import org.hibernate.persister.entity.Queryable; + +/** + * Base class for all strategies that select the ids to be updated/deleted prior to executing the update/delete operation. + * + * @author Vlad Mihalcea + */ +public abstract class AbstractIdsBulkIdHandler + extends AbstractTableBasedBulkIdHandler { + + private final Queryable targetedPersister; + + private final String idSelect; + private final List idSelectParameterSpecifications; + + public AbstractIdsBulkIdHandler( + SessionFactoryImplementor sessionFactory, HqlSqlWalker walker) { + super(sessionFactory, walker); + + final AbstractRestrictableStatement statement = (AbstractRestrictableStatement) walker.getAST(); + final FromElement fromElement = statement.getFromClause().getFromElement(); + + this.targetedPersister = fromElement.getQueryable(); + + final ProcessedWhereClause processedWhereClause = processWhereClause( statement.getWhereClause() ); + this.idSelectParameterSpecifications = processedWhereClause.getIdSelectParameterSpecifications(); + + final String bulkTargetAlias = fromElement.getTableAlias(); + + this.idSelect = generateIdSelect( bulkTargetAlias, processedWhereClause ).toStatementString(); + } + + @Override + public Queryable getTargetedQueryable() { + return targetedPersister; + } + + protected Dialect dialect() { + return factory().getServiceRegistry().getService( JdbcServices.class ).getDialect(); + } + + protected JDBCException convert( + SQLException e, + String message, + String sql) { + throw factory().getServiceRegistry().getService( JdbcServices.class ).getSqlExceptionHelper().convert( e, message, sql ); + } + + protected List selectIds( + SharedSessionContractImplementor session, + QueryParameters queryParameters) { + List ids = new ArrayList<>(); + try { + try (PreparedStatement ps = session.getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( idSelect, false )) { + int position = 1; + for ( ParameterSpecification parameterSpecification : idSelectParameterSpecifications ) { + position += parameterSpecification.bind( ps, queryParameters, session, position ); + } + + ResultSet rs = session + .getJdbcCoordinator() + .getResultSetReturn() + .extract( ps ); + while ( rs.next() ) { + Object[] result = new Object[targetedPersister.getIdentifierColumnNames().length]; + for ( String columnName : targetedPersister.getIdentifierColumnNames() ) { + Object column = rs.getObject( columnName ); + result[rs.findColumn( columnName ) - 1] = column; + } + ids.add( result ); + } + } + } + catch ( SQLException e ) { + throw convert( e, "could not select ids for bulk operation", idSelect ); + } + + return ids; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractTableBasedBulkIdHandler.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractTableBasedBulkIdHandler.java index 0bb4268f0c..370e7c2e53 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractTableBasedBulkIdHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/AbstractTableBasedBulkIdHandler.java @@ -127,6 +127,22 @@ public abstract class AbstractTableBasedBulkIdHandler { IdTableInfo idTableInfo, ProcessedWhereClause whereClause) { + final Dialect dialect = sessionFactory.getJdbcServices().getJdbcEnvironment().getDialect(); + final Select select = generateIdSelect( tableAlias, whereClause ); + + InsertSelect insert = new InsertSelect( dialect ); + if ( sessionFactory.getSessionFactoryOptions().isCommentsEnabled() ) { + insert.setComment( "insert-select for " + getTargetedQueryable().getEntityName() + " ids" ); + } + insert.setTableName( idTableInfo.getQualifiedIdTableName() ); + insert.setSelect( select ); + return insert.toStatementString(); + } + + protected Select generateIdSelect( + String tableAlias, + ProcessedWhereClause whereClause) { + final Dialect dialect = sessionFactory.getJdbcServices().getJdbcEnvironment().getDialect(); final Select select = new Select( dialect ); @@ -160,14 +176,7 @@ public abstract class AbstractTableBasedBulkIdHandler { } } select.setWhereClause( whereJoinFragment + whereClause.getUserWhereClauseFragment() ); - - InsertSelect insert = new InsertSelect( dialect ); - if ( sessionFactory.getSessionFactoryOptions().isCommentsEnabled() ) { - insert.setComment( "insert-select for " + getTargetedQueryable().getEntityName() + " ids" ); - } - insert.setTableName( idTableInfo.getQualifiedIdTableName() ); - insert.setSelect( select ); - return insert.toStatementString(); + return select; } /** diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedDeleteHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedDeleteHandlerImpl.java index ea1bea5fd7..dcd18144e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedDeleteHandlerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedDeleteHandlerImpl.java @@ -114,10 +114,10 @@ public class TableBasedDeleteHandlerImpl try { try { ps = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( idInsertSelect, false ); - int pos = 1; - pos += handlePrependedParametersOnIdSelection( ps, session, pos ); + int position = 1; + position += handlePrependedParametersOnIdSelection( ps, session, position ); for ( ParameterSpecification parameterSpecification : idSelectParameterSpecifications ) { - pos += parameterSpecification.bind( ps, queryParameters, session, pos ); + position += parameterSpecification.bind( ps, queryParameters, session, position ); } resultCount = session.getJdbcCoordinator().getResultSetReturn().executeUpdate( ps ); } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedUpdateHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedUpdateHandlerImpl.java index c7d1e0a13c..c4586a0984 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedUpdateHandlerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/TableBasedUpdateHandlerImpl.java @@ -116,10 +116,10 @@ public class TableBasedUpdateHandlerImpl try { try { ps = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( idInsertSelect, false ); - int sum = 1; - sum += handlePrependedParametersOnIdSelection( ps, session, sum ); + int position = 1; + position += handlePrependedParametersOnIdSelection( ps, session, position ); for ( ParameterSpecification parameterSpecification : idSelectParameterSpecifications ) { - sum += parameterSpecification.bind( ps, queryParameters, session, sum ); + position += parameterSpecification.bind( ps, queryParameters, session, position ); } resultCount = session.getJdbcCoordinator().getResultSetReturn().executeUpdate( ps ); } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/AbstractCteValuesListBulkIdHandler.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/AbstractCteValuesListBulkIdHandler.java new file mode 100644 index 0000000000..b0bff2b9ec --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/AbstractCteValuesListBulkIdHandler.java @@ -0,0 +1,98 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.cte; + +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.relational.QualifiedTableName; +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.QueryParameters; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.AbstractIdsBulkIdHandler; +import org.hibernate.persister.entity.Queryable; + +/** + * Defines how identifier values are selected from the updatable/deletable tables. + * + * @author Evandro Pires da Silva + * @author Vlad Mihalcea + */ +public abstract class AbstractCteValuesListBulkIdHandler extends + AbstractIdsBulkIdHandler { + + private final String catalog; + private final String schema; + + private final JdbcEnvironment jdbcEnvironment; + + public AbstractCteValuesListBulkIdHandler( + SessionFactoryImplementor sessionFactory, HqlSqlWalker walker, + String catalog, String schema) { + super( sessionFactory, walker ); + Dialect dialect = sessionFactory.getServiceRegistry().getService( JdbcServices.class ).getDialect(); + if ( !dialect.supportsNonQueryWithCTE() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support CTE that can take UPDATE or DELETE statements as well!" + ); + } + if ( !dialect.supportsValuesList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support VALUES lists!" + ); + } + if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support IN clause row-value expressions (for composite identifiers)!" + ); + } + + this.jdbcEnvironment = sessionFactory.getServiceRegistry().getService( + JdbcServices.class ).getJdbcEnvironment(); + this.catalog = catalog; + this.schema = schema; + } + + protected String determineIdTableName(Queryable persister) { + return jdbcEnvironment.getQualifiedObjectNameFormatter().format( + new QualifiedTableName( + Identifier.toIdentifier( catalog ), + Identifier.toIdentifier( schema ), + Identifier.toIdentifier( "HT_" + persister.getTableName() ) + ), + jdbcEnvironment.getDialect() + ); + } + + protected String generateIdSubselect(Queryable persister) { + return new StringBuilder() + .append( "select " ) + .append( String.join( + ", ", + (CharSequence[]) persister.getIdentifierColumnNames() + ) ) + .append( " from " ) + .append( determineIdTableName( persister ) ) + .toString(); + } + + protected CteValuesListBuilder prepareCteStatement( + SharedSessionContractImplementor session, + QueryParameters queryParameters) { + + return new CteValuesListBuilder( + determineIdTableName( getTargetedQueryable() ), + getTargetedQueryable().getIdentifierColumnNames(), + selectIds( session, queryParameters ) + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListBuilder.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListBuilder.java new file mode 100644 index 0000000000..ff31983257 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListBuilder.java @@ -0,0 +1,68 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.cte; + +import java.util.Collections; +import java.util.List; + +/** + * Builds the CTE with VALUES list clause that wraps the identifiers to be updated/deleted. + * + * @author Evandro Pires da Silva + * @author Vlad Mihalcea + */ +public class CteValuesListBuilder { + + private final String tableName; + + private final String[] columns; + + private final List ids; + + private String cteStatement; + + public CteValuesListBuilder( + String tableName, + String[] columns, + List ids) { + this.tableName = tableName; + this.columns = columns; + this.ids = ids; + + this.cteStatement = buildStatement(); + } + + public List getIds() { + return ids; + } + + public String toStatement(String statement) { + return cteStatement + statement; + } + + private String buildStatement() { + String columnNames = String.join(",", columns); + + String singleIdValuesParam = '(' + String.join( ",", Collections.nCopies( columns.length, "?")) + ')'; + String parameters = String.join(",", Collections.nCopies(ids.size(), singleIdValuesParam)); + + return new StringBuilder() + .append( "with " ) + .append( tableName ) + .append( " (" ) + .append( columnNames ) + .append( " ) as ( select " ) + .append( columnNames ) + .append( " from ( values " ) + .append( parameters ) + .append( ") as HT " ) + .append( "(" ) + .append( columnNames ) + .append( ") ) " ) + .toString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListBulkIdStrategy.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListBulkIdStrategy.java new file mode 100644 index 0000000000..7a83da863b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListBulkIdStrategy.java @@ -0,0 +1,83 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.cte; + +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.boot.spi.SessionFactoryOptions; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * This bulk-id strategy uses a CTE with a VALUE list to hold the identifiers, + * which are later used by the update or delete statement: + * + *
+ * with HT_Person (id ) as (
+ *     select
+ *         id
+ *     from (
+ *         values
+ *             (?),
+ *             (?),
+ *             (?)
+ *             (?)
+ *     ) as HT (id)
+ * )
+ * delete
+ * from
+ *     Person
+ * where
+ *     ( id ) in (
+ *         select
+ *             id
+ *         from
+ *             HT_Person
+ *     )
+ * 
+ * + * @author Evandro Pires da Silva + * @author Vlad Mihalcea + */ +public class CteValuesListBulkIdStrategy + implements MultiTableBulkIdStrategy { + + public static final CteValuesListBulkIdStrategy INSTANCE = new CteValuesListBulkIdStrategy(); + + @Override + public void prepare( + JdbcServices jdbcServices, + JdbcConnectionAccess jdbcConnectionAccess, + MetadataImplementor metadataImplementor, + SessionFactoryOptions sessionFactoryOptions) { + // nothing to do + } + + @Override + public void release( + JdbcServices jdbcServices, + JdbcConnectionAccess connectionAccess) { + // nothing to do + } + + @Override + public UpdateHandler buildUpdateHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new CteValuesListUpdateHandlerImpl( factory, walker ); + } + + @Override + public DeleteHandler buildDeleteHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new CteValuesListDeleteHandlerImpl( factory, walker ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListDeleteHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListDeleteHandlerImpl.java new file mode 100644 index 0000000000..b587554dd7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListDeleteHandlerImpl.java @@ -0,0 +1,135 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.cte; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.engine.spi.QueryParameters; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; +import org.hibernate.persister.collection.AbstractCollectionPersister; +import org.hibernate.sql.Delete; +import org.hibernate.type.CollectionType; +import org.hibernate.type.Type; + +/** + * Bulk-id delete handler that uses CTE and VALUES lists. + * + * @author Evandro Pires da Silva + * @author Vlad Mihalcea + */ +public class CteValuesListDeleteHandlerImpl + extends AbstractCteValuesListBulkIdHandler + implements MultiTableBulkIdStrategy.DeleteHandler { + + private final List deletes = new ArrayList<>(); + + public CteValuesListDeleteHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + this( factory, walker, null, null ); + } + + public CteValuesListDeleteHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker, + String catalog, + String schema) { + super( factory, walker, catalog, schema ); + + final String idSubselect = generateIdSubselect( getTargetedQueryable() ); + + for ( Type type : getTargetedQueryable().getPropertyTypes() ) { + if ( type.isCollectionType() ) { + CollectionType cType = (CollectionType) type; + AbstractCollectionPersister cPersister = (AbstractCollectionPersister) factory.getMetamodel().collectionPersister( cType.getRole() ); + if ( cPersister.isManyToMany() ) { + deletes.add( generateDelete( + cPersister.getTableName(), + cPersister.getKeyColumnNames(), + idSubselect, + "bulk delete - m2m join table cleanup" + ) ); + } + } + } + + String[] tableNames = getTargetedQueryable().getConstraintOrderedTableNameClosure(); + String[][] columnNames = getTargetedQueryable().getContraintOrderedTableKeyColumnClosure(); + for ( int i = 0; i < tableNames.length; i++ ) { + // TODO : an optimization here would be to consider cascade deletes and not gen those delete statements; + // the difficulty is the ordering of the tables here vs the cascade attributes on the persisters -> + // the table info gotten here should really be self-contained (i.e., a class representation + // defining all the needed attributes), then we could then get an array of those + deletes.add( generateDelete( tableNames[i], columnNames[i], idSubselect, "bulk delete" ) ); + } + } + + @Override + public int execute( + SharedSessionContractImplementor session, + QueryParameters queryParameters) { + + CteValuesListBuilder values = prepareCteStatement( session, queryParameters ); + + if ( !values.getIds().isEmpty() ) { + // Start performing the deletes + for ( String deleteSuffix : deletes ) { + if ( deleteSuffix == null) { + continue; + } + + String delete = values.toStatement( deleteSuffix ); + + try { + try ( PreparedStatement ps = session + .getJdbcCoordinator().getStatementPreparer() + .prepareStatement( delete, false ) ) { + int pos = 1; + for ( Object[] result : values.getIds() ) { + for ( Object column : result ) { + ps.setObject( pos++, column ); + } + } + session + .getJdbcCoordinator().getResultSetReturn() + .executeUpdate( ps ); + } + } + catch ( SQLException e ) { + throw convert( e, "error performing bulk delete", delete ); + } + } + } + + return values.getIds().size(); + } + + private String generateDelete( + String tableName, + String[] columnNames, + String idSubselect, + String comment) { + final Delete delete = new Delete().setTableName( tableName ).setWhere( + "(" + String.join( ", ", (CharSequence[]) columnNames ) + ") in (" + + idSubselect + ")" ); + if ( factory().getSessionFactoryOptions().isCommentsEnabled() ) { + delete.setComment( comment ); + } + return delete.toStatementString(); + } + + @Override + public String[] getSqlStatements() { + return deletes.toArray( new String[deletes.size()] ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListUpdateHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListUpdateHandlerImpl.java new file mode 100644 index 0000000000..6c2270a351 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/cte/CteValuesListUpdateHandlerImpl.java @@ -0,0 +1,139 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.cte; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.QueryParameters; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; +import org.hibernate.param.ParameterSpecification; +import org.hibernate.sql.Update; + +/** + * Bulk-id update handler that uses CTE and VALUES lists. + * + * @author Evandro Pires da Silva + * @author Vlad Mihalcea + */ +public class CteValuesListUpdateHandlerImpl + extends AbstractCteValuesListBulkIdHandler + implements MultiTableBulkIdStrategy.UpdateHandler { + + private final String[] updates; + private final ParameterSpecification[][] assignmentParameterSpecifications; + + public CteValuesListUpdateHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + this( factory, walker, null, null ); + } + + public CteValuesListUpdateHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker, + String catalog, + String schema) { + super( factory, walker, catalog, schema ); + + String[] tableNames = getTargetedQueryable().getConstraintOrderedTableNameClosure(); + String[][] columnNames = getTargetedQueryable().getContraintOrderedTableKeyColumnClosure(); + String idSubselect = generateIdSubselect( getTargetedQueryable() ); + + updates = new String[tableNames.length]; + assignmentParameterSpecifications = new ParameterSpecification[tableNames.length][]; + for ( int tableIndex = 0; tableIndex < tableNames.length; tableIndex++ ) { + boolean affected = false; + final List parameterList = new ArrayList<>(); + final Update update = new Update( factory.getServiceRegistry().getService( JdbcServices.class ).getDialect() ) + .setTableName( tableNames[tableIndex] ) + .setWhere( "(" + String.join( ", ", (CharSequence[]) columnNames[tableIndex] ) + ") in (" + idSubselect + ")" ); + if ( factory().getSessionFactoryOptions().isCommentsEnabled() ) { + update.setComment( "bulk update" ); + } + final List assignmentSpecifications = walker.getAssignmentSpecifications(); + for ( AssignmentSpecification assignmentSpecification : assignmentSpecifications ) { + if ( assignmentSpecification.affectsTable( tableNames[tableIndex] ) ) { + affected = true; + update.appendAssignmentFragment( assignmentSpecification.getSqlAssignmentFragment() ); + if ( assignmentSpecification.getParameters() != null ) { + Collections.addAll( parameterList, assignmentSpecification.getParameters() ); + } + } + } + if ( affected ) { + updates[tableIndex] = update.toStatementString(); + assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[parameterList.size()] ); + } + } + } + + @Override + public String[] getSqlStatements() { + return updates; + } + + @Override + public int execute( + SharedSessionContractImplementor session, + QueryParameters queryParameters) { + + CteValuesListBuilder values = prepareCteStatement( session, queryParameters ); + + if ( !values.getIds().isEmpty() ) { + + // Start performing the updates + for ( int i = 0; i < updates.length; i++ ) { + String updateSuffix = updates[i]; + if ( updateSuffix == null) { + continue; + } + String update = values.toStatement( updateSuffix ); + try { + try (PreparedStatement ps = session + .getJdbcCoordinator().getStatementPreparer() + .prepareStatement( update, false )) { + int position = 1; // jdbc params are 1-based + for ( Object[] result : values.getIds() ) { + for ( Object column : result ) { + ps.setObject( position++, column ); + } + } + if ( assignmentParameterSpecifications[i] != null ) { + for ( int x = 0; x < assignmentParameterSpecifications[i].length; x++ ) { + position += assignmentParameterSpecifications[i][x] + .bind( ps, queryParameters, session, + position + ); + } + } + session + .getJdbcCoordinator().getResultSetReturn() + .executeUpdate( ps ); + } + } + catch ( SQLException e ) { + throw convert( + e, + "error performing bulk update", + update + ); + } + } + } + + return values.getIds().size(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsBulkIdHandler.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsBulkIdHandler.java new file mode 100644 index 0000000000..ff65c4abee --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsBulkIdHandler.java @@ -0,0 +1,38 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.engine.spi.QueryParameters; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.AbstractIdsBulkIdHandler; + +/** + * Base class for all bulk-id strategy handlers that inline the identifiers of the updatable/deletable rows. + * + * @author Vlad Mihalcea + */ +public abstract class AbstractInlineIdsBulkIdHandler + extends AbstractIdsBulkIdHandler { + + public AbstractInlineIdsBulkIdHandler( + SessionFactoryImplementor sessionFactory, + HqlSqlWalker walker) { + super( sessionFactory, walker ); + } + + protected IdsClauseBuilder prepareInlineStatement( + SharedSessionContractImplementor session, + QueryParameters queryParameters) { + return newIdsClauseBuilder( selectIds( session, queryParameters ) ); + } + + protected abstract IdsClauseBuilder newIdsClauseBuilder(List ids); +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsDeleteHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsDeleteHandlerImpl.java new file mode 100644 index 0000000000..498aa24fd9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsDeleteHandlerImpl.java @@ -0,0 +1,118 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.engine.spi.QueryParameters; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; +import org.hibernate.persister.collection.AbstractCollectionPersister; +import org.hibernate.sql.Delete; +import org.hibernate.type.CollectionType; +import org.hibernate.type.Type; + +/** + * Inline bulk-id delete handler that selects the identifiers of the rows to be updated. + * + * @author Vlad Mihalcea + */ +public abstract class AbstractInlineIdsDeleteHandlerImpl + extends AbstractInlineIdsBulkIdHandler + implements MultiTableBulkIdStrategy.DeleteHandler { + + private final List deletes = new ArrayList<>(); + + public AbstractInlineIdsDeleteHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + } + + @Override + public String[] getSqlStatements() { + return deletes.toArray( new String[deletes.size()] ); + } + + @Override + public int execute( + SharedSessionContractImplementor session, + QueryParameters queryParameters) { + + IdsClauseBuilder values = prepareInlineStatement( session, queryParameters ); + + if ( !values.getIds().isEmpty() ) { + final String idSubselect = values.toStatement(); + + for ( Type type : getTargetedQueryable().getPropertyTypes() ) { + if ( type.isCollectionType() ) { + CollectionType cType = (CollectionType) type; + AbstractCollectionPersister cPersister = (AbstractCollectionPersister) factory().getMetamodel().collectionPersister( cType.getRole() ); + if ( cPersister.isManyToMany() ) { + deletes.add( generateDelete( + cPersister.getTableName(), + cPersister.getKeyColumnNames(), + idSubselect, + "bulk delete - m2m join table cleanup" + ).toStatementString() ); + } + } + } + + String[] tableNames = getTargetedQueryable().getConstraintOrderedTableNameClosure(); + String[][] columnNames = getTargetedQueryable().getContraintOrderedTableKeyColumnClosure(); + for ( int i = 0; i < tableNames.length; i++ ) { + // TODO : an optimization here would be to consider cascade deletes and not gen those delete statements; + // the difficulty is the ordering of the tables here vs the cascade attributes on the persisters -> + // the table info gotten here should really be self-contained (i.e., a class representation + // defining all the needed attributes), then we could then get an array of those + deletes.add( generateDelete( tableNames[i], columnNames[i], idSubselect, "bulk delete" ).toStatementString() ); + } + + // Start performing the deletes + for ( String delete : deletes ) { + if ( delete == null) { + continue; + } + + try { + try ( PreparedStatement ps = session + .getJdbcCoordinator().getStatementPreparer() + .prepareStatement( delete, false ) ) { + session + .getJdbcCoordinator().getResultSetReturn() + .executeUpdate( ps ); + } + } + catch ( SQLException e ) { + throw convert( e, "error performing bulk delete", delete ); + } + } + } + + return values.getIds().size(); + } + + protected Delete generateDelete( + String tableName, + String[] columnNames, + String idSubselect, + String comment) { + final Delete delete = new Delete().setTableName( tableName ).setWhere( + "(" + String.join( ", ", (CharSequence[]) columnNames ) + ") in (" + + idSubselect + ")" ); + if ( factory().getSessionFactoryOptions().isCommentsEnabled() ) { + delete.setComment( comment ); + } + return delete; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsUpdateHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsUpdateHandlerImpl.java new file mode 100644 index 0000000000..22d66a8e65 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/AbstractInlineIdsUpdateHandlerImpl.java @@ -0,0 +1,140 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.QueryParameters; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; +import org.hibernate.param.ParameterSpecification; +import org.hibernate.sql.Update; + +/** + * Inline bulk-id delete handler that selects the identifiers of the rows to be deleted. + * + * @author Vlad Mihalcea + */ +public abstract class AbstractInlineIdsUpdateHandlerImpl + extends AbstractInlineIdsBulkIdHandler + implements MultiTableBulkIdStrategy.UpdateHandler { + + private final Map updates = new LinkedHashMap<>(); + + private ParameterSpecification[][] assignmentParameterSpecifications; + + public AbstractInlineIdsUpdateHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + } + + @Override + public String[] getSqlStatements() { + return updates.values().toArray( new String[updates.values().size()] ); + } + + @Override + public int execute( + SharedSessionContractImplementor session, + QueryParameters queryParameters) { + + IdsClauseBuilder values = prepareInlineStatement( session, queryParameters ); + + if ( !values.getIds().isEmpty() ) { + + String[] tableNames = getTargetedQueryable().getConstraintOrderedTableNameClosure(); + String[][] columnNames = getTargetedQueryable().getContraintOrderedTableKeyColumnClosure(); + + String idSubselect = values.toStatement(); + + assignmentParameterSpecifications = new ParameterSpecification[tableNames.length][]; + for ( int tableIndex = 0; tableIndex < tableNames.length; tableIndex++ ) { + boolean affected = false; + final List parameterList = new ArrayList<>(); + + Update update = generateUpdate( tableNames[tableIndex], columnNames[tableIndex], idSubselect, "bulk update" ); + + final List assignmentSpecifications = walker().getAssignmentSpecifications(); + for ( AssignmentSpecification assignmentSpecification : assignmentSpecifications ) { + if ( assignmentSpecification.affectsTable( tableNames[tableIndex] ) ) { + affected = true; + update.appendAssignmentFragment( assignmentSpecification.getSqlAssignmentFragment() ); + if ( assignmentSpecification.getParameters() != null ) { + Collections.addAll( parameterList, assignmentSpecification.getParameters() ); + } + } + } + if ( affected ) { + updates.put( tableIndex, update.toStatementString() ); + assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[parameterList.size()] ); + } + } + + // Start performing the updates + for ( Map.Entry updateEntry: updates.entrySet()) { + int i = updateEntry.getKey(); + String update = updateEntry.getValue(); + + if ( update == null) { + continue; + } + try { + try (PreparedStatement ps = session + .getJdbcCoordinator().getStatementPreparer() + .prepareStatement( update, false )) { + int position = 1; // jdbc params are 1-based + if ( assignmentParameterSpecifications[i] != null ) { + for ( int x = 0; x < assignmentParameterSpecifications[i].length; x++ ) { + position += assignmentParameterSpecifications[i][x] + .bind( ps, queryParameters, session, + position + ); + } + } + session + .getJdbcCoordinator().getResultSetReturn() + .executeUpdate( ps ); + } + } + catch ( SQLException e ) { + throw convert( + e, + "error performing bulk update", + update + ); + } + } + } + + return values.getIds().size(); + } + + protected Update generateUpdate( + String tableName, + String[] columnNames, + String idSubselect, + String comment) { + final Update update = new Update( factory().getServiceRegistry().getService( JdbcServices.class ).getDialect() ) + .setTableName( tableName ) + .setWhere( "(" + String.join( ", ", (CharSequence[]) columnNames ) + ") in (" + idSubselect + ")" ); + if ( factory().getSessionFactoryOptions().isCommentsEnabled() ) { + update.setComment( comment ); + } + return update; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/IdsClauseBuilder.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/IdsClauseBuilder.java new file mode 100644 index 0000000000..d3f0d6ac6a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/IdsClauseBuilder.java @@ -0,0 +1,95 @@ +package org.hibernate.hql.spi.id.inline; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.CompositeType; +import org.hibernate.type.LiteralType; +import org.hibernate.type.Type; +import org.hibernate.type.TypeResolver; + +/** + * Builds the where clause that wraps the identifiers to be updated/deleted. + * + * @author Vlad Mihalcea + */ +public abstract class IdsClauseBuilder { + + private final Dialect dialect; + + private final Type identifierType; + + private final TypeResolver typeResolver; + + private final String[] columns; + + private final List ids; + + protected IdsClauseBuilder( + Dialect dialect, + Type identifierType, + TypeResolver typeResolver, + String[] columns, + List ids) { + this.dialect = dialect; + this.identifierType = identifierType; + this.typeResolver = typeResolver; + this.columns = columns; + this.ids = ids; + } + + public Type getIdentifierType() { + return identifierType; + } + + public TypeResolver getTypeResolver() { + return typeResolver; + } + + protected String[] getColumns() { + return columns; + } + + public List getIds() { + return ids; + } + + public abstract String toStatement(); + + protected String quoteIdentifier(Object... value) { + if ( value.length == 1 ) { + return quoteIdentifier( value[0], identifierType ); + } + else { + if ( identifierType instanceof CompositeType ) { + CompositeType compositeType = (CompositeType) identifierType; + List quotedIdentifiers = new ArrayList<>(); + + for ( int i = 0; i < value.length; i++ ) { + quotedIdentifiers.add(quoteIdentifier( value[i], compositeType.getSubtypes()[i] )); + } + return String.join( ",", quotedIdentifiers ); + } + else { + throw new IllegalArgumentException("Composite identifier does not implement CompositeType"); + } + } + } + + private String quoteIdentifier(Object value, Type type) { + Type resolvedType = ( !type.getReturnedClass().equals( value.getClass() ) ) ? + typeResolver.heuristicType( value.getClass().getName() ) : type; + + if ( resolvedType instanceof LiteralType ) { + LiteralType literalType = (LiteralType) resolvedType; + try { + return literalType.objectToSQLString( value, dialect ); + } + catch ( Exception e ) { + throw new IllegalArgumentException( e ); + } + } + return String.valueOf( value ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsIdsInClauseDeleteHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsIdsInClauseDeleteHandlerImpl.java new file mode 100644 index 0000000000..f53929cea9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsIdsInClauseDeleteHandlerImpl.java @@ -0,0 +1,49 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * Inline bulk-id delete handler that uses an IN clause. + * + * @author Vlad Mihalcea + */ +public class InlineIdsIdsInClauseDeleteHandlerImpl + extends AbstractInlineIdsDeleteHandlerImpl + implements MultiTableBulkIdStrategy.DeleteHandler { + + public InlineIdsIdsInClauseDeleteHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + Dialect dialect = factory.getServiceRegistry().getService( JdbcServices.class ).getDialect(); + if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support IN clause row-value expressions (for composite identifiers)!" + ); + } + } + + @Override + protected IdsClauseBuilder newIdsClauseBuilder(List ids) { + return new InlineIdsInClauseBuilder( + dialect(), + getTargetedQueryable().getIdentifierType(), + factory().getTypeResolver(), + getTargetedQueryable().getIdentifierColumnNames(), + ids + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsIdsOrClauseDeleteHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsIdsOrClauseDeleteHandlerImpl.java new file mode 100644 index 0000000000..1074277453 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsIdsOrClauseDeleteHandlerImpl.java @@ -0,0 +1,55 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; +import org.hibernate.sql.Delete; + +/** + * Inline bulk-id delete handler that uses multiple identifier OR clauses. + * + * @author Vlad Mihalcea + */ +public class InlineIdsIdsOrClauseDeleteHandlerImpl + extends AbstractInlineIdsDeleteHandlerImpl + implements MultiTableBulkIdStrategy.DeleteHandler { + + public InlineIdsIdsOrClauseDeleteHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + } + + @Override + protected IdsClauseBuilder newIdsClauseBuilder(List ids) { + return new InlineIdsOrClauseBuilder( + dialect(), + getTargetedQueryable().getIdentifierType(), + factory().getTypeResolver(), + getTargetedQueryable().getIdentifierColumnNames(), + ids + ); + } + + protected Delete generateDelete( + String tableName, + String[] columnNames, + String idSubselect, + String comment) { + final Delete delete = new Delete() + .setTableName( tableName ) + .setWhere(idSubselect); + if ( factory().getSessionFactoryOptions().isCommentsEnabled() ) { + delete.setComment( comment ); + } + return delete; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseBuilder.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseBuilder.java new file mode 100644 index 0000000000..e18b314390 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseBuilder.java @@ -0,0 +1,55 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.Type; +import org.hibernate.type.TypeResolver; + +/** + * Builds the where IN clause that wraps the identifiers to be updated/deleted. + * + * @author Vlad Mihalcea + */ +public class InlineIdsInClauseBuilder extends IdsClauseBuilder { + + private final int chunkLimit; + + public InlineIdsInClauseBuilder( + Dialect dialect, Type identifierType, TypeResolver typeResolver, String[] columns, List ids) { + super( dialect, identifierType, typeResolver, columns, ids ); + this.chunkLimit = dialect.getInExpressionCountLimit(); + } + + @Override + public String toStatement() { + StringBuilder buffer = new StringBuilder(); + + String columnNames = String.join( ",", (CharSequence[]) getColumns() ); + + for ( int i = 0; i < getIds().size(); i++ ) { + Object[] idTokens = getIds().get( i ); + if ( i > 0 ) { + if( chunkLimit > 0 && i % chunkLimit == 0 ) { + buffer.append( " ) or ( " ); + buffer.append( columnNames ); + buffer.append( " ) in (" ); + } + else { + buffer.append( "," ); + } + } + buffer.append( "(" ); + buffer.append( quoteIdentifier( idTokens ) ); + buffer.append( ")" ); + } + + return buffer.toString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseBulkIdStrategy.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseBulkIdStrategy.java new file mode 100644 index 0000000000..90b95a1e89 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseBulkIdStrategy.java @@ -0,0 +1,71 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.boot.spi.SessionFactoryOptions; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * This bulk-id strategy inlines identifiers of the rows that need to be updated or deleted using an IN clause: + * + *
+ * delete
+ * from
+ *     Doctor
+ * where
+ *     ( id ) in (
+ *         ( 1 ),
+ *         ( 2 ),
+ *         ( 3 ),
+ *         ( 4 )
+ *     )
+ * 
+ * + * @author Vlad Mihalcea + */ +public class InlineIdsInClauseBulkIdStrategy + implements MultiTableBulkIdStrategy { + + public static final InlineIdsInClauseBulkIdStrategy INSTANCE = + new InlineIdsInClauseBulkIdStrategy(); + + @Override + public void prepare( + JdbcServices jdbcServices, + JdbcConnectionAccess jdbcConnectionAccess, + MetadataImplementor metadataImplementor, + SessionFactoryOptions sessionFactoryOptions) { + // nothing to do + } + + @Override + public void release( + JdbcServices jdbcServices, + JdbcConnectionAccess connectionAccess) { + // nothing to do + } + + @Override + public UpdateHandler buildUpdateHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new InlineIdsInClauseUpdateHandlerImpl( factory, walker ); + } + + @Override + public DeleteHandler buildDeleteHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new InlineIdsIdsInClauseDeleteHandlerImpl( factory, walker ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseUpdateHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseUpdateHandlerImpl.java new file mode 100644 index 0000000000..3122b9ca6a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsInClauseUpdateHandlerImpl.java @@ -0,0 +1,49 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * Inline bulk-id update handler that uses an IN clause. + * + * @author Vlad Mihalcea + */ +public class InlineIdsInClauseUpdateHandlerImpl + extends AbstractInlineIdsUpdateHandlerImpl + implements MultiTableBulkIdStrategy.UpdateHandler { + + public InlineIdsInClauseUpdateHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + Dialect dialect = factory.getServiceRegistry().getService( JdbcServices.class ).getDialect(); + if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support IN clause row-value expressions (for composite identifiers)!" + ); + } + } + + @Override + protected IdsClauseBuilder newIdsClauseBuilder(List ids) { + return new InlineIdsInClauseBuilder( + dialect(), + getTargetedQueryable().getIdentifierType(), + factory().getTypeResolver(), + getTargetedQueryable().getIdentifierColumnNames(), + ids + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseBuilder.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseBuilder.java new file mode 100644 index 0000000000..f86989cd99 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseBuilder.java @@ -0,0 +1,55 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.Type; +import org.hibernate.type.TypeResolver; + +/** + * Builds the where clause using OR expressions for the identifiers to be updated/deleted. + * This is useful for Dialects that do no support IN clause row value expressions. + * + * @author Vlad Mihalcea + */ +public class InlineIdsOrClauseBuilder extends IdsClauseBuilder { + + public InlineIdsOrClauseBuilder( + Dialect dialect, Type identifierType, TypeResolver typeResolver, String[] columns, List ids) { + super( dialect, identifierType, typeResolver, columns, ids ); + } + + @Override + public String toStatement() { + StringBuilder buffer = new StringBuilder(); + + for ( int i = 0; i < getIds().size(); i++ ) { + Object[] idTokens = getIds().get( i ); + + if ( i > 0 ) { + buffer.append( " or " ); + } + + buffer.append( "(" ); + + for ( int j = 0; j < getColumns().length; j++ ) { + if ( j > 0 ) { + buffer.append( " and " ); + } + buffer.append( getColumns()[j] ); + buffer.append( " = " ); + buffer.append( quoteIdentifier( idTokens[j] ) ); + + } + buffer.append( ")" ); + } + + return buffer.toString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseBulkIdStrategy.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseBulkIdStrategy.java new file mode 100644 index 0000000000..0ec0204f1f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseBulkIdStrategy.java @@ -0,0 +1,73 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.boot.spi.SessionFactoryOptions; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * This bulk-id strategy inlines identifiers of the rows that need to be updated or deleted using multiple identifier OR clauses. + * + *
+ * delete
+ * from
+ *     Engineer
+ * where
+ *     (
+ *         id = 0
+ *         and companyName = 'Red Hat Europe'
+ *     )
+ *     or (
+ *         id = 1
+ *       and companyName = 'Red Hat USA'
+ *   )
+ * 
+ * + * @author Vlad Mihalcea + */ +public class InlineIdsOrClauseBulkIdStrategy + implements MultiTableBulkIdStrategy { + + public static final InlineIdsOrClauseBulkIdStrategy INSTANCE = + new InlineIdsOrClauseBulkIdStrategy(); + + @Override + public void prepare( + JdbcServices jdbcServices, + JdbcConnectionAccess jdbcConnectionAccess, + MetadataImplementor metadataImplementor, + SessionFactoryOptions sessionFactoryOptions) { + // nothing to do + } + + @Override + public void release( + JdbcServices jdbcServices, + JdbcConnectionAccess connectionAccess) { + // nothing to do + } + + @Override + public UpdateHandler buildUpdateHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new InlineIdsOrClauseUpdateHandlerImpl( factory, walker ); + } + + @Override + public DeleteHandler buildDeleteHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new InlineIdsIdsOrClauseDeleteHandlerImpl( factory, walker ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseUpdateHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseUpdateHandlerImpl.java new file mode 100644 index 0000000000..50c931f80f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsOrClauseUpdateHandlerImpl.java @@ -0,0 +1,57 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; +import org.hibernate.sql.Update; + +/** + * Inline bulk-id update handler that uses multiple identifier OR clauses. + * + * @author Vlad Mihalcea + */ +public class InlineIdsOrClauseUpdateHandlerImpl + extends AbstractInlineIdsUpdateHandlerImpl + implements MultiTableBulkIdStrategy.UpdateHandler { + + public InlineIdsOrClauseUpdateHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + } + + @Override + protected IdsClauseBuilder newIdsClauseBuilder(List ids) { + return new InlineIdsOrClauseBuilder( + dialect(), + getTargetedQueryable().getIdentifierType(), + factory().getTypeResolver(), + getTargetedQueryable().getIdentifierColumnNames(), + ids + ); + } + + @Override + protected Update generateUpdate( + String tableName, + String[] columnNames, + String idSubselect, + String comment) { + final Update update = new Update( factory().getServiceRegistry().getService( JdbcServices.class ).getDialect() ) + .setTableName( tableName ) + .setWhere( idSubselect ); + if ( factory().getSessionFactoryOptions().isCommentsEnabled() ) { + update.setComment( comment ); + } + return update; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValueListBulkIdStrategy.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValueListBulkIdStrategy.java new file mode 100644 index 0000000000..09c15d520f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValueListBulkIdStrategy.java @@ -0,0 +1,82 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.boot.spi.SessionFactoryOptions; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * This bulk-id strategy inlines identifiers of the rows that need to be updated or deleted in a subselect using a VALUES list: + * + *
+ * delete
+ * from
+ *     Person
+ * where
+ *     ( id ) in (
+ *         select
+ *             id
+ *         from (
+ *             values
+ *                 ( 1 ),
+ *                 ( 2 ),
+ *                 ( 3 ),
+ *                 ( 4 )
+ *             ) as HT (id)
+ *     )
+ * 
+ * + * @author Vlad Mihalcea + */ +public class InlineIdsSubSelectValueListBulkIdStrategy + implements MultiTableBulkIdStrategy { + + public static final InlineIdsSubSelectValueListBulkIdStrategy INSTANCE = + new InlineIdsSubSelectValueListBulkIdStrategy(); + + @Override + public void prepare( + JdbcServices jdbcServices, + JdbcConnectionAccess jdbcConnectionAccess, + MetadataImplementor metadataImplementor, + SessionFactoryOptions sessionFactoryOptions) { + // nothing to do + } + + @Override + public void release( + JdbcServices jdbcServices, + JdbcConnectionAccess connectionAccess) { + // nothing to do + } + + @Override + public UpdateHandler buildUpdateHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new InlineIdsSubSelectValuesListUpdateHandlerImpl( + factory, + walker + ); + } + + @Override + public DeleteHandler buildDeleteHandler( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + return new InlineIdsSubSelectValuesListDeleteHandlerImpl( + factory, + walker + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListBuilder.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListBuilder.java new file mode 100644 index 0000000000..46c8589fd7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListBuilder.java @@ -0,0 +1,49 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.Type; +import org.hibernate.type.TypeResolver; + +/** + * Builds the where SELECT FROM VALUES clause that wraps the identifiers to be updated/deleted. + * + * @author Vlad Mihalcea + */ +public class InlineIdsSubSelectValuesListBuilder extends IdsClauseBuilder { + + public InlineIdsSubSelectValuesListBuilder( + Dialect dialect, Type identifierType, TypeResolver typeResolver, String[] columns, List ids) { + super( dialect, identifierType, typeResolver, columns, ids ); + } + + @Override + public String toStatement() { + StringBuilder buffer = new StringBuilder(); + + String columnNames = String.join( ",", (CharSequence[]) getColumns() ); + + buffer.append( "select " ).append( columnNames ).append( + " from ( values " ); + + for ( int i = 0; i < getIds().size(); i++ ) { + Object[] idTokens = getIds().get( i ); + if ( i > 0 ) { + buffer.append( "," ); + } + buffer.append( "(" ); + buffer.append( quoteIdentifier( idTokens ) ); + buffer.append( ")" ); + } + buffer.append( ") as HT (" ).append( columnNames ).append( ") " ); + + return buffer.toString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListDeleteHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListDeleteHandlerImpl.java new file mode 100644 index 0000000000..fe8d4a8ad8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListDeleteHandlerImpl.java @@ -0,0 +1,56 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * Inline bulk-id delete handler that uses a subselect with a VALUES clause. + * + * @author Vlad Mihalcea + */ +public class InlineIdsSubSelectValuesListDeleteHandlerImpl + extends AbstractInlineIdsDeleteHandlerImpl + implements MultiTableBulkIdStrategy.DeleteHandler { + + public InlineIdsSubSelectValuesListDeleteHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + + Dialect dialect = factory().getServiceRegistry().getService( JdbcServices.class ).getDialect(); + if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support IN clause row-value expressions (for composite identifiers)!" + ); + } + if ( !dialect.supportsValuesList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support VALUES lists!" + ); + } + } + + @Override + protected IdsClauseBuilder newIdsClauseBuilder(List ids) { + return new InlineIdsSubSelectValuesListBuilder( + dialect(), + getTargetedQueryable().getIdentifierType(), + factory().getTypeResolver(), + getTargetedQueryable().getIdentifierColumnNames(), + ids + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListUpdateHandlerImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListUpdateHandlerImpl.java new file mode 100644 index 0000000000..0657613cc9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/spi/id/inline/InlineIdsSubSelectValuesListUpdateHandlerImpl.java @@ -0,0 +1,56 @@ +/* + * 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 . + */ +package org.hibernate.hql.spi.id.inline; + +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +/** + * Inline bulk-id update handler that uses a subselect with a VALUES clause. + * + * @author Vlad Mihalcea + */ +public class InlineIdsSubSelectValuesListUpdateHandlerImpl + extends AbstractInlineIdsUpdateHandlerImpl + implements MultiTableBulkIdStrategy.DeleteHandler { + + public InlineIdsSubSelectValuesListUpdateHandlerImpl( + SessionFactoryImplementor factory, + HqlSqlWalker walker) { + super( factory, walker ); + + Dialect dialect = factory().getServiceRegistry().getService( JdbcServices.class ).getDialect(); + if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support IN clause row-value expressions (for composite identifiers)!" + ); + } + if ( !dialect.supportsValuesList() ) { + throw new UnsupportedOperationException( + "The " + getClass().getSimpleName() + + " can only be used with Dialects that support VALUES lists!" + ); + } + } + + @Override + protected IdsClauseBuilder newIdsClauseBuilder(List ids) { + return new InlineIdsSubSelectValuesListBuilder( + dialect(), + getTargetedQueryable().getIdentifierType(), + factory().getTypeResolver(), + getTargetedQueryable().getIdentifierColumnNames(), + ids + ); + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/AbstractBulkCompositeIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/AbstractBulkCompositeIdTest.java new file mode 100644 index 0000000000..dc4bfcaa93 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/AbstractBulkCompositeIdTest.java @@ -0,0 +1,197 @@ +package org.hibernate.test.bulkid; + +import java.io.Serializable; +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +import org.hibernate.testing.DialectChecks; +import org.hibernate.testing.RequiresDialectFeature; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Before; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractBulkCompositeIdTest extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Person.class, + Doctor.class, + Engineer.class + }; + } + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + configuration.setProperty( AvailableSettings.HQL_BULK_ID_STRATEGY, getMultiTableBulkIdStrategyClass().getName() ); + return configuration; + } + + protected abstract Class getMultiTableBulkIdStrategyClass(); + + @Override + protected boolean isCleanupTestDataRequired() { + return true; + } + + @Override + protected boolean isCleanupTestDataUsingBulkDelete() { + return true; + } + + @Before + public void setUp() { + doInHibernate( this::sessionFactory, session -> { + for ( int i = 0; i < entityCount(); i++ ) { + Doctor doctor = new Doctor(); + doctor.setId( i ); + doctor.setCompanyName( "Red Hat USA" ); + doctor.setEmployed( ( i % 2 ) == 0 ); + session.persist( doctor ); + } + + for ( int i = 0; i < entityCount(); i++ ) { + Engineer engineer = new Engineer(); + engineer.setId( i ); + engineer.setCompanyName( "Red Hat Europe" ); + engineer.setEmployed( ( i % 2 ) == 0 ); + engineer.setFellow( ( i % 2 ) == 1 ); + session.persist( engineer ); + } + }); + } + + protected int entityCount() { + return 100; + } + + @Test + public void testUpdate() { + doInHibernate( this::sessionFactory, session -> { + int updateCount = session.createQuery( "update Person set name = :name where employed = :employed" ) + .setParameter( "name", "John Doe" ) + .setParameter( "employed", true ) + .executeUpdate(); + + assertEquals(entityCount(), updateCount); + }); + } + + @Test + public void testDeleteFromPerson() { + doInHibernate( this::sessionFactory, session -> { + int updateCount = session.createQuery( "delete from Person where employed = :employed" ) + .setParameter( "employed", false ) + .executeUpdate(); + assertEquals( entityCount(), updateCount ); + }); + } + + @Test + public void testDeleteFromEngineer() { + doInHibernate( this::sessionFactory, session -> { + int updateCount = session.createQuery( "delete from Engineer where fellow = :fellow" ) + .setParameter( "fellow", true ) + .executeUpdate(); + assertEquals( entityCount() / 2, updateCount ); + }); + } + + @Entity(name = "Person") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Person implements Serializable { + + @Id + private Integer id; + + @Id + private String companyName; + + private String name; + + private boolean employed; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getCompanyName() { + return companyName; + } + + public void setCompanyName(String companyName) { + this.companyName = companyName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEmployed() { + return employed; + } + + public void setEmployed(boolean employed) { + this.employed = employed; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( !( o instanceof Person ) ) { + return false; + } + Person person = (Person) o; + return Objects.equals( getId(), person.getId() ) && + Objects.equals( getCompanyName(), person.getCompanyName() ); + } + + @Override + public int hashCode() { + return Objects.hash( getId(), getCompanyName() ); + } + } + + @Entity(name = "Doctor") + public static class Doctor extends Person { + } + + @Entity(name = "Engineer") + public static class Engineer extends Person { + + private boolean fellow; + + public boolean isFellow() { + return fellow; + } + + public void setFellow(boolean fellow) { + this.fellow = fellow; + } + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/AbstractBulkIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/AbstractBulkIdTest.java new file mode 100644 index 0000000000..31b15ca36e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/AbstractBulkIdTest.java @@ -0,0 +1,163 @@ +package org.hibernate.test.bulkid; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; + +import org.hibernate.testing.DialectChecks; +import org.hibernate.testing.RequiresDialectFeature; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Before; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; + +/** + * @author Vlad Mihalcea + */ +public abstract class AbstractBulkIdTest extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Person.class, + Doctor.class, + Engineer.class + }; + } + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + configuration.setProperty( AvailableSettings.HQL_BULK_ID_STRATEGY, getMultiTableBulkIdStrategyClass().getName() ); + return configuration; + } + + protected abstract Class getMultiTableBulkIdStrategyClass(); + + @Override + protected boolean isCleanupTestDataRequired() { + return true; + } + + @Override + protected boolean isCleanupTestDataUsingBulkDelete() { + return true; + } + + @Before + public void setUp() { + doInHibernate( this::sessionFactory, session -> { + for ( int i = 0; i < entityCount(); i++ ) { + Doctor doctor = new Doctor(); + doctor.setEmployed( ( i % 2 ) == 0 ); + session.persist( doctor ); + } + + for ( int i = 0; i < entityCount(); i++ ) { + Engineer engineer = new Engineer(); + engineer.setEmployed( ( i % 2 ) == 0 ); + engineer.setFellow( ( i % 2 ) == 1 ); + session.persist( engineer ); + } + }); + } + + protected int entityCount() { + return 100; + } + + @Test + public void testUpdate() { + doInHibernate( this::sessionFactory, session -> { + int updateCount = session.createQuery( "update Person set name = :name where employed = :employed" ) + .setParameter( "name", "John Doe" ) + .setParameter( "employed", true ) + .executeUpdate(); + + assertEquals(entityCount(), updateCount); + }); + } + + @Test + public void testDeleteFromPerson() { + doInHibernate( this::sessionFactory, session -> { + int updateCount = session.createQuery( "delete from Person where employed = :employed" ) + .setParameter( "employed", false ) + .executeUpdate(); + assertEquals( entityCount(), updateCount ); + }); + } + + @Test + public void testDeleteFromEngineer() { + doInHibernate( this::sessionFactory, session -> { + int updateCount = session.createQuery( "delete from Engineer where fellow = :fellow" ) + .setParameter( "fellow", true ) + .executeUpdate(); + assertEquals( entityCount() / 2, updateCount ); + }); + } + + @Entity(name = "Person") + @Inheritance(strategy = InheritanceType.JOINED) + public static class Person { + + @Id + @GeneratedValue + private Long id; + + private String name; + + private boolean employed; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEmployed() { + return employed; + } + + public void setEmployed(boolean employed) { + this.employed = employed; + } + } + + @Entity(name = "Doctor") + public static class Doctor extends Person { + } + + @Entity(name = "Engineer") + public static class Engineer extends Person { + + private boolean fellow; + + public boolean isFellow() { + return fellow; + } + + public void setFellow(boolean fellow) { + this.fellow = fellow; + } + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/CteValuesListBulkCompositeIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/CteValuesListBulkCompositeIdTest.java new file mode 100644 index 0000000000..f5266634d8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/CteValuesListBulkCompositeIdTest.java @@ -0,0 +1,19 @@ +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 getMultiTableBulkIdStrategyClass() { + return CteValuesListBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/CteValuesListBulkIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/CteValuesListBulkIdTest.java new file mode 100644 index 0000000000..3bf87faca5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/CteValuesListBulkIdTest.java @@ -0,0 +1,19 @@ +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 getMultiTableBulkIdStrategyClass() { + return CteValuesListBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsInClauseBulkCompositeIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsInClauseBulkCompositeIdTest.java new file mode 100644 index 0000000000..3d905e16ac --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsInClauseBulkCompositeIdTest.java @@ -0,0 +1,20 @@ +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 getMultiTableBulkIdStrategyClass() { + return InlineIdsInClauseBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsInClauseBulkIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsInClauseBulkIdTest.java new file mode 100644 index 0000000000..68c1f907e6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsInClauseBulkIdTest.java @@ -0,0 +1,19 @@ +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 getMultiTableBulkIdStrategyClass() { + return InlineIdsInClauseBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsOrClauseBulkCompositeIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsOrClauseBulkCompositeIdTest.java new file mode 100644 index 0000000000..514169a21d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsOrClauseBulkCompositeIdTest.java @@ -0,0 +1,19 @@ +package org.hibernate.test.bulkid; + +import org.hibernate.hql.spi.id.MultiTableBulkIdStrategy; +import org.hibernate.hql.spi.id.inline.InlineIdsOrClauseBulkIdStrategy; + +import org.hibernate.testing.DialectChecks; +import org.hibernate.testing.RequiresDialectFeature; + +/** + * @author Vlad Mihalcea + */ +public class InlineIdsOrClauseBulkCompositeIdTest extends + AbstractBulkCompositeIdTest { + + @Override + protected Class getMultiTableBulkIdStrategyClass() { + return InlineIdsOrClauseBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsOrClauseBulkIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsOrClauseBulkIdTest.java new file mode 100644 index 0000000000..99e395eecb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsOrClauseBulkIdTest.java @@ -0,0 +1,15 @@ +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 getMultiTableBulkIdStrategyClass() { + return InlineIdsOrClauseBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsSubSelectValuesListBulkCompositeIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsSubSelectValuesListBulkCompositeIdTest.java new file mode 100644 index 0000000000..2bce45b4ac --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsSubSelectValuesListBulkCompositeIdTest.java @@ -0,0 +1,19 @@ +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 getMultiTableBulkIdStrategyClass() { + return InlineIdsSubSelectValueListBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsSubSelectValuesListBulkIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsSubSelectValuesListBulkIdTest.java new file mode 100644 index 0000000000..e104ecfcef --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/InlineIdsSubSelectValuesListBulkIdTest.java @@ -0,0 +1,19 @@ +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 getMultiTableBulkIdStrategyClass() { + return InlineIdsSubSelectValueListBulkIdStrategy.class; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/test/bulkid/OracleInlineIdsInClauseBulkIdTest.java b/hibernate-core/src/test/java/org/hibernate/test/bulkid/OracleInlineIdsInClauseBulkIdTest.java new file mode 100644 index 0000000000..3594031189 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bulkid/OracleInlineIdsInClauseBulkIdTest.java @@ -0,0 +1,17 @@ +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; + } +} \ No newline at end of file diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java index 2c0015e1e6..0a5f349872 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java @@ -201,4 +201,26 @@ abstract public class DialectChecks { return dialect.supportsPartitionBy(); } } + + public static class SupportNonQueryValuesListWithCTE implements DialectCheck { + public boolean isMatch(Dialect dialect) { + return dialect.supportsValuesList() && + dialect.supportsNonQueryWithCTE() && + dialect.supportsRowValueConstructorSyntaxInInList(); + } + } + + public static class SupportValuesListAndRowValueConstructorSyntaxInInList + implements DialectCheck { + public boolean isMatch(Dialect dialect) { + return dialect.supportsValuesList() && + dialect.supportsRowValueConstructorSyntaxInInList(); + } + } + + public static class SupportRowValueConstructorSyntaxInInList implements DialectCheck { + public boolean isMatch(Dialect dialect) { + return dialect.supportsRowValueConstructorSyntaxInInList(); + } + } }