HHH-16433 Fix forced follow on locking with order by

This commit is contained in:
Jarkko Hyöty 2023-04-06 12:03:31 +03:00 committed by Christian Beikov
parent 79d2e208a6
commit 6c8bb03c93
7 changed files with 496 additions and 269 deletions

View File

@ -6,11 +6,14 @@
*/
package org.hibernate.community.dialect;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.LockMode;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.collections.Stack;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
import org.hibernate.query.IllegalQueryOperationException;
import org.hibernate.query.sqm.ComparisonOperator;
@ -24,7 +27,6 @@ import org.hibernate.sql.ast.tree.Statement;
import org.hibernate.sql.ast.tree.cte.CteMaterialization;
import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression;
import org.hibernate.sql.ast.tree.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression;
import org.hibernate.sql.ast.tree.expression.Expression;
import org.hibernate.sql.ast.tree.expression.FunctionExpression;
import org.hibernate.sql.ast.tree.expression.Literal;
@ -32,13 +34,16 @@ import org.hibernate.sql.ast.tree.expression.Over;
import org.hibernate.sql.ast.tree.expression.SqlTuple;
import org.hibernate.sql.ast.tree.expression.SqlTupleContainer;
import org.hibernate.sql.ast.tree.expression.Summarization;
import org.hibernate.sql.ast.tree.from.FromClause;
import org.hibernate.sql.ast.tree.from.FunctionTableReference;
import org.hibernate.sql.ast.tree.from.NamedTableReference;
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.UnionTableGroup;
import org.hibernate.sql.ast.tree.from.ValuesTableReference;
import org.hibernate.sql.ast.tree.insert.InsertSelectStatement;
import org.hibernate.sql.ast.tree.insert.Values;
import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate;
import org.hibernate.sql.ast.tree.predicate.Predicate;
import org.hibernate.sql.ast.tree.select.QueryGroup;
import org.hibernate.sql.ast.tree.select.QueryPart;
import org.hibernate.sql.ast.tree.select.QuerySpec;
@ -46,6 +51,7 @@ import org.hibernate.sql.ast.tree.select.SelectClause;
import org.hibernate.sql.ast.tree.select.SortSpecification;
import org.hibernate.sql.ast.tree.update.Assignment;
import org.hibernate.sql.exec.spi.JdbcOperation;
import org.hibernate.sql.results.internal.SqlSelectionImpl;
import org.hibernate.type.SqlTypes;
/**
@ -97,12 +103,6 @@ public class OracleLegacySqlAstTranslator<T extends JdbcOperation> extends Abstr
Boolean followOnLocking) {
LockStrategy strategy = super.determineLockingStrategy( querySpec, forUpdateClause, followOnLocking );
final boolean followOnLockingDisabled = Boolean.FALSE.equals( followOnLocking );
if ( strategy != LockStrategy.FOLLOW_ON && querySpec.hasSortSpecifications() ) {
if ( followOnLockingDisabled ) {
throw new IllegalQueryOperationException( "Locking with ORDER BY is not supported" );
}
strategy = LockStrategy.FOLLOW_ON;
}
// Oracle also doesn't support locks with set operators
// See https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10002.htm#i2066346
if ( strategy != LockStrategy.FOLLOW_ON && isPartOfQueryGroup() ) {
@ -117,29 +117,12 @@ public class OracleLegacySqlAstTranslator<T extends JdbcOperation> extends Abstr
}
strategy = LockStrategy.FOLLOW_ON;
}
if ( strategy != LockStrategy.FOLLOW_ON && useOffsetFetchClause( querySpec ) && !isRowsOnlyFetchClauseType( querySpec ) ) {
if ( strategy != LockStrategy.FOLLOW_ON && needsLockingWrapper( querySpec ) && !canApplyLockingWrapper( querySpec ) ) {
if ( followOnLockingDisabled ) {
throw new IllegalQueryOperationException( "Locking with FETCH is not supported" );
throw new IllegalQueryOperationException( "Locking with OFFSET/FETCH is not supported" );
}
strategy = LockStrategy.FOLLOW_ON;
}
if ( strategy != LockStrategy.FOLLOW_ON ) {
final boolean hasOffset;
if ( querySpec.isRoot() && hasLimit() && getLimit().getFirstRow() != null ) {
hasOffset = true;
// We must record that the generated SQL depends on the fact that there is an offset
addAppliedParameterBinding( getOffsetParameter(), null );
}
else {
hasOffset = querySpec.getOffsetClauseExpression() != null;
}
if ( hasOffset ) {
if ( followOnLockingDisabled ) {
throw new IllegalQueryOperationException( "Locking with OFFSET is not supported" );
}
strategy = LockStrategy.FOLLOW_ON;
}
}
return strategy;
}
@ -166,7 +149,7 @@ public class OracleLegacySqlAstTranslator<T extends JdbcOperation> extends Abstr
protected boolean shouldEmulateFetchClause(QueryPart queryPart) {
// Check if current query part is already row numbering to avoid infinite recursion
if (getQueryPartForRowNumbering() == queryPart) {
if ( getQueryPartForRowNumbering() == queryPart ) {
return false;
}
final boolean hasLimit = queryPart.isRoot() && hasLimit() || queryPart.getFetchClauseExpression() != null
@ -176,77 +159,12 @@ public class OracleLegacySqlAstTranslator<T extends JdbcOperation> extends Abstr
}
// Even if Oracle supports the OFFSET/FETCH clause, there are conditions where we still want to use the ROWNUM pagination
if ( supportsOffsetFetchClause() ) {
// When the query has no sort specifications and offset, we want to use the ROWNUM pagination as that is a special locking case
return !queryPart.hasSortSpecifications() && !hasOffset( queryPart )
// Workaround an Oracle bug, segmentation fault for insert queries with a plain query group and fetch clause
|| queryPart instanceof QueryGroup && getClauseStack().isEmpty() && getStatement() instanceof InsertSelectStatement;
// Workaround an Oracle bug, segmentation fault for insert queries with a plain query group and fetch clause
return queryPart instanceof QueryGroup && getClauseStack().isEmpty() && getStatement() instanceof InsertSelectStatement;
}
return true;
}
@Override
protected FetchClauseType getFetchClauseTypeForRowNumbering(QueryPart queryPart) {
final FetchClauseType fetchClauseType = super.getFetchClauseTypeForRowNumbering( queryPart );
final boolean hasOffset;
if ( queryPart.isRoot() && hasLimit() ) {
hasOffset = getLimit().getFirstRow() != null;
}
else {
hasOffset = queryPart.getOffsetClauseExpression() != null;
}
if ( queryPart instanceof QuerySpec && !hasOffset && fetchClauseType == FetchClauseType.ROWS_ONLY ) {
// We return null here, because in this particular case, we render a special rownum query
// which can be seen in #emulateFetchOffsetWithWindowFunctions
// Note that we also build upon this in #visitOrderBy
return null;
}
return fetchClauseType;
}
@Override
protected void emulateFetchOffsetWithWindowFunctions(
QueryPart queryPart,
Expression offsetExpression,
Expression fetchExpression,
FetchClauseType fetchClauseType,
boolean emulateFetchClause) {
if ( queryPart instanceof QuerySpec && offsetExpression == null && fetchClauseType == FetchClauseType.ROWS_ONLY ) {
// Special case for Oracle to support locking along with simple max results paging
final QuerySpec querySpec = (QuerySpec) queryPart;
withRowNumbering(
querySpec,
true, // we need select aliases to avoid ORA-00918: column ambiguously defined
() -> {
appendSql( "select * from " );
emulateFetchOffsetWithWindowFunctionsVisitQueryPart( querySpec );
appendSql( " where rownum<=" );
final Stack<Clause> clauseStack = getClauseStack();
clauseStack.push( Clause.WHERE );
try {
fetchExpression.accept( this );
// We render the FOR UPDATE clause in the outer query
clauseStack.pop();
clauseStack.push( Clause.FOR_UPDATE );
visitForUpdateClause( querySpec );
}
finally {
clauseStack.pop();
}
}
);
}
else {
super.emulateFetchOffsetWithWindowFunctions(
queryPart,
offsetExpression,
fetchExpression,
fetchClauseType,
emulateFetchClause
);
}
}
@Override
protected void visitOrderBy(List<SortSpecification> sortSpecifications) {
// If we have a query part for row numbering, there is no need to render the order by clause
@ -262,13 +180,49 @@ public class OracleLegacySqlAstTranslator<T extends JdbcOperation> extends Abstr
final QuerySpec querySpec = (QuerySpec) queryPartForRowNumbering;
if ( querySpec.getOffsetClauseExpression() == null
&& ( !querySpec.isRoot() || getOffsetParameter() == null ) ) {
// When rendering `rownum` for Oracle, we need to render the order by clause still
renderOrderBy( true, sortSpecifications );
// When we enter here, we need to handle the special ROWNUM pagination
if ( hasGroupingOrDistinct( querySpec ) || querySpec.getFromClause().hasJoins() ) {
// When the query spec has joins, a group by, having or distinct clause,
// we just need to render the order by clause, because the query is wrapped
renderOrderBy( true, sortSpecifications );
}
else {
// Otherwise we need to render the ROWNUM pagination predicate in here
final Predicate whereClauseRestrictions = querySpec.getWhereClauseRestrictions();
if ( whereClauseRestrictions != null && !whereClauseRestrictions.isEmpty() ) {
appendSql( " and " );
}
else {
appendSql( " where " );
}
appendSql( "rownum<=" );
final Stack<Clause> clauseStack = getClauseStack();
clauseStack.push( Clause.WHERE );
try {
if ( querySpec.isRoot() && hasLimit() ) {
getLimitParameter().accept( this );
}
else {
querySpec.getFetchClauseExpression().accept( this );
}
}
finally {
clauseStack.pop();
}
renderOrderBy( true, sortSpecifications );
visitForUpdateClause( querySpec );
}
}
}
}
}
private boolean hasGroupingOrDistinct(QuerySpec querySpec) {
return querySpec.getSelectClause().isDistinct()
|| !querySpec.getGroupByClauseExpressions().isEmpty()
|| querySpec.getHavingClauseRestrictions() != null;
}
@Override
protected void visitValuesList(List<Values> valuesList) {
if ( valuesList.size() < 2 ) {
@ -323,12 +277,142 @@ public class OracleLegacySqlAstTranslator<T extends JdbcOperation> extends Abstr
@Override
public void visitQuerySpec(QuerySpec querySpec) {
if ( shouldEmulateFetchClause( querySpec ) ) {
emulateFetchOffsetWithWindowFunctions( querySpec, true );
final EntityIdentifierMapping identifierMappingForLockingWrapper = identifierMappingForLockingWrapper( querySpec );
final Expression offsetExpression;
final Expression fetchExpression;
final FetchClauseType fetchClauseType;
if ( querySpec.isRoot() && hasLimit() ) {
prepareLimitOffsetParameters();
offsetExpression = getOffsetParameter();
fetchExpression = getLimitParameter();
fetchClauseType = FetchClauseType.ROWS_ONLY;
}
else {
super.visitQuerySpec( querySpec );
offsetExpression = querySpec.getOffsetClauseExpression();
fetchExpression = querySpec.getFetchClauseExpression();
fetchClauseType = querySpec.getFetchClauseType();
}
if ( shouldEmulateFetchClause( querySpec ) ) {
if ( identifierMappingForLockingWrapper == null ) {
emulateFetchOffsetWithWindowFunctions(
querySpec,
offsetExpression,
fetchExpression,
fetchClauseType,
true
);
}
else {
super.visitQuerySpec(
createLockingWrapper(
querySpec,
offsetExpression,
fetchExpression,
fetchClauseType,
identifierMappingForLockingWrapper
)
);
// Render the for update clause for the original query spec, because the locking wrapper is marked as non-root
visitForUpdateClause( querySpec );
}
}
else {
if ( identifierMappingForLockingWrapper == null ) {
super.visitQuerySpec( querySpec );
}
else {
super.visitQuerySpec(
createLockingWrapper(
querySpec,
offsetExpression,
fetchExpression,
fetchClauseType,
identifierMappingForLockingWrapper
)
);
// Render the for update clause for the original query spec, because the locking wrapper is marked as non-root
visitForUpdateClause( querySpec );
}
}
}
private QuerySpec createLockingWrapper(
QuerySpec querySpec,
Expression offsetExpression,
Expression fetchExpression,
FetchClauseType fetchClauseType,
EntityIdentifierMapping identifierMappingForLockingWrapper) {
final TableGroup rootTableGroup = querySpec.getFromClause().getRoots().get( 0 );
final List<ColumnReference> idColumnReferences = new ArrayList<>( identifierMappingForLockingWrapper.getJdbcTypeCount() );
identifierMappingForLockingWrapper.forEachSelectable(
0,
(selectionIndex, selectableMapping) -> {
idColumnReferences.add( new ColumnReference( rootTableGroup.getPrimaryTableReference(), selectableMapping ) );
}
);
final Expression idExpression;
if ( identifierMappingForLockingWrapper instanceof EmbeddableValuedModelPart ) {
idExpression = new SqlTuple( idColumnReferences, identifierMappingForLockingWrapper );
}
else {
idExpression = idColumnReferences.get( 0 );
}
final QuerySpec subquery = new QuerySpec( false, 1 );
for ( ColumnReference idColumnReference : idColumnReferences ) {
subquery.getSelectClause().addSqlSelection( new SqlSelectionImpl( 0, -1, idColumnReference ) );
}
subquery.getFromClause().addRoot( rootTableGroup );
subquery.applyPredicate( querySpec.getWhereClauseRestrictions() );
if ( querySpec.hasSortSpecifications() ) {
for ( SortSpecification sortSpecification : querySpec.getSortSpecifications() ) {
subquery.addSortSpecification( sortSpecification );
}
}
subquery.setOffsetClauseExpression( offsetExpression );
subquery.setFetchClauseExpression( fetchExpression, fetchClauseType );
// Mark the query spec as non-root even if it might be the root, to avoid applying the pagination there
final QuerySpec lockingWrapper = new QuerySpec( false, 1 );
lockingWrapper.getFromClause().addRoot( rootTableGroup );
for ( SqlSelection sqlSelection : querySpec.getSelectClause().getSqlSelections() ) {
lockingWrapper.getSelectClause().addSqlSelection( sqlSelection );
}
lockingWrapper.applyPredicate( new InSubQueryPredicate( idExpression, subquery, false ) );
return lockingWrapper;
}
private EntityIdentifierMapping identifierMappingForLockingWrapper(QuerySpec querySpec) {
// We only need a locking wrapper for very simple queries
if ( canApplyLockingWrapper( querySpec )
// There must be the need for locking in this query
&& needsLocking( querySpec )
// The query uses some sort of pagination which makes the wrapper necessary
&& needsLockingWrapper( querySpec )
// The query may not have a group by, having and distinct clause, or use aggregate functions,
// as these features will force the use of follow-on locking
&& querySpec.getGroupByClauseExpressions().isEmpty()
&& querySpec.getHavingClauseRestrictions() == null
&& !querySpec.getSelectClause().isDistinct()
&& !hasAggregateFunctions( querySpec ) ) {
return ( (EntityMappingType) querySpec.getFromClause().getRoots().get( 0 ).getModelPart() ).getIdentifierMapping();
}
return null;
}
private boolean canApplyLockingWrapper(QuerySpec querySpec) {
final FromClause fromClause;
return querySpec.isRoot()
// Must have a single root with no joins for an entity type
&& ( fromClause = querySpec.getFromClause() ).getRoots().size() == 1
&& !fromClause.hasJoins()
&& fromClause.getRoots().get( 0 ).getModelPart() instanceof EntityMappingType;
}
private boolean needsLockingWrapper(QuerySpec querySpec) {
return querySpec.getFetchClauseType() != FetchClauseType.ROWS_ONLY
|| hasOffset( querySpec )
|| hasLimit( querySpec );
}
@Override

View File

@ -6,10 +6,14 @@
*/
package org.hibernate.dialect;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.collections.Stack;
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
import org.hibernate.query.IllegalQueryOperationException;
import org.hibernate.query.sqm.ComparisonOperator;
@ -28,12 +32,15 @@ import org.hibernate.sql.ast.tree.expression.Over;
import org.hibernate.sql.ast.tree.expression.SqlTuple;
import org.hibernate.sql.ast.tree.expression.SqlTupleContainer;
import org.hibernate.sql.ast.tree.expression.Summarization;
import org.hibernate.sql.ast.tree.from.FromClause;
import org.hibernate.sql.ast.tree.from.FunctionTableReference;
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
import org.hibernate.sql.ast.tree.from.TableGroup;
import org.hibernate.sql.ast.tree.from.UnionTableGroup;
import org.hibernate.sql.ast.tree.from.ValuesTableReference;
import org.hibernate.sql.ast.tree.insert.InsertSelectStatement;
import org.hibernate.sql.ast.tree.insert.Values;
import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate;
import org.hibernate.sql.ast.tree.select.QueryGroup;
import org.hibernate.sql.ast.tree.select.QueryPart;
import org.hibernate.sql.ast.tree.select.QuerySpec;
@ -43,6 +50,7 @@ import org.hibernate.sql.ast.tree.update.Assignment;
import org.hibernate.sql.exec.spi.JdbcOperation;
import org.hibernate.sql.model.ast.ColumnValueBinding;
import org.hibernate.sql.model.internal.OptionalTableUpdate;
import org.hibernate.sql.results.internal.SqlSelectionImpl;
import org.hibernate.type.SqlTypes;
/**
@ -94,12 +102,6 @@ public class OracleSqlAstTranslator<T extends JdbcOperation> extends SqlAstTrans
Boolean followOnLocking) {
LockStrategy strategy = super.determineLockingStrategy( querySpec, forUpdateClause, followOnLocking );
final boolean followOnLockingDisabled = Boolean.FALSE.equals( followOnLocking );
if ( strategy != LockStrategy.FOLLOW_ON && querySpec.hasSortSpecifications() ) {
if ( followOnLockingDisabled ) {
throw new IllegalQueryOperationException( "Locking with ORDER BY is not supported" );
}
strategy = LockStrategy.FOLLOW_ON;
}
// Oracle also doesn't support locks with set operators
// See https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10002.htm#i2066346
if ( strategy != LockStrategy.FOLLOW_ON && isPartOfQueryGroup() ) {
@ -114,29 +116,12 @@ public class OracleSqlAstTranslator<T extends JdbcOperation> extends SqlAstTrans
}
strategy = LockStrategy.FOLLOW_ON;
}
if ( strategy != LockStrategy.FOLLOW_ON && useOffsetFetchClause( querySpec ) && !isRowsOnlyFetchClauseType( querySpec ) ) {
if ( strategy != LockStrategy.FOLLOW_ON && needsLockingWrapper( querySpec ) && !canApplyLockingWrapper( querySpec ) ) {
if ( followOnLockingDisabled ) {
throw new IllegalQueryOperationException( "Locking with FETCH is not supported" );
throw new IllegalQueryOperationException( "Locking with OFFSET/FETCH is not supported" );
}
strategy = LockStrategy.FOLLOW_ON;
}
if ( strategy != LockStrategy.FOLLOW_ON ) {
final boolean hasOffset;
if ( querySpec.isRoot() && hasLimit() && getLimit().getFirstRow() != null ) {
hasOffset = true;
// We must record that the generated SQL depends on the fact that there is an offset
addAppliedParameterBinding( getOffsetParameter(), null );
}
else {
hasOffset = querySpec.getOffsetClauseExpression() != null;
}
if ( hasOffset ) {
if ( followOnLockingDisabled ) {
throw new IllegalQueryOperationException( "Locking with OFFSET is not supported" );
}
strategy = LockStrategy.FOLLOW_ON;
}
}
return strategy;
}
@ -167,7 +152,7 @@ public class OracleSqlAstTranslator<T extends JdbcOperation> extends SqlAstTrans
protected boolean shouldEmulateFetchClause(QueryPart queryPart) {
// Check if current query part is already row numbering to avoid infinite recursion
if (getQueryPartForRowNumbering() == queryPart) {
if ( getQueryPartForRowNumbering() == queryPart ) {
return false;
}
final boolean hasLimit = queryPart.isRoot() && hasLimit() || queryPart.getFetchClauseExpression() != null
@ -177,99 +162,12 @@ public class OracleSqlAstTranslator<T extends JdbcOperation> extends SqlAstTrans
}
// Even if Oracle supports the OFFSET/FETCH clause, there are conditions where we still want to use the ROWNUM pagination
if ( supportsOffsetFetchClause() ) {
// When the query has no sort specifications and offset, we want to use the ROWNUM pagination as that is a special locking case
return !queryPart.hasSortSpecifications() && !hasOffset( queryPart )
// Workaround an Oracle bug, segmentation fault for insert queries with a plain query group and fetch clause
|| queryPart instanceof QueryGroup && getClauseStack().isEmpty() && getStatement() instanceof InsertSelectStatement;
// Workaround an Oracle bug, segmentation fault for insert queries with a plain query group and fetch clause
return queryPart instanceof QueryGroup && getClauseStack().isEmpty() && getStatement() instanceof InsertSelectStatement;
}
return true;
}
@Override
protected FetchClauseType getFetchClauseTypeForRowNumbering(QueryPart queryPart) {
final FetchClauseType fetchClauseType = super.getFetchClauseTypeForRowNumbering( queryPart );
final boolean hasOffset;
if ( queryPart.isRoot() && hasLimit() ) {
hasOffset = getLimit().getFirstRow() != null;
}
else {
hasOffset = queryPart.getOffsetClauseExpression() != null;
}
if ( queryPart instanceof QuerySpec && !hasOffset && fetchClauseType == FetchClauseType.ROWS_ONLY ) {
// We return null here, because in this particular case, we render a special rownum query
// which can be seen in #emulateFetchOffsetWithWindowFunctions
// Note that we also build upon this in #visitOrderBy
return null;
}
return fetchClauseType;
}
@Override
protected void emulateFetchOffsetWithWindowFunctions(
QueryPart queryPart,
Expression offsetExpression,
Expression fetchExpression,
FetchClauseType fetchClauseType,
boolean emulateFetchClause) {
if ( queryPart instanceof QuerySpec && offsetExpression == null && fetchClauseType == FetchClauseType.ROWS_ONLY ) {
// Special case for Oracle to support locking along with simple max results paging
final QuerySpec querySpec = (QuerySpec) queryPart;
withRowNumbering(
querySpec,
true, // we need select aliases to avoid ORA-00918: column ambiguously defined
() -> {
appendSql( "select * from " );
emulateFetchOffsetWithWindowFunctionsVisitQueryPart( querySpec );
appendSql( " where rownum<=" );
final Stack<Clause> clauseStack = getClauseStack();
clauseStack.push( Clause.WHERE );
try {
fetchExpression.accept( this );
// We render the FOR UPDATE clause in the outer query
clauseStack.pop();
clauseStack.push( Clause.FOR_UPDATE );
visitForUpdateClause( querySpec );
}
finally {
clauseStack.pop();
}
}
);
}
else {
super.emulateFetchOffsetWithWindowFunctions(
queryPart,
offsetExpression,
fetchExpression,
fetchClauseType,
emulateFetchClause
);
}
}
@Override
protected void visitOrderBy(List<SortSpecification> sortSpecifications) {
// If we have a query part for row numbering, there is no need to render the order by clause
// as that is part of the row numbering window function already, by which we then order by in the outer query
final QueryPart queryPartForRowNumbering = getQueryPartForRowNumbering();
if ( queryPartForRowNumbering == null ) {
renderOrderBy( true, sortSpecifications );
}
else {
// This logic is tightly coupled to #emulateFetchOffsetWithWindowFunctions and #getFetchClauseTypeForRowNumbering
// so that this is rendered when we end up in the special case for Oracle that renders a rownum filter
if ( getFetchClauseTypeForRowNumbering( queryPartForRowNumbering ) == null ) {
final QuerySpec querySpec = (QuerySpec) queryPartForRowNumbering;
if ( querySpec.getOffsetClauseExpression() == null
&& ( !querySpec.isRoot() || getOffsetParameter() == null ) ) {
// When rendering `rownum` for Oracle, we need to render the order by clause still
renderOrderBy( true, sortSpecifications );
}
}
}
}
@Override
protected void visitValuesList(List<Values> valuesList) {
if ( valuesList.size() < 2 ) {
@ -324,12 +222,142 @@ public class OracleSqlAstTranslator<T extends JdbcOperation> extends SqlAstTrans
@Override
public void visitQuerySpec(QuerySpec querySpec) {
if ( shouldEmulateFetchClause( querySpec ) ) {
emulateFetchOffsetWithWindowFunctions( querySpec, true );
final EntityIdentifierMapping identifierMappingForLockingWrapper = identifierMappingForLockingWrapper( querySpec );
final Expression offsetExpression;
final Expression fetchExpression;
final FetchClauseType fetchClauseType;
if ( querySpec.isRoot() && hasLimit() ) {
prepareLimitOffsetParameters();
offsetExpression = getOffsetParameter();
fetchExpression = getLimitParameter();
fetchClauseType = FetchClauseType.ROWS_ONLY;
}
else {
super.visitQuerySpec( querySpec );
offsetExpression = querySpec.getOffsetClauseExpression();
fetchExpression = querySpec.getFetchClauseExpression();
fetchClauseType = querySpec.getFetchClauseType();
}
if ( shouldEmulateFetchClause( querySpec ) ) {
if ( identifierMappingForLockingWrapper == null ) {
emulateFetchOffsetWithWindowFunctions(
querySpec,
offsetExpression,
fetchExpression,
fetchClauseType,
true
);
}
else {
super.visitQuerySpec(
createLockingWrapper(
querySpec,
offsetExpression,
fetchExpression,
fetchClauseType,
identifierMappingForLockingWrapper
)
);
// Render the for update clause for the original query spec, because the locking wrapper is marked as non-root
visitForUpdateClause( querySpec );
}
}
else {
if ( identifierMappingForLockingWrapper == null ) {
super.visitQuerySpec( querySpec );
}
else {
super.visitQuerySpec(
createLockingWrapper(
querySpec,
offsetExpression,
fetchExpression,
fetchClauseType,
identifierMappingForLockingWrapper
)
);
// Render the for update clause for the original query spec, because the locking wrapper is marked as non-root
visitForUpdateClause( querySpec );
}
}
}
private QuerySpec createLockingWrapper(
QuerySpec querySpec,
Expression offsetExpression,
Expression fetchExpression,
FetchClauseType fetchClauseType,
EntityIdentifierMapping identifierMappingForLockingWrapper) {
final TableGroup rootTableGroup = querySpec.getFromClause().getRoots().get( 0 );
final List<ColumnReference> idColumnReferences = new ArrayList<>( identifierMappingForLockingWrapper.getJdbcTypeCount() );
identifierMappingForLockingWrapper.forEachSelectable(
0,
(selectionIndex, selectableMapping) -> {
idColumnReferences.add( new ColumnReference( rootTableGroup.getPrimaryTableReference(), selectableMapping ) );
}
);
final Expression idExpression;
if ( identifierMappingForLockingWrapper instanceof EmbeddableValuedModelPart ) {
idExpression = new SqlTuple( idColumnReferences, identifierMappingForLockingWrapper );
}
else {
idExpression = idColumnReferences.get( 0 );
}
final QuerySpec subquery = new QuerySpec( false, 1 );
for ( ColumnReference idColumnReference : idColumnReferences ) {
subquery.getSelectClause().addSqlSelection( new SqlSelectionImpl( 0, -1, idColumnReference ) );
}
subquery.getFromClause().addRoot( rootTableGroup );
subquery.applyPredicate( querySpec.getWhereClauseRestrictions() );
if ( querySpec.hasSortSpecifications() ) {
for ( SortSpecification sortSpecification : querySpec.getSortSpecifications() ) {
subquery.addSortSpecification( sortSpecification );
}
}
subquery.setOffsetClauseExpression( offsetExpression );
subquery.setFetchClauseExpression( fetchExpression, fetchClauseType );
// Mark the query spec as non-root even if it might be the root, to avoid applying the pagination there
final QuerySpec lockingWrapper = new QuerySpec( false, 1 );
lockingWrapper.getFromClause().addRoot( rootTableGroup );
for ( SqlSelection sqlSelection : querySpec.getSelectClause().getSqlSelections() ) {
lockingWrapper.getSelectClause().addSqlSelection( sqlSelection );
}
lockingWrapper.applyPredicate( new InSubQueryPredicate( idExpression, subquery, false ) );
return lockingWrapper;
}
private EntityIdentifierMapping identifierMappingForLockingWrapper(QuerySpec querySpec) {
// We only need a locking wrapper for very simple queries
if ( canApplyLockingWrapper( querySpec )
// There must be the need for locking in this query
&& needsLocking( querySpec )
// The query uses some sort of pagination which makes the wrapper necessary
&& needsLockingWrapper( querySpec )
// The query may not have a group by, having and distinct clause, or use aggregate functions,
// as these features will force the use of follow-on locking
&& querySpec.getGroupByClauseExpressions().isEmpty()
&& querySpec.getHavingClauseRestrictions() == null
&& !querySpec.getSelectClause().isDistinct()
&& !hasAggregateFunctions( querySpec ) ) {
return ( (EntityMappingType) querySpec.getFromClause().getRoots().get( 0 ).getModelPart() ).getIdentifierMapping();
}
return null;
}
private boolean canApplyLockingWrapper(QuerySpec querySpec) {
final FromClause fromClause;
return querySpec.isRoot()
// Must have a single root with no joins for an entity type
&& ( fromClause = querySpec.getFromClause() ).getRoots().size() == 1
&& !fromClause.hasJoins()
&& fromClause.getRoots().get( 0 ).getModelPart() instanceof EntityMappingType;
}
private boolean needsLockingWrapper(QuerySpec querySpec) {
return querySpec.getFetchClauseType() != FetchClauseType.ROWS_ONLY
|| hasOffset( querySpec )
|| hasLimit( querySpec );
}
@Override
@ -582,6 +610,7 @@ public class OracleSqlAstTranslator<T extends JdbcOperation> extends SqlAstTrans
appendSql( " s" );
}
@Override
protected void renderMergeSource(OptionalTableUpdate optionalTableUpdate) {
final List<ColumnValueBinding> valueBindings = optionalTableUpdate.getValueBindings();
final List<ColumnValueBinding> keyBindings = optionalTableUpdate.getKeyBindings();

View File

@ -55,7 +55,6 @@ public enum Clause {
ORDER,
OFFSET,
FETCH,
FOR_UPDATE,
OVER,
/**
* The clause containing CTEs

View File

@ -593,6 +593,15 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
return limit != null && !limit.isEmpty();
}
protected boolean hasLimit(QueryPart queryPart) {
if ( queryPart.isRoot() && hasLimit() && limit.getMaxRows() != null ) {
return true;
}
else {
return queryPart.getFetchClauseExpression() != null;
}
}
protected boolean hasOffset(QueryPart queryPart) {
if ( queryPart.isRoot() && hasLimit() && limit.getFirstRow() != null ) {
return true;
@ -1350,12 +1359,15 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
}
protected LockMode getEffectiveLockMode(String alias) {
return getEffectiveLockMode( alias, getQueryPartStack().getCurrent().isRoot() );
}
protected LockMode getEffectiveLockMode(String alias, boolean isRoot) {
if ( getLockOptions() == null ) {
return LockMode.NONE;
}
final QueryPart currentQueryPart = getQueryPartStack().getCurrent();
LockMode lockMode = getLockOptions().getAliasSpecificLockMode( alias );
if ( currentQueryPart.isRoot() && lockMode == null ) {
if ( isRoot && lockMode == null ) {
lockMode = getLockOptions().getLockMode();
}
return lockMode == null ? LockMode.NONE : lockMode;
@ -4487,8 +4499,6 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
// We render the FOR UPDATE clause in the outer query
if ( queryPart instanceof QuerySpec ) {
clauseStack.pop();
clauseStack.push( Clause.FOR_UPDATE );
visitForUpdateClause( (QuerySpec) queryPart );
}
}
@ -5254,6 +5264,17 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
}
}
protected boolean needsLocking(QuerySpec querySpec) {
return querySpec.getFromClause().queryTableGroups(
tableGroup -> {
if ( LockMode.READ.lessThan( getEffectiveLockMode( tableGroup.getSourceAlias(), querySpec.isRoot() ) ) ) {
return true;
}
return null;
}
) != null;
}
protected boolean hasNestedTableGroupsToRender(List<TableGroupJoin> nestedTableGroupJoins) {
for ( TableGroupJoin nestedTableGroupJoin : nestedTableGroupJoins ) {
final TableGroup joinedGroup = nestedTableGroupJoin.getJoinedGroup();

View File

@ -244,4 +244,38 @@ public class FromClause implements SqlAstNode {
public void accept(SqlAstWalker sqlTreeWalker) {
sqlTreeWalker.visitFromClause( this );
}
public boolean hasJoins() {
for ( int i = 0; i < roots.size(); i++ ) {
final TableGroup tableGroup = roots.get( i );
if ( !tableGroup.getTableReferenceJoins().isEmpty() ) {
return true;
}
if ( hasJoins( tableGroup.getTableGroupJoins() ) ) {
return true;
}
if ( hasJoins( tableGroup.getNestedTableGroupJoins() ) ) {
return true;
}
}
return false;
}
private boolean hasJoins(List<TableGroupJoin> tableGroupJoins) {
for ( TableGroupJoin tableGroupJoin : tableGroupJoins ) {
final TableGroup joinedGroup = tableGroupJoin.getJoinedGroup();
if ( joinedGroup instanceof VirtualTableGroup ) {
if ( hasJoins( joinedGroup.getTableGroupJoins() ) ) {
return true;
}
if ( hasJoins( joinedGroup.getNestedTableGroupJoins() ) ) {
return true;
}
}
else if ( joinedGroup.isInitialized() ) {
return true;
}
}
return false;
}
}

View File

@ -24,14 +24,20 @@ import org.junit.Before;
import org.junit.Test;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.LockModeType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedQuery;
import jakarta.persistence.QueryHint;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -111,7 +117,7 @@ public class OracleFollowOnLockingTest extends
}
@Test
public void testPessimisticLockWithFirstResultsThenFollowOnLocking() {
public void testPessimisticLockWithFirstResultThenFollowOnLocking() {
final Session session = openSession();
session.beginTransaction();
@ -126,6 +132,29 @@ public class OracleFollowOnLockingTest extends
.setMaxResults( 10 )
.getResultList();
assertEquals( 10, products.size() );
assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() );
session.getTransaction().commit();
session.close();
}
@Test
public void testPessimisticLockWithFirstResultAndJoinThenFollowOnLocking() {
final Session session = openSession();
session.beginTransaction();
sqlStatementInterceptor.getSqlQueries().clear();
List<Product> products =
session.createQuery(
"select p from Product p left join p.vehicle v on v.id is null", Product.class )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) )
.setFirstResult( 40 )
.setMaxResults( 10 )
.getResultList();
assertEquals( 10, products.size() );
assertEquals( 11, sqlStatementInterceptor.getSqlQueries().size() );
@ -173,7 +202,29 @@ public class OracleFollowOnLockingTest extends
}
@Test
public void testPessimisticLockWithFirstResultsWhileExplicitlyDisablingFollowOnLockingThenFails() {
public void testPessimisticLockWithFirstResultWhileExplicitlyDisablingFollowOnLockingThenFails() {
final Session session = openSession();
session.beginTransaction();
sqlStatementInterceptor.getSqlQueries().clear();
List<Product> products = session.createQuery( "select p from Product p", Product.class )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE )
.setFollowOnLocking( false ) )
.setFirstResult( 40 )
.setMaxResults( 10 )
.getResultList();
assertEquals( 10, products.size() );
assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() );
session.getTransaction().commit();
session.close();
}
@Test
public void testPessimisticLockWithFirstResultAndJoinWhileExplicitlyDisablingFollowOnLockingThenFails() {
final Session session = openSession();
session.beginTransaction();
@ -183,7 +234,7 @@ public class OracleFollowOnLockingTest extends
try {
List<Product> products =
session.createQuery(
"select p from Product p", Product.class )
"select p from Product p left join p.vehicle v on v.id is null", Product.class )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE )
.setFollowOnLocking( false ) )
.setFirstResult( 40 )
@ -196,10 +247,9 @@ public class OracleFollowOnLockingTest extends
IllegalQueryOperationException.class,
expected.getCause().getClass()
);
assertTrue(
expected.getCause().getMessage().contains(
"Locking with OFFSET is not supported"
)
assertThat(
expected.getCause().getMessage(),
containsString( "Locking with OFFSET/FETCH is not supported" )
);
}
}
@ -230,7 +280,29 @@ public class OracleFollowOnLockingTest extends
@Test
public void testPessimisticLockWithMaxResultsAndOrderByThenFollowOnLocking() {
@TestForIssue(jiraKey = "HHH-16433")
public void testPessimisticLockWithOrderByThenNoFollowOnLocking() {
final Session session = openSession();
session.beginTransaction();
sqlStatementInterceptor.getSqlQueries().clear();
List<Product> products =
session.createQuery(
"select p from Product p order by p.id", Product.class )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) )
.getResultList();
assertTrue( products.size() > 1 );
assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() );
session.getTransaction().commit();
session.close();
}
@Test
public void testPessimisticLockWithMaxResultsAndOrderByThenNoFollowOnLocking() {
final Session session = openSession();
session.beginTransaction();
@ -245,43 +317,31 @@ public class OracleFollowOnLockingTest extends
.getResultList();
assertEquals( 10, products.size() );
assertEquals( 11, sqlStatementInterceptor.getSqlQueries().size() );
assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() );
session.getTransaction().commit();
session.close();
}
@Test
public void testPessimisticLockWithMaxResultsAndOrderByWhileExplicitlyDisablingFollowOnLockingThenFails() {
public void testPessimisticLockWithMaxResultsAndOrderByWhileExplicitlyDisablingFollowOnLocking() {
final Session session = openSession();
session.beginTransaction();
sqlStatementInterceptor.getSqlQueries().clear();
try {
List<Product> products =
session.createQuery(
"select p from Product p order by p.id",
Product.class
)
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE )
.setFollowOnLocking( false ) )
.setMaxResults( 10 )
.getResultList();
fail( "Should throw exception since Oracle does not support ORDER BY if follow on locking is disabled" );
}
catch ( IllegalStateException expected ) {
assertEquals(
IllegalQueryOperationException.class,
expected.getCause().getClass()
);
assertTrue(
expected.getCause().getMessage().contains(
"Locking with ORDER BY is not supported"
)
);
}
List<Product> products =
session.createQuery(
"select p from Product p order by p.id",
Product.class
)
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE )
.setFollowOnLocking( false ) )
.setMaxResults( 10 )
.getResultList();
assertEquals( 10, products.size() );
assertEquals( 1, sqlStatementInterceptor.getSqlQueries().size() );
}
@Test
@ -397,7 +457,7 @@ public class OracleFollowOnLockingTest extends
session.createQuery(
"select count(p), p " +
"from Product p " +
"group by p.id, p.name " )
"group by p.id, p.name, p.vehicle.id " )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ) )
.getResultList();
@ -421,7 +481,7 @@ public class OracleFollowOnLockingTest extends
session.createQuery(
"select count(p), p " +
"from Product p " +
"group by p.id, p.name " )
"group by p.id, p.name, p.vehicle.id " )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE )
.setFollowOnLocking( false ) )
.getResultList();
@ -452,7 +512,7 @@ public class OracleFollowOnLockingTest extends
session.createQuery(
"select count(p), p " +
"from Product p " +
"group by p.id, p.name " )
"group by p.id, p.name, p.vehicle.id " )
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE )
.setFollowOnLocking( true ) )
.getResultList();
@ -525,6 +585,9 @@ public class OracleFollowOnLockingTest extends
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Vehicle vehicle;
}
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)

View File

@ -132,8 +132,7 @@ public class OraclePaginationWithLocksTest {
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ).setFollowOnLocking( false ) )
.getResultList();
assertEquals( 10, people.size() );
assertFalse( mostRecentStatementInspector.sqlContains( "fetch" ) );
assertTrue( mostRecentStatementInspector.sqlContains( "rownum" ) );
assertSqlContainsFetch( session );
}
);
@ -146,7 +145,7 @@ public class OraclePaginationWithLocksTest {
.setMaxResults( 10 )
.getResultList();
assertEquals( 10, people.size() );
assertTrue( mostRecentStatementInspector.sqlContains( "rownum" ) );
assertSqlContainsFetch( session );
}
);
@ -176,8 +175,7 @@ public class OraclePaginationWithLocksTest {
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ).setFollowOnLocking( false ) )
.getResultList();
assertEquals( 10, people.size() );
assertFalse( mostRecentStatementInspector.sqlContains( "fetch" ) );
assertTrue( mostRecentStatementInspector.sqlContains( "rownum" ) );
assertSqlContainsFetch( session );
}
);
@ -188,7 +186,7 @@ public class OraclePaginationWithLocksTest {
.setMaxResults( 10 )
.getResultList();
assertEquals( 10, people.size() );
assertTrue( mostRecentStatementInspector.sqlContains( "rownum" ) );
assertSqlContainsFetch( session );
}
);
@ -213,8 +211,7 @@ public class OraclePaginationWithLocksTest {
.setLockOptions( new LockOptions( LockMode.PESSIMISTIC_WRITE ).setFollowOnLocking( false ) )
.getResultList();
assertEquals( 1, people.size() );
assertFalse( mostRecentStatementInspector.sqlContains( "fetch" ) );
assertTrue( mostRecentStatementInspector.sqlContains( "rownum" ) );
assertSqlContainsFetch( session );
}
);
@ -225,7 +222,7 @@ public class OraclePaginationWithLocksTest {
.setMaxResults( 10 )
.getResultList();
assertEquals( 1, people.size() );
assertTrue( mostRecentStatementInspector.sqlContains( "rownum" ) );
assertSqlContainsFetch( session );
}
);