/* * 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.datacache; import javax.persistence.EntityManager; import javax.persistence.LockModeType; import org.apache.openjpa.persistence.EntityManagerImpl; import org.apache.openjpa.persistence.EntityNotFoundException; import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; import org.apache.openjpa.persistence.OpenJPAEntityManagerSPI; import org.apache.openjpa.persistence.StoreCache; import org.apache.openjpa.persistence.StoreCacheImpl; import org.apache.openjpa.persistence.querycache.common.apps.BidirectionalOne2OneOwned; import org.apache.openjpa.persistence.querycache.common.apps.BidirectionalOne2OneOwner; import org.apache.openjpa.persistence.common.utils.AbstractTestCase; import org.apache.openjpa.persistence.datacache.common.apps.PObject; /** * Tests various application behavior with or without DataCache. * Ideally, an application should behave identically irrespective of the * DataCache. However, purpose of this test is to identify specific scenarios * where this ideal is violated. The test case also demonstrates, wherever * possible, what extra step an application may take to ensure that its * behavior with or without DataCache remains identical. * * So far following use cases are found to demonstrate behavioral differences: * 1. Inconsistent bidirectional relation * 2. Refresh * * @author Pinaki Poddar * */ public class TestDataCacheBehavesIdentical extends AbstractTestCase { private static OpenJPAEntityManagerFactorySPI emfWithDataCache; private static OpenJPAEntityManagerFactorySPI emfWithoutDataCache; private static final boolean WITH_DATACACHE = true; private static final boolean CONSISTENT = true; private static final boolean DIRTY = true; private static final boolean REFRESH_FROM_DATACACHE = true; private static final LockModeType NOLOCK = null; private static final Class<?> ENTITY_NOT_FOUND_ERROR = EntityNotFoundException.class; private static final Class<?> NO_ERROR = null; private static final String MARKER_DATACACHE = "in DataCache"; private static final String MARKER_DATABASE = "in Database"; private static final String MARKER_CACHE = "in Object Cache"; private static final String MARKER_DIRTY_CACHE = "in Object Cache (dirty)"; private static long ID_COUNTER = System.currentTimeMillis(); private static int TEST_COUNT = 0; /** * Sets up two EntityManagerFactory: one with DataCache another without. */ public void setUp() throws Exception { super.setUp(); if (emfWithDataCache == null) { emfWithDataCache = createEMF( "openjpa.jdbc.SynchronizeMappings", "buildSchema", "openjpa.RuntimeUnenhancedClasses", "unsupported", "openjpa.DataCache", "true", "openjpa.jdbc.UpdateManager", "constraint", PObject.class, BidirectionalOne2OneOwner.class, BidirectionalOne2OneOwned.class, CLEAR_TABLES); emfWithoutDataCache = createEMF( "openjpa.RuntimeUnenhancedClasses", "unsupported", "openjpa.DataCache", "false", "openjpa.jdbc.UpdateManager", "constraint", PObject.class, BidirectionalOne2OneOwned.class, BidirectionalOne2OneOwner.class, CLEAR_TABLES); assertNotNull(emfWithDataCache); assertNotNull(emfWithoutDataCache); // StoreCache is, by design, always non-null assertNotNull(emfWithDataCache.getStoreCache()); assertNotNull(emfWithoutDataCache.getStoreCache()); // however, following distinguishes whether DataCache is active assertTrue(isDataCacheActive(emfWithDataCache)); assertFalse(isDataCacheActive(emfWithoutDataCache)); } TEST_COUNT++; } public void tearDown() throws Exception { // HACK - need to manually close EMFs after all tests have run if (TEST_COUNT >= 21) { closeEMF(emfWithDataCache); emfWithDataCache = null; closeEMF(emfWithoutDataCache); emfWithoutDataCache = null; super.tearDown(); } } /** * Affirms via internal structures if the given factory is configured with * active DataCache. Because, even when DataCache is configured to be * false, a no-op StoreCache is instantiated by design. */ boolean isDataCacheActive(OpenJPAEntityManagerFactorySPI emf) { return ((StoreCacheImpl) emf.getStoreCache()).getDelegate() != null && emf.getConfiguration() .getDataCacheManagerInstance() .getSystemDataCache() != null; } /** * Create one-to-one bidirectional relation (may or may not be consistent) * between two pairs of instances. Creates four instances Owner1, Owned1, * Owner2, Owned2. The first instance has the given id. The id of the other * instances monotonically increase by 1. The relationship is set either * consistently or inconsistently. Consistent relation is when Owner1 points * to Owned1 and Owned1 points back to Owner1. Inconsistent relation is when * Owner1 points to Owned1 but Owned1 points to Owner2 instead of Owner1. * * * @param em * the entity manager to persist the instances * @param id * the identifier of the first owner instance. The identifier for * the other instances are sequential in order of creation. * @param consistent * if true sets the relationship as consistent. */ public void createBidirectionalRelation(EntityManager em, long id, boolean consistent) { BidirectionalOne2OneOwner owner1 = new BidirectionalOne2OneOwner(); BidirectionalOne2OneOwned owned1 = new BidirectionalOne2OneOwned(); BidirectionalOne2OneOwner owner2 = new BidirectionalOne2OneOwner(); BidirectionalOne2OneOwned owned2 = new BidirectionalOne2OneOwned(); owner1.setId(id++); owned1.setId(id++); owner2.setId(id++); owned2.setId(id++); owner1.setName("Owner1"); owned1.setName("Owned1"); owned2.setName("Owned2"); owner2.setName("Owner2"); owner1.setOwned(owned1); owner2.setOwned(owned2); if (consistent) { owned1.setOwner(owner1); owned2.setOwner(owner2); } else { owned1.setOwner(owner2); owned2.setOwner(owner1); } em.getTransaction().begin(); em.persist(owner1); em.persist(owned1); em.persist(owner2); em.persist(owned2); em.getTransaction().commit(); em.clear(); } /** * Verifies that bidirectionally related objects can be persisted * and later retrieved in a different transaction. * * Creates interrelated set of four instances. * Establish their relation either consistently or inconsistently based * on the given flag. * Persist them and then clear the context. * Fetch the instances in memory again by their identifiers. * Compare the interrelations between the fetched instances with the * relations of the original instances (which can be consistent or * inconsistent). * * The mapping specification is such that the bidirectional relation is * stored in database by a single foreign key. Hence database relation * is always consistent. Hence the instances retrieved from database are * always consistently related irrespective of whether they were created * with consistent or inconsistent relation. * However, when the instances are retrieved from the data cache, data cache * will preserve the in-memory relations even when they are inconsistent. * * @param useDataCache * use DataCache * @param consistent * assume that the relationship were created as consistent. */ public void verifyBidirectionalRelation(boolean useDataCache, boolean createConsistent, boolean expectConsistent) { EntityManager em = (useDataCache) ? emfWithDataCache.createEntityManager() : emfWithoutDataCache.createEntityManager(); long id = ID_COUNTER++; ID_COUNTER += 4; createBidirectionalRelation(em, id, createConsistent); BidirectionalOne2OneOwner owner1 = em.find(BidirectionalOne2OneOwner.class, id); BidirectionalOne2OneOwned owned1 = em.find(BidirectionalOne2OneOwned.class, id + 1); BidirectionalOne2OneOwner owner2 = em.find(BidirectionalOne2OneOwner.class, id + 2); BidirectionalOne2OneOwned owned2 = em.find(BidirectionalOne2OneOwned.class, id + 3); assertNotNull(owner1); assertNotNull(owner2); assertNotNull(owned1); assertNotNull(owned2); assertEquals(owner1, expectConsistent ? owner1.getOwned().getOwner() : owner2.getOwned().getOwner()); assertEquals(owner2, expectConsistent ? owner2.getOwned().getOwner() : owner1.getOwned().getOwner()); assertEquals(owned1, owner1.getOwned()); assertEquals(expectConsistent ? owner1 : owner2, owned1.getOwner()); assertEquals(owned2, owner2.getOwned()); assertEquals(expectConsistent ? owner2 : owner1, owned2.getOwner()); } public void testConsitentBidirectionalRelationIsPreservedWithDataCache() { verifyBidirectionalRelation(WITH_DATACACHE, CONSISTENT, CONSISTENT); } public void testConsitentBidirectionalRelationIsPreservedWithoutDataCache() { verifyBidirectionalRelation(!WITH_DATACACHE, CONSISTENT, CONSISTENT); } public void testInconsitentBidirectionalRelationIsPreservedWithDataCache() { verifyBidirectionalRelation(WITH_DATACACHE, !CONSISTENT, !CONSISTENT); } public void testInconsitentBidirectionalRelationIsNotPreservedWithoutDataCache() { verifyBidirectionalRelation(!WITH_DATACACHE, !CONSISTENT, CONSISTENT); } /** * Verify that refresh() may fetch state from either the data cache or the * database based on different conditions. * The conditions that impact are * a) whether current lock is stronger than NONE * b) whether the instance being refreshed is dirty * * An instance is created with data cache marker and persisted. * A native SQL is used to update the database record with database marker. * The in-memory instance is not aware of this out-of-band update. * Then the in-memory instance is refreshed. The marker of the refreshed * instance tells whether the instance is refreshed from the data cache * of the database. * * @param useDataCache flags if data cache is active. if not, then surely * refresh always fetch state from the database. * * @param datacache the marker for the copy of the data cached instance * @param database the marker for the database record * @param lock lock to be used * @param makeDirtyBeforeRefresh flags if the instance be dirtied before * refresh() * @param expected The expected marker i.e. where the state is refreshed * from. This should be always <code>MARKER_DATABASE</code>. * a) whether DataCache is active * b) whether current Lock is stronger than NOLOCK * c) whether the object to be refreshed is dirty * * The following truth table enumerates the possibilities * * Use Cache? Lock? Dirty? Target * Y Y Y Database * Y N Y Data Cache * Y Y N Data Cache * Y N N Data Cache * * N Y Y Database * N N Y Database * N Y N Object Cache * N N N Object Cache */ public void verifyRefresh(boolean useDataCache, LockModeType lock, boolean makeDirtyBeforeRefresh, boolean refreshFromDataCache, String expected) { OpenJPAEntityManagerFactorySPI emf = (useDataCache) ? emfWithDataCache : emfWithoutDataCache; emf.getConfiguration().setRefreshFromDataCache(refreshFromDataCache); OpenJPAEntityManagerSPI em = emf.createEntityManager(); em.getTransaction().begin(); PObject pc = new PObject(); pc.setName(useDataCache ? MARKER_DATACACHE : MARKER_CACHE); em.persist(pc); em.getTransaction().commit(); Object oid = pc.getId(); StoreCache dataCache = emf.getStoreCache(); assertEquals(useDataCache, dataCache.contains(PObject.class, oid)); // Modify the record in the database in a separate transaction using // native SQL so that the in-memory instance is not altered em.getTransaction().begin(); String sql = "UPDATE L2_PObject SET NAME='" + MARKER_DATABASE + "' WHERE id=" + oid; em.createNativeQuery(sql).executeUpdate(); em.getTransaction().commit(); assertEquals(useDataCache ? MARKER_DATACACHE : MARKER_CACHE, pc.getName()); em.getTransaction().begin(); if (makeDirtyBeforeRefresh) { pc.setName(MARKER_DIRTY_CACHE); } assertEquals(makeDirtyBeforeRefresh, em.isDirty(pc)); if (lock != null) { ((EntityManagerImpl)em).getFetchPlan().setReadLockMode(lock); } em.refresh(pc); assertEquals(expected, pc.getName()); em.getTransaction().commit(); } /** * The expected marker i.e. where the state is refreshed from depends on * a) whether DataCache is active * b) whether current Lock is stronger than NOLOCK * c) whether the object to be refreshed is dirty * * The following truth table enumerates the possibilities * * Use Cache? Lock? Dirty? Target * Y Y Y Database * Y N Y Data Cache * Y Y N Data Cache * Y N N Data Cache * * N Y Y Database * N N Y Database * N Y N Object Cache * N N N Object Cache * * @param datacache the marker for * @param database * @param useDataCache * @param lock * @param makeDirtyBeforeRefresh */ String getExpectedMarker(boolean useDataCache, LockModeType lock, boolean makeDirtyBeforeRefresh) { if (useDataCache) { return (lock != null) ? MARKER_DATABASE : MARKER_DATACACHE; } else { return MARKER_DATABASE; } } public void testDirtyRefreshWithNoLockHitsDatabase() { verifyRefresh(WITH_DATACACHE, NOLOCK, DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATABASE); } public void testDirtyRefreshWithNoLockHitsDataCache() { verifyRefresh(WITH_DATACACHE, NOLOCK, DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATACACHE); } public void testCleanRefreshWithNoLockDoesNotHitDatabase() { verifyRefresh(WITH_DATACACHE, NOLOCK, !DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATACACHE); } public void testCleanRefreshWithNoLockHitsDataCache() { verifyRefresh(WITH_DATACACHE, NOLOCK, !DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATACACHE); } public void testDirtyRefreshWithReadLockHitsDatabase() { verifyRefresh(WITH_DATACACHE, LockModeType.READ, DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATABASE); verifyRefresh(WITH_DATACACHE, LockModeType.READ, DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATABASE); } public void testCleanRefreshWithReadLockDoesNotHitDatabase() { verifyRefresh(WITH_DATACACHE, LockModeType.READ, !DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATACACHE); verifyRefresh(WITH_DATACACHE, LockModeType.READ, !DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATACACHE); } public void testDirtyRefreshWithWriteLockHitsDatabase() { verifyRefresh(WITH_DATACACHE, LockModeType.WRITE, DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATABASE); verifyRefresh(WITH_DATACACHE, LockModeType.WRITE, DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATABASE); } public void testCleanRefreshWithWriteLockDoesNotHitDatabase() { verifyRefresh(WITH_DATACACHE, LockModeType.WRITE, !DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATACACHE); verifyRefresh(WITH_DATACACHE, LockModeType.WRITE, !DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATACACHE); } public void testDirtyRefreshWithoutDataCacheAlwaysHitsDatabase() { verifyRefresh(!WITH_DATACACHE, NOLOCK, DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATABASE); verifyRefresh(!WITH_DATACACHE, LockModeType.READ, DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATABASE); verifyRefresh(!WITH_DATACACHE, LockModeType.WRITE, DIRTY, REFRESH_FROM_DATACACHE, MARKER_DATABASE); verifyRefresh(!WITH_DATACACHE, NOLOCK, DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATABASE); verifyRefresh(!WITH_DATACACHE, LockModeType.READ, DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATABASE); verifyRefresh(!WITH_DATACACHE, LockModeType.WRITE, DIRTY, !REFRESH_FROM_DATACACHE, MARKER_DATABASE); } public void testCleanRefreshWithoutDataCacheDoesNotHitDatabase() { verifyRefresh(!WITH_DATACACHE, NOLOCK, !DIRTY, REFRESH_FROM_DATACACHE, MARKER_CACHE); verifyRefresh(!WITH_DATACACHE, LockModeType.READ, !DIRTY, REFRESH_FROM_DATACACHE, MARKER_CACHE); verifyRefresh(!WITH_DATACACHE, LockModeType.WRITE, !DIRTY, REFRESH_FROM_DATACACHE, MARKER_CACHE); verifyRefresh(!WITH_DATACACHE, NOLOCK, !DIRTY, !REFRESH_FROM_DATACACHE, MARKER_CACHE); verifyRefresh(!WITH_DATACACHE, LockModeType.READ, !DIRTY, !REFRESH_FROM_DATACACHE, MARKER_CACHE); verifyRefresh(!WITH_DATACACHE, LockModeType.WRITE, !DIRTY, !REFRESH_FROM_DATACACHE, MARKER_CACHE); } /** * Verify behavior of refreshing an instance which has been deleted by * out-of-band process (e.g. a native SQL in a separate transaction). * The behavior differs when refresh() without a lock fetches the data from * DataCache even when the original database record is deleted. * * @param useDataCache * @param lock */ public void verifyDeleteDetectionOnRefresh(boolean useDataCache, boolean dirty, LockModeType lock, Class<?> expectedExceptionType) { OpenJPAEntityManagerFactorySPI emf = (useDataCache) ? emfWithDataCache : emfWithoutDataCache; OpenJPAEntityManagerSPI em = emf.createEntityManager(); em.getTransaction().begin(); PObject pc = new PObject(); pc.setName(useDataCache ? MARKER_DATACACHE : MARKER_CACHE); em.persist(pc); em.getTransaction().commit(); Object oid = pc.getId(); StoreCache dataCache = emf.getStoreCache(); assertEquals(useDataCache, dataCache.contains(PObject.class, oid)); // delete the record in the database in a separate transaction using // native SQL so that the in-memory instance is not altered em.getTransaction().begin(); String sql = "DELETE FROM L2_PObject WHERE id="+oid; em.createNativeQuery(sql).executeUpdate(); em.getTransaction().commit(); // the object cache does not know that the record was deleted assertTrue(em.contains(pc)); // nor does the data cache assertEquals(useDataCache, dataCache.contains(PObject.class, oid)); /** * refresh behavior no more depends on current lock. Refresh * will always attempt to fetch the instance from database * raising EntityNotFoundException. * */ em.getTransaction().begin(); em.getFetchPlan().setReadLockMode(lock); if (dirty) pc.setName("Dirty Name"); try { em.refresh(pc); if (expectedExceptionType != null) { fail("expected " + expectedExceptionType.getSimpleName() + " for PObject:" + oid); } } catch (Exception ex) { boolean expectedException = expectedExceptionType != null && expectedExceptionType.isAssignableFrom(ex.getClass()); if (!expectedException) { ex.printStackTrace(); String error = (expectedExceptionType == null) ? "no exception" : expectedExceptionType.getName(); fail("expected " + error + " for PObject:" + oid); } } finally { em.getTransaction().rollback(); } } public void testDeleteIsNotDetectedOnCleanRefreshWithoutLockWithDataCache() { verifyDeleteDetectionOnRefresh(WITH_DATACACHE, !DIRTY, NOLOCK, ENTITY_NOT_FOUND_ERROR); } public void testDeleteIsDetectedOnCleanRefreshWithLockWithDataCache() { verifyDeleteDetectionOnRefresh(WITH_DATACACHE, !DIRTY, LockModeType.READ, ENTITY_NOT_FOUND_ERROR); verifyDeleteDetectionOnRefresh(WITH_DATACACHE, !DIRTY, LockModeType.WRITE, ENTITY_NOT_FOUND_ERROR); } public void testDeleteIsDetectedOnDirtyRefreshWithoutLockWithDataCache() { verifyDeleteDetectionOnRefresh(WITH_DATACACHE, DIRTY, NOLOCK, ENTITY_NOT_FOUND_ERROR); } public void testDeleteIsDetectedOnDirtyRefreshWithLockWithDataCache() { verifyDeleteDetectionOnRefresh(WITH_DATACACHE, DIRTY, LockModeType.READ, ENTITY_NOT_FOUND_ERROR); verifyDeleteDetectionOnRefresh(WITH_DATACACHE, DIRTY, LockModeType.WRITE, ENTITY_NOT_FOUND_ERROR); } public void testDeleteIsDetectedOnDirtyRefreshWitDataCache() { verifyDeleteDetectionOnRefresh(WITH_DATACACHE, DIRTY, LockModeType.READ, ENTITY_NOT_FOUND_ERROR); verifyDeleteDetectionOnRefresh(WITH_DATACACHE, DIRTY, LockModeType.WRITE, ENTITY_NOT_FOUND_ERROR); } public void testDeleteIsDetectedOnCleanRefreshWithoutLockWithoutDataCache() { verifyDeleteDetectionOnRefresh(!WITH_DATACACHE, !DIRTY, NOLOCK, ENTITY_NOT_FOUND_ERROR); } public void testDeleteIsDetectedOnCleanRefreshWithLockWithoutDataCache() { verifyDeleteDetectionOnRefresh(!WITH_DATACACHE, !DIRTY, LockModeType.READ, ENTITY_NOT_FOUND_ERROR); verifyDeleteDetectionOnRefresh(!WITH_DATACACHE, !DIRTY, LockModeType.WRITE, ENTITY_NOT_FOUND_ERROR); } }