From 49bbeb84f52ab9f44071e87cd214ed2afed8fd20 Mon Sep 17 00:00:00 2001 From: Richard Zowalla Date: Tue, 13 Oct 2026 09:37:06 +0200 Subject: [PATCH] [OPENJPA-2950] Flush before a query only for the types it reads Since OPENJPA-2940 a query with FlushModeType.AUTO flushed as soon as anything in the persistence context was dirty, and overrode openjpa.IgnoreChanges whenever the flush mode was AUTO - which is the default. Both are reverted, and the gap the broadening was meant to close is fixed where it actually is. Why the access path is enough. Section 3.10.8 of the specification scopes the duty to the updates "which could potentially affect the result of the query", and lets the provider discharge it "by flushing those entities to the database or by some other means". A full-context flush is therefore permitted but not required, and the access path check is exactly such other means: only a dirty instance of a type the query reads can change its result. That holds, however, only for as long as the access path is complete, and in two places it was not. JPQL. AbstractExpressionBuilder.traversePath() registered the metadata of every hop, so Order, Customer and Spouse are all in the access path of "SELECT o FROM Order o WHERE o.customer.spouse.lastName = 'Miller'" - the case the comment added with the broadening claimed was missed. What it did miss is a collection- or map-valued path used without a join variable: the declared type of such a field is Collection or Map, which has no metadata, so SIZE(x.coll) and x.coll IS EMPTY left the element and key types out and a dirty element was not flushed. traversePath() now registers them, which also makes the JPQL and the Criteria side agree - the latter registers them for the same field. Criteria. CriteriaExpressionBuilder.evalAccessPaths() looked only at the roots and their immediate joins, so a type reached by an implicit join (a path navigated with Path.get), by a join of a join, by a fetch join, inside a subquery or behind a join made on a correlated join was left out - the subquery case was a standing "TODO -- need to handle subqueries". That is now fixed at the source: PathImpl registers each field it traverses on the query, SubqueryImpl hands its access path to the enclosing query, the join walk follows the whole join tree and starts at the correlated joins of the enclosing subquery as well, and CriteriaExpressionBuilder merges all of it into QueryExpressions.accessPath after the expressions have been converted. IgnoreChanges. openjpa.IgnoreChanges=true is a documented option whose purpose is to let a query run straight against the datastore, recommended in ref_guide_optimization.xml together with the default openjpa.FlushBeforeQueries=true. Overriding it whenever the flush mode is FLUSH_TRUE therefore silently removed it for every default configuration. It is restored unconditionally, as in 4.1.x, and no distinction is drawn between a flush mode set on the query and one inherited from the persistence context: section 3.10.8 states the same duty for both triggers, so there is no basis for honouring the option for one and not the other. The option is an OpenJPA extension that opts a SELECT out of that duty, which ref_guide_conf.xml now says where users enable it. Making the override conditional on the flush mode would also have required state the facade does not have. Writing through to the kernel's ignoreChanges instead, as this was first implemented, is not sound: the write is one-way, so setFlushMode(AUTO) followed by setFlushMode(COMMIT) on the same query left ignoreChanges false with flush FLUSH_FALSE, and QueryImpl.isInMemory() then entered the branch, could not flush and executed the query in memory over the whole extent - a different result set and an unbounded scan for the same query text and flush mode. It also fired for queries the application never set a flush mode on, because EntityManagerFactoryImpl.addNamedQuery() captures a flush mode that is never unset and createNamedQuery() replays it as setFlushMode(AUTO). The prohibitions of section 3.10.8 hold. _broker.isActive() already covers "if there is no transaction active, the persistence provider must not flush"; an unjoined context cannot reach the flush at all, because EntityManagerFactoryImpl rejects SynchronizationType.UNSYNCHRONIZED. No compatibility option is added: the broadened behaviour only ever existed on the unreleased 4.2.0 line, so there is nothing to be compatible with. The migration note is rewritten to describe what actually changes for an application upgrading from 4.1.x. Tests, asserting on emitted SQL rather than on results, because a full-context flush returns the same results: - TestQueryFlushAccessPath: a dirty instance of a type outside the access path must not flush; the candidate type, a dirty subclass of it, a type reached by JPQL navigation, a collection path in SIZE() and in IS EMPTY, and - for the Criteria API - an implicit join, a join of a join, a fetch join, a subquery and a join made on a correlated join must all be visible; COMMIT must not flush; no flush without an active transaction. - TestQueryFlushIgnoreChanges: IgnoreChanges=true keeps suppressing the flush for an inherited flush mode, for AUTO set on the query, across a setFlushMode(AUTO)-then-COMMIT sequence and through an addNamedQuery() / createNamedQuery() round trip. Each hunk is discriminated by reverting it alone: the kernel access-path hunk by the two collection-path tests, the correlated-join hunk by the correlated-join test, openjpa-persistence/.../criteria/ by the five Criteria tests, and the QueryImpl hunk by three of the four IgnoreChanges tests, each on an assertNotSQL for an UPDATE that should not have been emitted. Reverting the whole diff at once does not discriminate, and deliberately so: seven of the eighteen tests pass against master as well, because the blanket flush there masks the incomplete access paths - everything is flushed anyway. Those seven are regression guards (dirty candidate type, dirty subclass, JPQL navigation, subquery type, COMMIT, no active transaction, AUTO-then-COMMIT), and the two navigation ones also back up the finding that traversePath already covered o.customer.spouse. --- .../org/apache/openjpa/kernel/QueryImpl.java | 29 +- .../exps/AbstractExpressionBuilder.java | 26 ++ .../flush/TestQueryFlushAccessPath.java | 411 ++++++++++++++++++ .../flush/TestQueryFlushIgnoreChanges.java | 160 +++++++ .../flush/UnenhancedFlushExpressOrder.java | 48 ++ .../criteria/CriteriaExpressionBuilder.java | 73 +++- .../criteria/CriteriaQueryImpl.java | 55 +++ .../persistence/criteria/PathImpl.java | 21 +- .../persistence/criteria/SubqueryImpl.java | 2 + .../doc/manual/migration_considerations.xml | 48 +- .../src/doc/manual/ref_guide_conf.xml | 8 + 11 files changed, 825 insertions(+), 56 deletions(-) create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushAccessPath.java create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushIgnoreChanges.java create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/UnenhancedFlushExpressOrder.java diff --git a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/QueryImpl.java b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/QueryImpl.java index 463a90bc09..961b463c33 100644 --- a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/QueryImpl.java +++ b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/QueryImpl.java @@ -1007,6 +1007,15 @@ public long updateAll(Map params) { /** * Return whether we should execute this query in memory. + *

+ * Section 3.10.8 of the specification scopes the duty of a + * FlushModeType.AUTO query to the updates "which could potentially + * affect the result of the query", and lets the provider discharge it "by flushing + * those entities to the database or by some other means". The access path check + * below is that other means: only an instance of a type the query reads can change + * its result, so a query is never made to flush a transaction's unrelated writes. + * The same section forbids flushing when no transaction is active, which + * {@link Broker#isActive()} covers. */ private boolean isInMemory(int operation) { // if there are any dirty instances in the current trans that are @@ -1014,24 +1023,8 @@ private boolean isInMemory(int operation) { boolean inMem = !_storeQuery.supportsDataStoreExecution() || _collection != null; int flush = _fc.getFlushBeforeQueries(); - - // Per JPA spec, FlushModeType.AUTO (FLUSH_TRUE) requires that - // pending changes are visible to queries. The access path check - // may miss dirty entities reachable via navigation (e.g., - // o.customer.spouse), so when FLUSH_TRUE is set we check for - // ANY dirty objects rather than only those in the access path. - boolean dirty; - if (flush == FLUSH_TRUE && _broker.isActive()) { - dirty = !_broker.getPersistedTypes().isEmpty() - || !_broker.getUpdatedTypes().isEmpty() - || !_broker.getDeletedTypes().isEmpty(); - } else { - dirty = isAccessPathDirty(); - } - - if (!inMem && (!_ignoreChanges || operation != OP_SELECT - || flush == FLUSH_TRUE) - && _broker.isActive() && dirty) { + if (!inMem && (!_ignoreChanges || operation != OP_SELECT) + && _broker.isActive() && isAccessPathDirty()) { if ((flush == FLUSH_TRUE || (flush == FLUSH_WITH_CONNECTION && _broker.hasConnection()) || operation != OP_SELECT diff --git a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/AbstractExpressionBuilder.java b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/AbstractExpressionBuilder.java index a659b04c7a..4a34648d42 100644 --- a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/AbstractExpressionBuilder.java +++ b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/exps/AbstractExpressionBuilder.java @@ -127,6 +127,26 @@ protected ClassMetaData addAccessPath(ClassMetaData meta) { return meta; } + /** + * Register the element and key types of the given field in the query's access path. + * A collection or map field declares Collection/List/Map as its type, which has no + * metadata of its own, so the types such a field reads are its element and key + * types. Registering them keeps the access path complete for a predicate that uses + * the field without declaring a join variable - SIZE(x.coll), x.coll IS EMPTY - and + * mirrors what the Criteria builder records for the same field. + */ + protected void addElementAndKeyAccessPath(FieldMetaData fmd) { + if (fmd == null) { + return; + } + if (fmd.getElement() != null && fmd.getElement().getDeclaredTypeMetaData() != null) { + addAccessPath(fmd.getElement().getDeclaredTypeMetaData()); + } + if (fmd.getKey() != null && fmd.getKey().getDeclaredTypeMetaData() != null) { + addAccessPath(fmd.getKey().getDeclaredTypeMetaData()); + } + } + /** * Return the recorded query access path. */ @@ -310,6 +330,12 @@ protected Value traversePath(Path path, String field, boolean pcOnly, // ex2b). Resolve via the @MapsId @ManyToOne target entity. meta = resolveMapsIdTargetMeta(fmd); } + // A collection or map field has no declared type metadata of its own - its + // declared type is Collection/List/Map - so the types it actually reads are its + // element and key types. They have to be registered for predicates that use the + // field without declaring a join variable, such as SIZE(x.coll) or + // x.coll IS EMPTY; without them a dirty element would not be flushed. + addElementAndKeyAccessPath(fmd); if (meta != null) { addAccessPath(meta); path.setMetaData(meta); diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushAccessPath.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushAccessPath.java new file mode 100644 index 0000000000..263ba2a990 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushAccessPath.java @@ -0,0 +1,411 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openjpa.persistence.flush; + +import java.util.List; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; + +import org.apache.openjpa.persistence.test.SQLListenerTestCase; + +/** + * Asserts which dirty instances make a FlushModeType.AUTO query flush. + *

+ * Section 3.10.8 of the specification scopes the provider's duty to updates + * "which could potentially affect the result of the query", and lets the provider + * discharge it "by flushing those entities to the database or by some other means". + * OpenJPA's "other means" is the query access path: only a dirty instance of a type + * the query actually reads can affect its result, so only such an instance forces a + * flush. The assertions below are on emitted SQL, not on results, because a + * full-context flush returns the same results while costing an extra round trip. + */ +public class TestQueryFlushAccessPath extends SQLListenerTestCase { + + private static final String ANY_UPDATE = "(?i).*UPDATE\\s+FLUSH_.*"; + private static final String UPDATE_SPOUSE = "(?i).*UPDATE\\s+FLUSH_SPOUSE.*"; + private static final String UPDATE_CUSTOMER = "(?i).*UPDATE\\s+FLUSH_CUSTOMER.*"; + private static final String UPDATE_ORDER = "(?i).*UPDATE\\s+FLUSH_ORDER.*"; + private static final String INSERT_ORDER = "(?i).*INSERT\\s+INTO\\s+FLUSH_ORDER.*"; + private static final String DELETE_ORDER = "(?i).*DELETE\\s+FROM\\s+FLUSH_ORDER.*"; + + @Override + public void setUp() { + setUp(UnenhancedFlushSpouse.class, + UnenhancedFlushCustomer.class, + UnenhancedFlushOrder.class, + UnenhancedFlushExpressOrder.class, + "openjpa.RuntimeUnenhancedClasses", "supported", + DROP_TABLES); + + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + UnenhancedFlushCustomer cust = new UnenhancedFlushCustomer("10", "Kate P. Hudson"); + em.persist(cust); + UnenhancedFlushSpouse spouse = new UnenhancedFlushSpouse("4", "Thomas", "Mullen"); + spouse.setCustomer(cust); + em.persist(spouse); + cust.setSpouse(spouse); + em.persist(new UnenhancedFlushOrder("11", cust)); + UnenhancedFlushExpressOrder express = new UnenhancedFlushExpressOrder("12", cust); + express.setCarrier("DHL"); + em.persist(express); + em.getTransaction().commit(); + } + resetSQL(); + } + + /** + * A dirty instance of a type outside the query's access path cannot affect the + * query result, so it must not force a flush. + */ + public void testDirtyTypeOutsideAccessPathDoesNotFlush() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushSpouse.class, "4").setLastName("Miller"); + resetSQL(); + + List orders = em.createQuery( + "SELECT o FROM UnenhancedFlushOrder o WHERE o.totalPrice = 0", + UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertEquals(2, orders.size()); + assertNotSQL(ANY_UPDATE); + em.getTransaction().rollback(); + } + } + + /** + * A dirty instance of the queried type itself must be visible to the query. + */ + public void testDirtyCandidateTypeFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushOrder.class, "11").setTotalPrice(17d); + resetSQL(); + + List orders = em.createQuery( + "SELECT o FROM UnenhancedFlushOrder o WHERE o.totalPrice = 17", + UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_ORDER); + assertEquals(1, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * A dirty instance of a subclass of a type in the access path must be visible: + * the query reads the subclass rows too. + */ + public void testDirtySubclassOfCandidateTypeFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushExpressOrder.class, "12").setTotalPrice(23d); + resetSQL(); + + List orders = em.createQuery( + "SELECT o FROM UnenhancedFlushOrder o WHERE o.totalPrice = 23", + UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_ORDER); + assertEquals(1, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * A dirty instance of a type reached only by JPQL navigation is in the access + * path, so it must be visible to the query. + */ + public void testDirtyNavigatedTypeFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushSpouse.class, "4").setLastName("Miller"); + resetSQL(); + + List orders = em.createQuery( + "SELECT o FROM UnenhancedFlushOrder o " + + "WHERE o.customer.spouse.lastName = 'Miller'", + UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_SPOUSE); + assertEquals(2, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * The same navigation expressed with the Criteria API: an implicit join through + * {@code get()} must contribute to the access path exactly as the JPQL path does. + */ + public void testDirtyNavigatedTypeFlushesForCriteriaQuery() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushSpouse.class, "4").setLastName("Miller"); + resetSQL(); + + CriteriaBuilder cb = emf.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(UnenhancedFlushOrder.class); + Root order = cq.from(UnenhancedFlushOrder.class); + cq.where(cb.equal(order.get("customer").get("spouse").get("lastName"), "Miller")); + List orders = em.createQuery(cq) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_SPOUSE); + assertEquals(2, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * A dirty instance of a type that appears only in a subquery is still read by the + * query, so it must be visible. + */ + public void testDirtyTypeInSubqueryFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + List orders = em.createQuery( + "SELECT o FROM UnenhancedFlushOrder o WHERE o.customer.id IN " + + "(SELECT c.id FROM UnenhancedFlushCustomer c " + + "WHERE c.name = 'Michael Bouschen')", + UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_CUSTOMER); + assertEquals(2, orders.size()); + em.getTransaction().rollback(); + } + } + + + /** + * A collection-valued path used without a join variable - SIZE() - + * reads the element type, so a newly persisted element must be visible. The + * declared type of the field is Collection, which has no metadata of its own, so + * only the element type registered for the field puts Order into the access path. + */ + public void testDirtyElementOfCollectionPathInSizeFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + UnenhancedFlushCustomer cust = em.find(UnenhancedFlushCustomer.class, "10"); + em.persist(new UnenhancedFlushOrder("13", cust)); + resetSQL(); + + List customers = em.createQuery( + "SELECT c FROM UnenhancedFlushCustomer c WHERE SIZE(c.orders) = 3", + UnenhancedFlushCustomer.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(INSERT_ORDER); + assertEquals(1, customers.size()); + em.getTransaction().rollback(); + } + } + + /** + * The same for IS EMPTY: a removed element changes the result, so the + * delete has to be flushed first. + */ + public void testDirtyElementOfCollectionPathInIsEmptyFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.remove(em.find(UnenhancedFlushOrder.class, "11")); + em.remove(em.find(UnenhancedFlushExpressOrder.class, "12")); + resetSQL(); + + List customers = em.createQuery( + "SELECT c FROM UnenhancedFlushCustomer c WHERE c.orders IS EMPTY", + UnenhancedFlushCustomer.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(DELETE_ORDER); + assertEquals(1, customers.size()); + em.getTransaction().rollback(); + } + } + + /** + * A join of a join reads its type just as a join of the root does, so the join walk + * has to follow the whole join tree. + */ + public void testDirtyTypeBehindJoinOfJoinFlushesForCriteriaQuery() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushSpouse.class, "4").setLastName("Miller"); + resetSQL(); + + CriteriaBuilder cb = emf.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(UnenhancedFlushOrder.class); + Root order = cq.from(UnenhancedFlushOrder.class); + Join spouse = order.join("customer").join("spouse"); + cq.where(cb.equal(spouse.get("lastName"), "Miller")); + List orders = em.createQuery(cq) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_SPOUSE); + assertEquals(2, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * A fetch join reads the fetched type, so it belongs in the access path too. + */ + public void testDirtyTypeBehindFetchJoinFlushesForCriteriaQuery() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + CriteriaBuilder cb = emf.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(UnenhancedFlushOrder.class); + Root order = cq.from(UnenhancedFlushOrder.class); + order.fetch("customer"); + cq.where(cb.equal(order.get("totalPrice"), 0d)); + List orders = em.createQuery(cq) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_CUSTOMER); + assertEquals(2, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * A type read only inside a Criteria subquery must be visible: without it the + * IN-subquery misses the unflushed name and the query returns nothing. + */ + public void testDirtyTypeInCriteriaSubqueryFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + CriteriaBuilder cb = emf.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(UnenhancedFlushOrder.class); + Root order = cq.from(UnenhancedFlushOrder.class); + Subquery sq = cq.subquery(String.class); + Root cust = sq.from(UnenhancedFlushCustomer.class); + sq.select(cust.get("id")) + .where(cb.equal(cust.get("name"), "Michael Bouschen")); + cq.where(cb.in(order.get("customer").get("id")).value(sq)); + List orders = em.createQuery(cq) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_CUSTOMER); + assertEquals(2, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * A join made on a correlated join is reachable neither from the outer + * query's roots nor from the subquery's own roots, so the join walk has to start at + * the correlated joins of the enclosing subquery as well. + */ + public void testDirtyTypeBehindCorrelatedJoinFlushes() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushSpouse.class, "4").setLastName("Miller"); + resetSQL(); + + CriteriaBuilder cb = emf.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(UnenhancedFlushOrder.class); + Root order = cq.from(UnenhancedFlushOrder.class); + Join cust = order.join("customer"); + Subquery sq = cq.subquery(String.class); + Join spouse = sq.correlate(cust).join("spouse"); + sq.select(spouse.get("id")) + .where(cb.equal(spouse.get("lastName"), "Miller")); + cq.where(cb.exists(sq)); + List orders = em.createQuery(cq) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertSQL(UPDATE_SPOUSE); + assertEquals(2, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * With {@code FlushModeType.COMMIT} the effect of pending updates on queries is + * unspecified, and the provider must not flush on the query's behalf. + */ + public void testCommitFlushModeDoesNotFlush() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + em.createQuery("SELECT o FROM UnenhancedFlushOrder o " + + "WHERE o.customer.name = 'Michael Bouschen'", + UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.COMMIT) + .getResultList(); + + assertNotSQL(ANY_UPDATE); + em.getTransaction().rollback(); + } + } + + /** + * "If there is no transaction active, the persistence provider must not flush to + * the database" (section 3.10.8). + */ + public void testNoFlushWithoutActiveTransaction() { + try (EntityManager em = emf.createEntityManager()) { + UnenhancedFlushCustomer cust = em.find(UnenhancedFlushCustomer.class, "10"); + cust.setName("Michael Bouschen"); + resetSQL(); + + em.createQuery("SELECT o FROM UnenhancedFlushOrder o " + + "WHERE o.customer.name = 'Michael Bouschen'", + UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertNotSQL(ANY_UPDATE); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushIgnoreChanges.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushIgnoreChanges.java new file mode 100644 index 0000000000..f254bc83f4 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/TestQueryFlushIgnoreChanges.java @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openjpa.persistence.flush; + +import java.util.List; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.FlushModeType; + +import org.apache.openjpa.persistence.test.SQLListenerTestCase; + +/** + * Asserts how openjpa.IgnoreChanges=true interacts with the flush mode. + *

+ * openjpa.IgnoreChanges is a documented OpenJPA option that lets a query + * run straight against the datastore without considering in-memory changes; the + * reference guide recommends it together with the default + * openjpa.FlushBeforeQueries=true as a performance setting. It therefore + * has to keep working whether the flush mode is the inherited default or + * FlushModeType.AUTO set on the query object: section 3.10.8 states the + * same duty for both triggers, and the option is a vendor opt-out from it, not a + * distinction the specification draws. Enabling it is therefore a deliberate, documented + * departure from the visibility guarantee for SELECTs, and nothing about the flush mode + * re-enables it. + */ +public class TestQueryFlushIgnoreChanges extends SQLListenerTestCase { + + private static final String ANY_UPDATE = "(?i).*UPDATE\\s+FLUSH_.*"; + private static final String UPDATE_CUSTOMER = "(?i).*UPDATE\\s+FLUSH_CUSTOMER.*"; + + private static final String JPQL = + "SELECT o FROM UnenhancedFlushOrder o WHERE o.customer.name = 'Michael Bouschen'"; + + @Override + public void setUp() { + setUp(UnenhancedFlushSpouse.class, + UnenhancedFlushCustomer.class, + UnenhancedFlushOrder.class, + UnenhancedFlushExpressOrder.class, + "openjpa.RuntimeUnenhancedClasses", "supported", + "openjpa.IgnoreChanges", "true", + DROP_TABLES); + + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + UnenhancedFlushCustomer cust = new UnenhancedFlushCustomer("10", "Kate P. Hudson"); + em.persist(cust); + em.persist(new UnenhancedFlushOrder("11", cust)); + em.getTransaction().commit(); + } + resetSQL(); + } + + /** + * With the flush mode inherited from the persistence context, IgnoreChanges keeps + * its documented effect: the query runs against the datastore and nothing is flushed. + */ + public void testIgnoreChangesSuppressesFlushForInheritedFlushMode() { + try (EntityManager em = emf.createEntityManager()) { + assertEquals(FlushModeType.AUTO, em.getFlushMode()); + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + List orders = + em.createQuery(JPQL, UnenhancedFlushOrder.class).getResultList(); + + assertNotSQL(ANY_UPDATE); + assertEquals(0, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * A flush mode of AUTO set on the query object is the mode the query already has, + * and IgnoreChanges opts out of the visibility guarantee for either trigger, so the + * query still runs against the datastore. + */ + public void testExplicitAutoOnQueryKeepsIgnoreChanges() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + List orders = + em.createQuery(JPQL, UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .getResultList(); + + assertNotSQL(ANY_UPDATE); + assertEquals(0, orders.size()); + em.getTransaction().rollback(); + } + } + + /** + * Setting a flush mode must not leave the query in a different state than it was + * created in: AUTO followed by COMMIT has to behave exactly like COMMIT alone, and + * in particular the query must still be executed against the datastore rather than + * in memory over the whole extent. + */ + public void testAutoThenCommitBehavesLikeCommitAlone() { + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + List orders = + em.createQuery(JPQL, UnenhancedFlushOrder.class) + .setFlushMode(FlushModeType.AUTO) + .setFlushMode(FlushModeType.COMMIT) + .getResultList(); + + assertNotSQL(ANY_UPDATE); + assertEquals(0, orders.size()); + // the filter has to be pushed down, not evaluated in memory over the extent + assertSQL("(?i).*SELECT.*FROM\\s+FLUSH_ORDER.*WHERE.*"); + em.getTransaction().rollback(); + } + } + + /** + * A query obtained through {@code createNamedQuery()} for a registration made with + * {@code addNamedQuery()} must not lose the option either: the registration replays + * the source query's flush mode, which is never unset. + */ + public void testIgnoreChangesSurvivesNamedQueryRoundTrip() { + emf.addNamedQuery("TestQueryFlushIgnoreChanges.byName", + emf.createEntityManager().createQuery(JPQL, UnenhancedFlushOrder.class)); + try (EntityManager em = emf.createEntityManager()) { + em.getTransaction().begin(); + em.find(UnenhancedFlushCustomer.class, "10").setName("Michael Bouschen"); + resetSQL(); + + List orders = em.createNamedQuery( + "TestQueryFlushIgnoreChanges.byName", UnenhancedFlushOrder.class) + .getResultList(); + + assertNotSQL(ANY_UPDATE); + assertEquals(0, orders.size()); + em.getTransaction().rollback(); + } + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/UnenhancedFlushExpressOrder.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/UnenhancedFlushExpressOrder.java new file mode 100644 index 0000000000..4b13473680 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/flush/UnenhancedFlushExpressOrder.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.openjpa.persistence.flush; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; + +/** + * Subclass of {@link UnenhancedFlushOrder}, used to assert that a dirty instance of a + * subclass of a type in the query's access path forces a flush. + */ +@Entity +public class UnenhancedFlushExpressOrder extends UnenhancedFlushOrder { + + private String carrier; + + public UnenhancedFlushExpressOrder() { + } + + public UnenhancedFlushExpressOrder(String id, UnenhancedFlushCustomer customer) { + super(id, customer); + } + + @Column(name = "CARRIER") + public String getCarrier() { + return carrier; + } + + public void setCarrier(String carrier) { + this.carrier = carrier; + } +} diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaExpressionBuilder.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaExpressionBuilder.java index 18bb8a9def..5dbb7f2144 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaExpressionBuilder.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaExpressionBuilder.java @@ -20,6 +20,7 @@ package org.apache.openjpa.persistence.criteria; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -31,6 +32,7 @@ import jakarta.persistence.criteria.CompoundSelection; import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.Fetch; +import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Nulls; @@ -38,8 +40,6 @@ import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Selection; import jakarta.persistence.metamodel.Attribute; -import jakarta.persistence.metamodel.PluralAttribute; -import jakarta.persistence.metamodel.Type.PersistenceType; import org.apache.openjpa.kernel.FillStrategy; import org.apache.openjpa.kernel.QueryOperations; @@ -49,6 +49,7 @@ import org.apache.openjpa.kernel.exps.QueryExpressions; import org.apache.openjpa.kernel.exps.Value; import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.meta.FieldMetaData; import org.apache.openjpa.persistence.TupleFactory; import org.apache.openjpa.persistence.TupleImpl; import org.apache.openjpa.persistence.meta.AbstractManagedType; @@ -85,32 +86,68 @@ public QueryExpressions getQueryExpressions(ExpressionFactory factory, CriteriaQ exps.shape = evalResultShape(q); exps.parameterTypes = q.getParameterTypes(); exps.isAggregate(); + evalNavigatedAccessPaths(exps, q); return exps; } protected void evalAccessPaths(QueryExpressions exps, ExpressionFactory factory, CriteriaQueryImpl q) { Set metas = new HashSet<>(); - MetamodelImpl metamodel = q.getMetamodel(); for (Root root : q.getRoots()) { metas.add(((AbstractManagedType)root.getModel()).meta); - for (Join join : root.getJoins()) { - Attribute attr = join.getAttribute(); - Class cls = attr.getJavaType(); - // For plural attributes (Collection/List/Set/Map), use the - // element/value type for metadata lookup, not the collection type - if (attr instanceof PluralAttribute) { - cls = ((PluralAttribute)attr).getElementType().getJavaType(); - } - if (attr.isAssociation()) { - ClassMetaData meta = metamodel.getRepository().getMetaData(cls, null, true); - PersistenceType type = MetamodelImpl.getPersistenceType(meta); - if (type == PersistenceType.ENTITY || type == PersistenceType.EMBEDDABLE) - metas.add(meta); - } + registerJoinedTypes(q, root); + } + // A subquery's correlated joins are not reachable from its roots: correlate(Join) + // clones the parent join into the subquery without adding a root, and the joins + // the subquery then makes are made on that clone. Walk them from the delegator. + SubqueryImpl delegator = q.getDelegator(); + if (delegator != null) { + for (Join corr : delegator.getCorrelatedJoins()) { + q.addAccessPathMetaData(getFieldMetaData(corr.getAttribute())); + registerJoinedTypes(q, corr); } } - // TODO -- need to handle subqueries + // Types reached only by an implicit join (a navigated path) or by a subquery are + // added by evalNavigatedAccessPaths() once the expressions have been converted. + + exps.accessPath = metas.toArray(new ClassMetaData[metas.size()]); + } + /** + * Registers the types of the given from-element's joins and fetch joins, and of their + * own joins. A join of a join reads its type just as a join of the root does, so the + * recursion has to follow the whole join tree. + */ + private void registerJoinedTypes(CriteriaQueryImpl q, From from) { + for (Join join : from.getJoins()) { + q.addAccessPathMetaData(getFieldMetaData(join.getAttribute())); + registerJoinedTypes(q, join); + } + for (Fetch fetch : from.getFetches()) { + q.addAccessPathMetaData(getFieldMetaData(fetch.getAttribute())); + if (fetch instanceof From) { + registerJoinedTypes(q, (From) fetch); + } + } + } + + private FieldMetaData getFieldMetaData(Attribute attr) { + return attr instanceof Members.Member ? ((Members.Member) attr).fmd : null; + } + + /** + * Adds the types registered on the query while its expressions were converted to + * kernel values: the targets of implicit joins created by + * {@link jakarta.persistence.criteria.Path#get(String) get()} and the access paths of + * the query's subqueries. Both are only known after the conversion, so this runs last. + */ + protected void evalNavigatedAccessPaths(QueryExpressions exps, CriteriaQueryImpl q) { + Set navigated = q.getNavigatedAccessPathMetaDatas(); + if (navigated == null || navigated.isEmpty()) + return; + Set metas = new HashSet<>(navigated); + if (exps.accessPath != null) { + metas.addAll(Arrays.asList(exps.accessPath)); + } exps.accessPath = metas.toArray(new ClassMetaData[metas.size()]); } diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaQueryImpl.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaQueryImpl.java index 7fe757286a..68b06ee041 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaQueryImpl.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/CriteriaQueryImpl.java @@ -51,6 +51,8 @@ import org.apache.openjpa.kernel.exps.Value; import org.apache.openjpa.lib.util.Localizer; import org.apache.openjpa.lib.util.OrderedMap; +import org.apache.openjpa.meta.ClassMetaData; +import org.apache.openjpa.meta.FieldMetaData; import org.apache.openjpa.persistence.meta.MetamodelImpl; import org.apache.openjpa.persistence.meta.Types; @@ -87,6 +89,7 @@ class CriteriaQueryImpl implements OpenJPACriteriaQuery, AliasContext { private final TranslationLock _translationLock; private Set> _treatedRoots; + private Set _navigatedAccessPath; // AliasContext private int aliasCount = 0; @@ -499,6 +502,58 @@ Set> getTreatedRoots() { return _treatedRoots; } + /** + * Registers a type this query reads through a path traversal or a subquery. + * The roots and their joins are known statically, but a path navigated with + * {@link jakarta.persistence.criteria.Path#get(String) get()} creates an implicit + * join that is only discovered while the path is converted to a kernel value, and a + * subquery compiles its own access path. Both are collected here so that + * {@link CriteriaExpressionBuilder} can add them to + * {@link QueryExpressions#accessPath}, which decides whether a dirty instance can + * affect this query's result. + */ + void addAccessPathMetaData(ClassMetaData meta) { + if (meta == null) + return; + if (_navigatedAccessPath == null) { + _navigatedAccessPath = new LinkedHashSet<>(); + } + _navigatedAccessPath.add(meta); + } + + /** + * Registers the types the given field reads: the field's own type, and for a + * collection or map field its element and key types. + */ + void addAccessPathMetaData(FieldMetaData fmd) { + if (fmd == null) + return; + addAccessPathMetaData(fmd.isEmbedded() + ? fmd.getEmbeddedMetaData() : fmd.getDeclaredTypeMetaData()); + if (fmd.getElement() != null) + addAccessPathMetaData(fmd.getElement().getDeclaredTypeMetaData()); + if (fmd.getKey() != null) + addAccessPathMetaData(fmd.getKey().getDeclaredTypeMetaData()); + } + + /** + * Registers every type of the given access path on this query. + */ + void addAccessPathMetaDatas(ClassMetaData[] metas) { + if (metas == null) + return; + for (ClassMetaData meta : metas) { + addAccessPathMetaData(meta); + } + } + + /** + * Returns the types registered by {@link #addAccessPathMetaData}, or null if none. + */ + Set getNavigatedAccessPathMetaDatas() { + return _navigatedAccessPath; + } + /** * Affirms if selection of this query is distinct. */ diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/PathImpl.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/PathImpl.java index 08961feda2..7f71d22253 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/PathImpl.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/PathImpl.java @@ -161,6 +161,7 @@ public boolean isCorrelated() { public Value toValue(ExpressionFactory factory, CriteriaQueryImpl q) { if (q.isRegistered(this)) return q.getRegisteredValue(this); + recordAccessPath(q, _member == null ? null : _member.fmd); org.apache.openjpa.kernel.exps.Path path = null; SubqueryImpl subquery = q.getDelegator(); boolean allowNull = _parent != null && _parent instanceof Join @@ -180,7 +181,7 @@ public Value toValue(ExpressionFactory factory, CriteriaQueryImpl q) { path = factory.newPath(subQ); path.setMetaData(subQ.getMetaData()); path.setSchemaAlias(q.getAlias(_parent)); - traversePath(_parent, path, _member.fmd); + traversePath(q, _parent, path, _member.fmd); } else if (_parent != null) { Value val = _parent.toValue(factory, q); if (val instanceof org.apache.openjpa.kernel.exps.Path) { @@ -226,7 +227,9 @@ public boolean inSubquery(SubqueryImpl subquery) { return subquery != null && (subquery.getRoots() != null && subquery.getRoots().contains(this)); } - protected void traversePath(PathImpl parent, org.apache.openjpa.kernel.exps.Path path, FieldMetaData fmd) { + protected void traversePath(CriteriaQueryImpl q, PathImpl parent, + org.apache.openjpa.kernel.exps.Path path, FieldMetaData fmd) { + recordAccessPath(q, fmd); boolean allowNull = parent != null && parent instanceof Join && ((Join) parent).getJoinType() != JoinType.INNER; FieldMetaData fmd1 = parent._member == null ? null : parent._member.fmd; @@ -236,11 +239,23 @@ protected void traversePath(PathImpl parent, org.apache.openjpa.kernel.exp path.get(fmd, allowNull); return; } - traversePath(parent1, path, fmd1); + traversePath(q, parent1, path, fmd1); if (fmd != null) path.get(fmd, allowNull); } + /** + * Registers the type this path traverses into on the query's access path. + * A path navigated with {@link #get(String) get()} is an implicit join, and the + * types it reads must be part of the query's access path for the same reason the + * types of an explicit join are: a dirty instance of such a type can change the + * query's result and therefore has to be flushed before the query runs. + */ + private void recordAccessPath(CriteriaQueryImpl q, FieldMetaData fmd) { + if (q != null) + q.addAccessPathMetaData(fmd); + } + /** * Gets a new path that represents the given single-valued attribute from this path. */ diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/SubqueryImpl.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/SubqueryImpl.java index 5e2787397b..2fae0f46e8 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/SubqueryImpl.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/criteria/SubqueryImpl.java @@ -332,6 +332,8 @@ public Value toValue(ExpressionFactory factory, CriteriaQueryImpl q) { _delegate.setContexts(contexts); QueryExpressions subexp = exprBuilder.getQueryExpressions(factory, _delegate); _subq.setQueryExpressions(subexp); + // the enclosing query reads everything the subquery reads + q.addAccessPathMetaDatas(subexp.accessPath); if (subexp.projections.length > 0) JPQLExpressionBuilder.checkEmbeddable(subexp.projections[0], null); contexts.pop(); diff --git a/openjpa-project/src/doc/manual/migration_considerations.xml b/openjpa-project/src/doc/manual/migration_considerations.xml index 4b21c1234c..e33d42da3e 100644 --- a/openjpa-project/src/doc/manual/migration_considerations.xml +++ b/openjpa-project/src/doc/manual/migration_considerations.xml @@ -329,23 +329,37 @@

- FlushModeType.AUTO flushes whenever the context is dirty - - Previously a query only flushed pending changes when a dirty instance belonged to - a type in the query's access path, and never for SELECT queries - when was true. With - FlushModeType.AUTO (the default, - =true) and an active - transaction, a query now flushes whenever any instance in the persistence context - is new, dirty or deleted, regardless of the query's access path, and overrides - openjpa.IgnoreChanges. Applications with write-heavy transactions - may observe more flushes (and therefore earlier constraint or trigger evaluation). - - - To restore the previous behavior use FlushModeType.COMMIT on - the query or the EntityManager, or set - openjpa.FlushBeforeQueries to false or - with-connection. + Completed access paths for Criteria and collection-valued queries + + As before, a query with FlushModeType.AUTO (the default, + =true) flushes + pending changes only when a new, dirty or deleted instance belongs to a type in + the query's access path, that is to a type the query actually reads. This is what + section 3.10.8 of the specification asks for: the duty covers the updates "which + could potentially affect the result of the query", and the provider may discharge + it "by flushing those entities to the database or by some other means". + + + What changed is that the access paths themselves are now complete, so queries + that failed to flush a relevant change do so. For a Criteria query a type + reached through an implicit join (a path navigated with + Path.get), through a join of a join, through a fetch + join, inside a subquery or behind a join of a correlated join used to be left + out. For JPQL, a collection- or map-valued path used without a join variable - + SIZE(x.coll), x.coll IS EMPTY - did not + contribute its element and key types, because the declared type of such a field + is Collection or Map. A dirty + instance of any of those types was not flushed before the query ran, which + could yield a stale result; such a query now flushes where it did not before. + + + =true is unchanged + and keeps its documented effect for every SELECT, whatever + the flush mode of the query is. It is an OpenJPA extension that deliberately + opts a SELECT out of the visibility guarantee of section + 3.10.8; the specification states that guarantee equally for a flush mode set on + the query object and for one inherited from the persistence context, so the + option is not overridden by either.
diff --git a/openjpa-project/src/doc/manual/ref_guide_conf.xml b/openjpa-project/src/doc/manual/ref_guide_conf.xml index b3259d0f87..3e0373acd9 100644 --- a/openjpa-project/src/doc/manual/ref_guide_conf.xml +++ b/openjpa-project/src/doc/manual/ref_guide_conf.xml @@ -2091,6 +2091,14 @@ execute the query directly against the datastore. A value of false transaction affect the query, and if so to either evaluate the query in-memory or flush before running it against the datastore. + +Note that true is an OpenJPA extension that deliberately opts +every SELECT out of the guarantee of section 3.10.8 of the +specification that pending updates which could affect a query's result are visible +to it. The opt-out applies whatever the flush mode of the query is, including +FlushModeType.AUTO set on the Query +object itself. +
openjpa.Id