Bug 436871 - Fixed NPE in checkForCustomQuery (LRG shall pass now) Signed-off-by: Tomas Kraus <tomas.kraus@oracle.com> Reviewed-by: Lukas Jungmann <lukas.jungmann@oracle.com>, Martin Grebac <martin.grebac@oracle.com>
diff --git a/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/queries/ObjectLevelReadQueryTest.java b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/queries/ObjectLevelReadQueryTest.java new file mode 100644 index 0000000..833fbca --- /dev/null +++ b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/queries/ObjectLevelReadQueryTest.java
@@ -0,0 +1,356 @@ +/******************************************************************************* + * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 + * which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html + * and the Eclipse Distribution License is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * Contributors: + * 04/20/2015-2.7 Tomas Kraus - Concurrency test to trigger NPE in + * checkForCustomQuery(AbstractSession, AbstractRecord). + ******************************************************************************/ +package org.eclipse.persistence.testing.tests.queries; + +import static org.eclipse.persistence.logging.SessionLog.INFO; +import static org.eclipse.persistence.logging.SessionLog.WARNING; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.eclipse.persistence.internal.sessions.AbstractRecord; +import org.eclipse.persistence.internal.sessions.AbstractSession; +import org.eclipse.persistence.logging.SessionLog; +import org.eclipse.persistence.queries.DatabaseQuery; +import org.eclipse.persistence.queries.ObjectLevelReadQuery; +import org.eclipse.persistence.queries.ReadAllQuery; +import org.eclipse.persistence.queries.ReadObjectQuery; +import org.eclipse.persistence.queries.SQLCall; +import org.eclipse.persistence.sessions.Session; +import org.eclipse.persistence.testing.framework.TestCase; +import org.eclipse.persistence.testing.models.employee.domain.Employee; + +/** + * Test read queries using objects. + */ +public abstract class ObjectLevelReadQueryTest extends TestCase { + + /** Fail message for NPE in checkForCustomQuery method. */ + private static final String FAIL_NPE + = "NullPointerException thrown in checkForCustomQuery(AbstractSession, AbstractRecord)"; + + /** Logger used in test. */ + protected SessionLog log; + + /** Current database session. */ + protected Session session; + + /** Some entity to be used in query. */ + protected final Class entity = Employee.class; + + /** Some SQL call to be used in query. */ + protected final SQLCall call = new SQLCall("SELECT t0.EMP_ID FROM EMPLOYEE t0"); + + /** + * Creates an instance of jUnit tests for read queries using objects. + * @param name jUnit test name. + */ + public ObjectLevelReadQueryTest() { + super(); + } + + /** + * Creates an instance of jUnit tests for read queries using objects. + * @param name jUnit test name. + */ + public ObjectLevelReadQueryTest(final String name) { + super(); + setName(name); + } + + /** + * Initialize this test suite. + */ + public void setup() { + session = getSession(); + log = session.getSessionLog(); + } + + /** + * Clean this test suite. + */ + public void reset() { + session = null; + log = null; + } + + /** + * {@link TestCase#test()} method overwritten as public to avoid failures. + */ + @Override + public abstract void test() throws Throwable; + + /** + * {@code [ObjectLevelReadQuery].isCustomQueryUsed} value changer + * for {@link #testCheckForCustomQueryRaceConditions()}. + */ + private static final class ValueChanger extends Thread { + + /** Logger used in test. */ + private final SessionLog log; + + /** Query where to change {@code isCustomQueryUsed} value. */ + private final ObjectLevelReadQuery query; + + /** Instance used for synchronization. */ + private final Object mutex; + + /** [ObjectLevelReadQuery].isCustomQueryUsed} field. */ + private final Field isCustomQueryUsed; + + /** Thread execution flag. */ + private boolean execute; + + /** + * Creates an instance of value changer thread. + * @param objectLevelReadQuery Query object where {@code isCustomQueryUsed} value will be changed. + * @param syncMutex Instance used for synchronization. + * @param sessionLog Logger used in test. + */ + private ValueChanger(final ObjectLevelReadQuery objectLevelReadQuery, final Object syncMutex, + final SessionLog sessionLog) { + log = sessionLog; + // Calling run() directly will do nothing. + query = objectLevelReadQuery; + mutex = syncMutex; + execute = false; + // Placeholder for initialization. + Field isCustomQueryUsedLocal; + try { + isCustomQueryUsedLocal = DatabaseQuery.class.getDeclaredField("isCustomQueryUsed"); + } catch (SecurityException e) { + log.logThrowable(WARNING, e); + isCustomQueryUsedLocal = null; + } catch (NoSuchFieldException e) { + log.logThrowable(WARNING, e); + isCustomQueryUsedLocal = null; + } + if (isCustomQueryUsedLocal != null) { + try { + isCustomQueryUsedLocal.setAccessible(true); + } catch (SecurityException e) { + log.logThrowable(WARNING, e); + isCustomQueryUsedLocal = null; + } + } + isCustomQueryUsed = isCustomQueryUsedLocal; + } + + /** + * Check if value changer initialization was successful. + * @return Value of {@code true} when {@code isCustomQueryUsed} field is available or {@code false} otherwise. + */ + private boolean isOk() { + return isCustomQueryUsed != null; + } + + /** + * Start thread execution. + */ + public void start() { + execute = true; + super.start(); + } + + /** + * Request thread to finish. + */ + public void finish() { + execute = false; + } + + /** + * Thread code to be executed. + */ + @Override + public void run() { + synchronized(mutex) { + mutex.notify(); + } + while(execute) { + try { + isCustomQueryUsed.set(query, Boolean.TRUE); + isCustomQueryUsed.set(query, (Boolean)null); + } catch (IllegalArgumentException e) { + log.logThrowable(WARNING, e); + execute = false; + } catch (IllegalAccessException e) { + log.logThrowable(WARNING, e); + execute = false; + } + } + } + } + + /** + * Get {@code [ObjectLevelReadQuery].checkForCustomQuery(AbstractSession, AbstractRecord)} method accessor. + * @param c Class where to search for method. + * @return {@code [ObjectLevelReadQuery].checkForCustomQuery(AbstractSession, AbstractRecord)} method accessor. + */ + protected Method getCheckForCustomQueryMethod(final Class c) { + Method method; + try { + method = c.getDeclaredMethod( + "checkForCustomQuery", AbstractSession.class, AbstractRecord.class); + method.setAccessible(true); + } catch (NoSuchMethodException e) { + log.logThrowable(WARNING, e); + method = null; + } catch (SecurityException e) { + log.logThrowable(WARNING, e); + method = null; + } + return method; + } + + /** + * Call {@code [ObjectLevelReadQuery].checkForCustomQuery(AbstractSession, AbstractRecord)} method. + * @param instance ObjectLevelReadQuery instance on which method is called. + * @param mathod {@code [ObjectLevelReadQuery].checkForCustomQuery(AbstractSession, AbstractRecord)} + * method accessor. + * @throws Throwable when any exception except NPE in invoked method occurs. + */ + protected DatabaseQuery callCheckForCustomQueryMethod(final Object instance, final Method method) throws Throwable { + DatabaseQuery databaseQuery; + try { + databaseQuery = (DatabaseQuery)method.invoke(instance, session, (AbstractRecord)null); + } catch (InvocationTargetException e) { + if (e.getTargetException() instanceof NullPointerException) { + log.log(WARNING, FAIL_NPE); + e.printStackTrace(); + fail(FAIL_NPE); + } + log.logThrowable(WARNING, e); + throw e; + } catch (Throwable e) { + log.logThrowable(WARNING, e); + e.printStackTrace(); + throw e; + } + return databaseQuery; + } + + /** + * jUnit test to verify NPE in {@code checkForCustomQuery} method in {@link ReadAllQuery} class. + */ + public static final class CustomQueryRaceConditionsInReadAllQueryTest extends ObjectLevelReadQueryTest { + + /** + * Creates an instance of jUnit test. + */ + public CustomQueryRaceConditionsInReadAllQueryTest() { + super(); + } + + /** + * Creates an instance of jUnit test. + * @param name jUnit test name. + */ + public CustomQueryRaceConditionsInReadAllQueryTest(final String name) { + super(name); + } + + /** + * Test {@code [ReadAllQuery].checkForCustomQuery(AbstractSession, AbstractRecord)} + * when {@code isCustomQueryUsed} is changed to null during query execution. + * Reproduction scenario for bug# 436871 which works with high enough probability. + * @throws Throwable when any exception except NPE in checkForCustomQuery occurs. + */ + @Override + public void test() throws Throwable { + log.log(INFO, "Running checkCustomQueryRaceConditionsInReadAllQuery test."); + final Method checkForCustomQuery = getCheckForCustomQueryMethod(ReadAllQuery.class); + final ReadAllQuery query = new ReadAllQuery(entity, call); + final Object mutex = new Object(); + final ValueChanger changer = new ValueChanger(query, mutex, log); + assertTrue("Initialization of isCustomQueryUsed property value changer failed.", changer.isOk()); + try { + synchronized (mutex) { + changer.start(); + mutex.wait(); + } + } catch (InterruptedException e) { + log.logThrowable(WARNING, e); + } + query.setReferenceClass(entity); + query.setDescriptor(session.getDescriptor(entity)); + // 100 iterations is enough to get exception in almost 100% of tests. + for (int i = 0; i < 100; i++) + callCheckForCustomQueryMethod(query, checkForCustomQuery); + changer.finish(); + try { + changer.join(); + } catch (InterruptedException e) { + log.logThrowable(WARNING, e); + } + } + } + + /** + * jUnit test to verify NPE in {@code checkForCustomQuery} method in {@link ReadObjectQuery} class. + */ + public static final class CustomQueryRaceConditionsInReadObjectQueryTest extends ObjectLevelReadQueryTest { + + /** + * Creates an instance of jUnit test. + */ + public CustomQueryRaceConditionsInReadObjectQueryTest() { + super(); + } + + /** + * Creates an instance of jUnit test. + * @param name jUnit test name. + */ + public CustomQueryRaceConditionsInReadObjectQueryTest(final String name) { + super(name); + } + + /** + * Test {@code [ReadObjectQuery].checkForCustomQuery(AbstractSession, AbstractRecord)} + * when {@code isCustomQueryUsed} is changed to null during query execution. + * Reproduction scenario for bug# 436871 which works with high enough probability. + * @throws Throwable when any exception except NPE in checkForCustomQuery occurs. + */ + @Override + public void test() throws Throwable { + log.log(INFO, "Running checkCustomQueryRaceConditionsInReadObjectQuery test."); + final Method checkForCustomQuery = getCheckForCustomQueryMethod(ReadObjectQuery.class); + final ReadObjectQuery query = new ReadObjectQuery(entity, call); + final Object mutex = new Object(); + final ValueChanger changer = new ValueChanger(query, mutex, log); + assertTrue("Initialization of isCustomQueryUsed property value changer failed.", changer.isOk()); + try { + synchronized (mutex) { + changer.start(); + mutex.wait(); + } + } catch (InterruptedException e) { + log.logThrowable(WARNING, e); + } + query.setReferenceClass(entity); + query.setDescriptor(session.getDescriptor(entity)); + // 100 iterations is enough to get exception in almost 100% of tests. + for (int i = 0; i < 100; i++) + callCheckForCustomQueryMethod(query, checkForCustomQuery); + changer.finish(); + try { + changer.join(); + } catch (InterruptedException e) { + log.logThrowable(WARNING, e); + } + } + } +}
diff --git a/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/queries/QueryFrameworkTestSuite.java b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/queries/QueryFrameworkTestSuite.java index eb86097..510657d 100644 --- a/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/queries/QueryFrameworkTestSuite.java +++ b/foundation/eclipselink.core.test/src/org/eclipse/persistence/testing/tests/queries/QueryFrameworkTestSuite.java
@@ -1,30 +1,52 @@ -/******************************************************************************* - * Copyright (c) 1998, 2013 Oracle and/or its affiliates. All rights reserved. - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 - * which accompanies this distribution. +/******************************************************************************* + * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 + * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink - ******************************************************************************/ -package org.eclipse.persistence.testing.tests.queries; - -import java.util.*; -import org.eclipse.persistence.testing.models.employee.domain.*; -import org.eclipse.persistence.testing.models.employee.domain.Project; -import org.eclipse.persistence.sessions.*; -import org.eclipse.persistence.expressions.*; -import org.eclipse.persistence.internal.sessions.*; -import org.eclipse.persistence.queries.*; -import org.eclipse.persistence.sessions.Record; -import org.eclipse.persistence.testing.framework.*; -import org.eclipse.persistence.testing.tests.clientserver.*; -import org.eclipse.persistence.tools.schemaframework.PopulationManager; - -public class QueryFrameworkTestSuite extends TestSuite { + ******************************************************************************/ +package org.eclipse.persistence.testing.tests.queries; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Vector; + +import org.eclipse.persistence.expressions.ExpressionBuilder; +import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; +import org.eclipse.persistence.queries.ComplexQueryResult; +import org.eclipse.persistence.queries.Cursor; +import org.eclipse.persistence.queries.DatabaseQuery; +import org.eclipse.persistence.queries.MethodBaseQueryRedirector; +import org.eclipse.persistence.queries.ObjectLevelReadQuery; +import org.eclipse.persistence.queries.QueryRedirector; +import org.eclipse.persistence.queries.ReadAllQuery; +import org.eclipse.persistence.queries.ReadObjectQuery; +import org.eclipse.persistence.queries.ReportQuery; +import org.eclipse.persistence.queries.ScrollableCursorPolicy; +import org.eclipse.persistence.sessions.DatabaseRecord; +import org.eclipse.persistence.sessions.Record; +import org.eclipse.persistence.sessions.UnitOfWork; +import org.eclipse.persistence.testing.framework.TestCase; +import org.eclipse.persistence.testing.framework.TestSuite; +import org.eclipse.persistence.testing.models.employee.domain.Employee; +import org.eclipse.persistence.testing.models.employee.domain.EmployeePopulator; +import org.eclipse.persistence.testing.models.employee.domain.LargeProject; +import org.eclipse.persistence.testing.models.employee.domain.Project; +import org.eclipse.persistence.testing.tests.clientserver.ServerSessionTestAdapter; +import org.eclipse.persistence.tools.schemaframework.PopulationManager; + +public class QueryFrameworkTestSuite extends TestSuite { public QueryFrameworkTestSuite() { setDescription("This suite tests all of the functionality of the query framework."); } @@ -34,18 +56,20 @@ setDescription("This suite tests all of the functionality of the query framework."); } - public void addTests() { - addSRGTests(); - addTest(new QueryTimeoutTest()); - //Add new tests here, if any. - addTest(new ServerSessionTestAdapter(new PessimisticLockNoLockJoinedTest())); - addTest(new ReadAllNoDistinctTest()); - addTest(new PartialAttributeTestWithJoinAttribute()); - addTest(new PartialAttributeDistinctOrderByTest()); - addTest(new FourPartialAttributeTestsWithJoinAttribute()); - addTest(buildReadOnlyQueryTest()); - addTest(buildGetSQLTest()); - addTest(buildJoinSubclassesQueryTest()); + public void addTests() { + addSRGTests(); + addTest(new QueryTimeoutTest()); + //Add new tests here, if any. + addTest(new ServerSessionTestAdapter(new PessimisticLockNoLockJoinedTest())); + addTest(new ReadAllNoDistinctTest()); + addTest(new ObjectLevelReadQueryTest.CustomQueryRaceConditionsInReadAllQueryTest()); + addTest(new ObjectLevelReadQueryTest.CustomQueryRaceConditionsInReadObjectQueryTest()); + addTest(new PartialAttributeTestWithJoinAttribute()); + addTest(new PartialAttributeDistinctOrderByTest()); + addTest(new FourPartialAttributeTestsWithJoinAttribute()); + addTest(buildReadOnlyQueryTest()); + addTest(buildGetSQLTest()); + addTest(buildJoinSubclassesQueryTest()); addTest(buildRecordTest()); // Bug 6450867 - TOPLINK-6069 ON READOBJECTQUERY ON ENTITY USING MULTIPLE TABLE MAPPING addTest(new ConformResultsWithMultitableAndJoiningTest());
diff --git a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadAllQuery.java b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadAllQuery.java index 79ed566..733bb4e 100644 --- a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadAllQuery.java +++ b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadAllQuery.java
@@ -204,32 +204,36 @@ * Check to see if a custom query should be used for this query. * This is done before the query is copied and prepared/executed. * null means there is none. - */ - @Override - protected DatabaseQuery checkForCustomQuery(AbstractSession session, AbstractRecord translationRow) { - checkDescriptor(session); - - // Check if user defined a custom query. - if (isCustomQueryUsed() == null) { - setIsCustomQueryUsed((!isUserDefined()) && isExpressionQuery() && (getSelectionCriteria() == null) && isDefaultPropertiesQuery() && (!hasOrderByExpressions()) && (this.descriptor.getQueryManager().hasReadAllQuery())); - } - if (isCustomQueryUsed().booleanValue()) { - ReadAllQuery customQuery = this.descriptor.getQueryManager().getReadAllQuery(); - if (this.accessors != null) { - customQuery = (ReadAllQuery) customQuery.clone(); - customQuery.setIsExecutionClone(true); - customQuery.setAccessors(this.accessors); - } - return customQuery; - } else { - return null; - } - } - - /** - * INTERNAL: - * Clone the query. - */ + */ + @Override + protected DatabaseQuery checkForCustomQuery(AbstractSession session, AbstractRecord translationRow) { + Boolean useCustomQuery = isCustomQueryUsed; + + checkDescriptor(session); + // Check if user defined a custom query. + if (useCustomQuery == null) { + setIsCustomQueryUsed((!isUserDefined()) && isExpressionQuery() && (getSelectionCriteria() == null) && isDefaultPropertiesQuery() && (!hasOrderByExpressions()) && (this.descriptor.getQueryManager().hasReadAllQuery())); + // Value of isCustomQueryUsed is updated by setIsCustomQueryUsed method. + useCustomQuery = isCustomQueryUsed; + } + if (useCustomQuery != null && useCustomQuery.booleanValue()) { + ReadAllQuery customQuery = this.descriptor.getQueryManager().getReadAllQuery(); + if (this.accessors != null) { + customQuery = (ReadAllQuery) customQuery.clone(); + customQuery.setIsExecutionClone(true); + customQuery.setAccessors(this.accessors); + } + isCustomQueryUsed = useCustomQuery; + return customQuery; + } + isCustomQueryUsed = useCustomQuery; + return null; + } + + /** + * INTERNAL: + * Clone the query. + */ @Override public Object clone() { ReadAllQuery cloneQuery = (ReadAllQuery)super.clone();
diff --git a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadObjectQuery.java b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadObjectQuery.java index fed5d30..0abfb37 100644 --- a/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadObjectQuery.java +++ b/foundation/org.eclipse.persistence.core/src/org/eclipse/persistence/queries/ReadObjectQuery.java
@@ -1,8 +1,8 @@ -/******************************************************************************* - * Copyright (c) 1998, 2013 Oracle and/or its affiliates. All rights reserved. - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 - * which accompanies this distribution. +/******************************************************************************* + * Copyright (c) 1998, 2016 Oracle and/or its affiliates. All rights reserved. + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 + * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. @@ -30,13 +30,12 @@ import org.eclipse.persistence.internal.sessions.AbstractSession; import org.eclipse.persistence.internal.sessions.ResultSetRecord; import org.eclipse.persistence.internal.sessions.SimpleResultSetRecord; -import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; -import org.eclipse.persistence.internal.helper.*; -import org.eclipse.persistence.descriptors.ClassDescriptor; -import org.eclipse.persistence.descriptors.DescriptorQueryManager; -import org.eclipse.persistence.exceptions.*; -import org.eclipse.persistence.expressions.*; -import org.eclipse.persistence.sessions.DatabaseRecord; +import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; +import org.eclipse.persistence.internal.helper.*; +import org.eclipse.persistence.descriptors.ClassDescriptor; +import org.eclipse.persistence.exceptions.*; +import org.eclipse.persistence.expressions.*; +import org.eclipse.persistence.sessions.DatabaseRecord; import org.eclipse.persistence.sessions.SessionProfiler; import org.eclipse.persistence.sessions.remote.*; import org.eclipse.persistence.tools.profiler.QueryMonitor; @@ -295,62 +294,58 @@ * INTERNAL: * Check to see if a custom query should be used for this query. * This is done before the query is copied and prepared/executed. - * null means there is none. - */ - protected DatabaseQuery checkForCustomQuery(AbstractSession session, AbstractRecord translationRow) { - if (this.descriptor == null) { - checkDescriptor(session); - } - - if (this.isCustomQueryUsed == null) { - // Check if user defined a custom query in the query manager. - if (!this.isUserDefined) { - if (!isCallQuery()) { - DescriptorQueryManager descriptorQueryManager = this.descriptor.getQueryManager(); - - // By default all descriptors have a custom ("static") read-object query. - // This allows the read-object query and SQL to be prepare once. - if (descriptorQueryManager.hasReadObjectQuery()) { - // If the query require special SQL generation or execution do not use the static read object query. - // PERF: the read-object query should always be static to ensure no regeneration of SQL. - if ((!hasJoining() || !this.joinedAttributeManager.hasJoinedAttributeExpressions()) && (!hasPartialAttributeExpressions()) && (redirector == null) && !doNotRedirect && (!hasAsOfClause()) && (!hasNonDefaultFetchGroup()) - && (this.shouldUseSerializedObjectPolicy == shouldUseSerializedObjectPolicyDefault) - && this.wasDefaultLockMode && (shouldBindAllParameters == null) && (this.hintString == null)) { - if ((this.selectionId != null) || (this.selectionObject != null)) {// Must be primary key. - this.isCustomQueryUsed = true; - } else { - Expression selectionCriteria = getSelectionCriteria(); - if (selectionCriteria != null) { - AbstractRecord primaryKeyRow = this.descriptor.getObjectBuilder().extractPrimaryKeyRowFromExpression(selectionCriteria, translationRow, session); - // Only execute the query if the selection criteria has the primary key fields set - if (primaryKeyRow != null) { - this.isCustomQueryUsed = true; - } - } - } - } - } - } - } - if (this.isCustomQueryUsed == null) { - this.isCustomQueryUsed = false; - } - } - - if (this.isCustomQueryUsed.booleanValue()) { - ReadObjectQuery customQuery = this.descriptor.getQueryManager().getReadObjectQuery(); - if (this.accessors != null) { - customQuery = (ReadObjectQuery) customQuery.clone(); - customQuery.setIsExecutionClone(true); - customQuery.setAccessors(this.accessors); - } - return customQuery; - } else { - return null; - } - } - - /** + * null means there is none. + */ + protected DatabaseQuery checkForCustomQuery(AbstractSession session, AbstractRecord translationRow) { + Boolean useCustomQuery = isCustomQueryUsed; + + checkDescriptor(session); + if (useCustomQuery == null) { + // Check if user defined a custom query in the query manager. + if (!this.isUserDefined) { + if (!isCallQuery() + // By default all descriptors have a custom ("static") read-object query. + // This allows the read-object query and SQL to be prepare once. + && this.descriptor.getQueryManager().hasReadObjectQuery()) { + // If the query require special SQL generation or execution do not use the static read object query. + // PERF: the read-object query should always be static to ensure no regeneration of SQL. + if ((!hasJoining() || !this.joinedAttributeManager.hasJoinedAttributeExpressions()) && (!hasPartialAttributeExpressions()) + && (redirector == null) && !doNotRedirect && (!hasAsOfClause()) && (!hasNonDefaultFetchGroup()) + && (this.shouldUseSerializedObjectPolicy == shouldUseSerializedObjectPolicyDefault) + && this.wasDefaultLockMode && (shouldBindAllParameters == null) && (this.hintString == null)) { + if ((this.selectionId != null) || (this.selectionObject != null)) {// Must be primary key. + useCustomQuery = Boolean.TRUE; + } else { + Expression selectionCriteria = getSelectionCriteria(); + if (selectionCriteria != null) { + AbstractRecord primaryKeyRow = this.descriptor.getObjectBuilder().extractPrimaryKeyRowFromExpression(selectionCriteria, translationRow, session); + // Only execute the query if the selection criteria has the primary key fields set + if (primaryKeyRow != null) { + useCustomQuery = Boolean.TRUE; + } + } + } + } + } + } + } + + //#436871 - attempt to limit cases for race-condition + if (useCustomQuery != null && useCustomQuery.booleanValue()) { + ReadObjectQuery customQuery = this.descriptor.getQueryManager().getReadObjectQuery(); + if (this.accessors != null) { + customQuery = (ReadObjectQuery) customQuery.clone(); + customQuery.setIsExecutionClone(true); + customQuery.setAccessors(this.accessors); + } + isCustomQueryUsed = useCustomQuery; + return customQuery; + } + isCustomQueryUsed = useCustomQuery; + return null; + } + + /** * INTERNAL: * Conform the result in the UnitOfWork. */