Spring Data: managing a bidirectional many-to-many relationship - java

I have a bidirectional many-to-many relationship between a Role and Scope. Creating both entities and even their childs with the help of CascadeType.PERSIST is easy and straightforward.
The Role entity is simples as that:
#Entity
#Table(uniqueConstraints = #UniqueConstraint(name = "role_name", columnNames = "name"))
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST, mappedBy = "roles")
private Set<Scope> scopes;
}
And the Scope:
#Entity
#Table(uniqueConstraints = #UniqueConstraint(name = "scope_name", columnNames = "name"))
public class Scope {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String name;
#JoinTable(name = "role_scopes", joinColumns = #JoinColumn(name = "scope_id"), inverseJoinColumns = #JoinColumn(name = "role_id"))
#ManyToMany(cascade = CascadeType.REMOVE)
private Set<Role> roles;
}
Their repositories are simply CrudRepository extensions:
public interface RoleRepository extends CrudRepository<Role, Long> {}
public interface ScopeRepository extends CrudRepository<Scope, Long> {}
The following snippet exemplifies the entities insertion:
Role adminRole = roleRepository.save(new Role("ADMIN"));
Scope allReadScope = scopeRepository.save(new Scope("all.read"));
Scope allWriteScope = scopeRepository.save(new Scope("all.write"));
Role and Scope can be both automatically easily persisted with the help of the CascadeType.PERSIST, as follows:
Role managedRole = roleRepository.save(new Role("ADMIN", new Scope("all.read"), new Scope("all.write")));
However... Updating managedRole leads to org.hibernate.PersistentObjectException: detached entity passed to persist exception:
managedRole.getScopes().remove(allReadScope);
roleRepository.save(managedRole); // PersistentObjectException!
I tried modifying the Role::scopes's CascadeType to also include DETACH, MERGE and/or REFRESH with no success. How do we get around this?

Most likely you face the problem, because you don't maintain both sides of the relationship in the bidirectional mapping. Lets say in Role:
void add(Scope scope) {
this.scopes.add(scope);
scope.getRoles().add(this);
}
To be honest with you, I'd resign fully from bidirectional mapping. Maintaining this is a real nightmare.

Related

Spring Boot JPA many to many delete cascade

So i have a MyUser entity which refers to a Role entity with a many to many relationship.
But when i try to delete the user, i always get the errror that the user_id is always referenced on the user_role jointable...
I have already tried every cascade type... but didn't get the solution
Please help
Thanks
#Table(name = "users")
public class MyUser {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
... Other properties ...
#ManyToMany
#JoinTable(
name = "users_roles",
joinColumns = #JoinColumn(
name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = #JoinColumn(
name = "role_id", referencedColumnName = "id"))
private Collection<Role> roles;
// getters setters
}
#Entity
#Table(name = "roles")
public class Role {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String role;
// getters setters
}
As it stated in the documentation:
For #ManyToMany associations, the REMOVE entity state transition doesn’t make sense to be cascaded because it will propagate beyond the link table. Since the other side might be referenced by other entities on the parent-side, the automatic removal might end up in a ConstraintViolationException.
So, before removing Role entity you should be assured that there is no MyUser entity linked with this role.

Hibernate annotations: Many-to-many with shared composite key attribute

I'm trying to map up an existing database schema using Hibernate+JPA annotations.
One of my entities are mapped like this:
#Entity
#Table(name = "users")
public class User implements Serializable {
#Id
private int department;
#Id
private int userId;
...
And another entity, Group:
#Entity
#Table(name = "groups")
public class Group implements Serializable {
#Id
private int department;
#Id
private int groupId;
...
Group and User should have a many-to-many relationship between them, but the issue is that the join table ("user_group") only has columns "DEPARTMENT, USERID, GROUPID" - i.e. the DEPARTMENT column needs to be used in both joinColumns and inverseJoinColumns:
#ManyToMany(cascade = { CascadeType.ALL })
#JoinTable(
name = "user_groups",
joinColumns = { #JoinColumn(name = "department"), #JoinColumn(name = "groupid") },
inverseJoinColumns = {#JoinColumn(name = "department"), #JoinColumn(name = "userid") }
)
private List<User> groupUsers = new ArrayList<>();
which gives a mapping error - "Repeated column in mapping for entity".
However, it looks like this was/is possible using XML, because this exact example exists in the old Hibernate documentation. But I cannot find any evidence that this ever worked using annotations? I tried with #JoinFormula instead of #JoinColumn, but that does not compile. Is it possible?
Okay, I'm pretty sure it's not possible.
I found a promising workaround:
Create an #Embeddable for the "user_group" table:
#Embeddable
public class UserGroupMembership implements Serializable {
#ManyToOne
#JoinColumnsOrFormulas(
value = {
#JoinColumnOrFormula(column = #JoinColumn(referencedColumnName = "userid", name = "userid")),
#JoinColumnOrFormula(formula = #JoinFormula(referencedColumnName = "department", value = "department"))
})
private User user;
public UserGroupMembership(User user) {
this.user = user;
}
public UserGroupMembership() {
}
public User getUser() {
return user;
}
}
The trick is that #ManyToOne allows you to use #JoinColumnsOrFormulas, so one of the join conditions can be a formula, which I doesn't seem to work for #ManyToMany (the #JoinColumnsOrFormulas annotation is ignored as it expects the join columns to be part of the #JoinTable annotation).
The UserGroupMemberships are then mapped as a ElementCollection:
#ElementCollection
#CollectionTable(name = "user_group", joinColumns = {
#JoinColumn(name = "department", referencedColumnName = "department"),
#JoinColumn(name = "groupid", referencedColumnName = "groupid")
})
#OrderColumn(name = "seq", nullable = false)
private List<UserGroupMemberships> groupUsers = new ArrayList<>();
This only works right now for a unidirectional many-to-many relationship.

Get id's of a ManyToMany mapping table

I'm writing an API using Spring Boot and Hibernate where my persisted entity objects are also used as DTOs sent to and from the client. This is a simplified version of a typical entity I use:
#Entity
#Table(name = "STUDENT")
public class Student {
#Id
#GeneratedValue
#Column(name = "ID")
private Long id;
#ElementCollection
#CollectionTable(name = "GROUP_STUDENT",
joinColumns = #JoinColumn(name = "GROUP_ID"))
#Column(name="STUDENT_ID")
private Set<Long> groupIds;
#JsonIgnore
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name="GROUP_STUDENT",
joinColumns = #JoinColumn(name="GROUP_ID"),
inverseJoinColumns = #JoinColumn(name="STUDENT_ID")
)
private Set<Group> groups = new HashSet<>();
// getters and setters
}
and this is the associated class:
#Entity
#Table(name = "GROUP")
public class Group {
#Id
#GeneratedValue
#Column(name = "ID")
private Long id;
#JsonIgnore
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "groups")
private Set<Student> students = new HashSet<>();
// getters and setters
}
As you can see, there is a #ManyToMany association between Student and Group.
Since I send objects like these to the client, I choose to send only the id's of the associations and not the associations themselves. I've solved this using this answer and it works as expected.
The problem is this. When hibernate tries to persist a Student object, it inserts the groups as expected, but it also tries to insert the groupIds into the mapping table GROUP_STUDENT. This will of course fail because of the unique constraint of the mapping table composite id. And it isn't possible to mark the groupIds as insertable = false since it is an #ElementCollection. And I don't think I can use #Formula since I require a Set and not a reduced value.
This can of course be solved by always emptying either the groups of the groupIds before saving or persisting such an entity, but this is extremely risky and easy to forget.
So what I want is basically a read only groupIds in the Student class that loads the data from the GROUP_STUDENT mapping table. Is this possible? I'm grateful for any suggestions and glad to ellaborate on the question if it seems unclear.
I've managed to solve this by making the id-collection #Transient and populating it using #PostLoad:
#Entity
#Table(name = "STUDENT")
public class Student {
#PostLoad
private void postLoad() {
groupIds = groups.stream().map(Group::getId).collect(Collectors.toSet());
}
#Id
#GeneratedValue
#Column(name = "ID")
private Long id;
#Transient
private Set<Long> groupIds;
#JsonIgnore
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name="GROUP_STUDENT",
joinColumns = #JoinColumn(name="GROUP_ID"),
inverseJoinColumns = #JoinColumn(name="STUDENT_ID")
)
private Set<Group> groups = new HashSet<>();
// getters and setters
}

JPA. Stackoverflow on cascade merge

Here is my JPA structure:
Movie (look at cascade types):
#Entity
#Table(name = "movie")
public class Movie {
#Id
#Column(name = "movie_id")
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
//#OneToMany(cascade = CascadeType.ALL, mappedBy = "primaryKey.movie") //stack overflow
#OneToMany(mappedBy = "primaryKey.movie") //works fine
private List<Rating> ratings;
....
}
Rating:
#Entity
#Table(name = "rating")
#AssociationOverrides({#AssociationOverride(name = "primaryKey.movie", joinColumns = #JoinColumn(name = "movie_id")),
#AssociationOverride(name = "primaryKey.user", joinColumns = #JoinColumn(name = "imdb_user_id"))})
public class Rating {
#EmbeddedId
private RatingId primaryKey = new RatingId();
#Column(name = "rating_value")
private Integer ratingValue;
.....
}
RatingId:
#Embeddable
public class RatingId implements Serializable{
#ManyToOne
private Movie movie;
#ManyToOne
private User user;
}
When I call entityManager.merge(Movie movie) with CascadeType.ALL I get the StackOverflowError. If remove cascading, merge call doesn't throw the error. Where may be a problem?
I think this problem related to composite primary key. There is no error when merge performed on another entities with the same one-to-many relationship, but without composite id.
StackOverflow was caused by cyclic relations. To avoid exception I marked keys in many-to-many table as #ManyToOne(fetch = FetchType.LAZY).
That's how my tables look after modifications: https://stackoverflow.com/a/32544519/2089491

Hibernate - ManyToOne & Inheritance / JOINED / mappedBy

I have some problems with inheritance mapping. Here my database structure:
And associated entities:
AbstractEntity:
#MappedSuperclass
public abstract class AbstractEntity<ID extends Serializable> implements Serializable {
#Id #GeneratedValue(strategy = IDENTITY)
#Column(unique = true, updatable = false, nullable = false)
private ID id;
public ID getId() {
return id;
}
#SuppressWarnings("unused")
public void setId(ID id) {
this.id = id;
}
UserAcitvity entity:
#Entity #Table(name = "user_activity")
#Inheritance(strategy = JOINED)
#AttributeOverride(name = "id", column = #Column(name = "ua_id"))
public abstract class UserActivity extends AbstractEntity<Long> {
#ManyToOne(cascade = { MERGE, PERSIST }, fetch = LAZY)
#JoinColumn(name = "ua_user_id")
private User user;
...
}
Comment entity:
#Entity #Table(name = "comment")
#PrimaryKeyJoinColumn(name = "cm_id")
public class Comment extends UserActivity {
#ManyToOne(cascade = { MERGE, PERSIST }, fetch = LAZY)
#JoinColumn(name = "cm_question_id")
private Question question;
...
}
Question entity:
#Entity #Table(name = "question")
#PrimaryKeyJoinColumn(name = "qs_id")
public class Question extends UserActivity {
...
#OneToMany(fetch = LAZY, cascade = ALL, mappedBy = "question")
private List<Answer> answers = new ArrayList<>();
#OneToMany(fetch = LAZY, cascade = ALL, mappedBy = "question")
private List<Comment> comments = new ArrayList<>();
...
}
Answer entity:
#Entity #Table(name = "answer")
#PrimaryKeyJoinColumn(name = "asw_id")
public class Answer extends UserActivity {
#ManyToOne(cascade = { MERGE, PERSIST }, fetch = LAZY)
#JoinColumn(name = "asw_question_id")
private Question question;
...
}
and User entity:
#Entity #Table(name = "user")
#AttributeOverride(name = "id", column = #Column(name = "user_id"))
public class User extends AbstractEntity<Long> {
...
#OneToMany(cascade = REMOVE)
private List<Question> questions = new ArrayList<>();
#OneToMany(cascade = REMOVE)
private List<Answer> answers = new ArrayList<>();
#OneToMany(cascade = REMOVE)
private List<Comment> comments = new ArrayList<>();
...
}
Problem:
When I try to save or delete a User I get an exceptions:
org.springframework.dao.InvalidDataAccessResourceUsageException: could not prepare statement; SQL [insert into user_question (user_user_id, questions_qs_id) values (?, ?)]; nested exception is org.hibernate.exception.SQLGrammarException: could not prepare statement
and:
org.hibernate.engine.jdbc.spi.SqlExceptionHelper : 147 = user lacks privilege or object not found: USER_ANSWER
Hibernate is trying to create a table: user_question and user_answer which me do not need.
What I should doing for fixes ?
I don't think you can achieve this by mapping the ManyToOne association to User generically in the UserActivity entity. That's probably too confusing for the JPA provider (Hibernate).
Instead, I think you need to map the association to User in each of the Question, Answer and Comment entities. Yes, I know that would be duplicated code, but it looks like the only way you will then be able to qualify the OneToMany mappings in User using the mappedBy reference.
For instance, your Question entity would have an association defined as:
#ManyToOne(cascade = { MERGE, PERSIST }, fetch = LAZY)
#JoinColumn(name = "ua_user_id")
private User questionUser;
Depending on how clever (or not) Hibernate is about the above association, you may need to specify the table="USER_ACTIVITY" in the JoinColumn annotation.
Then the User would have the OneToMany as:
#OneToMany(mappedBy="questionUser", cascade = REMOVE)
private List<Question> questions = new ArrayList<>();
Similarly for each of Answer and Comment.
Of course, I haven't tried this, so I could be wrong.
It's probably happening because when you set the #OneToMany mapping then the hibernate will create an auxiliary table that will store the id from the entities on the relationship.
In this case you should try the following:
#OneToMany(cascade = REMOVE)
#JoinColumn(name = "answer_id")
private List<Answer> answers = new ArrayList<>();
The #JoinColumn annotation will map the relationship without the creation of the auxiliary table, so it's pretty likely this solution will help you in this situation.
Try this mapping, this should work as you expect according to section 2.2.5.3.1.1 of the documentation:
#Entity
public class User {
#OneToMany(cascade = REMOVE)
#JoinColumn(name="user_fk") //we need to duplicate the physical information
private List<Question> questions = new ArrayList<>();
...
}
#Entity
public class Question {
#ManyToOne
#JoinColumn(name="user_fk", insertable=false, updatable=false)
private User user;
...
}
The reason why the auxiliary association is created, is that there is no way for Hibernate to know that the Many side of the relation (for example Question) has a foreign key back to User that corresponds to the exact same relation as User.questions.
The association Question.user could be a completely different association, for example User.questionCreator or User.previousSuccessfulAnswerer.
Just by looking at Question.user, there is no way for Hibernate to know that it's the same association as User.questions.
So without the mappedBy indicating that the relation is the same, or #JoinColumn to indicate that there is no join table (but only a join column), Hibernate will trigger the generic one-to-many association mapping solution that consists in creating an auxiliary mapping table.
The schema misses such association tables, which causes the error that can be solved with the mapping above.
If you want unidirectional one-to-many usage in your entity relationship.
Try with..JoinTable
#OneToMany(cascade = REMOVE)
#JoinTable(name = "user_question", joinColumns = {
#JoinColumn(name = "user_id")}, inverseJoinColumns = {
#JoinColumn(name = "qs_id")})
private List<Question> questions = new ArrayList<>();

Categories

Resources