Sring data and Hibernate, how to avoid child (non-owning side) entity deletion by Hibernate (without cascade)?

I am developping a web application of REST type, with Angular (Typescript) for the frontend, and Spring (boot, rest, data, hibernate) for the backend which is connected to a MySQL database. In this application I’m using JPA annotation.

My relations between objets are all bi-directionals :

  • A User has one Address, and an Address can corresponds to one or several User(s) : User n - 1 Address
  • A user provides one or more Report(s), and a Report is provided by only one User : User 1 - n Report
  • User n - n Role (so association table User_Role)
  • User n - n Group (so association table User_Group)

My issue is that hibernate deletes the link with the child entity (the non-owning) when I modify the owner side, and I don’t want that. I will detail my issue and ask questions below, after my code.

In User.java

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name = "userId")
private Long userId;
private String firstName;
private String lastName;
private String email;
...

@JsonSerialize(using = AddressCustomSerializer.class)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="address_id")
private Address address;

@ManyToMany
@JoinTable( name = "user_role",
    joinColumns = @JoinColumn( name = "role_id" ),
    inverseJoinColumns = @JoinColumn( name = "user_id" ) )
private List<Role> roles = new ArrayList<>();

@OneToMany(mappedBy = "user")
private List<Report> report = new ArrayList<>();

// List of groups the user belongs to.
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
    name = "user_group", 
    joinColumns = { @JoinColumn(name = "user_id") }, 
    inverseJoinColumns = { @JoinColumn(name = "group_id") }
)
private List<Group> userGroups = new ArrayList<>();

In Address.java

@OneToMany(mappedBy = "address")
private List<User> users = new ArrayList<>();

In Role.java

@JsonSerialize(using = UserListForRoleCustomSerializer.class)
@ManyToMany(mappedBy = "roles")
private List<User> users = new ArrayList<>();

In Group.java

// List of users included in the group.
@JsonSerialize(using = UserListForGroupCustomSerializer.class)
@ManyToMany(mappedBy = "userGroups")
private List<User> users = new ArrayList<>();

in Report.java 

@JsonSerialize(using = UserCustomSerializer.class) 
@ManyToOne @JoinColumn(name="user_id") 
private User user;

When I click on a user, my backend sends me a json file containing all values for the user attributes and all linked objects which is OK.
But when I modify a simple User attribute like firstname or lastname, I have to send to the backend a json file containing all attributes and all linked objects.
If not, Hibernate deletes the link with the child entity (the non-owning), like you can see below in SQL traces :

2020-04-27 org.hibernate.SQL : update user set address_id=?, charter_acceptance_date=?, creation_date=?..
The column “address_id” is set to null, but the corresponding address is still in the Address table.

2020-04-27 org.hibernate.SQL : delete from user_role where role_id=?
In the association table “User_Role” the row, containing “user_id” and “role_id”, is deleted. But the corresponding role is still in Role table, and the corresponding user is still in User table.

2020-04-27 org.hibernate.SQL : delete from user_group where user_id=?
Same problem in association table “User_Group”.

1) From my understanding :

  • if I don’t send all linked entities, Hibernate “thinks” that the owner-side (the parent entity) User is not linked any more to entities, and hibernate deletes theses links.
  • Regarding “User 1 - n Report” relation, the link beetwen User and Report is not deleted because Report is the owning-side (parent). But when I will want to modify a simple attribute of a report, I will have the same issue.
    Am I right ?

2) How to avoid this problem, knowing that :

  • I wouldn’t like to send a json file containing ALL attributes and ALL linked objects, if I modify only a simple attribute of the user.
  • I would like to managed myself what link, or entity is deleted or updated, when I modify or delete a linked entity (owner-side or not).
  • I’m going to have ternary and quaternary relations in my datase. I will need to customize each update and deletion, object by object.
  • I tried @SelectBeforeUpdate(value=true) @DynamicUpdate(value = true) annotations, but I have the same issue.
  • I don’t use EntityManager, Sring does it for me.

3) At the beginning I used Hibernate cascades (CascadeType.MERGE and CascadeType.PERSIST) on all relations quoted in my code. But :

  • I had the same issue ;
  • And someone told me that hibernate cascades must be reserved for the UML composition relation. For example : if I delete a user and I want all his/her reports to be deleted. But it is not my need !! If I delete a user I want to keep his/her reports.
  • What do you think about the fact that hibernate cascades must be reserved for the UML composition relation ?

Many thanks in advance for your answers.

Using Spring Data Rest? Try issuing PATCH update instead of PUT - PUT does complete replace by default, while PATCH will do partial updates, based only on the fields you send in body

Many thanks you for your answer. But it does not work for me.
Yes I am using Spring Data Rest.
I tried PATCH update like this :

In my controller :

     @PatchMapping(path="/user/{id}")
     public User updateUser(@RequestBody User user){
         return userService.updateUser(user);
     }

In the user service :

    @Override
    public User updateUser(User user) {
    	System.out.println("updateUserById : user id : " + user.getUserId());
    	System.out.println("user.getUserGroups().size() : " + user.getUserGroups().size());
    	System.out.println("user.getReports().size() : " + user.getReport().size());
		
        return userRepository.save(user);
    }

The console trace :

updateUserById : user id : 1
user.getUserGroups().size() : 0
user.getReport().size() : 0
2020-05-19 11:12:09.232 DEBUG org.hibernate.SQL : select user0_.user_id as user_id1_24_0_…
2020-05-19 11:12:09.232 DEBUG org.hibernate.SQL : select roles0_.role_id as role_id1_26_0_, …
2020-05-19 11:12:09.233 DEBUG org.hibernate.SQL : select managedgro0_.user_id as user_id1_9_0_ …
2020-05-19 11:12:09.234 DEBUG org.hibernate.SQL : select usergroups0_.user_id as user_id1_25_0_, usergroups0_.group_id as group_id2_25_0_, …
2020-05-19 11:12:09.235 DEBUG org.hibernate.SQL : update user set address_id=?, charter_acceptance_date=?, creation_date=?, email=?, first_name=?, …
2020-05-19 11:12:09.236 DEBUG org.hibernate.SQL : delete from managed_group where user_id=?
2020-05-19 11:12:09.236 DEBUG org.hibernate.SQL : delete from user_role where role_id=?
2020-05-19 11:12:09.237 DEBUG org.hibernate.SQL : delete from user_group where user_id=?
2020-05-19 11:12:09.249 DEBUG org.hibernate.SQL : select report0_.user_id as user_id7_15_0_, report0_.report_id as report_i1_15_0_, 14_9_ …
2020-05-19 11:12:09.251 DEBUG org.hibernate.SQL : select reports0_.device_id as device_i3_15_0_, reports0_.report_id as report_i1_15_0_, reports0_ …

I understand that the user I give to the service, has no group and no report because my frontend sent a json with only modyfied user fields and not linked objects.
But, as you can see, and even with PATCH method, roles, groups are deleted (the user is the owner of the relation) and reports are not deleted because Report is the owner of the relation beetwen User and Report object.

How to avoid theses delations ?

Best regards

Pascal

Goog Morning,

Many thanks for your answer. But it does not work for me.
Yes I am using Spring Data Rest.
I tried PATCH update like this :

In my controller :

@PatchMapping(path="/user/{id}")
public User updateUser(@RequestBody User user){
return userService.updateUser(user);
}

In the user service :

@Override
public User updateUser(User user) {
System.out.println("updateUserById : user id : " + user.getUserId());
System.out.println("user.getUserGroups().size() : " + user.getUserGroups().size());
System.out.println("user.getReports().size() : " + user.getReport().size());

return userRepository.save(user);
}

The console traces after I changed only the firstname of the user :

updateUserById : user id : 1
user.getUserGroups().size() : 0
user.getReport().size() : 0
2020-05-19 11:12:09.232 DEBUG org.hibernate.SQL : select user0_.user_id as user_id1_24_0_…
2020-05-19 11:12:09.232 DEBUG org.hibernate.SQL : select roles0_.role_id as role_id1_26_0_, …
2020-05-19 11:12:09.233 DEBUG org.hibernate.SQL : select managedgro0_.user_id as user_id1_9_0_ …
2020-05-19 11:12:09.234 DEBUG org.hibernate.SQL : select usergroups0_.user_id as user_id1_25_0_, usergroups0_.group_id as group_id2_25_0_, …
2020-05-19 11:12:09.235 DEBUG org.hibernate.SQL : update user set address_id=?, charter_acceptance_date=?, creation_date=?, email=?, first_name=?, …
2020-05-19 11:12:09.236 DEBUG org.hibernate.SQL : delete from managed_group where user_id=?
2020-05-19 11:12:09.236 DEBUG org.hibernate.SQL : delete from user_role where role_id=?
2020-05-19 11:12:09.237 DEBUG org.hibernate.SQL : delete from user_group where user_id=?
2020-05-19 11:12:09.249 DEBUG org.hibernate.SQL : select report0_.user_id as user_id7_15_0_, report0_.report_id as report_i1_15_0_, 14_9_ …
2020-05-19 11:12:09.251 DEBUG org.hibernate.SQL : select reports0_.device_id as device_i3_15_0_, reports0_.report_id as report_i1_15_0_, reports0_ …

I understand that the user I give to the service, has no group and no report because my frontend sent a json with only modyfied user fields and not linked objects.
But, as you can see, and even with PATCH method, roles, groups are deleted (the user is the owner of the relation) and reports are not deleted because Report is the owner of the relation beetwen User and Report object.

How to avoid that ?

Best regards

Pascal

No, in Spring Data Rest, i’m talking about @RepositoryRestResource like:

@RepositoryRestResource
public interface UserRepository extends JpaAuditRepository<User, Long> {}

Call those generated endpoints and PATCH will do partial updates - your version is the manual Spring MVC approach.

@see:


If you want to do your manual mvc version, it looks more like:

 @PatchMapping(path="/user/{id}")
     public User updateUser(@RequestBody User body){
         User user = userService.getUser(body.getId());
         
         //you'd do manual updates like this:
         user.setField(body.getField());

         //But i'm guessing you'd need to change the way the update works in your service here: 
         return userService.updateUser(user);
     }