Description
A certain sequence of events can result in an Embeddable object instance, with a null identity, being admitted to the DataCache. This has a number of consequences, ranging from errors by third party datacache implementations which cannot accept a null key value (which the native OpenJPA datacache impl can), to possible data integrity issues since a null key can refer to any kind of object type (since the regular key includes both
identity and identity type in its information.)
Consider the following entities:
@Entity
public class LeftHand {
@Id private long id;
private String strData;
@OneToMany private Collection<RightHand> rhList;
...
@Entity
public class RightHand {
@Id private long id;
@Basic private String strData;
@Embedded private EmbeddableData emb;
...
@Embeddable
public class EmbeddableData {
@Basic private String embeddedString;
@Basic(fetch=FetchType.LAZY) private String lazyEmbeddedString;
After committing the above entities with filled data, the contents of the L2 datacache is as follows:
this TestDataCache (id=42)
cm ConcurrentDataCache$1 (id=43)
[0] ConcurrentHashMap$Entry (id=68)
key LongId (id=73)
value DataCachePCDataImpl (id=74)
_cache "default" (id=85)
_data Object[4] (id=88)
[0] Long (id=102)
[1] AbstractPCData$ProxyDataList (id=104)
[2] "left hand" (id=110)
[3] Long (id=111)
_exp -1
_fieldImpl null
_impl null
_loaded BitSet (id=91)
_oid LongId (id=73)
_type Class<T> (org.apache.openjpa.persistence.cache.jpa.model.LeftHand) (id=94)
_version Integer (id=99)
[1] ConcurrentHashMap$Entry (id=69)
key LongId (id=116)
value DataCachePCDataImpl (id=117)
_cache "default" (id=85)
_data Object[3] (id=120)
[0] DataCachePCDataImpl (id=125)
_cache "default" (id=85)
_data Object[2] (id=128)
[0] "Embedded String" (id=130)
[1] "Lazy String" (id=131)
_exp -1
_fieldImpl null
_impl null
_loaded BitSet (id=129)
_oid BrokerImpl$StateManagerId (id=3611)
_type Class<T> (org.apache.openjpa.persistence.cache.jpa.model.EmbeddableData) (id=2336)
_version null
[1] Long (id=126)
[2] "right hand" (id=127)
_exp -1
_fieldImpl null
_impl null
_loaded BitSet (id=121)
_oid LongId (id=116)
_type Class<T> (org.apache.openjpa.persistence.cache.jpa.model.RightHand) (id=122)
_version null
Here we see that the datacache contains two entries, one for LeftHand, one for RightHand. Completely expected, and at this point Life is Good.
After purging the persistence context and L2 cache, a query "SELECT lh from LeftHand lh" is executed, and iterating through its result list yields the following L2 datacache state:
this TestDataCache (id=42)
cm ConcurrentDataCache$1 (id=43)
[0] ConcurrentHashMap$Entry (id=3657)
key LongId (id=3660)
value DataCachePCDataImpl (id=3661)
_cache "default" (id=85)
_data Object[4] (id=3662)
[0] Long (id=3665)
[1] null
[2] "left hand" (id=3666)
[3] Long (id=111)
_exp -1
_fieldImpl null
_impl null
_loaded BitSet (id=3667)
_oid LongId (id=3660)
_type Class<T> (org.apache.openjpa.persistence.cache.jpa.model.LeftHand) (id=94)
_version Long (id=111)
[1] ConcurrentHashMap$Entry (id=3658)
key null
value DataCachePCDataImpl (id=3670)
_cache "default" (id=85)
_data Object[2] (id=3671)
[0] "Embedded String" (id=3673)
[1] "Lazy String" (id=3674)
_exp -1
_fieldImpl null
_impl null
_loaded BitSet (id=3675)
_oid null
_type Class<T> (org.apache.openjpa.persistence.cache.jpa.model.EmbeddableData) (id=2336)
_version null
[2] ConcurrentHashMap$Entry (id=3659)
key LongId (id=3682)
value DataCachePCDataImpl (id=3683)
_cache "default" (id=85)
_data Object[3] (id=3686)
[0] DataCachePCDataImpl (id=3688)
_cache "default" (id=85)
_data Object[2] (id=3691)
[0] "Embedded String" (id=3673)
[1] "Lazy String" (id=3674)
_exp -1
_fieldImpl null
_impl null
_loaded BitSet (id=3694)
_oid BrokerImpl$StateManagerId (id=3695)
_type Class<T> (org.apache.openjpa.persistence.cache.jpa.model.EmbeddableData) (id=2336)
_version null
[1] Long (id=3689)
[2] "right hand" (id=3690)
_exp -1
_fieldImpl null
_impl null
_loaded BitSet (id=3687)
_oid LongId (id=3682)
_type Class<T> (org.apache.openjpa.persistence.cache.jpa.model.RightHand) (id=122)
_version null
A specific sequence of events, requiring the embeddable to contain a lazy loaded field (which forces a ROPStoreManager.load() in the AbstractPCData.toEmbeddedData() path results in the DataCacheStoreManager.load() operation attempting to admit the embeddable into the Datacache as if it was an entity type (object ConcurrentHashMap$Entry(id=3658) at index [1].) The embeddable has no identity of its own, so is inserted into the cache with a null key value. The OpenJPA datacache impl replaces the null value with an object that represents the null value – while other datacache implementations attempt to add the key to a regular map instance which results in a NullPointerException.
The attached patch looks for the attempt to insert a new (embeddable) item into the datacache at updateDataCache() - admission into the datacache is rejected if this condition is met. I've further modified cacheStateManager() to reject any attempt to admit an entry into the datacache that has a null entity to guard against other unidentified paths which could lead to a similar issue, as a null key identity in a datacache is meaningless. A unit test has been added to verify that the fix corrects the issue.