Further EntityLookupCache enhancements and fixes

- Support for update and delete operations
 - Some cache-only operations
 - TODO: expose cache clear


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@15650 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Derek Hulley
2009-08-10 09:27:06 +00:00
parent 8fa726f7df
commit 720defedb4
2 changed files with 372 additions and 9 deletions

View File

@@ -27,8 +27,10 @@ package org.alfresco.repo.cache.lookup;
import java.io.Serializable; import java.io.Serializable;
import org.alfresco.repo.cache.SimpleCache; import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.transaction.RetryingTransactionHelper;
import org.alfresco.util.Pair; import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck; import org.alfresco.util.ParameterCheck;
import org.springframework.dao.ConcurrencyFailureException;
/** /**
* A cache for two-way lookups of database entities. These are characterized by having a unique * A cache for two-way lookups of database entities. These are characterized by having a unique
@@ -117,6 +119,84 @@ public class EntityLookupCache<K extends Serializable, V extends Object, VK exte
* @return Return the newly-created entity ID-value pair * @return Return the newly-created entity ID-value pair
*/ */
Pair<K1, V1> createValue(V1 value); Pair<K1, V1> createValue(V1 value);
/**
* Update the entity identified by the given key.
* <p/>
* It is up to the client code to decide if a <tt>0</tt> return value indicates a concurrency violation
* or not.
*
* @param key the existing key (ID) used to identify the entity (never <tt>null</tt>)
* @param value the new value
* @return Returns the row update count.
* @throws UnsupportedOperationException if entity updates are not supported
*/
int updateValue(K1 key, V1 value);
/**
* Delete an entity for the given key.
* <p/>
* It is up to the client code to decide if a <tt>0</tt> return value indicates a concurrency violation
* or not.
*
* @param key the key (ID) used to identify the entity (never <tt>null</tt>)
* @return Returns the row deletion count.
* @throws UnsupportedOperationException if entity deletion is not supported
*/
int deleteByKey(K1 key);
/**
* Delete an entity for the given value.
* <p/>
* It is up to the client code to decide if a <tt>0</tt> return value indicates a concurrency violation
* or not.
*
* @param value the value (business object) used to identify the enitity (<tt>null</tt> allowed)
* @return Returns the row deletion count.
* @throws UnsupportedOperationException if entity deletion is not supported
*/
int deleteByValue(V1 value);
}
/**
* Adaptor for implementations that support immutable entities. The update and delete operations
* throw {@link UnsupportedOperationException}.
*
* @author Derek Hulley
* @since 3.3
*/
public static abstract class EntityLookupCallbackDAOAdaptor<K2 extends Serializable, V2 extends Object, VK2 extends Serializable>
implements EntityLookupCallbackDAO<K2, V2, VK2>
{
/**
* Disallows the operation.
*
* @throws UnsupportedOperationException always
*/
public int updateValue(K2 key, V2 value)
{
throw new UnsupportedOperationException();
}
/**
* Disallows the operation.
*
* @throws UnsupportedOperationException always
*/
public int deleteByKey(K2 key)
{
throw new UnsupportedOperationException("Entity deletion by key is not supported");
}
/**
* Disallows the operation.
*
* @throws UnsupportedOperationException always
*/
public int deleteByValue(V2 value)
{
throw new UnsupportedOperationException("Entity deletion by value is not supported");
}
} }
/** /**
@@ -180,6 +260,17 @@ public class EntityLookupCache<K extends Serializable, V extends Object, VK exte
this.entityLookup = entityLookup; this.entityLookup = entityLookup;
} }
/**
* Find the entity associated with the given key.
* The {@link EntityLookupCallbackDAO#findByKey(Serializable) entity callback} will be used if necessary.
* <p/>
* It is up to the client code to decide if a <tt>null</tt> return value indicates a concurrency violation
* or not; the former would normally result in a concurrency-related exception such as
* {@link ConcurrencyFailureException}.
*
* @param key The entity key, which may be valid or invalid (<tt>null</tt> not allowed)
* @return Returns the key-value pair or <tt>null</tt> if the key doesn't reference an entity
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Pair<K, V> getByKey(K key) public Pair<K, V> getByKey(K key)
{ {
@@ -193,9 +284,9 @@ public class EntityLookupCache<K extends Serializable, V extends Object, VK exte
return entityLookup.findByKey(key); return entityLookup.findByKey(key);
} }
CacheRegionKey cacheKey = new CacheRegionKey(cacheRegion, key); CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
// Look in the cache // Look in the cache
V value = (V) cache.get(cacheKey); V value = (V) cache.get(keyCacheKey);
if (value != null) if (value != null)
{ {
if (value.equals(VALUE_NOT_FOUND)) if (value.equals(VALUE_NOT_FOUND))
@@ -217,20 +308,39 @@ public class EntityLookupCache<K extends Serializable, V extends Object, VK exte
if (entityPair == null) if (entityPair == null)
{ {
// Cache "not found" // Cache "not found"
cache.put(cacheKey, VALUE_NOT_FOUND); cache.put(keyCacheKey, VALUE_NOT_FOUND);
} }
else else
{ {
value = entityPair.getSecond(); value = entityPair.getSecond();
// Cache the value // Get the value key
cache.put( VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
cacheKey, // Check if the value has a good key
(value == null ? VALUE_NULL : value)); if (valueKey != null)
{
CacheRegionKey valueCacheKey = new CacheRegionKey(cacheRegion, valueKey);
// The key is good, so we can cache the value
cache.put(valueCacheKey, key);
cache.put(
keyCacheKey,
(value == null ? VALUE_NULL : value));
}
} }
// Done // Done
return entityPair; return entityPair;
} }
/**
* Find the entity associated with the given value.
* The {@link EntityLookupCallbackDAO#findByValue(Object) entity callback} will be used if no entry exists in the cache.
* <p/>
* It is up to the client code to decide if a <tt>null</tt> return value indicates a concurrency violation
* or not; the former would normally result in a concurrency-related exception such as
* {@link ConcurrencyFailureException}.
*
* @param value The entity value, which may be valid or invalid (<tt>null</tt> is allowed)
* @return Returns the key-value pair or <tt>null</tt> if the value doesn't reference an entity
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Pair<K, V> getByValue(V value) public Pair<K, V> getByValue(V value)
{ {
@@ -288,6 +398,14 @@ public class EntityLookupCache<K extends Serializable, V extends Object, VK exte
return entityPair; return entityPair;
} }
/**
* Find the entity associated with the given value and create it if it doesn't exist.
* The {@link EntityLookupCallbackDAO#findByValue(Object)} and {@link EntityLookupCallbackDAO#createValue(Object)}
* will be used if necessary.
*
* @param value The entity value (<tt>null</tt> is allowed)
* @return Returns the key-value pair (new or existing and never <tt>null</tt>)
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Pair<K, V> getOrCreateByValue(V value) public Pair<K, V> getOrCreateByValue(V value)
{ {
@@ -342,9 +460,122 @@ public class EntityLookupCache<K extends Serializable, V extends Object, VK exte
return entityPair; return entityPair;
} }
/**
* Update the entity associated with the given key.
* The {@link EntityLookupCallbackDAO#updateValue(Serializable, Object)} callback
* will be used if necessary.
* <p/>
* It is up to the client code to decide if a <tt>0</tt> return value indicates a concurrency violation
* or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised
* by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}.
*
* @param key The entity key, which may be valid or invalid (<tt>null</tt> not allowed)
* @param value The new entity value (may be null <tt>null</tt>)
* @return Returns the row update count.
*/
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void remove(K key) public int updateValue(K key, V value)
{ {
// Handle missing cache
if (cache == null)
{
return entityLookup.updateValue(key, value);
}
// Remove entries for the key (bidirectional removal removes the old value as well)
removeByKey(key);
// Do the update
int updateCount = entityLookup.updateValue(key, value);
if (updateCount == 0)
{
// Nothing was done
return updateCount;
}
// Get the value key.
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
// Check if the value has a good key
if (valueKey == null)
{
// No good key, so no caching
return updateCount;
}
// Cache the key and value
CacheRegionKey valueCacheKey = new CacheRegionKey(cacheRegion, valueKey);
cache.put(valueCacheKey, key);
cache.put(
new CacheRegionKey(cacheRegion, key),
(value == null ? VALUE_NULL : value));
// Done
return updateCount;
}
/**
* Delete the entity associated with the given key.
* The {@link EntityLookupCallbackDAO#deleteByKey(Serializable)} callback will be used if necessary.
* <p/>
* It is up to the client code to decide if a <tt>0</tt> return value indicates a concurrency violation
* or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised
* by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}.
*
* @param key the entity key, which may be valid or invalid (<tt>null</tt> not allowed)
* @return Returns the row deletion count
*/
public int deleteByKey(K key)
{
// Handle missing cache
if (cache == null)
{
return entityLookup.deleteByKey(key);
}
// Remove entries for the key (bidirectional removal removes the old value as well)
removeByKey(key);
// Do the delete
return entityLookup.deleteByKey(key);
}
/**
* Delete the entity having the given value..
* The {@link EntityLookupCallbackDAO#deleteByValue(Object)} callback will be used if necessary.
* <p/>
* It is up to the client code to decide if a <tt>0</tt> return value indicates a concurrency violation
* or not; usually the former will generate {@link ConcurrencyFailureException} or something recognised
* by the {@link RetryingTransactionHelper#RETRY_EXCEPTIONS RetryingTransactionHelper}.
*
* @param key the entity value, which may be valid or invalid (<tt>null</tt> allowed)
* @return Returns the row deletion count
*/
public int deleteByValue(V value)
{
// Handle missing cache
if (cache == null)
{
return entityLookup.deleteByValue(value);
}
// Remove entries for the value
removeByValue(value);
// Do the delete
return entityLookup.deleteByValue(value);
}
/**
* Cache-only operation: Remove all cache values associated with the given key.
*/
@SuppressWarnings("unchecked")
public void removeByKey(K key)
{
// Handle missing cache
if (cache == null)
{
return;
}
CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key); CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
V value = (V) cache.get(keyCacheKey); V value = (V) cache.get(keyCacheKey);
if (value != null && !value.equals(VALUE_NOT_FOUND)) if (value != null && !value.equals(VALUE_NOT_FOUND))
@@ -357,6 +588,39 @@ public class EntityLookupCache<K extends Serializable, V extends Object, VK exte
cache.remove(keyCacheKey); cache.remove(keyCacheKey);
} }
/**
* Cache-only operation: Remove all cache values associated with the given value
*
* @param value The entity value (<tt>null</tt> is allowed)
*/
@SuppressWarnings("unchecked")
public void removeByValue(V value)
{
// Handle missing cache
if (cache == null)
{
return;
}
// Get the value key
VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
if (valueKey == null)
{
// No key generated for the value. There is nothing that can be done.
return;
}
// Look in the cache
CacheRegionKey valueCacheKey = new CacheRegionKey(cacheRegion, valueKey);
K key = (K) cache.get(valueCacheKey);
// Check if the value is already mapped to a key
if (key != null && !key.equals(VALUE_NOT_FOUND))
{
CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
cache.remove(keyCacheKey);
}
cache.remove(valueCacheKey);
}
/** /**
* Key-wrapper used to separate cache regions, allowing a single cache to be used for different * Key-wrapper used to separate cache regions, allowing a single cache to be used for different
* purposes. * purposes.

View File

@@ -163,7 +163,56 @@ public class EntityLookupCacheTest extends TestCase implements EntityLookupCallb
assertTrue(database.containsKey(entityPairNull.getFirst())); assertTrue(database.containsKey(entityPairNull.getFirst()));
assertNull(database.get(entityPairNull.getFirst())); assertNull(database.get(entityPairNull.getFirst()));
assertEquals(entityPairNull, entityPairCheck); assertEquals(entityPairNull, entityPairCheck);
} }
public void testUpdate() throws Exception
{
TestValue valueOne = new TestValue(getName() + "-ONE");
TestValue valueTwo = new TestValue(getName() + "-TWO");
Pair<Long, Object> entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
assertNotNull(entityPairOne);
Long id = entityPairOne.getFirst();
assertEquals(valueOne.val, database.get(id));
assertEquals(2, cache.getKeys().size());
// Update
int updateCount = entityLookupCacheA.updateValue(id, valueTwo);
assertEquals("Update count was incorrect.", 1, updateCount);
assertEquals(valueTwo.val, database.get(id));
assertEquals(2, cache.getKeys().size());
}
public void testDeleteByKey() throws Exception
{
TestValue valueOne = new TestValue(getName() + "-ONE");
Pair<Long, Object> entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
assertNotNull(entityPairOne);
Long id = entityPairOne.getFirst();
assertEquals(valueOne.val, database.get(id));
assertEquals(2, cache.getKeys().size());
// Delete
int deleteCount = entityLookupCacheA.deleteByKey(id);
assertEquals("Delete count was incorrect.", 1, deleteCount);
assertNull(database.get(id));
assertEquals(0, cache.getKeys().size());
}
public void testDeleteByValue() throws Exception
{
TestValue valueOne = new TestValue(getName() + "-ONE");
Pair<Long, Object> entityPairOne = entityLookupCacheA.getOrCreateByValue(valueOne);
assertNotNull(entityPairOne);
Long id = entityPairOne.getFirst();
assertEquals(valueOne.val, database.get(id));
assertEquals(2, cache.getKeys().size());
// Delete
int deleteCount = entityLookupCacheA.deleteByValue(valueOne);
assertEquals("Delete count was incorrect.", 1, deleteCount);
assertNull(database.get(id));
assertEquals(0, cache.getKeys().size());
}
/** /**
* Helper class to represent business object * Helper class to represent business object
@@ -251,4 +300,54 @@ public class EntityLookupCacheTest extends TestCase implements EntityLookupCallb
database.put(newKey, dbValue); database.put(newKey, dbValue);
return new Pair<Long, Object>(newKey, value); return new Pair<Long, Object>(newKey, value);
} }
public int updateValue(Long key, Object value)
{
assertNotNull(key);
assertTrue(value == null || value instanceof TestValue);
// Find it
Pair<Long, Object> entityPair = findByKey(key);
if (entityPair == null)
{
return 0;
}
else
{
database.put(key, ((TestValue)value).val);
return 1;
}
}
public int deleteByKey(Long key)
{
assertNotNull(key);
if (database.containsKey(key))
{
database.remove(key);
return 1;
}
else
{
return 0;
}
}
public int deleteByValue(Object value)
{
assertTrue(value == null || value instanceof TestValue);
// Find it
Pair<Long, Object> entityPair = findByValue(value);
if (entityPair == null)
{
return 0;
}
else
{
database.remove(entityPair.getFirst());
return 1;
}
}
} }