Deleting referencing side entity in #ManyToMany relationship: JPA - java

I am working on a Spring Boot Project in which I have used a Many TO Many relationship between two entities- User and Category.
For User-
//Set of categories a user is following
#ManyToMany(cascade = {CascadeType.DETACH,CascadeType.MERGE,CascadeType.PERSIST, CascadeType.REFRESH}, fetch = FetchType.EAGER)
#JoinTable(name = "user_category",
joinColumns = #JoinColumn(
name = "user_id",
referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(
name = "category_id",
referencedColumnName = "categoryId"))
private Set<Category> categories=new HashSet<>();
For Category-
// set of users who follow the category
#ManyToMany(mappedBy = "categories",cascade={CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
Set<User> users = new HashSet<>();
What I want-
When I delete a User only user should be deleted and not the Category.
When I delete a Category only category should be deleted and not the User.
Problem-
When I delete a User, it gets deleted successfully without affecting category. But, when I delete a Category, I get an error-
java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`blog_application_database`.`user_category`, CONSTRAINT `FK4x6ipr43cfvhqc3aqda6j8c2l` FOREIGN KEY (`category_id`) REFERENCES `categories` (`category_id`))
Now, to remove the associated users, in the category I use-
#PreRemove
public void removeUsers() {
System.out.println("Removing users for " + this.categoryName + " before deleting");
this.users=null;
this.users=new HashSet<>();
}
But, the problem is not resolved.
One approach I came across from several other posts was to manually remove each and every mapping from each user (from user side) but it would be highly inefficient.
I am adding a screenshot of the db tables-
Please help me understand what is going wrong here and how to resolve it.

Changing the Category#users collections has no effect on the relationship, because that is not the owning side of it. The owning side is the one which has no use of mappedBy.
So if you want to remove a category, you will first have to remove the category from every User#categories collection.
Maybe it's easier if you don't map that relationship as a #ManyToMany association and instead introduce an entity for the join table. Then you can use #OneToMany on both sides with DELETE cascading.

Related

JPA delete an entry from list in Many-To-Many relationship

Those are my classes. I want to delete items from likedCourses. So I expect JPA to delete item from course_like table. I looked and try to understand from other examples but couldn't. It is like deletion doesn't exist in JPA realm when there is relation. It is good for selecting though. I'd want to share what I tried but I couldn't find anything about it.
Note : I see that in ManyToMany relationship there is not orphanRemoval option.
#Entity
class Student {
#Id
Long id;
#ManyToMany
#JoinTable(
name = "course_like",
joinColumns = #JoinColumn(name = "student_id"),
inverseJoinColumns = #JoinColumn(name = "course_id"))
Set<Course> likedCourses
}
#Entity
class Course {
#Id
Long id;
#ManyToMany(mappedBy = "likedCourses")
Set<Student> likes;
}
If you want to remove an entry from the course_like table, you will have to load the Student and remove the element from the likedCourses set that should be removed. If you do that in a #Transactional method, you will see that Hibernate will emit delete statements to delete the rows that represent the removed objects from the likedCourses set. That's the magic of an ORM, it synchronizes the object graph with the database, without you telling it to emit statement A, B, ...

Why does persisting entity twice avoid errors around cascading for entity with join columns

Why does this unit test fail if i do not perform the setup of the entity Role in two steps (two persists).
The error being:
java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: io.osram.olt.extension.jpa.Role#16daa399.
private Role addRoleWithId(String roleId){
Role myRole = new Role();
myRole.setRoleId(roleId);
myRole.setRealmId("my");
myRole.setDescription("role-description-0");
myRole.setExternalCreator(true);
myRole.setName("role-name-0");
em.persist(myRole); //<--- Without this persisting the role fails with the error above.
//Setup joins:
myRole.setAContext(getApplications().get(0));
myRole.setAnotherContext(getTenants().get(0));
em.persist(myRole);
return myRole;
}
...
The Role Entity:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ANOTHER_CONTEXT_ID")
private AnotherContext anotherContext;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "ACONTEXT_ID")
private AContext aContext;
...
public Role setAContext(AContext aContext) {
this.aContext = aContext;
if(aContext != null) {
aContext.addRole(this);
}
return this;
}
public Role setAnotherContext(AnotherContext anotherContext) {
this.anotherContext = anotherContext;
if(anotherContext != null){
anotherContext.addRole(this);
}
return this;
}
...
The AContext and AnotherContext both contain similar relations towards role:
#OneToMany
#JoinTable(
name="OLT_ROLES_ACONTEXT",
joinColumns = #JoinColumn( name="ACONTEXT_ID"),
inverseJoinColumns = #JoinColumn( name="ROLE_ID")
)
private Set<Role> roles = new HashSet<Role>();
It seems by creating the object in two steps I can avoid using cascading.
In your setAContext and setAnotherContext methods, you are trying to set the Role object which is not yet persisted.
So It's clear that it will not work without em.persist(myRole); before you set contexts since you have not specified CaseCadeType.PERSIST.
The default setting for cascading is cascade NONE , which causes the relationships in the persisted entity not to be persisted by default.
the corollary is that if you try to persist an entity without cascade.PERSIST to its relationship while the relationship is not managed , you will get the above exception.
An exception of the corollary is that if the entity you are persisting is the owner of the relation and the attribute in the relation is already in the database, yo will be able to persist it.
One small thing that I noticed in your mapping : It's a double unidirectional, one with a join column and the reverse with a join table, so is this intended?

Configure many-to-many relationships in Spring Data REST so deleting one object does not delete the other

I am using the latest Spring Data REST (with JPA and MySQL DB) project. I have two objects that I want to setup a relationship between. But when I delete one of the two, I only want to delete the relationship and the object that received the delete request (not both objects).
What I'm relating:
A task
A user
where a task can be related to ("owned by") multiple users and a user can be related to ("own") multiple tasks
Task Class:
#ManyToMany
#JoinTable(name = "task_user", joinColumns = #JoinColumn(name = "task_id", referencedColumnName = "id"), inverseJoinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id"))
private Set<User> users;
User class:
#ManyToMany(mappedBy = "users")
private Set<Task> tasks;
The task_user table consists of two columns
task_id (the id of the task)
user_id (the id of the user)
When I delete a task (via DELETE to /task/{id}), it behaves correctly (the task is deleted from the task table and the relationship is deleted from the task_user table). The issue I have is that when I delete a user, it is deleted from the user table but the relationship still exists in the task_user table.
What I desire:
Deleting a task, deletes all corresponding entries in "task_user" and the entry in "task"; does NOT delete entries in "user"
Deleting a user, deletes all corresponding entries in "task_user" and the entry in "user"; does not delete entries in "task"
Is it possible to achieve that through configuration? Or do I need to use either foreign keys or custom logic, such as event handlers?
Try this:
public class User {
//...
#ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Task> tasks;
//...
}
public class Task {
//...
#ManyToMany(mappedBy = "tasks")
private Set<User> users;
//...
}
You chose a bidirectional variant of many-to-many so don't forget about 'helper methods' in User class. See documentation: associations many-to-many.
See my example and tests.
You need to change cascade type of foreign key user_id of table task_user to cascade delete. E.g using IntelliJ built-in database tool:

Delete hibernate entity which is referenced by #ManyToMany in other entity

I want to delete Recipe (using spring data DAO) but I got SQL exception: org.postgresql.util.PSQLException: ERROR: update or delete on table "recipe" violates foreign key constraint "fkacys689tmdmfggtf4thdoc83k" on table "favourite_recipes"
Detail: Key (id)=(76823) is still referenced from table "favourite_recipes".
My entities:
#Entity
public class Account {
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "favourite_recipes",
joinColumns = #JoinColumn(name = "account_id"),
inverseJoinColumns = #JoinColumn(name = "recipe_id"))
private Set<Recipe> favouriteRecipes = new HashSet<>(0);
...
}
#Entity
public class Recipe {
...
}
How to remove recipe instance?
You need to handle the cascade type, by default is set to ALL.
For example you can work around the contraints like this:
#ManyToMany(cascade = CascadeType.DETACH)
more info : cascade type docs
in you need to delete from the owning entity side which is the Account.
So first remove the recipe from recipe list in Account and save the account, then remove the recipe itself.
As Amer Qarabsa metioned I had to remove recipe from Account.
I added new field in Recipe to get bidirectional mapping
#ManyToMany(cascade = CascadeType.MERGE, mappedBy = "favouriteRecipes")
private Set<Account> recipeLovers = new HashSet<>(0);
Code in service class to remove recipe from all accounts + clear lovers in recipe (recipe and recipeId variables are not initialized here)
Set<Account> recipeLovers = recipe.getRecipeLovers();
recipeLovers.forEach(account ->
account.getFavouriteRecipes()
.removeIf(r -> r.getId() == recipeId));
recipeLovers.clear();
recipeDao.delete(recipe);

Hibernate Many-to-many association: left hand side collection contains elements, but right hand side collection is empty

I got a problem with a many to many association in my persistence layer. My scenario is the following:
A user can has several roles and a role can have several user attached to it. During the tests I encountered a strange behavior. I created role object and several user objects. The role was set to each of the users. After this the users were saved using a DAO. Then one of the user gets loaded to check whether he got the role that was passed to him before saving the user object. Calling getRoles() on the user shows that the role was set correctly.
To check whether the inverse direction also works the role object gets loaded from the database using a role DAO. But calling getUsers() on the role object just returns an empty set, although it should contain all the users with this role.
I double checked the database table but everything seems all right. User, role and user_role table were all filled correctly.
So why doesn't the role object contain any user?
I'm using Hibernate and Spring with the following classes.
User class
#Entity
#Table
public class User extends BusinessObject {
...
// Option 1
#ManyToMany(fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
targetEntity=Role.class)
#JoinTable(name= "user_role",
joinColumns = {#JoinColumn(name="user_id")},
inverseJoinColumns = {#JoinColumn(name="role_id")})
// Option 2
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name= "user_role",
joinColumns = {#JoinColumn(name="user_id")},
inverseJoinColumns = {#JoinColumn(name="role_id")})
private Set<Role> roles = new HashSet<Role>();
...
}
Role class
#Entity
#Table
public class Role extends BusinessObject {
...
// Option 1
#ManyToMany(fetch = FetchType.LAZY,
cascade = CascadeType.ALL,
mappedBy= "roles",
targetEntity = User.class)
// Option 2
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name= "user_role",
joinColumns = {#JoinColumn(name="role_id")},
inverseJoinColumns = {#JoinColumn(name="user_id")})
private Set<User> users = new HashSet<User>();
...
}
To test I'm using the following code in a JUnit test class.
#Test
public void test(){
Transaction trans = sessionFactory.getCurrentSession().beginTransaction();
Role userAdminRole = new Role();
userAdminRole.setName(RoleName.USER_ADMIN);
Role userRole = new Role();
userRole.setName(RoleName.USER);
User user1 = new User();
user1.setEmail("user1#user.de");
user1.getRoles().add(userAdminRole);
user1.getRoles().add(userRole);
userDao.save(user1);
User user2 = new User();
user2.setEmail("user2#user.de");
user2.getRoles().add(role);
userDao.save(user2);
User user3 = new User();
user3.setEmail("user3#user.de");
user3.getRoles().add(role);
userDao.save(user3);
trans.commit();
User loadedUser = userDao.load(user1.getId());
// Tests passes
Assert.assertNotNull(loadedUser);
Assert.assertEquals(user1, loadedUser);
Set<Role> roles = loadedUser.getRoles();
// Tests passes
Assert.assertEquals(2, roles.size());
Role loadedUserAdminRole = roleDao.findByName(RoleName.USER_ADMIN);
Set<User> users = loadedUserAdminRole.getUsers();
// Test fails: Count is 0 instead of 3 !!!!!!!
Assert.assertEquals(3, users.size());
}
UPDATE
Sorry I forgot to mention one thing. When I tested the code I of course didn't mark the many to many association twice in each class file. Instead I used either option 1 or option 2 in each class file.
The problem probably comes from the fact that you mapped the same bidirectional association twice. If you tell Hibernate twice about the same join table or join column, there is a problem. In a bidirectional association, one of the ends of the association must map the association, and the other one must tell Hibernate that it's the inverse of the other end, using the mappedBy attribute.
Since a many-to-many is completely symmetric, choose one of the end to be the owner (i.e. the end which maps the association, and thus have the #JoinTable annotation). The other side is just the inverse, and thus doesn't have a #JoinTable annotation, but has a mappedBy attribute.
Example:
#Entity
#Table
public class User extends BusinessObject {
...
// This end is the owner of the association
#ManyToMany
#JoinTable(name= "user_role",
joinColumns = {#JoinColumn(name="user_id")},
inverseJoinColumns = {#JoinColumn(name="role_id")})
private Set<Role> roles = new HashSet<Role>();
...
}
#Entity
#Table
public class Role extends BusinessObject {
...
// This end is not the owner. It's the inverse of the User.roles association
#ManyToMany(mappedBy = "roles")
private Set<User> users = new HashSet<User>();
...
}
Additional notes:
targetEntity isn't useful, since Hibernate knows it thanks to the generic type of the Set. It would be useful if the Set was a Set<SomeInterface>
CascadeType.ALL is certainly not what you want. Do you want to delete all the roles of a user when deleting a user? What should happen to the other users having these roles?
You should almost always initialize both ends of a bidirectional association. Hibernate takes into account the owner end (i.e. the end without the mappedBy attribute) to persist the association.
All of this is explained in the Hibernate reference documentation. Read it: it's full of useful information, and is not hard to understand.
When dealing with a bidirectional many-to-many association you have to maintain both ends of the association. In your case, you have to add the user to the role as well. Adding the role to the user isn't sufficient to establish a bidirectional association as you can read in book Java Persistance with Hibernate:
As always, a bidirectional association (no matter of what multiplicity) requires that you set both ends of the association.

Categories