16

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.

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
ptomli
  • 11,730
  • 4
  • 40
  • 68
  • 1
    i think that what made the thing work, is the option nullable=false on @JoinColumn and optional=false on the @ManyToOne, not the custom hibernate cascade. – Thierry Jun 18 '10 at 13:21
  • Even when I added the nullable and optional flags, it still failed, but this time when it realized it had created a scenario where it was violating an annotated constraint, rather than an SQL one. It seems that may still be a few issues with 3.5.3 and JPA2, as I can't get @ElementCollection to act nice either. – ptomli Jun 18 '10 at 13:30
  • set CascadeType as ALL will perform all the opertaions you dont have to be explicit as DELETE_ORPHAN – tinker_fairy Apr 17 '13 at 12:57
  • In my case, which I use only unidirectional relation from parent to chhild, orphanRemoval = true and nullable = false that what made it works. If I remove nullable = false, it failed triggering constraint violation when trying to update reference field to null. If I remove ophanRemoval, it does not execute any update or delete. @Cascade(org.hibernate.annotations.CascadeType.DELETE_ORPHAN) is not necessary – Harun Jan 14 '21 at 10:34

1 Answers1

22

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.

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
  • It seems that somewhere in the changes I made to get Hibernate specific API working, I solved whatever it was that was breaking the JPA2 version. Somewhat embarrassingly I suspect it was the equals/hashCode... – ptomli Jun 21 '10 at 08:10
  • hello sir, I'm not using `EntityManager`, I'm using `Session#saveOrUpdate` hibernate specific, In DB I've **3 children**, and from view I've **2 children** to be updated and want to remove **3rd or last one**, How to deal with that situation? Should I use `Session#get()` or `Session#load()`, to know which element to remove? – Shantaram Tupe Feb 26 '18 at 08:01