Ali
Ali

Reputation: 426

how to stop Hibernate/JPA from deleting join table records when I update entity in ManyToMany relationship?

I would like to update an entity without updating/editing child entities. Here is the entity in question:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;
    
    //other fields omitted for brevity
    
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "project_user",
    joinColumns = @JoinColumn(name="user_id", updatable = false),
    inverseJoinColumns = @JoinColumn(name="project_id",updatable = false))
    @JsonIgnore
    private Set<Project> projects;
    
    //getters and setters omitted for brevity

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        User other = (User) obj;
        if (id != other.id)
            return false;
        return true;
    }
    
}

Here is the project entity:

@Entity
@Table(name = "project")
public class Project {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;
    
    //other fields omitted for brevity
    
    @ManyToMany
    @JoinTable(name = "project_user",
    joinColumns = @JoinColumn(name="project_id"),
    inverseJoinColumns = @JoinColumn(name="user_id"))
    @JsonIgnoreProperties(value = {"projects", "connected", "notifications"})
    private Set<User> members;

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Project other = (Project) obj;
        if (id != other.getId())
            return false;
        return true;
    }
    
}

Here is the merge procedure:

@Override
    public void update(User user) {
        //user is created from a dto as a new object (that has its id set manually)
        entityManager.merge(user);
    }

The issue is the hibernate logs delete from the join table. How can I stop that?

Hibernate logs

Hibernate: //login query
    select
        distinct authuser0_.id as id1_7_,
        authuser0_.enabled as enabled8_7_,
        authuser0_.password as password5_7_,
        authuser0_.username as username6_7_,
        roles1_.username as username1_0_0__,
        roles1_.authority as authorit2_0_0__ 
    from
        users authuser0_ 
    inner join
        authorities roles1_ 
            on authuser0_.username=roles1_.username 
    where
        authuser0_.username=?
Hibernate: //entityManager.merge() calls this to merge it
    select 
        user0_.id as id1_7_0_,
        user0_.email as email2_7_0_,
        user0_.first_name as first_na3_7_0_,
        user0_.image as image7_7_0_,
        user0_.last_name as last_nam4_7_0_,
        user0_.password as password5_7_0_,
        user0_.username as username6_7_0_ 
    from
        users user0_ 
    where
        user0_.id=?
Hibernate: //the actual update statement
    update 
        users 
    set
        email=?,
        first_name=?,
        image=?,
        last_name=?,
        password=?,
        username=? 
    where
        id=?
Hibernate: // not wanted, need to stop this query
    delete 
    from
        project_user 
    where
        user_id=?

I have read online that it has to do with equals and hashcode. However, I can't seem to get it to work.

I know of two options both of which are not optimal:

  1. write a native query instead of the merge method. Issue: not scalable, need to edit manually every change
  2. create a new entity to the table (so the table would have 2 entities mapping to it) without attachments to other tables. Issue: duplicate code and entities

Thanks In Advance

Upvotes: 2

Views: 2219

Answers (2)

K.Nicholas
K.Nicholas

Reputation: 11551

Well, I know you accepted an answer already, but ...

There is more going on there than you have thought about.

The reason there is a Hibernate: delete from project_user where user_id=? is that there are inserts afterwards. Did you not see them? Perhaps the relationship was not updated if the relationship table was emptied for that user but not inserted later? A delete only can be reproduced as well but you should test your code and verify that there are no projects for that user. That probably means that you either didn't update the projects field in the merged user.

So, running a basic JPA example, let's start with your problem by creating a starting point:

    em.getTransaction().begin();
    Project project = new Project();
    em.persist(project);
    User user = new User();
    Set<Project> projects = new HashSet<>();
    projects.add(project);
    user.setProjects(projects);
    em.persist(user);
    em.getTransaction().commit();

Then, clear the entityManager context:

    em.clear();

And then reproduce your problem:

    em.getTransaction().begin();
    Project p2 = new Project();
    em.persist(p2);
    projects.add(p2);
    User un = em.find(User.class, 1);
    un.setProjects(projects);
    em.merge(un);
    em.getTransaction().commit();

This second part gives me the logs:

Hibernate: insert into project (id) values (default)
Hibernate: select user0_.id as id1_2_0_ from users user0_ where user0_.id=?
Hibernate: select project0_.id as id1_0_0_ from project project0_ where project0_.id=?
Hibernate: delete from project_user where user_id=?
Hibernate: insert into project_user (user_id, project_id) values (?, ?)
Hibernate: insert into project_user (user_id, project_id) values (?, ?)

Now let's remove the offending delete statement by changing the code and running it again:

    Project p2 = new Project();
    em.persist(p2);
    User un = em.find(User.class, 1);
    projects = un.getProjects();
    projects.add(p2);
    em.merge(un);
    em.getTransaction().commit();

Which gives me the logs:

Hibernate: insert into project (id) values (default)
Hibernate: select user0_.id as id1_2_0_ from users user0_ where user0_.id=?
Hibernate: select projects0_.user_id as user_id1_1_0_, projects0_.project_id as project_2_1_0_, project1_.id as id1_0_1_ from project_user projects0_ inner join project project1_ on projects0_.project_id=project1_.id where projects0_.user_id=?
Hibernate: insert into project_user (user_id, project_id) values (?, ?)

No delete statement. Look carefully at the first "second part" and notice that I added the new project to the original set of projects which was no longer part of the JPA persistence context. In the second example I specifically retrieved the set of projects from the user object that was part of the JPA persistence context. Now, when I update the set of projects and merge the user JPA doesn't have to start over fresh with the user/project relationship.

BTW: The accepted answer is accurate in terms of the mappedBy logic, but this code was done with your original example. As long as JPA knows what you are doing it will handle it properly. When you do things outside of JPA then JPA will "punt" and recreate.

Upvotes: 3

crizzis
crizzis

Reputation: 10716

Why are two distinct associations (User.projects and Project.users) mapped to the same join table?

If you want a bidirectional association, only one of the sides should be the owning side, the other side should be mapped with mappedBy. Using mappedBy for the User.projects side will solve your problem, by the way.

Upvotes: 2

Related Questions