entityLookup)
    {
        this(cache, CACHE_REGION_DEFAULT, entityLookup);
    }
    
    /**
     * Construct the lookup cache, using the given cache region.
     * 
     * All keys will be unique to the given cache region, allowing the cache to be shared
     * between instances of this class.
     * 
     * @param cache                 the cache that will back the two-way lookups; null to have no backing
     *                              in a cache.
     * @param cacheRegion           the region within the cache to use.
     * @param entityLookup          the instance that is able to find and persist entities
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public EntityLookupCache(SimpleCache cache, String cacheRegion, EntityLookupCallbackDAO entityLookup)
    {
        ParameterCheck.mandatory("cacheRegion", cacheRegion);
        ParameterCheck.mandatory("entityLookup", entityLookup);
        this.cache = cache;
        this.cacheRegion = cacheRegion;
        this.entityLookup = entityLookup;
    }
    
    /**
     * Find the entity associated with the given key.
     * The {@link EntityLookupCallbackDAO#findByKey(Serializable) entity callback} will be used if necessary.
     * 
     * It is up to the client code to decide if a null 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 (null not allowed)
     * @return                      Returns the key-value pair or null if the key doesn't reference an entity
     */
    @SuppressWarnings("unchecked")
    public Pair getByKey(K key)
    {
        if (key == null)
        {
            throw new IllegalArgumentException("An entity lookup key may not be null");
        }
        // Handle missing cache
        if (cache == null)
        {
            return entityLookup.findByKey(key);
        }
        
        CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
        // Look in the cache
        V value = (V) cache.get(keyCacheKey);
        if (value != null)
        {
            if (value.equals(VALUE_NOT_FOUND))
            {
                // We checked before
                return null;
            }
            else if (value.equals(VALUE_NULL))
            {
                return new Pair(key, null);
            }
            else
            {
                return new Pair(key, value);
            }
        }
        // Resolve it
        Pair entityPair = entityLookup.findByKey(key);
        if (entityPair == null)
        {
            // Cache "not found"
            cache.put(keyCacheKey, VALUE_NOT_FOUND);
        }
        else
        {
            value = entityPair.getSecond();
            // 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)
            {
                CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(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
        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.
     * 
     * It is up to the client code to decide if a null 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 (null is allowed)
     * @return                      Returns the key-value pair or null if the value doesn't reference an entity
     */
    @SuppressWarnings("unchecked")
    public Pair getByValue(V value)
    {
        // Handle missing cache
        if (cache == null)
        {
            return entityLookup.findByValue(value);
        }
        
        // Get the value key.
        // The cast to (VK) is counter-intuitive, but works because they're all just Serializable
        // It's nasty, but hidden from the cache client code.
        VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
        // Check if the value has a good key
        if (valueKey == null)
        {
            return entityLookup.findByValue(value);
        }
        
        // Look in the cache
        CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
        K key = (K) cache.get(valueCacheKey);
        // Check if we have looked this up already
        if (key != null)
        {
            // We checked before and ...
            if (key.equals(VALUE_NOT_FOUND))
            {
                // ... it didn't exist
                return null;
            }
            else
            {
                // ... it did exist
                return getByKey(key);
            }
        }
        // Resolve it
        Pair entityPair = entityLookup.findByValue(value);
        if (entityPair == null)
        {
            // Cache "not found"
            cache.put(valueCacheKey, VALUE_NOT_FOUND);
        }
        else
        {
            key = entityPair.getFirst();
            // Cache the key
            cache.put(valueCacheKey, key);
            cache.put(
                    new CacheRegionKey(cacheRegion, key),
                    (entityPair.getSecond() == null ? VALUE_NULL : entityPair.getSecond()));
        }
        // Done
        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 (null is allowed)
     * @return                      Returns the key-value pair (new or existing and never null)
     */
    @SuppressWarnings("unchecked")
    public Pair getOrCreateByValue(V value)
    {
        // Handle missing cache
        if (cache == null)
        {
            Pair entityPair = entityLookup.findByValue(value);
            if (entityPair == null)
            {
                entityPair = entityLookup.createValue(value);
            }
            return entityPair;
        }
        
        // Get the value key
        // The cast to (VK) is counter-intuitive, but works because they're all just Serializable.
        // It's nasty, but hidden from the cache client code.
        VK valueKey = (value == null) ? (VK)VALUE_NULL : entityLookup.getValueKey(value);
        // Check if the value has a good key
        if (valueKey == null)
        {
            Pair entityPair = entityLookup.findByValue(value);
            if (entityPair == null)
            {
                entityPair = entityLookup.createValue(value);
                // Cache the value
                cache.put(
                        new CacheRegionKey(cacheRegion, entityPair.getFirst()),
                        (entityPair.getSecond() == null ? VALUE_NULL : entityPair.getSecond()));
            }
            return entityPair;
        }
        
        // Look in the cache
        CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(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))
        {
            return new Pair(key, value);
        }
        // Resolve it
        Pair entityPair = entityLookup.findByValue(value);
        if (entityPair == null)
        {
            // Create it
            entityPair = entityLookup.createValue(value);
        }
        key = entityPair.getFirst();
        // Cache the key and value
        cache.put(valueCacheKey, key);
        cache.put(
                new CacheRegionKey(cacheRegion, key),
                (value == null ? VALUE_NULL : value));
        // Done
        return entityPair;
    }
    
    /**
     * Update the entity associated with the given key.
     * The {@link EntityLookupCallbackDAO#updateValue(Serializable, Object)} callback
     * will be used if necessary.
     * 
     * It is up to the client code to decide if a 0 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 (null not allowed)
     * @param value                 The new entity value (may be null)
     * @return                      Returns the row update count.
     */
    @SuppressWarnings("unchecked")
    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)
        // but leave the key as it will get updated
        removeByKey(key, false);
        
        // 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)
        {
            // There is a good value key, cache by value
            CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
            cache.put(valueCacheKey, key);
        }
        // Cache by key
        cache.put(
                new CacheRegionKey(cacheRegion, key),
                (value == null ? VALUE_NULL : value));
        // Done
        return updateCount;
    }
    
    /**
     * Cache-only operation: Get the key for a given value key (note: not 'value' but 'value key').
     * 
     * @param value                 The entity value key, which must be valid (null not allowed)
     * @return                      The entity key (may be null)
     */
    @SuppressWarnings("unchecked")
    public K getKey(VK valueKey)
    {
        // There is a good value key, cache by value
        CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
        K key = (K) cache.get(valueCacheKey);
        // Check if we have looked this up already
        if (key != null && key.equals(VALUE_NOT_FOUND))
        {
            key = null;
        }
        return key;
    }
    
    /**
     * Cache-only operation: Get the value for a given key
     * 
     * @param key                   The entity key, which may be valid or invalid (null not allowed)
     * @return                      The entity value (may be null)
     */
    @SuppressWarnings("unchecked")
    public V getValue(K key)
    {
        CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
        // Look in the cache
        V value = (V) cache.get(keyCacheKey);
        if (value == null)
        {
            return null;
        }
        else if (value.equals(VALUE_NOT_FOUND))
        {
            // We checked before
            return null;
        }
        else if (value.equals(VALUE_NULL))
        {
            return null;
        }
        else
        {
            return value;
        }
    }
    
    /**
     * Cache-only operation: Update the cache's value
     * 
     * @param key                   The entity key, which may be valid or invalid (null not allowed)
     * @param value                 The new entity value (may be null)
     */
    @SuppressWarnings("unchecked")
    public void setValue(K key, V value)
    {
        // Handle missing cache
        if (cache == null)
        {
            return;
        }
        
        // Remove entries for the key (bidirectional removal removes the old value as well)
        // but leave the key as it will get updated
        removeByKey(key, false);
        
        // 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)
        {
            // There is a good value key, cache by value
            CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
            cache.put(valueCacheKey, key);
        }
        // Cache by key
        cache.put(
                new CacheRegionKey(cacheRegion, key),
                (value == null ? VALUE_NULL : value));
        // Done
    }
    
    /**
     * Delete the entity associated with the given key.
     * The {@link EntityLookupCallbackDAO#deleteByKey(Serializable)} callback will be used if necessary.
     * 
     * It is up to the client code to decide if a 0 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 (null 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.
     * 
     * It is up to the client code to decide if a 0 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 (null 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.
     */
    public void removeByKey(K key)
    {
        // Handle missing cache
        if (cache == null)
        {
            return;
        }
        
        removeByKey(key, true);
    }
    
    /**
     * Cache-only operation: Remove all cache values associated with the given key.
     * 
     * @param removeKey             true to remove the given key's entry
     */
    @SuppressWarnings("unchecked")
    private void removeByKey(K key, boolean removeKey)
    {
        CacheRegionKey keyCacheKey = new CacheRegionKey(cacheRegion, key);
        V value = (V) cache.get(keyCacheKey);
        if (value != null && !value.equals(VALUE_NOT_FOUND))
        {
            // Get the value key and remove it
            VK valueKey = entityLookup.getValueKey(value);
            if (valueKey != null)
            {
                CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(cacheRegion, valueKey);
                cache.remove(valueCacheKey);
            }
        }
        if (removeKey)
        {
            cache.remove(keyCacheKey);
        }
    }
    
    /**
     * Cache-only operation: Remove all cache values associated with the given value
     * 
     * @param value                 The entity value (null 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
        CacheRegionValueKey valueCacheKey = new CacheRegionValueKey(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);
    }
    
    /**
     * Cache-only operation: Remove all cache entries
     * 
     * NOTE: This operation removes ALL entries for ALL cache regions.
     */
    public void clear()
    {
        // Handle missing cache
        if (cache == null)
        {
            return;
        }
        cache.clear();
    }
    
    /**
     * Key-wrapper used to separate cache regions, allowing a single cache to be used for different
     * purposes.
     * This class is distinct from the ID key so that ID-based lookups don't class with value-based lookups. 
     */
    private static class CacheRegionKey implements Serializable
    {
        private static final long serialVersionUID = -213050301938804468L;
        private final String cacheRegion;
        private final Serializable cacheKey;
        private final int hashCode;
        private CacheRegionKey(String cacheRegion, Serializable cacheKey)
        {
            this.cacheRegion = cacheRegion;
            this.cacheKey = cacheKey;
            this.hashCode = cacheRegion.hashCode() + cacheKey.hashCode();
        }
        @Override
        public String toString()
        {
            return cacheRegion + "." + cacheKey.toString();
        }
        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
            {
                return true;
            }
            else if (!(obj instanceof CacheRegionKey))
            {
                return false;
            }
            CacheRegionKey that = (CacheRegionKey) obj;
            return this.cacheRegion.equals(that.cacheRegion) && this.cacheKey.equals(that.cacheKey);
        }
        @Override
        public int hashCode()
        {
            return hashCode;
        }
    }
    
    /**
     * Value-key-wrapper used to separate cache regions, allowing a single cache to be used for different
     * purposes.
     * This class is distinct from the region key so that ID-based lookups don't class with value-based lookups. 
     */
    private static class CacheRegionValueKey implements Serializable
    {
        private static final long serialVersionUID = 5838308035326617927L;
        
        private final String cacheRegion;
        private final Serializable cacheValueKey;
        private final int hashCode;
        private CacheRegionValueKey(String cacheRegion, Serializable cacheValueKey)
        {
            this.cacheRegion = cacheRegion;
            this.cacheValueKey = cacheValueKey;
            this.hashCode = cacheRegion.hashCode() + cacheValueKey.hashCode();
        }
        @Override
        public String toString()
        {
            return cacheRegion + "." + cacheValueKey.toString();
        }
        @Override
        public boolean equals(Object obj)
        {
            if (this == obj)
            {
                return true;
            }
            else if (!(obj instanceof CacheRegionValueKey))
            {
                return false;
            }
            CacheRegionValueKey that = (CacheRegionValueKey) obj;
            return this.cacheRegion.equals(that.cacheRegion) && this.cacheValueKey.equals(that.cacheValueKey);
        }
        @Override
        public int hashCode()
        {
            return hashCode;
        }
    }
}