0

I have three entities EntityA, EntityB and EntityC as follows:

EntityA:

import lombok.*;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "Entity_A")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "locationA")
@ToString(of = "locationA")
public class EntityA {
    @Id
    @Column(name = "Name_A", length = 10)
    private String nameA;
    @Column(name = "Loc_A", length = 10)
    private String locationA;
    @ManyToMany(cascade = { CascadeType.MERGE })
    @JoinTable(
            name = "En_A_On_B",
            joinColumns = { @JoinColumn(name = "Name_A") },
            inverseJoinColumns = { @JoinColumn(name = "B_id") }
    )
    private Set<EntityB> bs;
}

EntityB:

import lombok.*;

import javax.persistence.*;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "Entity_B")
@Setter
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "locationB")
@ToString(of = "locationB")
public class EntityB {
    @Id
    @GeneratedValue
    @Column(name = "B_id")
    private int id;
    @Column(name = "Loc_B", length = 10)
    private String locationB;
    @ManyToMany(cascade = { CascadeType.MERGE })
    @JoinTable(
            name = "En_C_on_B",
            joinColumns = { @JoinColumn(name = "B_id") },
            inverseJoinColumns = { @JoinColumn(name = "C") }
    )
    private Set<EntityC> cs;
 }

EntityC:

 import lombok.*;

import javax.persistence.*;
import java.util.Set;

@Entity
@Table(name = "Entity_C")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "c")
@ToString(of = "c")
public class EntityC {
    @Id
    @Column(name = "C", length = 20)
    private String c;    
}

SERVICE CLASS TO SAVE:

@Service
@Slf4j
public class ServiceClass {
    @Autowired
    private EntityARepository entityARepository;

    private Set<EntityC> cs1 = new HashSet<>(asList(
            EntityC.builder().c("100").build(),
            EntityC.builder().c("10").build()
    ));

    private Set<EntityC> cs2 = new HashSet<>(asList(
            EntityC.builder().c("100").build(),
            EntityC.builder().c("200").build()
    ));

    //METHOD TO SAVE
    public void save() {
        Map<String, Set<EntityC>> map = new HashMap<>();
        map.put("B1", cs1);
        map.put("B2", cs2);

        List<String> bs = asList("B1", "B2");
        EntityA aa = EntityA.builder().nameA("abcd").locationA("mon").build();
        EntityA ab = EntityA.builder().nameA("abcde").locationA("money").build();

        bs.forEach(b -> {
            EntityB entityB = EntityB.builder().locationB("100xxx").build()
            entityB.getCs().addAll(map.get(b));            

            aa.getBs().add(entityB);
            ab.getBs().add(entityB);
        });

        entityARepository.save(aa);
        entityARepository.save(ab);
    }
}

Execution of above code throws following exception

Caused by: java.lang.IllegalStateException: Multiple representations of the same entity [com.xxx.xxx.xxx.xxx.EntityC#100] are being merged. Detached: [(c=100)]; Detached: [(c=100)]

Note: I have explored on the internet but none of them matcches with my scenario

Any idea how can I rectify the issue

bpa.mdl
  • 396
  • 1
  • 5
  • 19

1 Answers1

1

The problem is right here:

private Set<EntityC> cs1 = new HashSet<>(asList(
        EntityC.builder().c("100").build(), //this entity instance has the same identifier...
        EntityC.builder().c("10").build()
));

private Set<EntityC> cs2 = new HashSet<>(asList(
        EntityC.builder().c("100").build(), //...as this one
        EntityC.builder().c("200").build()
));

You are trying to persist two versions of the same entity, in a single unit of work. Imagine you put:

EntityC.builder().c("100").name("A name").build()

in cs1 and:

EntityC.builder().c("100").name("Another name").build()

in cs2 instead. Since both entities have the same id (c="100"), how is Hibernate supposed to know which version 'wins'?

Try putting the same instance of EntityC in both sets and the problem should go away.

crizzis
  • 9,978
  • 2
  • 28
  • 47
  • It wouldn't be same instance as in actual scenario, it would be two different unmarshalled instances. I am using equals and hashCode methods for that. – bpa.mdl Jan 14 '19 at 17:51
  • Well then, I'm afraid the same problem persists. Wherever you get the entities from, if you try to merge different entity instances with the same ids, Hibernate cannot decide on its own which version is the right one. You need to enforce *reference equality*, otherwise it won't work. If you want to save **the association between `EntityB` and `EntityC`**, rather than **the entire unmarshalled `EntityC` state**, you need to obtain a reference to the pre-existing `EntityC` using `repository.getOne()`, and use that instance instead of `map.get(b);` in the `entityB.getCs().addAll(...)` line – crizzis Jan 14 '19 at 18:31
  • With Spring Boot and H2 database the OP code works just fine. –  Jan 15 '19 at 08:14
  • @EugenCovaci did you wrap the call to `save()` in a transaction? If not, then `entityARepository.save(aa)` and `entityARepository.save(ab)` create two separate transactions, so the problem does not surface – crizzis Jan 15 '19 at 08:51
  • @crizzis It's working regardless `entityARepository.save` is marked as `@Transactional`. BTW, making or not making `entityARepository.save` transactional doesn't create two transactions. –  Jan 15 '19 at 12:40
  • I meant making the call to `ServiceClass.save()` transactional. The `JpaRepository.save()` method is transactional by default – crizzis Jan 15 '19 at 12:48
  • Yes, I annotated `ServiceClass.save()` with @Transactional. –  Jan 15 '19 at 13:02
  • @EugenCovaci I tested the code myself and finally understood where the source of the confusion comes from. The `equals` of `EntityB` is written in such a way that only a single `EntityB` ends up in both `aa.bs` and `ab.bs`, contrary to what the creation of two `EntityB`s in the `forEach` loop would suggest. Indeed, as it is written, the code works without any exception. If you 'break' the `equals` a little bit, for instance by writing `EntityB entityB = EntityB.builder().locationB("100xxx" + b).build()`, then you get the exact error the OP mentioned – crizzis Jan 15 '19 at 21:07
  • Good point, I haven't notice that. To avoid the error just add `spring.jpa.properties.hibernate.event.merge.entity_copy_observer=allow` to `application.properties` file. Please modify the answer for me to upvote it. –  Jan 16 '19 at 08:43
  • @EugenCovaci Sorry, but I would not advocate the use of `hibernate.event.merge.entity_copy_observer=allow` for most use cases, for exactly the reasons I've already detailed in my answer. The default `disallow` is there for a reason, if you think otherwise, please write your own answer – crizzis Jan 16 '19 at 09:13
  • You got me wrong. I wanted to say that if someone want this code working, it has to use `hibernate.event.merge.entity_copy_observer`. It's not a matter of advocating here, also if this is configurable I don't think it is that bad to tweak the value accordingly. I asked you to modify your answer not by adding `hibernate.event.merge.entity_copy_observer` stuff, but because I downvoted it and I cannot upvote it unless you modify it again. –  Jan 16 '19 at 10:25
  • @EugenCovaci Ahh, OK, I didn't realize that. Well, it's OK to tweak the value to your needs, **provided that you understand the implications**. In any case, I've edited my answer, thanks – crizzis Jan 16 '19 at 10:29