I have a three classes scenario. Classes User and UserDetail are associated by a #OneToOne relationship, while UserDetail and Document are associated by a #OneToMany relationship. I have used CascadeType.ALL and orphanRemoval=true on both of them, as described below.
As the classes specify, a User does not necessarilly have a UserDetail, but every UserDetail must have a User (therefore the optional flag). Every Document must be associated to a UserDetail. This is not an optional relationship!
public class User {
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL, optional = true, orphanRemoval = true)
private UserDetail details;
}
public class UserDetail {
#OneToOne(optional = false)
private User user;
#OneToMany(mappedBy = "detail", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Document> documents;
}
public class Document {
#ManyToOne
private UserDetail detail;
}
Everything works fine on persisting, whith all data being saved, but when I try to set UserDetail to null whith a document list is populated, I get an integrity constraint violation exception at the Documents. The foreign key to UserDetail can't be set to null.
As far as I could understand, when setting details to null on User object and trying to merge it, the provider tries to delete the orphan UserDetail, but does not cascade the delete command correctly. If I try do DELETE the User, then the cascading operation works perfectly and no exception gets thrown.
Question:
Shouldn't merge operation trigger the deletion of everything, since I'm cascading and using orphanRemoval?
Obs.: Using (Hibernate 4.3.7) as provider
Related
Consider the following code:
#Entity
public class User {
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "USER_ID")
private List<UserRole> roles;
}
According to the code above User.roles will be loaded lazily. However, we can change this behavior using fetchgraph, something like this:
EntityGraph<User> graph = entityManager.createEntityGraph(User.class);
graph.addSubgraph("roles");
typedQuery.setHint("javax.persistence.fetchgraph", graph);
List<User> entities = typedQuery.getResultList();//roles will be eagerly loaded
Is is possible to make JPA provider/Hibernate create and update User with roles field when #OneToMany.cascade = null? By other words, I want to add cascade = CascadeType.ALL dynamically to have a full and dynamic control over entity tree for all create/update/delete operations.
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?
I'm having a hard time understanding this JPA behavior which to me doesn't seem to follow the specification.
I have 2 basic entities:
public class User {
#Id
#Column(name = "id", unique = true, nullable = false, length = 36)
#Access(AccessType.PROPERTY)
private ID id;
#OrderBy("sequence ASC")
#OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = { CascadeType.REMOVE })
private final Set<UserProfile> userprofiles = new HashSet<UserProfile>(0);
//Ommiting rest of fields since they aren't relevant
}
public class UserProfile {
#Id
#Column(name = "id", unique = true, nullable = false, length = 36)
#Access(AccessType.PROPERTY)
private ID id;
#NotNull
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "userID", nullable = false, foreignKey = #ForeignKey(name = "FK_UserProfile_User"))
private User user;
//Ommiting rest of fields since they aren't relevant
}
As you can see I only have cascading set to REMOVE, the behavior will be the same if I don't have cascade set at all.
Now if I call:
User user = new User();
user.setId(UUIDGenerator.generateId());
UserProfile userProfile = new UserProfile();
userProfile.setId(UUIDGenerator.generateId());
userProfile.setUser(user);
user.getUserProfiles().add(userProfile);
em.merge(user);
merge will throw an exception.
I see Hibernate is executing a SQL query against the UserProfile table:
select userprofil0_.userProfileID as userProf1_4_0_, userprofil0_.profileID as profileI3_4_0_, userprofil0_.sequence as sequence2_4_0_, userprofil0_.userID as userID4_4_0_ from UserProfile userprofil0_ where userprofil0_.userProfileID=?
And then it will throw an exception
org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find com.mytest.domain.UserProfile with id 6aaab891-872d-41e6-8362-314601324847;
Why is this query even called?
Since I don't have cascade type set to MERGE in userprofiles my expectation would be that JPA/Hibernate would simply ignore the entities inside userprofiles set and only insert/update the user record, doesn't this go against the JPA specs?
If I change cascadetype to MERGE things will work as expected and both User and UserProfile will be added to the database, so no problem there. What puzzles me is why is Hibernate querying the database and erroring out about an entity that's not supposed to be merged at all since I don't have it set to cascade.
This is more of an academic scenario that I ran into, of course I could simply clear the userprofiles set and things would work, but I'm trying to understand why the above behavior happens since I'm probably missing some crucial piece of information about how merge works. It seems it will always try to attach all entities to the session regardless cascade type being set or not.
Why is this query even called?
It's because you are trying to merge the entity, in JPA merge() is used to make the entity managed/attached. To "merge" User, JPA needs to still maintian the references it holds(UserProfile). In your case its not trying to persist UserProfile its trying to get a reference to it to merge User. Read here
If you use persist rather than merge this should not happen.
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.
I am using JPA (Hibernate) with the following entity class with one one-to-many relationship.
When I add elements to the list, and then persist the Organization entity, it adds the new elements to the proyects table, but when I remove elements from the list, nothing happens when persist (or merge), and I would like these elements to be removed from the database.
I tried also orphanRemoval=true in the OneToMany annotation, but it doesn't work.
#Entity
public class Organization {
#Id
#GeneratedValue
public long internalId;
#Basic
#Column(nullable = false, length = 100)
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "organization")
private List<Proyect> proyects;
// Getters and Setters
}
You need to set Proyect.organization to null and update that entity, since this property is responsible for the database entry (Proyect is the owning side in this case ).