Search code examples
spring-boothibernatespring-data-jpaone-to-manymany-to-one

Why Hibernate giving Multiple representations of the same entity in One to Many Bidirectional update actions?


I tried so many solutions but failed. What trying to achieve is Update operation. I have two entities like person and person communication.

@Entity
@Table(name = "person")
@SequenceGenerator(name = "person_id_seq", sequenceName = "person_id_seq", allocationSize = 1)
@Inheritance(strategy = InheritanceType.JOINED)
@Getter
@Setter
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "person_id_seq")
    protected int id;

    @OrderBy(value = "communicationAddress ASC")
    @OneToMany(targetEntity = PersonCommunication.class,cascade=CascadeType.ALL, mappedBy = "person")
    protected List<PersonCommunication> communications;
}

@Getter
@Setter
@Entity
@Table(name = "person_communication")
@SequenceGenerator(name = "person_communication_id_seq", sequenceName = "person_communication_id_seq", allocationSize = 1)
public class PersonCommunication {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "person_communication_id_seq")
    private int id;

    @ManyToOne(cascade = CascadeType.ALL)
    private Person person;
}

As I'm trying to add and update, in my service class, suppose I want to add some communications, find a person by id and if exist then update communications and not save as a new person like this.

Optional<StaffMember> staffMemberOptional = staffRepository.findById(id);
StaffMember incoming = modelMapper.map(staffMemberDTO, StaffMember.class);
if(staffMemberOptional.isPresent()){
  staffMemberOptional.get().setCommunications(incoming.getCommunications());
  staffMemberOptional.get().setFirstName(incoming.getFirstName());
  staffRepository.save(staffMemberOptional.get());
}else{
  staffRepository.save(incoming);
}

When I try to update it gives exceptions.

***"Multiple representations of the same entity [com.impactivo.smartpcmh.server.model.StaffMember#2050] are being merged. Detached: [com.impactivo.smartpcmh.server.model.Person@1ad3d552]; Managed: [com.impactivo.smartpcmh.server.model.StaffMember@12f382f3]; nested exception is java.lang.IllegalStateException: Multiple representations of the same entity [com.impactivo.smartpcmh.server.model.StaffMember#2050] are being merged. Detached: [com.impactivo.smartpcmh.server.model.Person@1ad3d552]; Managed: [com.impactivo.smartpcmh.server.model.StaffMember@12f382f3]",***

Why this and how to fix this?

Thanks.


Solution

  • This happens because you are trying to merge a detached entity after you have already collected a managed version of the same entity.

    When fetching an entity within a transaction, you get a managed entity back, and fetching it again in the same transaction will give you the same object in return. If you on the other hand cross a transaction boundary, the entity will become detached, and if you start a new transaction and fetch the same entity again, you will actually get a different object. Then, if you try to reintroduce the detached entity (using merge), Hibernate will complain since it will have two objects for the same entity, and it can't know which of them contains the correct state to write to the database.

    The whole detach-merge pattern is in my mind an anti-pattern, and should be avoided. You should rather, within a transaction, fetch the entity, make changes, and commit the transaction (which should be configured to flush all changes, no need to run merge).

    // (1)this will load a managed version of the entity
    Optional<StaffMember> staffMemberOptional = staffRepository.findById(id); 
    // (2) this will create a detached version of the entity
    StaffMember incoming = modelMapper.map(staffMemberDTO, StaffMember.class); 
    if(staffMemberOptional.isPresent()){
      staffMemberOptional.get().setCommunications(incoming.getCommunications());
      staffMemberOptional.get().setFirstName(incoming.getFirstName());
      staffRepository.save(staffMemberOptional.get());
    }else{
      //this will try to reintroduce the detached entity, but since it was already
      //loaded in (1), the entity manager will end up with 2 versions of the same entity 
      staffRepository.save(incoming);
     }