diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/QueryTranslatorImpl.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/QueryTranslatorImpl.java index 2313ed4d63..f40389bfb1 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/QueryTranslatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/QueryTranslatorImpl.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.hibernate.HibernateException; import org.hibernate.MappingException; @@ -33,8 +34,9 @@ import org.hibernate.hql.internal.ast.exec.DeleteExecutor; import org.hibernate.hql.internal.ast.exec.InsertExecutor; import org.hibernate.hql.internal.ast.exec.MultiTableDeleteExecutor; import org.hibernate.hql.internal.ast.exec.MultiTableUpdateExecutor; +import org.hibernate.hql.internal.ast.exec.SimpleUpdateExecutor; import org.hibernate.hql.internal.ast.exec.StatementExecutor; -import org.hibernate.hql.internal.ast.exec.UpdateExecutor; +import org.hibernate.hql.internal.ast.exec.IdSubselectUpdateExecutor; import org.hibernate.hql.internal.ast.tree.AggregatedSelectExpression; import org.hibernate.hql.internal.ast.tree.FromElement; import org.hibernate.hql.internal.ast.tree.QueryNode; @@ -61,8 +63,6 @@ import antlr.RecognitionException; import antlr.TokenStreamException; import antlr.collections.AST; -import static java.util.Collections.singleton; - /** * A QueryTranslator that uses an Antlr-based parser. * @@ -609,13 +609,14 @@ public class QueryTranslatorImpl implements FilterTranslator { final FromElement fromElement = walker.getFinalFromClause().getFromElement(); final Queryable persister = fromElement.getQueryable(); - boolean affectsExtraTables = persister.isMultiTable() - && !singleton( persister.getTableName() ).containsAll( walker.getQuerySpaces() ); - if ( affectsExtraTables ) { + if ( persister.isMultiTable() && affectsExtraTables( walker, persister ) ) { return new MultiTableUpdateExecutor( walker ); } + else if ( persister.isMultiTable() && walker.getQuerySpaces().size() > 1 ) { + return new IdSubselectUpdateExecutor( walker ); + } else { - return new UpdateExecutor( walker ); + return new SimpleUpdateExecutor( walker ); } } else if ( walker.getStatementType() == HqlSqlTokenTypes.INSERT ) { @@ -625,6 +626,16 @@ public class QueryTranslatorImpl implements FilterTranslator { throw new QueryException( "Unexpected statement type" ); } } + + private static boolean affectsExtraTables(HqlSqlWalker walker, Queryable persister) { + String[] tableNames = persister.getConstraintOrderedTableNameClosure(); + return IntStream.range( 0, tableNames.length ).filter( + table -> walker.getAssignmentSpecifications().stream().anyMatch( + assign -> assign.affectsTable( tableNames[table] ) + ) + ).count() > 1; + } + @Override public ParameterTranslations getParameterTranslations() { if ( paramTranslations == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java index ab5681f123..03fc4f6259 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java @@ -64,9 +64,7 @@ public abstract class BasicExecutor implements StatementExecutor { ); } - int doExecute(String sql, QueryParameters parameters, - List parameterSpecifications, - SharedSessionContractImplementor session) + int doExecute(String sql, QueryParameters parameters, List parameterSpecifications, SharedSessionContractImplementor session) throws HibernateException{ try { PreparedStatement st = null; diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/IdSubselectUpdateExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/IdSubselectUpdateExecutor.java new file mode 100644 index 0000000000..82e28f568a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/IdSubselectUpdateExecutor.java @@ -0,0 +1,138 @@ +/* + * 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.internal.ast.exec; + +import antlr.RecognitionException; +import org.hibernate.AssertionFailure; +import org.hibernate.HibernateException; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.QuerySyntaxException; +import org.hibernate.hql.internal.ast.SqlGenerator; +import org.hibernate.hql.internal.ast.tree.AssignmentSpecification; +import org.hibernate.hql.internal.ast.tree.UpdateStatement; +import org.hibernate.persister.entity.Queryable; +import org.hibernate.sql.Select; +import org.hibernate.sql.SelectValues; +import org.hibernate.sql.Update; + +import java.util.List; +import java.util.stream.IntStream; + +/** + * Executes HQL bulk updates against a single table, using a subselect + * against multiple tables to collect ids, which is needed when the + * where condition of the query touches columns from multiple tables. + * + * @author Gavin King + */ +public class IdSubselectUpdateExecutor extends BasicExecutor { + + public IdSubselectUpdateExecutor(HqlSqlWalker walker) { + super( walker.getFinalFromClause().getFromElement().getQueryable() ); + + Dialect dialect = walker.getDialect(); + UpdateStatement updateStatement = (UpdateStatement) walker.getAST(); + List assignments = walker.getAssignmentSpecifications(); + + String whereClause; + if ( updateStatement.getWhereClause().getNumberOfChildren() == 0 ) { + whereClause = ""; + } + else { + try { + SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() ); + gen.whereClause( updateStatement.getWhereClause() ); + gen.getParseErrorHandler().throwQueryException(); + whereClause = gen.getSQL().substring( 7 ); // strip the " where " + } + catch ( RecognitionException e ) { + throw new HibernateException( "Unable to generate id select for DML operation", e ); + } + } + String tableAlias = updateStatement.getFromClause().getFromElement().getTableAlias(); + String idSelect = generateIdSelect( tableAlias, whereClause, dialect, persister ); + + String[] tableNames = persister.getConstraintOrderedTableNameClosure(); + String[][] columnNames = persister.getContraintOrderedTableKeyColumnClosure(); + + int[] affectedTables = + IntStream.range( 0, tableNames.length ).filter( + table -> assignments.stream().anyMatch( + assign -> assign.affectsTable( tableNames[table] ) + ) + ).toArray(); + if ( affectedTables.length > 1 ) { + throw new AssertionFailure("more than one affected table"); + } + int affectedTable = affectedTables[0]; + + String tableName = tableNames[affectedTable]; + String idColumnNames = String.join( ", ", columnNames[affectedTable] ); + Update update = new Update( dialect ).setTableName( tableName ); + if ( dialect instanceof MySQLDialect) { + //MySQL needs an extra subselect to hack the query optimizer + String selectedIdColumns = String.join( ", ", persister.getIdentifierColumnNames() ); + update.setWhere( "(" + idColumnNames + ") in (select " + selectedIdColumns + " from (" + idSelect + ") as ht_ids)" ); + } + else { + update.setWhere( "(" + idColumnNames + ") in (" + idSelect + ")" ); + } + for ( AssignmentSpecification assignment: assignments ) { + update.appendAssignmentFragment( assignment.getSqlAssignmentFragment() ); + } + sql = update.toStatementString(); + + // now collect the parameters from the whole query + // parameters included in the list + try { + SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() ); + gen.statement( walker.getAST() ); + parameterSpecifications = gen.getCollectedParameters(); + } + catch ( RecognitionException e ) { + throw QuerySyntaxException.convert( e ); + } + + } + + //TODO: this is a copy/paste of a method from AbstractTableBasedBulkIdHandler + private String generateIdSelect(String tableAlias, String whereClause, Dialect dialect, Queryable queryable) { + Select select = new Select( dialect ); + SelectValues selectClause = new SelectValues( dialect ).addColumns( + tableAlias, + queryable.getIdentifierColumnNames(), + queryable.getIdentifierColumnNames() + ); + select.setSelectClause( selectClause.render() ); + + String rootTableName = queryable.getTableName(); + String fromJoinFragment = queryable.fromJoinFragment( tableAlias, true, false ); + String whereJoinFragment = queryable.whereJoinFragment( tableAlias, true, false ); + + select.setFromClause( rootTableName + ' ' + tableAlias + fromJoinFragment ); + + if ( whereJoinFragment == null ) { + whereJoinFragment = ""; + } + else { + whereJoinFragment = whereJoinFragment.trim(); + if ( whereJoinFragment.startsWith( "and" ) ) { + whereJoinFragment = whereJoinFragment.substring( 4 ); + } + } + + if ( !whereClause.isEmpty() ) { + if ( whereJoinFragment.length() > 0 ) { + whereJoinFragment += " and "; + } + } + select.setWhereClause( whereJoinFragment + whereClause ); + return select.toStatementString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/InsertExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/InsertExecutor.java index ae283a6288..a57f851cff 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/InsertExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/InsertExecutor.java @@ -1,3 +1,9 @@ +/* + * 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.internal.ast.exec; import antlr.RecognitionException; @@ -18,17 +24,17 @@ import java.util.List; * @author Gavin King */ public class InsertExecutor extends BasicExecutor { - public InsertExecutor(HqlSqlWalker walker) { - super( ( (InsertStatement) walker.getAST() ).getIntoClause().getQueryable() ); - try { - SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() ); - gen.statement( walker.getAST() ); - sql = gen.getSQL(); - gen.getParseErrorHandler().throwQueryException(); - parameterSpecifications = gen.getCollectedParameters(); - } - catch ( RecognitionException e ) { - throw QuerySyntaxException.convert( e ); - } - } + public InsertExecutor(HqlSqlWalker walker) { + super( ( (InsertStatement) walker.getAST() ).getIntoClause().getQueryable() ); + try { + SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() ); + gen.statement( walker.getAST() ); + sql = gen.getSQL(); + gen.getParseErrorHandler().throwQueryException(); + parameterSpecifications = gen.getCollectedParameters(); + } + catch ( RecognitionException e ) { + throw QuerySyntaxException.convert( e ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/SimpleUpdateExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/SimpleUpdateExecutor.java new file mode 100644 index 0000000000..fb734972c9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/SimpleUpdateExecutor.java @@ -0,0 +1,45 @@ +/* + * 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.internal.ast.exec; + +import antlr.RecognitionException; +import org.hibernate.AssertionFailure; +import org.hibernate.hql.internal.ast.HqlSqlWalker; +import org.hibernate.hql.internal.ast.QuerySyntaxException; +import org.hibernate.hql.internal.ast.SqlGenerator; + +/** + * Executes HQL bulk updates against a single table, where the + * query only touches columns from the table it's updating, and + * so we don't need to use a subselect. + * + * @author Gavin King + */ +public class SimpleUpdateExecutor extends BasicExecutor { + public SimpleUpdateExecutor(HqlSqlWalker walker) { + super( walker.getFinalFromClause().getFromElement().getQueryable() ); + + if ( persister.isMultiTable() && walker.getQuerySpaces().size() > 1 ) { + throw new AssertionFailure("not a simple update"); + } + + try { + SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() ); + gen.statement( walker.getAST() ); + gen.getParseErrorHandler().throwQueryException(); + // workaround for a problem where HqlSqlWalker actually generates + // broken SQL with undefined aliases in the where clause, because + // that is what MultiTableUpdateExecutor is expecting to get + String alias = walker.getFinalFromClause().getFromElement().getTableAlias(); + sql = gen.getSQL().replace( alias + ".", "" ); + parameterSpecifications = gen.getCollectedParameters(); + } + catch ( RecognitionException e ) { + throw QuerySyntaxException.convert( e ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/UpdateExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/UpdateExecutor.java deleted file mode 100644 index 2f80e94519..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/UpdateExecutor.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.hibernate.hql.internal.ast.exec; - -import antlr.RecognitionException; -import org.hibernate.hql.internal.antlr.HqlSqlTokenTypes; -import org.hibernate.hql.internal.ast.HqlSqlWalker; -import org.hibernate.hql.internal.ast.QuerySyntaxException; -import org.hibernate.hql.internal.ast.SqlGenerator; -import org.hibernate.param.ParameterSpecification; -import org.hibernate.persister.entity.Queryable; - -import java.util.List; - -/** - * Executes HQL bulk updates against a single table. - * - * @author Gavin King - */ -public class UpdateExecutor extends BasicExecutor { - public UpdateExecutor(HqlSqlWalker walker) { - super( walker.getFinalFromClause().getFromElement().getQueryable() ); - try { - SqlGenerator gen = new SqlGenerator( walker.getSessionFactoryHelper().getFactory() ); - gen.statement( walker.getAST() ); - // workaround for a problem where HqlSqlWalker actually generates - // broken SQL with undefined aliases in the where clause, because - // that is what MultiTableUpdateExecutor is expecting to get - String alias = walker.getFinalFromClause().getFromElement().getTableAlias(); - sql = gen.getSQL().replace( alias + ".", "" ); - gen.getParseErrorHandler().throwQueryException(); - parameterSpecifications = gen.getCollectedParameters(); - } - catch ( RecognitionException e ) { - throw QuerySyntaxException.convert( e ); - } - } -} 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 6b47360d04..b72ebae72d 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 @@ -43,7 +43,6 @@ public class TableBasedUpdateHandlerImpl private final String[] updates; private final ParameterSpecification[][] assignmentParameterSpecifications; - @SuppressWarnings("unchecked") public TableBasedUpdateHandlerImpl( SessionFactoryImplementor factory, HqlSqlWalker walker, @@ -90,7 +89,7 @@ public class TableBasedUpdateHandlerImpl } if ( affected ) { updates[tableIndex] = update.toStatementString(); - assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[parameterList.size()] ); + assignmentParameterSpecifications[tableIndex] = parameterList.toArray( new ParameterSpecification[0] ); } } }