diff --git a/openjpa-kernel/src/main/java/org/apache/openjpa/datacache/DataCacheStoreManager.java b/openjpa-kernel/src/main/java/org/apache/openjpa/datacache/DataCacheStoreManager.java index 127d50a6e..4980e629e 100644 --- a/openjpa-kernel/src/main/java/org/apache/openjpa/datacache/DataCacheStoreManager.java +++ b/openjpa-kernel/src/main/java/org/apache/openjpa/datacache/DataCacheStoreManager.java @@ -24,7 +24,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -437,8 +436,13 @@ public class DataCacheStoreManager public boolean load(OpenJPAStateManager sm, BitSet fields, FetchConfiguration fetch, int lockLevel, Object edata) { DataCache cache = _mgr.selectCache(sm); - if (cache == null || sm.isEmbedded() || bypass(fetch, StoreManager.FORCE_LOAD_NONE)) - return super.load(sm, fields, fetch, lockLevel, edata); + + boolean found = false; + if (cache == null || sm.isEmbedded() || bypass(fetch, StoreManager.FORCE_LOAD_NONE)) { + found = super.load(sm, fields, fetch, lockLevel, edata); + updateDataCache(found, sm, fetch); + return found; + } CacheStatistics stats = cache.getStatistics(); DataCachePCData data = cache.get(sm.getObjectId()); @@ -454,20 +458,59 @@ public class DataCacheStoreManager // load from store manager; clone the set of still-unloaded fields // so that if the store manager decides to modify it it won't affect us - if (!super.load(sm, (BitSet) fields.clone(), fetch, lockLevel, edata)) - return false; - if (_ctx.getPopulateDataCache()) { - cacheStateManager(cache, sm, data); - } - return true; + found = super.load(sm, (BitSet) fields.clone(), fetch, lockLevel, edata); + // Get new instance of cache after DB load since it may have changed + updateDataCache(found, sm, fetch); + + return found; + } + + /** + * Updates or inserts and item into the data cache. If storeMode=USE and not in the cache, + * the item is inserted. If storeMode=REFRESH the item is inserted, updated, or if found=false, + * removed from the cache. + * @param found whether the entity was found by the store manager + * @param sm the state manager + * @param fetch fetch configuration + */ + private void updateDataCache(boolean found, OpenJPAStateManager sm, FetchConfiguration fetch) { + + if (!_ctx.getPopulateDataCache() || sm == null || fetch.getCacheStoreMode() == DataCacheStoreMode.BYPASS) { + return; + } + + DataCache cache = _mgr.selectCache(sm); + if (cache == null) { + return; + } + + DataCachePCData data = cache.get(sm.getObjectId()); + boolean alreadyCached = data != null; + + if ((fetch.getCacheStoreMode() == DataCacheStoreMode.USE && !alreadyCached) || + fetch.getCacheStoreMode() == DataCacheStoreMode.REFRESH) { + // If not found in the DB and the item is in the cache, and not locking remove the item + if (!found && data != null && !isLocking(fetch)) { + cache.remove(sm.getObjectId()); + return; + } + // Update or insert the item into the cache + if (found) { + cacheStateManager(cache, sm, data); + CacheStatistics stats = cache.getStatistics(); + if (stats.isEnabled()) { + ((CacheStatisticsSPI) stats).newPut(sm.getMetaData().getDescribedType()); + } + } + } } public Collection loadAll(Collection sms, PCState state, int load, - FetchConfiguration fetch, Object edata) { - if (bypass(fetch, load)) { - return super.loadAll(sms, state, load, fetch, edata); - } + FetchConfiguration fetch, Object edata) { + if (bypass(fetch, load)) { + return super.loadAll(sms, state, load, fetch, edata); + } Map unloaded = null; List smList = null; diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/cache/jpa/TestMultiEMFCacheModes.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/cache/jpa/TestMultiEMFCacheModes.java new file mode 100644 index 000000000..b0acea7aa --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/cache/jpa/TestMultiEMFCacheModes.java @@ -0,0 +1,223 @@ +/* + * 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.cache.jpa; + +import java.util.Random; + +import javax.persistence.Cache; +import javax.persistence.CacheStoreMode; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityNotFoundException; +import javax.persistence.LockModeType; + + +import org.apache.openjpa.persistence.cache.jpa.model.CacheableEntity; +import org.apache.openjpa.persistence.test.SingleEMFTestCase; + +/** + * Verifies L2 operations using multiple entity manager factories. The EMF which + * does updates does not have the data cache enabled thus, it does not send + * sjvm updates upon commit. + */ +public class TestMultiEMFCacheModes extends SingleEMFTestCase { + + public void setUp() { + setUp( CLEAR_TABLES, CacheableEntity.class, + "openjpa.ConnectionFactoryProperties", "PrintParameters=true", + "openjpa.DataCache", "true"); + } + + /** + * Verifies that the data cache us updated via a em.refresh operation when + * javax.persistence.cache.storeMode = CacheStoreMode.REFRESH and the + * entity is updated in the database. + */ + public void testCacheRefreshModeRefresh() { + + EntityManager em = emf.createEntityManager(); + + // Create a new cacheable entity + CacheableEntity ce = createEntity(em); + int ceid = ce.getId(); + + // Clear the L1 + em.clear(); + + // Clear the L2 cache + Cache cache = emf.getCache(); + cache.evictAll(); + assertFalse(cache.contains(CacheableEntity.class, ceid)); + + // Find the entity, reloading it into the L2 + em.getTransaction().begin(); + ce = em.find(CacheableEntity.class, ceid); + assertTrue(em.contains(ce)); + assertTrue(cache.contains(CacheableEntity.class, ceid)); + assertTrue(em.getLockMode(ce) == LockModeType.NONE); + assertEquals(ce.getName(), "Cached Entity"); + assertEquals(ce.getVersion(), 1); + em.getTransaction().commit(); + + // Create a new EMF -WITHOUT- the L2 enabled. If the L2 was enabled, the + // sjvm remote commit provider would evict the entity upon update, throwing + // off the intent of this variation. + EntityManagerFactory emf2 = this.createEMF(CacheableEntity.class, + "openjpa.LockManager", "mixed", + "openjpa.ConnectionFactoryProperties", "PrintParameters=true"); + EntityManager em2 = emf2.createEntityManager(); + + // Find + lock, then update the entity and commit + em2.getTransaction().begin(); + CacheableEntity ce2 = em2.find(CacheableEntity.class, ceid, LockModeType.PESSIMISTIC_FORCE_INCREMENT); + ce2.setName("Updated Cached Entity"); + em2.getTransaction().commit(); + em2.close(); + emf2.close(); + + // Refresh the entity - this will load the entity into the L1 and with storeMode=REFRESH, + // should also refresh it in the L2 + java.util.Map cacheStoreModeMap = new java.util.HashMap(); + cacheStoreModeMap.put("javax.persistence.cache.storeMode", CacheStoreMode.REFRESH); + em.refresh(ce, cacheStoreModeMap); + + // Verify the entity was updated + verifyUpdatedEntity(ce, ceid); + + // Verify loading from the L1 + ce = em.find(CacheableEntity.class, ceid); + + // Verify the entity was updated + verifyUpdatedEntity(ce, ceid); + + // Clear the L1 + em.clear(); + + // Assert the L2 contains the entity + assertTrue(cache.contains(CacheableEntity.class, ceid)); + + // Reload the entity from the L2 + ce = em.find(CacheableEntity.class, ceid); + + // Verify the entity in the L2 was updated + verifyUpdatedEntity(ce, ceid); + + em.close(); + } + + /** + * Verifies that the data cache us updated via a em.refresh operation when + * javax.persistence.cache.storeMode = CacheStoreMode.REFRESH and the + * record is removed from the database. + */ + public void testCacheRefreshModeRefreshDelete() { + + EntityManager em = emf.createEntityManager(); + + // Create a new cachable entity + CacheableEntity ce = createEntity(em); + int ceid = ce.getId(); + + // Clear the L2 cache + Cache cache = emf.getCache(); + cache.evictAll(); + assertFalse(cache.contains(CacheableEntity.class, ceid)); + + // Find the entity, reloading it into the L2 + em.getTransaction().begin(); + ce = em.find(CacheableEntity.class, ceid); + assertTrue(em.contains(ce)); + assertTrue(cache.contains(CacheableEntity.class, ceid)); + assertTrue(em.getLockMode(ce) == LockModeType.NONE); + assertEquals(ce.getName(), "Cached Entity"); + assertEquals(ce.getVersion(), 1); + em.getTransaction().commit(); + + // Create a new EMF -WITHOUT- the L2 enabled. If the L2 was enabled, the + // sjvm remote commit provider would evict the entity upon delete, throwing + // off the intent of this variation. + EntityManagerFactory emf2 = this.createEMF(CacheableEntity.class, + "openjpa.LockManager", "mixed", + "openjpa.ConnectionFactoryProperties", "PrintParameters=true"); + EntityManager em2 = emf2.createEntityManager(); + + // Find and delete the entity in a separate context with no L2 configured + em2.getTransaction().begin(); + CacheableEntity ce2 = em2.find(CacheableEntity.class, ceid); + assertNotNull(ce2); + em2.remove(ce2); + em2.getTransaction().commit(); + em2.close(); + emf2.close(); + + // Refresh the entity with storeMode=REFRESH. The entity has been deleted so it will be + // purged from the L2 cache when the DB load fails. + java.util.Map cacheStoreModeMap = new java.util.HashMap(); + cacheStoreModeMap.put("javax.persistence.cache.storeMode", CacheStoreMode.REFRESH); + try { + em.refresh(ce, cacheStoreModeMap); + fail("Refresh operation should have thrown an exception"); + } catch (EntityNotFoundException e) { + // expected exception + } + + // Try loading from the L1 - OpenJPA will detect the entity was + // removed in another transaction. + try { + ce = em.find(CacheableEntity.class, ceid); + fail("OpenJPA should have detected the removed entity"); + } catch (EntityNotFoundException e) { + // expected exception + } + + // Clear the L1 + em.clear(); + + // Assert the L2 no longer contains the entity + assertFalse(cache.contains(CacheableEntity.class, ceid)); + + // Attempt to reload entity from the L2 or database + ce = em.find(CacheableEntity.class, ceid); + + // Verify the entity was removed from L2 and DB + assertNull(ce); + + em.close(); + } + + private CacheableEntity createEntity(EntityManager em) { + CacheableEntity ce = new CacheableEntity(); + int ceid = new Random().nextInt(); + ce.setId(ceid); + ce.setName("Cached Entity"); + + // Persist the new cachable entity + em.getTransaction().begin(); + em.persist(ce); + em.getTransaction().commit(); + em.clear(); + return ce; + } + + private void verifyUpdatedEntity(CacheableEntity ce, int id) { + assertEquals(id, ce.getId()); + assertEquals("Updated Cached Entity", ce.getName()); + assertEquals(2, ce.getVersion()); + } +} diff --git a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java index 83d28ae35..18e39c930 100644 --- a/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java +++ b/openjpa-persistence/src/main/java/org/apache/openjpa/persistence/EntityManagerImpl.java @@ -761,7 +761,8 @@ public class EntityManagerImpl assertValidAttchedEntity(REFRESH, entity); _broker.assertWriteOperation(); - configureCurrentFetchPlan(pushFetchPlan(), properties, mode, true); + configureCurrentCacheModes(pushFetchPlan(), properties); + configureCurrentFetchPlan(getFetchPlan(), properties, mode, true); DataCacheRetrieveMode rmode = getFetchPlan().getCacheRetrieveMode(); if (DataCacheRetrieveMode.USE.equals(rmode) || rmode == null) { getFetchPlan().setCacheRetrieveMode(DataCacheRetrieveMode.BYPASS);