Search code examples
javahibernatesetcontainstreeset

Using Hibernate and TreeSet does not work the remove() and contains() methods


I want to have a sorted set by age;

The method compareTo() in this case works fine but the problem is that remove() and contians() methods returns always false;

INTERESTING: In case I uncomment the lines form compareTo() method, remove() and contains() methods works fine; but I want to use the other field as sorting.

Does someone have any idea why does not work properly; Found old Hibernate issue: https://hibernate.atlassian.net/browse/HHH-2634; is this already fixed?

Bellow are the used classes:

@Entity(name = "CAMPAIGN")
public class Campaign implements Identifiable, Serializable {
    public static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Long id;


    @OneToMany(mappedBy = "campaign", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
    @OrderBy("age ASC")
    private SortedSet<MailingAddress> mailingAddresses = new TreeSet<>();

    ...

    public void removeMailingAddress(MailingAddress mailingAddress) {
        this.mailingAddresses.remove(mailingAddress);
        //this.mailingAddresses.contains(mailingAddress);

        mailingAddress.setCampaign(null);
    }
}

And

@Entity(name = "MAILING_ADDRESS")
public class MailingAddress implements Identifiable, Comparable, Serializable {
    public static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "CAMPAIGN_ID")
    private Campaign campaign;

    @Column(name = "AGE")
    private Integer age;

    @Override
    public int compareTo(Object o) {
        if (o == null) {
            return 1;
        }

        if (!(o instanceof MailingAddress)) {
            throw new ClassCastException("Cannot compare MailingAddress with " + o.getClass());
        }

        MailingAddress o1 = (MailingAddress) o;
        int comparison;

        // comparison for id
        /*comparison = compareFields(this.id, o1.id);
        if (comparison != 0) {
            return comparison;
        }*/

        // comparison for ageBand
        comparison = compareFields(this.age, o1.age);
        if (comparison != 0) {
            return comparison;
        }

        return 0;
    }

    private int compareFields(Comparable field1, Comparable field2) {
        if (field1 == null && field2 == null) {
            return 0;
        } else if (field1 == null && field2 != null) {
            return -1;
        } else if (field1 != null && field2 == null) {
            return 1;
        }
        return field1.compareTo(field2);
    }

    @Override
    public boolean equals(Object o) {
        return this.compareTo(o) == 0;
    }

}

UPDATE:

Found that using SortedSet as interface for TreeSet in combination with Hibernate the methods remove() and contains() does not work properly. "SortedSet mailingAddresses = new TreeSet<>();"

Changed the definition to "Set mailingAddresses = new TreeSet<>();" and the methods remove() and contains() works fine; Also the sorting that is using compareTo() is working also for other fields than id.

Probably there is a bug in combination of TreeSet, SortedSet and Hibernate. If someone found an explanation for this "bug" please let me know.

Here is a working version:

@Entity
public class MailingAddress implements Identifiable, Comparable, Serializable {
    public static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Long id;

    private Integer age;

    @Override
    public int compareTo(Object o) {
        if (o == null) {
            return 1;
        }

        if (!(o instanceof MailingAddress)) {
            throw new ClassCastException("Cannot compare MailingAddress with " + o.getClass());
        }

        MailingAddress o1 = (MailingAddress) o;
        int comparison = compareFields(this.age, o1.age);
        if (comparison != 0) {
            return comparison;
        }

        return 0;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        MailingAddress that = (MailingAddress) o;

        if (id != null ? !id.equals(that.id) : that.id != null) return false;
        return age != null ? age.equals(that.age) : that.age == null;
    }

    @Override
    public int hashCode() {
        return 31;
    }

    private int compareFields(Comparable field1, Comparable field2) {
        if (field1 == null && field2 == null) {
            return 0;
        } else if (field1 == null && field2 != null) {
            return -1;
        } else if (field1 != null && field2 == null) {
            return 1;
        }
        return field1.compareTo(field2);
    }
}

AND

@Entity
public class Campaign implements Identifiable, Serializable {
    public static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "ID")
    private Long id;


    @OneToMany(mappedBy = "campaign", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
    @OrderBy("age ASC")
    private Set<MailingAddress> mailingAddresses = new TreeSet<>();

    ...
}

Solution

  • The problem here is that you override equals without overriding hashCode.

    Also, the reference check does not work for the merge entity state transition.

    Since you don't have a natural business key in MailingAddress, you need to use the entity identifier like this:

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof MailingAddress)) return false;
        MailingAddress ma = (MailingAddress) o;
        return getId() != null && Objects.equals(getId(), ma.getId());
    }
    
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
    

    The getClass().hashCode() returns a constant value for allinstances, therefore allowing an entity that has a null id to be found in a HashSet even after the id is changed after calling persist on the transient entity.

    But, that's not all.

    Why do you use a TreeSet with @OrderBy("age ASC"). The order is given at query-time, and then you override that in Java. Since you use @OrderBy, it makes more sense to use a List since the sorting is done when executing the SELECT statement.