null in maps are handled inconsistently
Description
is duplicated by
relates to
Activity
Former user June 3, 2011 at 6:54 PM
This is an issue for me, as well. In my case, the value is a POJO with several fields. They're normally not null, but can be cleared by application users. If they're all null, this issue occurs.
As a workaround, I put a dummy property on my POJO. I'm a bit irritated that I have to waste DB space with a dummy column and pollute my POJOs to get around this buggy behavior.
Former user April 29, 2011 at 1:12 PM
I want to share how this old and insane hibernate issue could be circumvented fairly easy.
Let's take the next example:
public class Owner {
private Map<KeyClass, ValueClass> mapping = new HashMap<KeyClass, ValueClass>();
public ValueClass getMappingValue(KeyClass keyClass) {
return mapping.get(keyClass);
}
public void addMapping(KeyClass keyClass, ValueClass valueClass) {
mapping.put(keyClass, valueClass);
}
}
This wouldn't work if the value of the map is null.
owner.addMapping(keyClass, null)
1) Add a new dummy boolean column to the mapping table:
ALTER TABLE MAPPING_TABLE ADD DUMMY SMALLINT DEFAULT 0 NOT NULL;
2) Add a <composite-element> as the value of the map:
<map name="mapping" cascade="all" fetch="select" lazy="true" mutable="true" inverse="false" table="MAPPING_TABLE">
<cache usage="read-write" />
<key column="OWNER_ID" not-null="true" />
<map-key-many-to-many class="KeyClass" column="KEY_ID" />
<!-- This composite element is only created as Hibernate doesn't allow null as map value, see https://hibernate.atlassian.net/browse/HHH-772#icft=HHH-772 -->
<composite-element class="MapValueClass">
<many-to-one name="mapValue" column="VALUE_ID" class="ValueClass" />
<property name="dummy" column="DUMMY" not-null="true" type="boolean" />
</composite-element>
</map>
3) Create a MapValueClass class:
public class MapValueClass {
private ValueClass valueClass;
@SuppressWarnings("unused")
private boolean dummy;
@SuppressWarnings("unused")
private MapValueClass() {
// Hibernate
}
public MapValueClass(ValueClass valueClass) {
this.valueClass = valueClass;
}
public ValueClass getValueClass() {
return valueClass;
}
}
4) Change the mapping configuration in Owner class:
class Owner {
Map<KeyClass, MapValueClass> mapping = new HashMap<KeyClass, MapValueClass>();
public ValueClass getMappingValue(KeyClass keyClass) {
return mapping.get(keyClass).getValueClass();
}
public void addMapping(KeyClass keyClass, ValueClass valueClass) {
mapping.put(keyClass, new MapValueClass(valueClass));
}
}
Now the owner.addMapping(keyObject, null) works fine.
Kai Kunstmann April 5, 2011 at 8:19 PM
After long thinking about it, I realize now how this "feature request" of allowing null-values in a map is hard to implement reasonably. I'm writing here to help other people understand the problem and maybe introduce another idea for a solution:
It is true, that a java.util.Map
is defined in terms of a java.util.Set
of java.util.Map.Entry
with no entry having a key equal to any other entry's key in the same entry-set, and that people want to persist every entry of that set no matter if its missing a value, i.e. they want to persist the absence of a value while not giving up the relation between key and map-owner – especially with a <map-key-many-to-many>
. There doubtlessly are use-cases for this, and it truly is a reasonable request – if you look at it from the Java point-of-view. I would like to do that, too, instead of coding complicated workarounds. However, with a simple <one-to-many>
mapping in place of the map-value (even so with a <composite-element>
) it becomes impossible to express the desired absence of value, since the key would have to go into the same row as the missing entity (or component), which means having an empty row with nothing in it – especially no id – but a key and a reference to the owner to express the absence of an entity. This is obviously absurd, and in my opinion the only true reason why this issue won't fix. With a <one-to-many>
mapping the value of a map-entry essentially is the entry, which is likewise questionable.
A solution to this predicament could be the introduction of an <idmap>
with a surrogate <collection-id>
, which obviously cannot contain a <one-to-many>
mapping but could be made capable of expressing absence of value by the requirement of table
attribute and a <many-to-many>
, <many-to-any>
, <element>
or <composite-element>
mapping as with the <idbag>
to serve as a standalone entry-row with an explicit id. There certainly are other considerations to make – it's just an idea...
Stefan Fromm February 10, 2010 at 2:54 PM
I also stumbled on this issue. I expected the map to persist null values. Could not there be a setting in the mappings definition, with which one could enable the saving of keys with null values for map properties? Thus old behaviour could be preserved.
Nelson Murphy February 5, 2010 at 9:34 PM
In case this is useful to anyone else, I ended up forking the Hibernate core (v3.3.1.GA) and modifying PersistentMap to handle null values.
PersistentMap.java.ApplyTo.3.3.1.svn.patch
@@ -182,12 +182,15 @@
}
}
initialize( true );
+ boolean newKey = !map.containsKey( key );
Object old = map.put( key, value );
// would be better to use the element-type to determine
// whether the old and the new are equal here; the problem being
// we do not necessarily have access to the element type in all
// cases
- if ( value != old ) {
+ if ( newKey
+ || value != old
+ || ( (old==null) != (value==null) ) ) {
dirty();
}
return old;
@@ -279,7 +282,7 @@
throws HibernateException, SQLException {
Object element = persister.readElement( rs, owner, descriptor.getSuffixedElementAliases(), getSession() );
Object index = persister.readIndex( rs, descriptor.getSuffixedIndexAliases(), getSession() );
- if ( element!=null ) map.put(index, element);
+ map.put(index, element);
return element;
}
@@ -408,8 +411,12 @@
while ( iter.hasNext() ) {
Map.Entry e = (Map.Entry) iter.next();
Object key = e.getKey();
- if ( e.getValue()!=null && map.get(key)==null ) {
- deletes.add( indexIsFormula ? e.getValue() : key );
+ if ( !map.containsKey(key) ) {
+ if ( !indexIsFormula ) {
+ deletes.add( key );
+ } else if ( e.getValue() != null ) {
+ deletes.add( e.getValue() );
+ }
}
}
return deletes.iterator();
@@ -419,18 +426,17 @@
throws HibernateException {
final Map sn = (Map) getSnapshot();
Map.Entry e = (Map.Entry) entry;
- return e.getValue()!=null && sn.get( e.getKey() )==null;
+ return !sn.containsKey( e.getKey() );
}
- public boolean needsUpdating(Object entry, int i, Type elemType)
- throws HibernateException {
- final Map sn = (Map) getSnapshot();
- Map.Entry e = (Map.Entry) entry;
- Object snValue = sn.get( e.getKey() );
- return e.getValue()!=null &&
- snValue!=null &&
- elemType.isDirty( snValue, e.getValue(), getSession() );
- }
+ public boolean needsUpdating(Object entry, int i, Type elemType)
+ throws HibernateException {
+ final Map sn = (Map) getSnapshot();
+ Map.Entry e = (Map.Entry) entry;
+ Object snValue = sn.get( e.getKey() );
+ return sn.containsKey(e.getKey()) &&
+ elemType.isDirty( snValue, e.getValue(), getSession() );
+ }
public Object getIndex(Object entry, int i, CollectionPersister persister) {
@@ -457,7 +463,7 @@
}
public boolean entryExists(Object entry, int i) {
- return ( (Map.Entry) entry ).getValue()!=null;
+ return map.containsKey( ((Map.Entry) entry).getKey() );
}
final class Clear implements DelayedOperation {
Again, this patch is for v3.3.1 from a year ago – I haven't checked to see if the class has changed since then.
regarding case 00004729.
group.getUsers().put("something", null);
Does not result in any insert.
Inserting "something", null manually into the underlying table and
when hibernate reads the map will have "something"->null in the map.