-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 1 post ] 
Author Message
 Post subject: _not_ a yet another broken hashCode problem!
PostPosted: Wed Nov 04, 2009 10:15 am 
Newbie

Joined: Wed Nov 04, 2009 9:30 am
Posts: 1
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!


Last edited by julkopki on Fri Nov 06, 2009 1:36 pm, edited 1 time in total.

Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 1 post ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.