Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hibernate triggering constraint violations using orphanRemoval

I'm having trouble with a JPA/Hibernate (3.5.3) setup, where I have an entity, an "Account" class, which has a list of child entities, "Contact" instances. I'm trying to be able to add/remove instances of Contact into a List<Contact> property of Account.

Adding a new instance into the set and calling saveOrUpdate(account) persists everything lovely. If I then choose to remove the contact from the list and again call saveOrUpdate, the SQL Hibernate seems to produce involves setting the account_id column to null, which violates a database constraint.

What am I doing wrong?

The code below is clearly a simplified abstract but I think it covers the problem as I'm seeing the same results in different code, which really is about this simple.

SQL:

CREATE TABLE account ( INT account_id );
CREATE TABLE contact ( INT contact_id, INT account_id REFERENCES account (account_id) );

Java:

@Entity
class Account {
  @Id
  @Column
  public Long id;

  @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
  @JoinColumn(name = "account_id")
  public List<Contact> contacts;
}

@Entity
class Contact {
  @Id
  @Column
  public Long id;

  @ManyToOne(optional = false)
  @JoinColumn(name = "account_id", nullable = false)
  public Account account;
}

Account account = new Account();
Contact contact = new Contact();

account.contacts.add(contact);
saveOrUpdate(account);

// some time later, like another servlet request....

account.contacts.remove(contact);
saveOrUpdate(account);

Result:

UPDATE contact SET account_id = null WHERE contact_id = ?

Edit #1:

It might be that this is actually a bug http://opensource.atlassian.com/projects/hibernate/browse/HHH-5091

Edit #2:

I've got a solution that seems to work, but involves using the Hibernate API

class Account {
    @SuppressWarnings("deprecation")
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "account")
    @Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
    @JoinColumn(name = "account_id", nullable = false)
    private Set<Contact> contacts = new HashSet<Contact>();
}

class Contact {
    @ManyToOne(optional = false)
    @JoinColumn(name = "account_id", nullable = false)
    private Account account;
}

Since Hibernate CascadeType.DELETE_ORPHAN is deprecated, I'm having to assume that it has been superseded by the JPA2 version, but the implementation is lacking something.

like image 430
ptomli Avatar asked Jun 18 '10 10:06

ptomli


1 Answers

Some remarks:

  • Since you have a bi-directional association, you need to add a mappedBy attribute to declare the owning side of the association.
  • Also don't forget that you need to manage both sides of the link when working with bi-directional associations and I suggest to use defensive methods for this (shown below).
  • And you must implement equals and hashCode on Contact.

So, in Account, modify the mapping like this:

@Entity
public class Account {
    @Id @GeneratedValue
    public Long id;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "account", orphanRemoval = true)
    public List<Contact> contacts = new ArrayList<Contact>();

    public void addToContacts(Contact contact) {
        this.contacts.add(contact);
        contact.setAccount(this);
    }

    public void removeFromContacts(Contact contact) {
        this.contacts.remove(contact);
        contact.setAccount(null);
    }

    // getters, setters
}

In Contact, the important part is that the @ManyToOne field should have the optional flag set to false:

@Entity
public class Contact {
    @Id @GeneratedValue
    public Long id;

    @ManyToOne(optional = false)
    public Account account;

    // getters, setters, equals, hashCode

}

With these modifications, the following just works:

Account account = new Account();
Contact contact = new Contact();

account.addToContact(contact);
em.persist(account);
em.flush();

assertNotNull(account.getId());
assertNotNull(account.getContacts().get(0).getId());
assertEquals(1, account.getContacts().size());

account.removeFromContact(contact);
em.merge(account);
em.flush();
assertEquals(0, account.getContacts().size());

And the orphaned Contact gets deleted, as expected. Tested with Hibernate 3.5.3-Final.

like image 96
Pascal Thivent Avatar answered Oct 19 '22 01:10

Pascal Thivent