diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java index 0943cd1f0..29f786ed2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanData.java @@ -1,10 +1,13 @@ package com.avaje.ebeaninternal.server.cache; +import java.util.Arrays; + /** * Data held in the bean cache for cached beans. */ public class CachedBeanData { + private final long whenCreated; private final Object sharableBean; private final boolean[] loaded; private final Object[] data; @@ -14,6 +17,7 @@ public class CachedBeanData { private final Object oldNaturalKey; public CachedBeanData(Object sharableBean, boolean[] loaded, Object[] data, Object naturalKey, Object oldNaturalKey) { + this.whenCreated = System.currentTimeMillis(); this.sharableBean = sharableBean; this.loaded = loaded; this.data = data; @@ -22,6 +26,10 @@ public class CachedBeanData { this.oldNaturalKey = oldNaturalKey; } + public String toString() { + return Arrays.toString(data); + } + /** * Return a copy of the property data. */ @@ -41,35 +49,52 @@ public class CachedBeanData { } return dest; } - + + /** + * Return when the cached data was created. + */ + public long getWhenCreated() { + return whenCreated; + } + + /** + * Return a sharable (immutable read only) bean. + */ public Object getSharableBean() { return sharableBean; } + /** + * Return true if this data requires an update to the natural key cache. + */ public boolean isNaturalKeyUpdate() { return naturalKeyUpdate; } + /** + * Return the new/current natural key value. + */ public Object getNaturalKey() { return naturalKey; } + /** + * Return the old natural key (its entry should be removed). + */ public Object getOldNaturalKey() { return oldNaturalKey; } - public boolean containsProperty(int propIndex) { - return loaded[propIndex]; - } - - public boolean[] getLoaded() { - return loaded; - } - + /** + * Return the data for the specific property. + */ public Object getData(int i) { return data[i]; } + /** + * Return true if the property is contained in this data. + */ public boolean isLoaded(int i) { return loaded[i]; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataToBean.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataToBean.java index 9167d3a00..1c05d4e18 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataToBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataToBean.java @@ -30,13 +30,7 @@ public class CachedBeanDataToBean { BeanPropertyAssocMany[] manys = desc.propertiesMany(); for (int i = 0; i < manys.length; i++) { - BeanPropertyAssocMany prop = manys[i]; - if (ebi.isLoadedProperty(prop.getPropertyIndex())) { - // already loaded property - } else { - // set a lazy loading proxy - prop.createReference(bean); - } + manys[i].createReferenceIfNull(bean); } ebi.setLoadedLazy(); diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataUpdate.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataUpdate.java index e274b5d4a..7235e72a9 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataUpdate.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedBeanDataUpdate.java @@ -30,7 +30,7 @@ public class CachedBeanDataUpdate { int propertyIndex = props[i].getPropertyIndex(); if (ebi.isLoadedProperty(propertyIndex)) { if (props[i].isNaturalKey()) { - newNaturalKey = updateBean._ebean_getField(i); + newNaturalKey = updateBean._ebean_getField(propertyIndex); } // set the cache safe value for the property and mark it as loaded copyData[propertyIndex] = props[i].getCacheDataValue(updateBean); diff --git a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedManyIds.java b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedManyIds.java index 2513ee382..822d477e5 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/cache/CachedManyIds.java +++ b/src/main/java/com/avaje/ebeaninternal/server/cache/CachedManyIds.java @@ -16,6 +16,10 @@ public class CachedManyIds { this.idList = idList; } + public String toString() { + return idList.toString(); + } + public List getIdList() { return idList; } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/CacheOptions.java b/src/main/java/com/avaje/ebeaninternal/server/core/CacheOptions.java index 5a0b6b242..1eed47deb 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/CacheOptions.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/CacheOptions.java @@ -4,87 +4,126 @@ package com.avaje.ebeaninternal.server.core; * Options for controlling cache behaviour for a given type. */ public class CacheOptions { - - private boolean useCache; - private boolean readOnly; - - private String naturalKey; - - private String warmingQuery; - - /** - * Construct with options. - */ - public CacheOptions() { - } - - /** - * Return true if this should use a cache for lazy loading. - */ - public boolean isUseCache() { - return useCache; - } - - /** - * Set whether to use the bean cache for the associated type. - */ - public void setUseCache(boolean useCache) { - this.useCache = useCache; + private boolean useCache; + + private boolean readOnly; + + private String naturalKey; + + private String warmingQuery; + + private int maxIdleSecs; + + private long maxSecsToLive; + + /** + * Construct with options. + */ + public CacheOptions() { + } + + /** + * Return true if this should use a cache for lazy loading. + */ + public boolean isUseCache() { + return useCache; + } + + /** + * Set whether to use the bean cache for the associated type. + */ + public void setUseCache(boolean useCache) { + this.useCache = useCache; + } + + /** + * Return the readOnly default setting. + */ + public boolean isReadOnly() { + return readOnly; + } + + /** + * Set read Only default setting. + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + } + + /** + * Return the query used to warm the cache. + */ + public String getWarmingQuery() { + return warmingQuery; + } + + /** + * Set the cache warming query. + */ + public void setWarmingQuery(String warmingQuery) { + this.warmingQuery = warmingQuery; + } + + /** + * Return true if a natural key is set. + */ + public boolean isUseNaturalKeyCache() { + return naturalKey != null; + } + + /** + * Return the natural key property. + */ + public String getNaturalKey() { + return naturalKey; + } + + /** + * Set the natural key property. + */ + public void setNaturalKey(String naturalKey) { + if (naturalKey == null || naturalKey.length() == 0) { + naturalKey = null; + } else { + this.naturalKey = naturalKey.trim(); } + } + + /** + * Return the max age of entries in seconds. + */ + public long getMaxSecsToLive() { + return maxSecsToLive; + } - /** - * Return the readOnly default setting. - */ - public boolean isReadOnly() { - return readOnly; - } + /** + * Set the max age of entries in seconds. + */ + public void setMaxSecsToLive(long maxSecsToLive) { + this.maxSecsToLive = maxSecsToLive; + } - /** - * Set read Only default setting. - */ - public void setReadOnly(boolean readOnly) { - this.readOnly = readOnly; - } + /** + * Set the max idle seconds. + */ + public void setMaxIdleSecs(int maxIdleSecs) { + this.maxIdleSecs = maxIdleSecs; + } - /** - * Return the query used to warm the cache. - */ - public String getWarmingQuery() { - return warmingQuery; - } + /** + * Return the max idle seconds. + */ + public int getMaxIdleSecs() { + return maxIdleSecs; + } - /** - * Set the cache warming query. - */ - public void setWarmingQuery(String warmingQuery) { - this.warmingQuery = warmingQuery; - } + /** + * Return true if the entry exceeds the maxIdleSecs or maxSecsToLive. + */ + public boolean isTooOldInMillis(long ageMillis) { + long secs = ageMillis / 1000; + return (maxIdleSecs > 0 && secs > maxIdleSecs) || (maxSecsToLive > 0 && secs > maxSecsToLive); + } - - /** - * Return true if a natural key is set. - */ - public boolean isUseNaturalKeyCache() { - return naturalKey != null; - } - - /** - * Return the natural key property. - */ - public String getNaturalKey() { - return naturalKey; - } - - /** - * Set the natural key property. - */ - public void setNaturalKey(String naturalKey) { - if (naturalKey == null || naturalKey.length() == 0){ - naturalKey = null; - } else { - this.naturalKey = naturalKey.trim(); - } - } - } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index 91ef80d96..313d56664 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -1180,23 +1180,7 @@ public final class DefaultServer implements SpiEbeanServer { } // Hit the L2 bean cache - Object cachedBean = beanDescriptor.cacheBeanGet(query.getId(), query.isReadOnly()); - if (cachedBean != null) { - if (context == null) { - context = new DefaultPersistenceContext(); - - } - context.put(query.getId(), cachedBean); - - DLoadContext loadContext = new DLoadContext(this, beanDescriptor, query.isReadOnly(), query); - loadContext.setPersistenceContext(context); - - EntityBeanIntercept ebi = ((EntityBean) cachedBean)._ebean_getIntercept(); - ebi.setPersistenceContext(context); - loadContext.register(null, ebi); - } - - return (T) cachedBean; + return beanDescriptor.cacheBeanGet(query, context); } @SuppressWarnings("unchecked") @@ -1245,17 +1229,9 @@ public final class DefaultServer implements SpiEbeanServer { BeanDescriptor desc = beanDescriptorManager.getBeanDescriptor(q.getBeanType()); - if (desc.calculateUseNaturalKeyCache(q.isUseBeanCache())) { - // check if it is a find by unique id - NaturalKeyBindParam keyBindParam = q.getNaturalKeyBindParam(); - if (keyBindParam != null && desc.cacheIsNaturalKey(keyBindParam.getName())) { - Object id2 = desc.cacheNaturalKeyLookup(keyBindParam.getValue()); - if (id2 != null) { - SpiQuery copy = q.copy(); - copy.convertWhereNaturalKeyToId(id2); - return findId(copy, t); - } - } + T bean = desc.cacheNaturalKeyLookup(q, (SpiTransaction)t); + if (bean != null) { + return bean; } // a query that is expected to return either 0 or 1 rows @@ -1263,9 +1239,10 @@ public final class DefaultServer implements SpiEbeanServer { if (list.size() == 0) { return null; + } else if (list.size() > 1) { - String m = "Unique expecting 0 or 1 rows but got [" + list.size() + "]"; - throw new PersistenceException(m); + throw new PersistenceException("Unique expecting 0 or 1 rows but got [" + list.size() + "]"); + } else { return list.get(0); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java index 206739ba7..08236988b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/PersistRequestBean.java @@ -1,6 +1,7 @@ package com.avaje.ebeaninternal.server.core; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -21,6 +22,7 @@ import com.avaje.ebeaninternal.api.TransactionEvent; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.deploy.BeanManager; import com.avaje.ebeaninternal.server.deploy.BeanProperty; +import com.avaje.ebeaninternal.server.deploy.BeanPropertyAssocMany; import com.avaje.ebeaninternal.server.persist.BatchControl; import com.avaje.ebeaninternal.server.persist.PersistExecute; import com.avaje.ebeaninternal.server.persist.dml.GenerateDmlRequest; @@ -60,7 +62,7 @@ public class PersistRequestBean extends PersistRequest implements BeanPersist */ protected final Object parentBean; - protected final boolean isDirty; + protected final boolean dirty; protected ConcurrencyMode concurrencyMode; @@ -82,6 +84,17 @@ public class PersistRequestBean extends PersistRequest implements BeanPersist private final Set dirtyPropertyNames; + /** + * Flag used to detect when only many properties where updated via a cascade. Used to ensure + * appropriate cache updates occur in that case. + */ + private boolean updatedManysOnly; + + /** + * Many properties that were cascade saved (and hence might need cache update later). + */ + private List> updatedManys; + public PersistRequestBean(SpiEbeanServer server, T bean, Object parentBean, BeanManager mgr, SpiTransaction t, PersistExecute persistExecute, PersistRequest.Type type) { @@ -110,7 +123,7 @@ public class PersistRequestBean extends PersistRequest implements BeanPersist this.controller = beanDescriptor.getPersistController(); this.concurrencyMode = beanDescriptor.getConcurrencyMode(intercept); // this is ok to not use isNewOrDirty() as used for updates only - this.isDirty = intercept.isDirty(); + this.dirty = intercept.isDirty(); } /** @@ -288,7 +301,7 @@ public class PersistRequestBean extends PersistRequest implements BeanPersist * for EntityBeans that have not been modified. */ public boolean isDirty() { - return isDirty; + return dirty; } /** @@ -574,5 +587,48 @@ public class PersistRequestBean extends PersistRequest implements BeanPersist public boolean isReference() { return beanDescriptor.isReference(intercept); } + + /** + * This many property has been cascade saved. Keep note of this and update the 'many property' + * cache on post commit. + */ + public void addUpdatedManyProperty(BeanPropertyAssocMany updatedAssocMany) { + //if (notifyCache) { + if (updatedManys == null) { + updatedManys = new ArrayList>(5); + } + updatedManys.add(updatedAssocMany); + //} + } + + /** + * Return the list of cascade updated many properties (can be null). + */ + public List> getUpdatedManyCollections() { + return updatedManys; + } + + /** + * A reference bean was saved. Check if any of its many properties where + * cascade saved and hence we need to update related many property caches. + */ + public void checkUpdatedManysOnly() { + if (!dirty && updatedManys != null) { + // set the flag and register for post commit processing if there + // is caching or registered listeners + if (idValue == null) { + this.idValue = beanDescriptor.getId(entityBean); + } + updatedManysOnly = true; + addEvent(); + } + } + + /** + * Return true if only many properties where updated. + */ + public boolean isUpdatedManysOnly() { + return updatedManysOnly; + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanCollectionUtil.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanCollectionUtil.java new file mode 100644 index 000000000..220633a62 --- /dev/null +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanCollectionUtil.java @@ -0,0 +1,42 @@ +package com.avaje.ebeaninternal.server.deploy; + +import java.util.Collection; +import java.util.Map; + +import javax.persistence.PersistenceException; + +import com.avaje.ebean.bean.BeanCollection; + +/** + * Utility methods for BeanCollections. + */ +public class BeanCollectionUtil { + + /** + * Return the details of the collection or map taking care to avoid + * unnecessary fetching of the data. + */ + public static Collection getActualEntries(Object o) { + if (o == null) { + return null; + } + if (o instanceof BeanCollection) { + BeanCollection bc = (BeanCollection) o; + if (!bc.isPopulated()) { + return null; + } + // For maps this is a collection of Map.Entry, otherwise it + // returns a collection of beans + return bc.getActualEntries(); + } + + if (o instanceof Map) { + // yes, we want the entrySet (to set the keys) + return ((Map) o).entrySet(); + + } else if (o instanceof Collection) { + return ((Collection) o); + } + throw new PersistenceException("expecting a Map or Collection but got [" + o.getClass().getName() + "]"); + } +} diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java index 54932257f..be314287b 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptor.java @@ -39,6 +39,7 @@ import com.avaje.ebean.text.json.JsonWriteBeanVisitor; import com.avaje.ebeaninternal.api.HashQueryPlan; import com.avaje.ebeaninternal.api.SpiEbeanServer; import com.avaje.ebeaninternal.api.SpiQuery; +import com.avaje.ebeaninternal.api.SpiTransaction; import com.avaje.ebeaninternal.api.SpiUpdatePlan; import com.avaje.ebeaninternal.api.TransactionEventTable.TableIUD; import com.avaje.ebeaninternal.server.cache.CachedBeanData; @@ -681,10 +682,10 @@ public class BeanDescriptor implements MetaBeanInfo { return (queryUseCache != null) ? queryUseCache.booleanValue() : isBeanCaching(); } - public boolean calculateUseNaturalKeyCache(Boolean queryUseCache) { - return (queryUseCache != null) ? queryUseCache.booleanValue() : isBeanCaching(); + public T cacheNaturalKey(SpiQuery query, SpiTransaction t) { + return cacheHelp.naturalKeyLookup(query, t); } - + /** * Return the cache options. */ @@ -820,13 +821,6 @@ public class BeanDescriptor implements MetaBeanInfo { return cacheHelp.manyPropGet(parentId, propertyName); } - /** - * Put the CachedManyIds into the cache. - */ - public void cacheManyPropPutEntry(Object parentId, String propertyName, CachedManyIds ids) { - cacheHelp.manyPropPutEntry(parentId, propertyName, ids); - } - /** * Clear the bean cache. */ @@ -844,12 +838,12 @@ public class BeanDescriptor implements MetaBeanInfo { public void cacheBeanPutData(EntityBean bean) { cacheHelp.beanCachePut(bean); } - + /** - * Return a bean from the bean cache. + * Return a bean from the bean cache (or null). */ - public T cacheBeanGet(Object id, Boolean readOnly) { - return cacheHelp.beanCacheGet(id, readOnly); + public T cacheBeanGet(SpiQuery query, PersistenceContext context) { + return cacheHelp.beanCacheGet(query, context); } /** @@ -859,24 +853,27 @@ public class BeanDescriptor implements MetaBeanInfo { cacheHelp.beanCacheRemove(id); } + /** + * Returns true if it managed to populate/load the bean from the cache. + */ public boolean cacheBeanLoad(EntityBean bean, EntityBeanIntercept ebi, Object id) { return cacheHelp.beanCacheLoad(bean, ebi, id); } + /** + * Returns true if it managed to populate/load the bean from the cache. + */ public boolean cacheBeanLoad(EntityBeanIntercept ebi) { EntityBean bean = ebi.getOwner(); Object id = getId(bean); - return cacheBeanLoad(bean, ebi, id); } - - public boolean cacheIsNaturalKey(String propName) { - return cacheHelp.isNaturalKey(propName); - } - - public Object cacheNaturalKeyLookup(Object uniqueKeyValue) { - return cacheHelp.naturalKeyLookup(uniqueKeyValue); + /** + * Try to hit the cache using the natural key. + */ + public T cacheNaturalKeyLookup(SpiQuery query, SpiTransaction t) { + return cacheHelp.naturalKeyLookup(query, t); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java index c2ed3f5c0..a60ce3e41 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/BeanDescriptorCacheHelp.java @@ -15,6 +15,8 @@ import com.avaje.ebean.bean.EntityBeanIntercept; import com.avaje.ebean.bean.PersistenceContext; import com.avaje.ebean.cache.ServerCache; import com.avaje.ebean.cache.ServerCacheManager; +import com.avaje.ebeaninternal.api.SpiQuery; +import com.avaje.ebeaninternal.api.SpiTransaction; import com.avaje.ebeaninternal.api.TransactionEventTable.TableIUD; import com.avaje.ebeaninternal.server.cache.CachedBeanData; import com.avaje.ebeaninternal.server.cache.CachedBeanDataFromBean; @@ -23,6 +25,9 @@ import com.avaje.ebeaninternal.server.cache.CachedBeanDataUpdate; import com.avaje.ebeaninternal.server.cache.CachedManyIds; import com.avaje.ebeaninternal.server.core.CacheOptions; import com.avaje.ebeaninternal.server.core.PersistRequestBean; +import com.avaje.ebeaninternal.server.loadcontext.DLoadContext; +import com.avaje.ebeaninternal.server.querydefn.NaturalKeyBindParam; +import com.avaje.ebeaninternal.server.transaction.DefaultPersistenceContext; /** * Helper for BeanDescriptor that manages the bean, query and collection caches. @@ -31,8 +36,12 @@ import com.avaje.ebeaninternal.server.core.PersistRequestBean; */ public final class BeanDescriptorCacheHelp { - private static final Logger logger = LoggerFactory.getLogger(BeanDescriptorCacheHelp.class); - + public static final Logger queryLog = LoggerFactory.getLogger("org.avaje.ebean.cache.QUERY"); + public static final Logger beanLog = LoggerFactory.getLogger("org.avaje.ebean.cache.BEAN"); + public static final Logger manyLog = LoggerFactory.getLogger("org.avaje.ebean.cache.COLL"); + public static final Logger natLog = LoggerFactory.getLogger("org.avaje.ebean.cache.NATKEY"); + + private final BeanDescriptor desc; private final ServerCacheManager cacheManager; @@ -46,6 +55,8 @@ public final class BeanDescriptorCacheHelp { private final Class beanType; + private final String cacheName; + private final BeanPropertyAssocOne[] propertiesOneImported; private ServerCache beanCache; @@ -57,6 +68,7 @@ public final class BeanDescriptorCacheHelp { this.desc = desc; this.beanType = desc.getBeanType(); + this.cacheName = beanType.getSimpleName(); this.cacheManager = cacheManager; this.cacheOptions = cacheOptions; this.cacheSharableBeans = cacheSharableBeans; @@ -89,9 +101,8 @@ public final class BeanDescriptorCacheHelp { query.setReadOnly(true); query.setLoadBeanCache(true); List list = query.findList(); - if (logger.isInfoEnabled()) { - String msg = "Loaded " + beanType + " cache with [" + list.size() + "] beans"; - logger.info(msg); + if (beanLog.isInfoEnabled()) { + beanLog.info("Loaded {} cache with [{}] beans", cacheName, list.size()); } } } @@ -135,6 +146,9 @@ public final class BeanDescriptorCacheHelp { */ public void queryCacheClear() { if (queryCache != null) { + if (queryLog.isDebugEnabled()) { + queryLog.debug(" CLEAR {}", cacheName); + } queryCache.clear(); } } @@ -159,17 +173,26 @@ public final class BeanDescriptorCacheHelp { if (queryCache == null) { queryCache = cacheManager.getQueryCache(beanType); } + if (queryLog.isDebugEnabled()) { + queryLog.debug(" PUT {} {}", cacheName, id); + } queryCache.put(id, query); } public void manyPropRemove(Object parentId, String propertyName) { ServerCache collectionIdsCache = cacheManager.getCollectionIdsCache(beanType, propertyName); + if (manyLog.isDebugEnabled()) { + manyLog.debug(" REMOVE {}({}).{}", cacheName, parentId, propertyName); + } collectionIdsCache.remove(parentId); } public void manyPropClear(String propertyName) { ServerCache collectionIdsCache = cacheManager.getCollectionIdsCache(beanType, propertyName); + if (manyLog.isDebugEnabled()) { + manyLog.debug(" CLEAR {}(*).{} ", cacheName, propertyName); + } collectionIdsCache.clear(); } @@ -178,15 +201,15 @@ public final class BeanDescriptorCacheHelp { */ public CachedManyIds manyPropGet(Object parentId, String propertyName) { ServerCache collectionIdsCache = cacheManager.getCollectionIdsCache(beanType, propertyName); - return (CachedManyIds) collectionIdsCache.get(parentId); - } - - /** - * Put the CachedManyIds into the cache. - */ - public void manyPropPutEntry(Object parentId, String propertyName, CachedManyIds ids) { - ServerCache collectionIdsCache = cacheManager.getCollectionIdsCache(beanType, propertyName); - collectionIdsCache.put(parentId, ids); + CachedManyIds entry = (CachedManyIds) collectionIdsCache.get(parentId); + if (entry == null) { + if (manyLog.isTraceEnabled()) { + manyLog.trace(" GET {}({}).{} - cache miss", cacheName, parentId, propertyName); + } + } else if (manyLog.isDebugEnabled()) { + manyLog.debug(" GET {}({}).{} - hit", cacheName, parentId, propertyName); + } + return entry; } /** @@ -194,19 +217,19 @@ public final class BeanDescriptorCacheHelp { */ public boolean manyPropLoad(BeanPropertyAssocMany many, BeanCollection bc, Object parentId, Boolean readOnly) { - CachedManyIds ids = manyPropGet(parentId, many.getName()); - if (ids == null) { + CachedManyIds entry = manyPropGet(parentId, many.getName()); + if (entry == null) { // not in cache so return unsuccessful return false; } - + Object ownerBean = bc.getOwnerBean(); EntityBeanIntercept ebi = ((EntityBean) ownerBean)._ebean_getIntercept(); PersistenceContext persistenceContext = ebi.getPersistenceContext(); BeanDescriptor targetDescriptor = many.getTargetDescriptor(); - List idList = ids.getIdList(); + List idList = entry.getIdList(); bc.checkEmptyLazyLoad(); for (int i = 0; i < idList.size(); i++) { Object id = idList.get(i); @@ -223,33 +246,101 @@ public final class BeanDescriptorCacheHelp { /** * Put the beanCollection into the cache. */ - public void manyPropPut(BeanPropertyAssocMany many, BeanCollection bc, Object parentId) { - + public void manyPropPut(BeanPropertyAssocMany many, Object details, Object parentId) { + BeanDescriptor targetDescriptor = many.getTargetDescriptor(); ArrayList idList = new ArrayList(); // get the underlying collection of beans (in the List, Set or Map) - Collection actualDetails = bc.getActualDetails(); + Collection actualDetails = BeanCollectionUtil.getActualEntries(details); + for (Object bean : actualDetails) { // Collect the id values idList.add(targetDescriptor.getId((EntityBean) bean)); } - CachedManyIds ids = new CachedManyIds(idList); - manyPropPutEntry(parentId, many.getName(), ids); + + CachedManyIds entry = new CachedManyIds(idList); + ServerCache collectionIdsCache = cacheManager.getCollectionIdsCache(beanType, many.getName()); + if (manyLog.isDebugEnabled()) { + manyLog.debug(" PUT {}({}).{} - ids:{}", cacheName, parentId, many.getName(), entry); + } + collectionIdsCache.put(parentId, entry); } - public boolean isNaturalKey(String propName) { + + public T naturalKeyLookup(SpiQuery query, SpiTransaction t) { + + if (!isNaturalKeyCaching(query.isUseBeanCache())) { + // no natural key caching for this query + return null; + } + + // check if it is a find by unique id (using the natural key) + NaturalKeyBindParam keyBindParam = query.getNaturalKeyBindParam(); + if (keyBindParam == null || !isNaturalKey(keyBindParam.getName())) { + // query is not appropriate + return null; + } + + // try to lookup the id using the natural key + Object id = naturalKeyCache.get(keyBindParam.getValue()); + if (natLog.isTraceEnabled()) { + natLog.trace(" LOOKUP {}({}) - id:{}", cacheName, keyBindParam.getValue(), id); + } + if (id == null) { + return null; + } + + // try looking up into the bean cache using the id + T cacheBean = beanCacheGetInternal(id, query.isReadOnly()); + if (cacheBean != null) { + setupContext(cacheBean, query, getPersistenceContext(t)); + } + return cacheBean; + } + + private PersistenceContext getPersistenceContext(SpiTransaction t) { + PersistenceContext context = null; + if (t == null) { + t = desc.getEbeanServer().getCurrentServerTransaction(); + } + if (t != null) { + context = t.getPersistenceContext(); + } + return context; + } + + private boolean isNaturalKeyCaching(Boolean queryUseCache) { + return naturalKeyCache != null && (queryUseCache == null || queryUseCache.booleanValue()); + } + + private boolean isNaturalKey(String propName) { return propName != null && propName.equals(cacheOptions.getNaturalKey()); } - public Object naturalKeyLookup(Object uniqueKeyValue) { - if (naturalKeyCache != null) { - return naturalKeyCache.get(uniqueKeyValue); - } - return null; - } + + /** + * For a bean built from the cache this sets up its persistence context for future lazy loading etc. + */ + private void setupContext(Object bean, SpiQuery query, PersistenceContext context) { + if (context == null) { + context = new DefaultPersistenceContext(); + } + context.put(query.getId(), bean); + + DLoadContext loadContext = new DLoadContext(desc.getEbeanServer(), desc, query.isReadOnly(), query); + loadContext.setPersistenceContext(context); + + EntityBeanIntercept ebi = ((EntityBean) bean)._ebean_getIntercept(); + ebi.setPersistenceContext(context); + loadContext.register(null, ebi); + } + + /** + * Return the beanCache creating it if necessary. + */ private ServerCache getBeanCache() { if (beanCache == null) { beanCache = cacheManager.getBeanCache(beanType); @@ -262,6 +353,9 @@ public final class BeanDescriptorCacheHelp { */ public void beanCacheClear() { if (beanCache != null) { + if (beanLog.isDebugEnabled()) { + beanLog.debug(" CLEAR {}", cacheName); + } beanCache.clear(); } } @@ -274,10 +368,17 @@ public final class BeanDescriptorCacheHelp { CachedBeanData beanData = CachedBeanDataFromBean.extract(desc, bean); Object id = desc.getId(bean); + if (beanLog.isDebugEnabled()) { + beanLog.debug(" PUT {}({})", cacheName, id); + } getBeanCache().put(id, beanData); + if (beanData.isNaturalKeyUpdate() && naturalKeyCache != null) { Object naturalKey = beanData.getNaturalKey(); if (naturalKey != null) { + if (natLog.isDebugEnabled()) { + natLog.debug(" PUT {}({}, {})", cacheName, naturalKey, id); + } naturalKeyCache.put(naturalKey, id); } } @@ -287,19 +388,33 @@ public final class BeanDescriptorCacheHelp { return (CachedBeanData) getBeanCache().get(id); } + public T beanCacheGet(SpiQuery query, PersistenceContext context) { + T bean = beanCacheGetInternal(query.getId(), query.isReadOnly()); + if (bean != null) { + setupContext(bean, query, context); + } + return bean; + } + /** * Return a bean from the bean cache. */ @SuppressWarnings("unchecked") - public T beanCacheGet(Object id, Boolean readOnly) { + private T beanCacheGetInternal(Object id, Boolean readOnly) { CachedBeanData d = (CachedBeanData) getBeanCache().get(id); if (d == null) { + if (beanLog.isTraceEnabled()) { + beanLog.trace(" GET {}({}) - cache miss", cacheName, id); + } return null; } if (cacheSharableBeans && !Boolean.FALSE.equals(readOnly)) { Object bean = d.getSharableBean(); if (bean != null) { + if (beanLog.isTraceEnabled()) { + beanLog.trace(" GET {}({}) - hit shared bean", cacheName, id); + } return (T) bean; } } @@ -311,6 +426,9 @@ public final class BeanDescriptorCacheHelp { } CachedBeanDataToBean.load(desc, bean, d); + if (beanLog.isTraceEnabled()) { + beanLog.trace(" GET {}({}) - hit", cacheName, id); + } return (T) bean; } @@ -320,6 +438,9 @@ public final class BeanDescriptorCacheHelp { */ public void beanCacheRemove(Object id) { if (beanCache != null) { + if (beanLog.isDebugEnabled()) { + beanLog.debug(" REMOVE {}({})", cacheName, id); + } beanCache.remove(id); } for (int i = 0; i < propertiesOneImported.length; i++) { @@ -327,18 +448,30 @@ public final class BeanDescriptorCacheHelp { } } + /** + * Returns true if it managed to populate/load the bean from the cache. + */ public boolean beanCacheLoad(EntityBean bean, EntityBeanIntercept ebi, Object id) { CachedBeanData cacheData = (CachedBeanData) getBeanCache().get(id); if (cacheData == null) { + if (beanLog.isTraceEnabled()) { + beanLog.trace(" LOAD {}({}) - cache miss", cacheName, id); + } return false; } int lazyLoadProperty = ebi.getLazyLoadPropertyIndex(); - if (lazyLoadProperty > -1 && !cacheData.containsProperty(lazyLoadProperty)) { + if (lazyLoadProperty > -1 && !cacheData.isLoaded(lazyLoadProperty)) { + if (beanLog.isTraceEnabled()) { + beanLog.trace(" LOAD {}({}) - cache miss on property", cacheName, id); + } return false; } CachedBeanDataToBean.load(desc, bean, cacheData); + if (beanLog.isDebugEnabled()) { + beanLog.debug(" LOAD {}({}) - hit", cacheName, id); + } return true; } @@ -347,9 +480,15 @@ public final class BeanDescriptorCacheHelp { */ public void handleDelete(Object id, PersistRequestBean deleteRequest) { if (queryCache != null) { + if (queryLog.isDebugEnabled()) { + queryLog.debug(" CLEAR {}(*) - delete trigger", cacheName); + } queryCache.clear(); } if (beanCache != null) { + if (beanLog.isDebugEnabled()) { + beanLog.debug(" REMOVE {}({})", cacheName, id); + } beanCache.remove(id); } for (int i = 0; i < propertiesOneImported.length; i++) { @@ -362,6 +501,9 @@ public final class BeanDescriptorCacheHelp { public void handleInsert(Object id, PersistRequestBean insertRequest) { if (queryCache != null) { + if (queryLog.isDebugEnabled()) { + queryLog.debug(" CLEAR {}(*) - insert trigger", cacheName); + } queryCache.clear(); } for (int i = 0; i < propertiesOneImported.length; i++) { @@ -375,26 +517,73 @@ public final class BeanDescriptorCacheHelp { public void handleUpdate(Object id, PersistRequestBean updateRequest) { if (queryCache != null) { + if (queryLog.isDebugEnabled()) { + queryLog.debug(" CLEAR {}(*) - update trigger", cacheName); + } queryCache.clear(); } - ServerCache cache = getBeanCache(); - CachedBeanData existingData = (CachedBeanData) cache.get(id); - if (existingData != null) { - CachedBeanData newData = CachedBeanDataUpdate.update(desc, existingData, updateRequest.getEntityBean()); - cache.put(id, newData); - if (newData.isNaturalKeyUpdate() && naturalKeyCache != null) { + List> manyCollections = updateRequest.getUpdatedManyCollections(); + if (manyCollections != null) { + // clear the appropriate manyProp caches first + for (int i = 0; i < manyCollections.size(); i++) { + manyPropRemove(id, manyCollections.get(i).getName()); + } + } + + // check if the bean itself was updated + if (!updateRequest.isUpdatedManysOnly()) { + + // update the bean cache entry if it exists + ServerCache cache = getBeanCache(); + CachedBeanData existingData = (CachedBeanData) cache.get(id); + if (existingData != null) { - Object oldKey = newData.getOldNaturalKey(); - Object newKey = newData.getNaturalKey(); - if (oldKey != null) { - naturalKeyCache.remove(oldKey); - } - if (newKey != null) { - naturalKeyCache.put(newKey, id); + if (isCachedDataTooOld(existingData)) { + // just remove the entry from the cache + if (beanLog.isDebugEnabled()) { + beanLog.debug(" REMOVE {}({}) - entry too old", cacheName, id); + } + cache.remove(id); + + } else { + // Update the cache data with the changes from our update + CachedBeanData newData = CachedBeanDataUpdate.update(desc, existingData, updateRequest.getEntityBean()); + if (beanLog.isDebugEnabled()) { + beanLog.debug(" UPDATE {}({})", cacheName, id); + } + cache.put(id, newData); + if (newData.isNaturalKeyUpdate() && naturalKeyCache != null) { + + Object oldKey = newData.getOldNaturalKey(); + Object newKey = newData.getNaturalKey(); + if (natLog.isDebugEnabled()) { + natLog.debug(".. update {} PUT({}, {}) REMOVE({})", cacheName, newKey, id, oldKey); + } + + if (oldKey != null) { + naturalKeyCache.remove(oldKey); + } + if (newKey != null) { + naturalKeyCache.put(newKey, id); + } + } } } } + + if (manyCollections != null) { + for (int i = 0; i < manyCollections.size(); i++) { + BeanPropertyAssocMany many = manyCollections.get(i); + Object manyValue = many.getValue(updateRequest.getEntityBean()); + manyPropPut(many, manyValue, id); + } + } + + } + + private boolean isCachedDataTooOld(CachedBeanData existingData) { + return cacheOptions.isTooOldInMillis(System.currentTimeMillis() - existingData.getWhenCreated()); } /** diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java index ba9bd24cb..56ffda4aa 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/meta/DeployBeanDescriptor.java @@ -337,7 +337,7 @@ public class DeployBeanDescriptor { } /** - * Return the reference options. + * Return the cache options. */ public CacheOptions getCacheOptions() { return cacheOptions; diff --git a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java index 68929f8e3..27ba7ce6e 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java +++ b/src/main/java/com/avaje/ebeaninternal/server/deploy/parse/AnnotationClass.java @@ -8,6 +8,7 @@ import javax.persistence.Table; import javax.persistence.UniqueConstraint; import com.avaje.ebean.annotation.CacheStrategy; +import com.avaje.ebean.annotation.CacheTuning; import com.avaje.ebean.annotation.EntityConcurrencyMode; import com.avaje.ebean.annotation.NamedUpdate; import com.avaje.ebean.annotation.NamedUpdates; @@ -108,8 +109,9 @@ public class AnnotationClass extends AnnotationParser { } CacheStrategy cacheStrategy = cls.getAnnotation(CacheStrategy.class); - if (cacheStrategy != null) { - readCacheStrategy(cacheStrategy); + CacheTuning cacheTuning = cls.getAnnotation(CacheTuning.class); + if (cacheStrategy != null || cacheTuning != null) { + readCacheStrategy(cacheStrategy, cacheTuning); } EntityConcurrencyMode entityConcurrencyMode = cls.getAnnotation(EntityConcurrencyMode.class); @@ -118,18 +120,24 @@ public class AnnotationClass extends AnnotationParser { } } - private void readCacheStrategy(CacheStrategy cacheStrategy) { + private void readCacheStrategy(CacheStrategy cacheStrategy, CacheTuning cacheTuning) { CacheOptions cacheOptions = descriptor.getCacheOptions(); - cacheOptions.setUseCache(cacheStrategy.useBeanCache()); - cacheOptions.setReadOnly(cacheStrategy.readOnly()); - cacheOptions.setWarmingQuery(cacheStrategy.warmingQuery()); - if (cacheStrategy.naturalKey().length() > 0) { - String propName = cacheStrategy.naturalKey().trim(); - DeployBeanProperty beanProperty = descriptor.getBeanProperty(propName); - if (beanProperty != null) { - beanProperty.setNaturalKey(true); - cacheOptions.setNaturalKey(propName); + if (cacheTuning != null) { + cacheOptions.setMaxSecsToLive(cacheTuning.maxSecsToLive()); + cacheOptions.setMaxIdleSecs(cacheTuning.maxIdleSecs()); + } + if (cacheStrategy != null) { + cacheOptions.setUseCache(cacheStrategy.useBeanCache()); + cacheOptions.setReadOnly(cacheStrategy.readOnly()); + cacheOptions.setWarmingQuery(cacheStrategy.warmingQuery()); + if (cacheStrategy.naturalKey().length() > 0) { + String propName = cacheStrategy.naturalKey().trim(); + DeployBeanProperty beanProperty = descriptor.getBeanProperty(propName); + if (beanProperty != null) { + beanProperty.setNaturalKey(true); + cacheOptions.setNaturalKey(propName); + } } } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java index 7c4a86f33..172cdd6b2 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java +++ b/src/main/java/com/avaje/ebeaninternal/server/persist/DefaultPersister.java @@ -32,6 +32,7 @@ import com.avaje.ebeaninternal.server.core.PersistRequestOrmUpdate; import com.avaje.ebeaninternal.server.core.PersistRequestUpdateSql; import com.avaje.ebeaninternal.server.core.Persister; import com.avaje.ebeaninternal.server.core.PstmtBatch; +import com.avaje.ebeaninternal.server.deploy.BeanCollectionUtil; import com.avaje.ebeaninternal.server.deploy.BeanDescriptor; import com.avaje.ebeaninternal.server.deploy.BeanDescriptorManager; import com.avaje.ebeaninternal.server.deploy.BeanManager; @@ -230,6 +231,8 @@ public final class DefaultPersister implements Persister { saveAssocMany(false, request); intercept.setReference(-1); } + + request.checkUpdatedManysOnly(); } else { if (request.isInsert()) { @@ -299,6 +302,9 @@ public final class DefaultPersister implements Persister { // save all the beans in assocMany's after saveAssocMany(false, request); } + + request.checkUpdatedManysOnly(); + } finally { request.unRegisterBean(); } @@ -568,6 +574,9 @@ public final class DefaultPersister implements Persister { // check that property is loaded and not empty uninitialised collection if (request.isLoadedProperty(manys[i]) && !manys[i].isEmptyBeanCollection(parentBean)) { saveMany(new SaveManyPropRequest(insertedParent, manys[i], parentBean, request)); + if (!insertedParent) { + request.addUpdatedManyProperty(manys[i]); + } } } } @@ -580,7 +589,7 @@ public final class DefaultPersister implements Persister { private final boolean insertedParent; private final BeanPropertyAssocMany many; private final EntityBean parentBean; - private final SpiTransaction t; + private final SpiTransaction transaction; private final boolean cascade; private final boolean statelessUpdate; private final boolean deleteMissingChildren; @@ -590,7 +599,7 @@ public final class DefaultPersister implements Persister { this.many = many; this.cascade = many.getCascadeInfo().isSave(); this.parentBean = parentBean; - this.t = request.getTransaction(); + this.transaction = request.getTransaction(); this.statelessUpdate = request.isStatelessUpdate(); this.deleteMissingChildren = request.isDeleteMissingChildren(); } @@ -599,14 +608,14 @@ public final class DefaultPersister implements Persister { this.insertedParent = false; this.many = many; this.parentBean = parentBean; - this.t = t; + this.transaction = t; this.cascade = true; this.statelessUpdate = false; this.deleteMissingChildren = false; } public boolean isSaveIntersection() { - return t.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName()); + return transaction.isSaveAssocManyIntersection(many.getIntersectionTableJoin().getTable(), many.getBeanDescriptor().getName()); } private Object getValue() { @@ -638,7 +647,7 @@ public final class DefaultPersister implements Persister { } private SpiTransaction getTransaction() { - return t; + return transaction; } private boolean isCascade() { @@ -714,7 +723,7 @@ public final class DefaultPersister implements Persister { // check that the list is not null and if it is a BeanCollection // check that is has been populated (don't trigger lazy loading) // For a Map this is a collection of Map.Entry objects and not beans - Collection collection = getActualEntries(details); + Collection collection = BeanCollectionUtil.getActualEntries(details); if (collection == null) { // nothing to do here @@ -1182,33 +1191,7 @@ public final class DefaultPersister implements Persister { } } - /** - * Return the details of the collection or map taking care to avoid - * unnecessary fetching of the data. - */ - private Collection getActualEntries(Object o) { - if (o == null) { - return null; - } - if (o instanceof BeanCollection) { - BeanCollection bc = (BeanCollection) o; - if (!bc.isPopulated()) { - return null; - } - // For maps this is a collection of Map.Entry, otherwise it - // returns a collection of beans - return bc.getActualEntries(); - } - if (o instanceof Map) { - // yes, we want the entrySet (to set the keys) - return ((Map) o).entrySet(); - - } else if (o instanceof Collection) { - return ((Collection) o); - } - throw new PersistenceException("expecting a Map or Collection but got [" + o.getClass().getName() + "]"); - } /** * Create the Persist Request Object that wraps all the objects used to diff --git a/src/test/java/com/avaje/tests/cache/TestCacheCollectionIds.java b/src/test/java/com/avaje/tests/cache/TestCacheCollectionIds.java index b374aecb2..cec5ba8bf 100644 --- a/src/test/java/com/avaje/tests/cache/TestCacheCollectionIds.java +++ b/src/test/java/com/avaje/tests/cache/TestCacheCollectionIds.java @@ -101,24 +101,84 @@ public class TestCacheCollectionIds extends BaseTestCase { ResetBasicData.reset(); OCachedBean cachedBean = new OCachedBean(); + cachedBean.setName("hello"); cachedBean.getCountries().add(Ebean.find(Country.class, "NZ")); cachedBean.getCountries().add(Ebean.find(Country.class, "AU")); Ebean.save(cachedBean); + // used to just load the cache - trigger loading + OCachedBean dummyToLoad = Ebean.find(OCachedBean.class, cachedBean.getId()); + dummyToLoad.getCountries().size(); + + ServerCache cachedBeanCountriesCache = cacheManager.getCollectionIdsCache(OCachedBean.class, "countries"); + CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(cachedBean.getId()); + + // confirm the starting data and cache entry + Assert.assertEquals(2, dummyToLoad.getCountries().size()); + Assert.assertEquals(2, cachedManyIds.getIdList().size()); + + // act OCachedBean loadedBean = Ebean.find(OCachedBean.class, cachedBean.getId()); loadedBean.getCountries().clear(); loadedBean.getCountries().add(Ebean.find(Country.class, "AU")); + Ebean.save(loadedBean); + + // Get the data to assert/check against + OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId()); + cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(result.getId()); + + // assert that data and cache both show correct data + Assert.assertEquals(1, result.getCountries().size()); + Assert.assertEquals(1, cachedManyIds.getIdList().size()); + Assert.assertFalse(cachedManyIds.getIdList().contains("NZ")); + Assert.assertTrue(cachedManyIds.getIdList().contains("AU")); + } + + + /** + * When updating a ManyToMany relations also the collection cache must be updated. + * Alternate to above test where in this case the bean is dirty - loadedBean.setName("goodbye");. + */ + @Test + public void testUpdatingCollectionCacheForManyToManyRelationsWithUpdatedBean() { + // arrange + ResetBasicData.reset(); + + OCachedBean cachedBean = new OCachedBean(); + cachedBean.setName("hello"); + cachedBean.getCountries().add(Ebean.find(Country.class, "NZ")); + cachedBean.getCountries().add(Ebean.find(Country.class, "AU")); + Ebean.save(cachedBean); - OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId()); + // used to just load the cache - trigger loading + OCachedBean dummyToLoad = Ebean.find(OCachedBean.class, cachedBean.getId()); + dummyToLoad.getCountries().size(); + ServerCache cachedBeanCountriesCache = cacheManager.getCollectionIdsCache(OCachedBean.class, "countries"); - CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(result.getId()); + CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(cachedBean.getId()); + + // confirm the starting data and cache entry + Assert.assertEquals(2, dummyToLoad.getCountries().size()); + Assert.assertEquals(2, cachedManyIds.getIdList().size()); + + // act - this time update the name property so the bean is dirty + OCachedBean loadedBean = Ebean.find(OCachedBean.class, cachedBean.getId()); + loadedBean.setName("goodbye"); + loadedBean.getCountries().clear(); + loadedBean.getCountries().add(Ebean.find(Country.class, "AU")); - // assert + Ebean.save(loadedBean); + + // Get the data to assert/check against + OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId()); + cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(result.getId()); + + // assert that data and cache both show correct data Assert.assertEquals(1, result.getCountries().size()); Assert.assertEquals(1, cachedManyIds.getIdList().size()); Assert.assertFalse(cachedManyIds.getIdList().contains("NZ")); @@ -167,17 +227,19 @@ public class TestCacheCollectionIds extends BaseTestCase { Ebean.update(update); - Assert.assertEquals("countries entry was invalidated", 0, cachedBeanCountriesCache.size()); + Assert.assertEquals("countries entry still there (but updated)", 1, cachedBeanCountriesCache.size()); - OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId()); - CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(result.getId()); + CachedManyIds cachedManyIds = (CachedManyIds) cachedBeanCountriesCache.get(update.getId()); - - // assert - Assert.assertEquals(1, result.getCountries().size()); + // assert cache updated Assert.assertEquals(1, cachedManyIds.getIdList().size()); Assert.assertFalse(cachedManyIds.getIdList().contains("NZ")); Assert.assertTrue(cachedManyIds.getIdList().contains("AU")); + + // assert countries good + OCachedBean result = Ebean.find(OCachedBean.class, cachedBean.getId()); + Assert.assertEquals(1, result.getCountries().size()); + } } diff --git a/src/test/java/com/avaje/tests/cache/TestCacheDelete.java b/src/test/java/com/avaje/tests/cache/TestCacheDelete.java index 0dcd17ecb..f534d7fc1 100644 --- a/src/test/java/com/avaje/tests/cache/TestCacheDelete.java +++ b/src/test/java/com/avaje/tests/cache/TestCacheDelete.java @@ -22,20 +22,24 @@ public class TestCacheDelete extends BaseTestCase { OCachedBeanChild child = new OCachedBeanChild(); OCachedBeanChild child2 = new OCachedBeanChild(); - OCachedBean cachedBean = new OCachedBean(); - cachedBean.getChildren().add(child); - cachedBean.getChildren().add(child2); - Ebean.save(cachedBean); + OCachedBean parentBean = new OCachedBean(); + parentBean.getChildren().add(child); + parentBean.getChildren().add(child2); + Ebean.save(parentBean); + + // confirm there are 2 children loaded from the parent + Assert.assertEquals(2, Ebean.find(OCachedBean.class, parentBean.getId()).getChildren().size()); // ensure cache has been populated Ebean.find(OCachedBeanChild.class, child.getId()); child2 = Ebean.find(OCachedBeanChild.class, child2.getId()); - cachedBean = Ebean.find(OCachedBean.class, cachedBean.getId()); + parentBean = Ebean.find(OCachedBean.class, parentBean.getId()); // act Ebean.delete(child2); // assert - Assert.assertTrue(Ebean.find(OCachedBean.class, cachedBean.getId()).getChildren().size() == 1); + OCachedBean beanFromCache = Ebean.find(OCachedBean.class, parentBean.getId()); + Assert.assertEquals(1, beanFromCache.getChildren().size()); } } diff --git a/src/test/java/com/avaje/tests/model/basic/Contact.java b/src/test/java/com/avaje/tests/model/basic/Contact.java index 941e25530..5242b3a2d 100644 --- a/src/test/java/com/avaje/tests/model/basic/Contact.java +++ b/src/test/java/com/avaje/tests/model/basic/Contact.java @@ -13,7 +13,7 @@ import com.avaje.ebean.annotation.CacheStrategy; import com.avaje.ebean.annotation.CreatedTimestamp; @Entity -@CacheStrategy(useBeanCache=true,naturalKey="email") +@CacheStrategy(naturalKey="email") public class Contact { private static final long serialVersionUID = 1L; diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 002bdf8e4..8baa3f1fc 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -32,6 +32,12 @@ + + + + + +