Hello everyone!
I'm quite new to Hibernate (has been fiddling with it for a week now). About 3 days ago I came across a very annoying bug that I was unable to correct ever since.
The problem lies in a many-to-many relationship between objects called RawFilters, DataOrigins and RawData. Specifically a certain type of RawFilter (namely ExpiryFilter) has got a many-to-many relationship with RawData indexed by DataOrigins. The mappings I use are (I left out only the important parts):
A mapping for filters:
Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class abstract="true" lazy="false" name="calex.raw.RawFilter" table="filters">
<id access="field" column="filter_id" name="id" type="integer">
<generator class="native"/>
</id>
<discriminator column="filter_type" type="string"/>
<subclass discriminator-value="expire" name="calex.raw.ExpiryFilter">
<map cascade="all" lazy="false" inverse="false" name="history">
<key column="filter_id"/>
<index-many-to-many class="calex.origins.DataOrigin" column="origin_id"/>
<many-to-many class="calex.raw.RawData" column="raw_id"/>
</map>
<property access="field" column="minutes" name="minutes" type="integer"/>
</subclass>
... some other subclasses go in here ...
</class>
</hibernate-mapping>
A mapping for DataOrigins:
Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class abstract="true" lazy="false" name="calex.origins.DataOrigin" table="origins">
<id access="field" column="origin_id" name="id" type="integer">
<generator class="native"/>
</id>
<discriminator column="origin_type" type="string"/>
<subclass abstract="true" discriminator-value="uri" name="calex.origins.URIOrigin">
<property column="uri" length="65536" name="path" not-null="true" type="text"/>
</subclass>
<subclass discriminator-value="url" extends="calex.origins.URIOrigin" name="calex.origins.URLOrigin">
<property column="uri" length="65536" name="path" not-null="true" type="text"/>
</subclass>
... some other subclasses go in here ...
</class>
</hibernate-mapping>
A mapping for RawData:
Code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class lazy="false" name="calex.raw.RawData" table="raw_data">
<id access="field" column="raw_id" name="id" type="integer">
<generator class="native"/>
</id>
<many-to-one access="field" lazy="false" cascade="all" class="calex.origins.DataOrigin" column="origin_id" name="origin"/>
<property column="path" length="65536" name="pathStr" type="text"/>
<property access="field" column="name" length="1048576" name="name" type="text"/>
<property access="field" column="timestamp" name="timestamp" type="timestamp"/>
<property access="field" column="contents" length="1048576" name="contents" type="text"/>
</class>
</hibernate-mapping>
As you can see, neither RawData nor DataOrigin have a knowledge of the relationship with RawFilter.
Now the key part. I do override equals() and hashCode() methods in DataOrigins (the ones that I use for indexing):
Code:
public abstract class DataOrigin {
private Integer id;
... some code goes in here ...
}
public abstract class URIOrigin extends DataOrigin {
... some code goes in here ...
protected abstract String getPath() throws MalformedOrigin;
protected abstract void setPath(String path) throws MalformedOrigin;
}
class URLOrigin extends URIOrigin {
private URL url;
... some code goes in here ...
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof URLOrigin)) {
return false;
}
final URLOrigin other = (URLOrigin) obj;
if (this.url != other.url && (this.url == null || !this.url.equals(other.url))) {
return false;
}
return true;
}
@Override
public int hashCode() {
int hash = 7;
hash = 17 * hash + (this.url != null ? this.url.hashCode() : 0);
return hash;
}
@Override
protected String getPath() {
return url.toString();
}
@Override
protected void setPath(String path) throws MalformedOrigin {
try {
url = new URL(path);
} catch (MalformedURLException ex) {
throw new MalformedOrigin(this);
}
}
}
Well I use an implementation of equals() and hashCode() nearly identical to this generated by the GUI (the only change is use of "instanceof" instead of comparing classes through "=="). It's important to point out that all the DataOrigins are IMMUTABLE and any accessors that are out there are for hibernate use only. Next thing to point out is that there NEVER EXIST two instances of URLOrigin that are equal - there is only one.
Now implementation of ExpiryFilter looks as follows:
Code:
public class ExpiryFilter extends RawFilter {
private Map<DataOrigin, RawData> history = new HashMap<DataOrigin, RawData>();
private int minutes;
public ExpiryFilter() {
this(0);
}
public ExpiryFilter(int minutes) {
super();
this.minutes = minutes;
}
... some code goes in here ...
private Map<DataOrigin, RawData> getHistory() {
return history;
}
private void setHistory(Map<DataOrigin, RawData> history) {
this.history = history;
}
}
Now at last i can tell what's the problem: when loading objects from database setHistory is called. The assigned value of history is a PersistentMap<DataOrigin, RawData> and this is perfectly OK. However the map itself is broken - the elements hash codes are invalid. So when I extract some key from the map using .getKeySet().iterator().next() and pass it to get() I still get null (although from debugger i can clearly see that the key is bound to some non-null value).
Important thing is that if I change setHistory setter to:
Code:
private void setHistory(Map<DataOrigin, RawData> history) {
this.history = new HashMap(history);
}
everything works fine.
I spent 3 days googling this and reading hibernate reference. I even went trough the PersistentMap code to figure out where the problem is. However because I was unable to find source .jar's to use with debugger and didn't succeed with compiling the project myself, I had to pass on the part of the implementation that makes heavy use of Listeners.
I use hibernate-distrubution-3.3.2.GA. Please, help!