2

I'm trying to undestand @JoinTable for @OneToMany relations using JPA. I have the relation above:

CLIENT

public class Client {

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

    ...

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @JoinTable(name = "client_contact",
            joinColumns = @JoinColumn(name = "client_id"),
            foreignKey = @ForeignKey(name = "fk_client_contact__client"),
            inverseJoinColumns = @JoinColumn(name = "contact_id"),
            inverseForeignKey = @ForeignKey(name = "fk_client_contact__contact"))
    private Set<Contact> contactNumbers;

}

CONTACT

public class Contact {

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

}

Problem Description

My problem is: I can create a client with as many contacts as I want, I can remove contacts, but when I try to update one contact of the client or add a contact after the client is created I'm getting the following foreign key error:

Caused by: org.h2.jdbc.JdbcSQLException: Unique index or primary key violation: "PRIMARY_KEY_4 ON PUBLIC.CLIENT_CONTACT(CLIENT_ID, CONTACT_ID) VALUES ( /* key:3 */ 97, 194)"; SQL statement:
insert into client_contact (client_id, contact_id) values (?, ?) [23505-175]

It seams to me that hibernate is trying to re-insert the contact in the joinTable, what I'm doing wrong? *I'm updating the entity with entityManager.merge().

I'm using Hibernate 5.1.0, JPA 2.1.

I'm trying to avoid using mappedBy or JPA 2.1 @OneToMany relation with no JoinTable, because I have other entities that contains Contacts as well

Rafael Teles
  • 2,708
  • 2
  • 16
  • 32
  • Does your Contact entity also have a ref to Client, or is your model really unidirectional? – Nathan Aug 15 '16 at 12:43
  • Yes, it is unidirectional, like I said: "I'm trying to avoid using mappedBy or JPA 2.1 @OneToMany relation with no JoinTable, because I have other entities that contains Contacts as well" – Rafael Teles Aug 15 '16 at 12:47
  • Please post the code in which you are doing the `merge` operation. – Dragan Bozanovic Aug 15 '16 at 13:30
  • Hi @DraganBozanovic I'm just using entityManager.merge(client) – Rafael Teles Aug 15 '16 at 14:01
  • 1
    I know, that's the API for merge. :) But I thought it would be beneficial if you showed how you obtain the `client` instance in the first place, and what are you doing with it after merging. – Dragan Bozanovic Aug 15 '16 at 14:05
  • Oh ok, sorry xD. Well I'm going to post it later than, but I'm going to summarize it now: The project is a web application, when a user wants to edit a client contact it has to edit the client. When the client is saved I rebuild the client on my backend with all the fields set (all the "embedded" entities and the client have their id's set), and then I merge the client – Rafael Teles Aug 15 '16 at 14:11

3 Answers3

2

You have to redefine the hashcode and equals methods in your entities.

from the oracle doc https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

Now let's see what happens if you try this scenario:

     Client client = new Client();
        entityManager.persist(client);
        Contact contact = new Contact();
        entityManager.persist(contact);
        client.getContacts.add(contact);
        entityManager.merge(client);

        Contact contact2 =entityManager.find(Contact.class,contact.getId());
        //and now Unique index or primary key violation will appear 
       //because you are using the default equals and hashcode implementation  
       //adding contact2 will not replace the old one
        client.getClients().add(client2);
       entityManager.merge(client);//Unique index or primary key violation

To avoid this you must implement Equals and HashCode for your jpa entities.

see those links https://vladmihalcea.com/hibernate-facts-equals-and-hashcode/ and The JPA hashCode() / equals() dilemma

Vlad Mihalcea
  • 142,745
  • 71
  • 566
  • 911
SEY_91
  • 1,615
  • 15
  • 26
  • I override the class equals and hashcode... Actually I'm using Lombok for that, I just didn't added on the code to save space – Rafael Teles Aug 15 '16 at 12:46
  • 2
    equals and hashcode must be overrided based on primary key (id) or a not null unique attribute, can you add the generated equals and hashcode !!! – SEY_91 Aug 15 '16 at 13:28
  • 1
    @RafaelTeles I also think that the issue is hiding here. Which fields are you including for Lombok to generate `hashCode` and `equals`? – Dragan Bozanovic Aug 16 '16 at 11:17
  • I don't use Lombok to generate hashCode and equals. But i recommend to include only the id. Beacuse if for example you include other attribute and if you change it you will have a new entity(contact) and adding it to the client.getContact() which hold the old version of (contact) will cause an issue. – SEY_91 Aug 16 '16 at 11:41
  • HashSet use equlas defined in your entity contatc to verify that there is no duplications inside the Set contacts attribute. And your equlas implementation could make two Contact instances (contact1,contact2)with the same id looks differents and then when you add contatc2 to the same client hashset will not detect the duplications. – SEY_91 Aug 16 '16 at 12:16
  • I'm including all fields in the hashcode and equals. – Rafael Teles Aug 16 '16 at 22:45
  • @SEY_91 there can't be duplicates in the Set because I build the entity only with the new contacts, so I only add the new contacts to the set. But yeah, when I changed my equals and hashcode to use just the id it worked. Just making a few more tests to get this better – Rafael Teles Aug 16 '16 at 22:55
  • Ok, this is the answer. I just want to understand one thing, let's say there is a user (eq. and hash. with all fields) with a contact "a", and then I update the contact and save the user. Even though there is only one element in my set (modified "a"), hibernate keeps the old version of "a" because it is not equals to the new version?! – Rafael Teles Aug 16 '16 at 23:12
  • Just one more question, when I changed equals and hashcode to use just the id and I saved a user after removing one contact from the set, this contact was removed from the DB. But when I was using a "full" equals + hascode the older version of contact wasn't removed? – Rafael Teles Aug 16 '16 at 23:13
  • You are setting "orphanRemoval = true" which will trigger the remove operation when the contact is removed from the relationship ,see this link https://docs.oracle.com/cd/E19798-01/821-1841/giqxy/ – SEY_91 Aug 17 '16 at 08:41
  • hibernate keeps the old version of "a" because it is not equals to the new version?!Hibernate will keep only the eye on the set contacts if any new element is added it will be persisted(and any element is deleted from the set it will be deleted if you are using "orphanRemoval = true"),and if a contact is modified then it will be updated (cascade.all => cascade.merge) . – SEY_91 Aug 17 '16 at 09:15
  • I know the effect of the "orphanRemoval" and the "cascade". But why don't it override the old version of "a" when I'm using a "complete" equals and hash? My set only have the new version of "a" and no more contacts, all the others that were linked to the user should be removed and the original "a" updated. Hibernate first save the new version of "a" (which throws a violation) and after merging the list hibernate would remove the orphans? That would explain all for me. – Rafael Teles Aug 17 '16 at 11:45
0

For me following mapping works without an issue while updating with orphanRemoval = true removed from @OneToMany annotation.

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "client_contact",
        joinColumns = @JoinColumn(name = "client_id"),
        foreignKey = @ForeignKey(name = "fk_client_contact__client"),
        inverseJoinColumns = @JoinColumn(name = "contact_id"),
        inverseForeignKey = @ForeignKey(name = "fk_client_contact__contact"))
private Set<Contact> contactNumbers;
shazin
  • 21,379
  • 3
  • 54
  • 71
0

The problem is caused by the default merge implementation of the EntityManager. You should implement merge for client contacts explicitly like it is explained here.

Actually this is the duplication of this

Community
  • 1
  • 1
asch
  • 1,923
  • 10
  • 21
  • In your approach I would have to merge all the element properties manually, is that correct? – Rafael Teles Aug 16 '16 at 23:18
  • Yes. This manual merge should be used instead of entity manage merge, just like I explained in the example. – asch Aug 17 '16 at 05:25