The scenario is this. I have two entities, User and Post for a Twitter-like application. Consider the following
User Alice creates a post.
User Bob favorites the aforementioned post.
User Alice tries to delete the post.
When User Alice tries to delete the post, I get an error from Hibernate;
ERROR: update or delete on table "posts" violates foreign key constraint "fk_jof9iwt9m3lfjxix5ejri4iv9" on table "favorite_posts"
Detail: Key (id)=(16) is still referenced from table "favorite_posts".
Code for my entities;
#Table(name = "users")
public class User {
...
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Post> posts = new ArrayList<Post>();
#OneToMany(cascade = CascadeType.ALL)
#JoinTable(name = "favorite_posts")
private Set<Post> favoritePosts = new HashSet<Post>();
...
}
#Table(name = "posts")
public class Post {
...
#ManyToOne
private User user;
...
}
My alternatives as I see them;
Fetch all of the Users who have favorited a Post, and then clear all of them one by one.
Make the association bidirectional and then clear all of the favorites before deletion.
How can I get Hibernate to delete the association (= the corresponding row in favorite_posts) before trying to delete the entity?
I'd solve this problem at the database level using ON DELETE CASCADE on the foreign key. It's the simpliest solution, and it doesn't bring extra complexity to the application layer.
However, this approach doesn't play well with your domain model, because your model treats User - Post relationships (both post and favoritePosts) as parts of a User. Therefore, you'll get constraint violation if you try to save a User while one of the Posts it's associated with is being removed.
This can be solved by moving ownership of User - Posts relationships away from User, for example, as follows:
User <- Post: you probably would never need all Posts created by a User at once (without filtering or pagination). Therefore it makes no sense to map a relationship from User to Post, unidirectional relationship from Post to User is enough.
User <- FavoritePost -> Post: moving "favorite post" relationship to its own entity allows it to have its own lifecycle. Now FavoritePost can silently disappear when associated Post is removed, and it won't create any inconsistency at the application level.
Related
Let's say I have two entities: Group and User. Every user can be member of many groups and every group can have many users.
#Entity
public class User {
#ManyToMany
Set<Group> groups;
//...
}
#Entity
public class Group {
#ManyToMany(mappedBy="groups")
Set<User> users;
//...
}
Now I want to remove a group (let's say it has many members).
Problem is that when I call EntityManager.remove() on some Group, JPA provider (in my case Hibernate) does not remove rows from join table and delete operation fails due to foreign key constrains. Calling remove() on User works fine (I guess this has something to do with owning side of relationship).
So how can I remove a group in this case?
Only way I could come up with is to load all users in the group, then for every user remove current group from his groups and update user. But it seems ridiculous to me to call update() on every user from the group just to be able to delete this group.
The ownership of the relation is determined by where you place the 'mappedBy' attribute to the annotation. The entity you put 'mappedBy' is the one which is NOT the owner. There's no chance for both sides to be owners. If you don't have a 'delete user' use-case you could simply move the ownership to the Group entity, as currently the User is the owner.
On the other hand, you haven't been asking about it, but one thing worth to know. The groups and users are not combined with each other. I mean, after deleting User1 instance from Group1.users, the User1.groups collections is not changed automatically (which is quite surprising for me),
All in all, I would suggest you decide who is the owner. Let say the User is the owner. Then when deleting a user the relation user-group will be updated automatically. But when deleting a group you have to take care of deleting the relation yourself like this:
entityManager.remove(group)
for (User user : group.users) {
user.groups.remove(group);
}
...
// then merge() and flush()
The following works for me. Add the following method to the entity that is not the owner of the relationship (Group)
#PreRemove
private void removeGroupsFromUsers() {
for (User u : users) {
u.getGroups().remove(this);
}
}
Keep in mind that for this to work, the Group must have an updated list of Users (which is not done automatically). so everytime you add a Group to the group list in User entity, you should also add a User to the user list in the Group entity.
I found a possible solution, but... I don't know if it's a good solution.
#Entity
public class Role extends Identifiable {
#ManyToMany(cascade ={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinTable(name="Role_Permission",
joinColumns=#JoinColumn(name="Role_id"),
inverseJoinColumns=#JoinColumn(name="Permission_id")
)
public List<Permission> getPermissions() {
return permissions;
}
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
}
}
#Entity
public class Permission extends Identifiable {
#ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinTable(name="Role_Permission",
joinColumns=#JoinColumn(name="Permission_id"),
inverseJoinColumns=#JoinColumn(name="Role_id")
)
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
I have tried this and it works. When you delete Role, also the relations are deleted (but not the Permission entities) and when you delete Permission, the relations with Role are deleted too (but not the Role instance). But we are mapping a unidirectional relation two times and both entities are the owner of the relation. Could this cause some problems to Hibernate? Which type of problems?
Thanks!
The code above is from another post related.
As an alternative to JPA/Hibernate solutions : you could use a CASCADE DELETE clause in the database definition of your foreign key on your join table, such as (Oracle syntax) :
CONSTRAINT fk_to_group
FOREIGN KEY (group_id)
REFERENCES group (id)
ON DELETE CASCADE
That way the DBMS itself automatically deletes the row that points to the group when you delete the group. It works whether the delete is made from Hibernate/JPA, JDBC, manually in the DB, or any other way.
the cascade delete feature is supported by all major DBMS (Oracle, MySQL, SQL Server, PostgreSQL).
This works for me:
#Transactional
public void remove(Integer groupId) {
Group group = groupRepository.findOne(groupId);
group.getUsers().removeAll(group.getUsers());
// Other business logic
groupRepository.delete(group);
}
Also, mark the method #Transactional (org.springframework.transaction.annotation.Transactional), this will do whole process in one session, saves some time.
For what its worth, I am using EclipseLink 2.3.2.v20111125-r10461 and if I have a #ManyToMany unidirectional relationship I observe the problem that you describe. However, if I change it to be a bi-directional #ManyToMany relationship I am able to delete an entity from the non-owning side and the JOIN table is updated appropriately. This is all without the use of any cascade attributes.
This is what I ended up doing. Hopefully someone might find it useful.
#Transactional
public void deleteGroup(Long groupId) {
Group group = groupRepository.findById(groupId).orElseThrow();
group.getUsers().forEach(u -> u.getGroups().remove(group));
userRepository.saveAll(group.getUsers());
groupRepository.delete(group);
}
This is a good solution. The best part is on the SQL side – fine tuning to any level is easy.
I used MySql and MySql Workbench to Cascade on delete for the Required Foreign KEY.
ALTER TABLE schema.joined_table
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
This works for me on a similar issue where I failed to delete the user due to the reference. Thank you
#ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
If you are using Spring Data Jpa, then simply create a repository interface for the owner class Group.class, then use their deleteById(Long id) method extended from JpaRepository.class. Then, when you delete a Group, the related rows(containing the same group id as you specify) in the join table will also be removed. Be aware of the CascadeType, avoid CascadeType.All, otherwise it will attempt to delete the user from the User table, which would cause the foreign key constraint runtime error again.
I have these 2 tables
Users(
id PK,
name VARCHAR(30)
);
The other table is
Orders(
id PK,
orderBy FK Users.id,
orderTo FK Users.id
);
Now, what I want to do is to create Orders entity class which maps orderBy and orderTo to the user. But the most thing i am confuse about is what cascading i should use.
class Orders{
///
#ManyToOne(fetch = FetchType.Lazy
#JoinColumn(name="orderBy")
Users orderBy;
///
#ManyToOne(fetch = FetchType.Lazy
#JoinColumn(name="orderTo")
Users orderTo;
}
I am thinking to create two fields in Users Table such that
class Account{
///
#OneToMany(fetch = FetchType.Lazy)
#JoinColumn(name="orderTo")
List<Orders> ordersReceived;
///
#OneToMany(fetch = FetchType.Lazy)
#JoinColumn(name="orderBo")
List<Orders> ordersPlaced;
}
But again, I am not sure what cascading shall i use. My Users table will be populated by some other processes so orders has nothing to do with. I don't want when i am placing an order, that particular transaction should add/delete anything. HOWEVER, i might need to update a specific field of User whenever i place an order.
I'll suggest to avoid to use cascade at all (if possible)... When you place an order, you should follow the following steps:
1) load your user from your database
2) create your order ...
3) linkup your order to your user (this is, order.setOrderBy(user))
4) persist your order with your EntityManager.
5) Change your user attribute.
From my experience, Cascade should be used carefully. I only used it for persist entities in one shoot (Cascade.PERSIST) (example: persisting a newly user with another new entities like orders)
I am using hibernate with JPA annotations for relationship mapping.
I have three entities in my code User Group & User_Group
User & Group are in a ManyToMany relationship.
User_Group is a kinda bridge table but with some additional fields. So here is the modified mapping code.
User
#Entity
#Table(name = "USERS")
public class User {
#OneToMany(mappedBy = "user")
private Set<UserGroup> userGroups
}
Group
#Entity
#Table(name = "GROUPS")
public class Group {
#OneToMany(mappedBy = "group")
private Set<UserGroup> userGroups
}
UserGroup
#Entity
#Table(name = "USERS_GROUPS")
public class UserGroup {
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "USER_ID")
private User user;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "GROUP_ID")
private Group group;
}
When I set the user & group object to the usergroup & save it.
User user = new User("tommy", "ymmot", "tommy#gmail.com");
Group group = new Group("Coders");
UserGroup userGroup = new UserGroup();
userGroup.setGroup(group);
userGroup.setUser(user);
userGroup.setActivated(true);
userGroup.setRegisteredDate(new Date());
session.save(userGroup);
Things work fine. With CascadeType.ALL the group object & user object are updated too. But when I delete the userGroup object. The child object are deleted too.
Deletion of child objects is a strict no no.
There is no CascadeType.SAVE-UPDATE in JPA, which just does save or update but no delete. How do I achieve this.
If I remove the CascadeType.ALL from the mapping the child objects don't get updated & I need them to be updated.
SAVE_UPDATE is for save(), update(), and saveOrUpdate(), which are 3 Hibernate-proprietary methods. JPA only has persist() and merge(). So, if you want to use cascading on Hibernate-proprietary methods, you'll need to use Hibernate-proprietary annotations. In this case, Cascade.
Or you could stop using the Hibernate Session, and use the standard JPA API instead.
CascadeType.ALL includes CascadeType.REMOVE too.
The solution is to use all CascadeType.* you need except CascadeType.REMOVE, like so:
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE}))
in your UserGroup definitions.
It's almost always a code smell when propagating from child to parent entity, it should be the other way round.
From Cascading best practices:
Cascading only makes sense only for Parent – Child associations (the
Parent entity state transition being cascaded to its Child entities).
Cascading from Child to Parent is not very useful and usually, it’s a
mapping code smell.
From Hibernate best practices:
Avoid cascade remove for huge relationships
Most developers (myself included) get a little nervous when they see a
CascadeType.REMOVE definition for a relationship. It tells Hibernate
to also delete the related entities when it deletes this one. There is
always the fear that the related entity also uses cascade remove for
some of its relationships and that Hibernate might delete more
database records than intended. During all the years I’ve worked with
Hibernate, this has never happened to me, and I don’t think it’s a
real issue. But cascade remove makes it incredibly hard to understand
what exactly happens if you delete an entity. And that’s something you
should always avoid. If you have a closer look at how Hibernate
deletes the related entities, you will find another reason to avoid
it. Hibernate performs 2 SQL statements for each related entity: 1
SELECT statement to fetch the entity from the database and 1 DELETE
statement to remove it. This might be OK, if there are only 1 or 2
related entities but creates performance issues if there are large
numbers of them.
#Entity
public class EUser {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#ManyToMany(fetch=FetchType.EAGER)
private List<UserRole> roles;
}
when doing the following action
EUser approveUser = (EUser) userService.getOne(2);
approveUser.getRoles().clear();
userService.update(approveUser);
System.out.println(approveUser.getRoles().size());
it says the size is zero but when i go the db in the EUser_UserRole table i see the value still present. How to solve this??
also in the EUser_UserRole it says
This table does not contain a unique column. Grid edit, checkbox, Edit, Copy and Delete features are not available
how can i delete add edit delete manually??
Cascading is indeed a way to let Hibernate do the removal and if I see the posted code, that is most likely what is asked for. But since the question is about manually deleting while cascading is more automatic deletion, I have to add the suggestions to:
use EntityManager.remove()
invoke a JPQL delete query
Which more fit more the description of "manual" deletion.
se CascadeType. Reference
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<UserRole> roles;
Update :
orphanRemoval attribute can use JPA 2.x version. You have to find out the deleted UserRole data by comparing old rolesList and new rolesList`
orphanRemoval attribute does not support in ManyToMany mapping.
Do not use a cascade for ManyToMany relationships. This can result in an undesired rippling deletion over a wide entity cluster easier than one might hope.
If you want to clear the relationship (delete rows from the join table) for a single user to their roles, you will need to clear the relationship fields on both sides, meaning clearing the List of UserRole in EUser and removing the current EUser from the lists in the respective UserRole instances.
EDIT:
You are not deleting any entities from the database when clearing the lists of related entities. The only result will be that some rows in the join table will be deleted and after the next fetch/refresh, your EUser and UserRole instances will no longer be related.
If you want to remove the UserRoles DB entries, you can do so after removing the relationships to
Let's say I have two entities: Group and User. Every user can be member of many groups and every group can have many users.
#Entity
public class User {
#ManyToMany
Set<Group> groups;
//...
}
#Entity
public class Group {
#ManyToMany(mappedBy="groups")
Set<User> users;
//...
}
Now I want to remove a group (let's say it has many members).
Problem is that when I call EntityManager.remove() on some Group, JPA provider (in my case Hibernate) does not remove rows from join table and delete operation fails due to foreign key constrains. Calling remove() on User works fine (I guess this has something to do with owning side of relationship).
So how can I remove a group in this case?
Only way I could come up with is to load all users in the group, then for every user remove current group from his groups and update user. But it seems ridiculous to me to call update() on every user from the group just to be able to delete this group.
The ownership of the relation is determined by where you place the 'mappedBy' attribute to the annotation. The entity you put 'mappedBy' is the one which is NOT the owner. There's no chance for both sides to be owners. If you don't have a 'delete user' use-case you could simply move the ownership to the Group entity, as currently the User is the owner.
On the other hand, you haven't been asking about it, but one thing worth to know. The groups and users are not combined with each other. I mean, after deleting User1 instance from Group1.users, the User1.groups collections is not changed automatically (which is quite surprising for me),
All in all, I would suggest you decide who is the owner. Let say the User is the owner. Then when deleting a user the relation user-group will be updated automatically. But when deleting a group you have to take care of deleting the relation yourself like this:
entityManager.remove(group)
for (User user : group.users) {
user.groups.remove(group);
}
...
// then merge() and flush()
The following works for me. Add the following method to the entity that is not the owner of the relationship (Group)
#PreRemove
private void removeGroupsFromUsers() {
for (User u : users) {
u.getGroups().remove(this);
}
}
Keep in mind that for this to work, the Group must have an updated list of Users (which is not done automatically). so everytime you add a Group to the group list in User entity, you should also add a User to the user list in the Group entity.
I found a possible solution, but... I don't know if it's a good solution.
#Entity
public class Role extends Identifiable {
#ManyToMany(cascade ={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinTable(name="Role_Permission",
joinColumns=#JoinColumn(name="Role_id"),
inverseJoinColumns=#JoinColumn(name="Permission_id")
)
public List<Permission> getPermissions() {
return permissions;
}
public void setPermissions(List<Permission> permissions) {
this.permissions = permissions;
}
}
#Entity
public class Permission extends Identifiable {
#ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinTable(name="Role_Permission",
joinColumns=#JoinColumn(name="Permission_id"),
inverseJoinColumns=#JoinColumn(name="Role_id")
)
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
I have tried this and it works. When you delete Role, also the relations are deleted (but not the Permission entities) and when you delete Permission, the relations with Role are deleted too (but not the Role instance). But we are mapping a unidirectional relation two times and both entities are the owner of the relation. Could this cause some problems to Hibernate? Which type of problems?
Thanks!
The code above is from another post related.
As an alternative to JPA/Hibernate solutions : you could use a CASCADE DELETE clause in the database definition of your foreign key on your join table, such as (Oracle syntax) :
CONSTRAINT fk_to_group
FOREIGN KEY (group_id)
REFERENCES group (id)
ON DELETE CASCADE
That way the DBMS itself automatically deletes the row that points to the group when you delete the group. It works whether the delete is made from Hibernate/JPA, JDBC, manually in the DB, or any other way.
the cascade delete feature is supported by all major DBMS (Oracle, MySQL, SQL Server, PostgreSQL).
This works for me:
#Transactional
public void remove(Integer groupId) {
Group group = groupRepository.findOne(groupId);
group.getUsers().removeAll(group.getUsers());
// Other business logic
groupRepository.delete(group);
}
Also, mark the method #Transactional (org.springframework.transaction.annotation.Transactional), this will do whole process in one session, saves some time.
For what its worth, I am using EclipseLink 2.3.2.v20111125-r10461 and if I have a #ManyToMany unidirectional relationship I observe the problem that you describe. However, if I change it to be a bi-directional #ManyToMany relationship I am able to delete an entity from the non-owning side and the JOIN table is updated appropriately. This is all without the use of any cascade attributes.
This is what I ended up doing. Hopefully someone might find it useful.
#Transactional
public void deleteGroup(Long groupId) {
Group group = groupRepository.findById(groupId).orElseThrow();
group.getUsers().forEach(u -> u.getGroups().remove(group));
userRepository.saveAll(group.getUsers());
groupRepository.delete(group);
}
This is a good solution. The best part is on the SQL side – fine tuning to any level is easy.
I used MySql and MySql Workbench to Cascade on delete for the Required Foreign KEY.
ALTER TABLE schema.joined_table
ADD CONSTRAINT UniqueKey
FOREIGN KEY (key2)
REFERENCES schema.table1 (id)
ON DELETE CASCADE;
This works for me on a similar issue where I failed to delete the user due to the reference. Thank you
#ManyToMany(cascade = {CascadeType.MERGE, CascadeType.PERSIST,CascadeType.REFRESH})
If you are using Spring Data Jpa, then simply create a repository interface for the owner class Group.class, then use their deleteById(Long id) method extended from JpaRepository.class. Then, when you delete a Group, the related rows(containing the same group id as you specify) in the join table will also be removed. Be aware of the CascadeType, avoid CascadeType.All, otherwise it will attempt to delete the user from the User table, which would cause the foreign key constraint runtime error again.