I have three separate entities in my Spring JPA application - User, Department, Role
I have a single join table in my database to relate each of these Entities: USER_DEPARTMENT_ROLE
My question is, how can I define this relation in my entity classes? Do I have to define a #ManyToMany relationship in each of the separate entities? I know how to define this relationship between two tables, but for more than two I'm not sure where to start.
Any help is appreciated!
If you have more than two relations mapped in your join table then i would suggest creating a separate entity which would be used for mapping that particular table.
The question is whether you can have a distinct id column which would serve as an artificial primary key or you have to stick with the composite primary key build from the three foreign keys.
If you can add that artificial id (which is the modern way of designing your database) then your mapping should look something like the following:
Option 1
class User {
#OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<UserDepartmentRoleLink> userDepartmentRoleLinks;
}
class Department{
#OneToMany(mappedBy = "department", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<UserDepartmentRoleLink> userDepartmentRoleLinks;
}
class Role{
#OneToMany(mappedBy = "role", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<UserDepartmentRoleLink> userDepartmentRoleLinks;
}
class UserDepartmentRoleLink {
#Id
#GeneratedValue
private Long id;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "user_id")
private User user;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "department_id")
private Department department;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "role_id")
private Role role;
}
Regarding setting the cascade types for the many to many relatioship is tricky and for many to many involving three tables is even trickier as every entity can play a role of parent or child depending on the circumstances.. i would suggest sticking only with the cascade = {CascadeType.PERSIST, CascadeType.MERGE} and handling other operations manually.
If you have to stay with the composite primary key then you should add additional Id class and change the link entity to the following:
Option 2
class User {
#OneToMany(mappedBy = "linkPk.user", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<UserDepartmentRoleLink> userDepartmentRoleLinks;
}
class Department{
#OneToMany(mappedBy = "linkPk.department", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<UserDepartmentRoleLink> userDepartmentRoleLinks;
}
class Role{
#OneToMany(mappedBy = "linkPk.role", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<UserDepartmentRoleLink> userDepartmentRoleLinks;
}
Linkage table
class UserDepartmentRoleLink {
#EmbeddedId
private UserDepartmentRoleLinkId linkPk
= new UserDepartmentRoleLinkId();
#Transient
public User getUser() {
return getLinkPk().getUser();
}
#Transient
public User getDepartment() {
return getLinkPk().getDepartment();
}
#Transient
public User getRole() {
return getLinkPk().getRole();
}
}
#Embeddable
public class UserDepartmentRoleLinkId implements java.io.Serializable {
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "user_id")
private User user;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "department_id")
private Department department;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "role_id")
private Role role;
The bottom line is that you can use Many To Many here like outlined in this post -> example. But in my opinion you would save yourself a lot of headache if you map that link table as above. In the end the call is yours..
Related
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.
I have an Invoice entity which contains another entity - Counterparty and a list of another entities - Items. While setting up relations between list of items and invoice, there were no problems. However, when I try to set up a similar relation between invoice entity and counterparty entity, I get an error:
#OneToOne or #ManyToOne on pl.coderstrust.model.Invoice.counterparty references an unknown entity: pl.coderstrust.model.counterparty.Counterparty
This is my invoice, which expects to contain only one counterparty and a list of items.
#Entity
#Table(name = "invoices")
public class Invoice implements Comparable<Invoice>, Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "invoice_id")
private int id;
#Column(name = "date")
private LocalDate date = LocalDate.now();
#ManyToOne(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST,
CascadeType.REFRESH})
#JoinColumn(name = "nip")
private Counterparty counterparty;
#OneToMany(mappedBy = "invoice", cascade = {CascadeType.PERSIST, CascadeType.MERGE,
CascadeType.DETACH, CascadeType.REFRESH})
#JsonBackReference
private List<InvoiceItem> invoiceItems = new ArrayList<>();
This is my item entity, which can be related to one invoice:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "item_id")
private int id;
private String description;
private int numberOfItems;
private BigDecimal amount;
private BigDecimal vatAmount;
#JoinColumn(name = "vat_code")
#Enumerated(EnumType.ORDINAL)
private Vat vat;
#OneToOne(mappedBy = "invoice_id", fetch = FetchType.EAGER, cascade = {CascadeType.DETACH,
CascadeType.MERGE, CascadeType.PERSIST,
CascadeType.REFRESH})
#JsonManagedReference
private Invoice invoice;
This is my counterparty, which is supposed to be related to many invoices:
#Entity
#Table(name = "counterparties")
public class Counterparty implements Serializable {
#Id
#Column(name = "nip")
private String nip;
private String companyName;
private String phoneNumber;
private String bankName;
private String bankNumber;
#OneToOne(fetch = FetchType.LAZY, mappedBy = "counterparty", cascade = CascadeType.ALL)
private Address address;
#OneToMany(mappedBy = "counterparty", fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH, CascadeType.REFRESH})
private List<Invoice> invoices;
What is wrong with invoice-counterparty relations?
That Hibernate error is usually thrown when the class is not added to the hibernate configuration. Hibernate needs to be told all of the classes that serve as entities before it can use them.
How do you make your classes known to Hibernate? I.e. either by adding the class to the Configuration object:
configuration.addClass(Counterparty.class);
or by adding the class into a package that is scanned for entities when you are using Spring?
On another note: there seems to be something odd with the Item class perhaps? It specifies a OneToOne relation to Invoice; should this not be a ManyToOne (meaning that a single invoice can have 0 or more Items)?
I have an issue with a delete to a Many side of a ManyToOne relationship. I've already removed all CascadeTypes from the relationship but the issue still remains. The entry won't be removed (only selects are executed and no delete query). I'm trying to delete it through a CRUD repository call to delete. It calls the method and executes successfully but nothing happens.
The relationship goes as follows: an Activity has an assigned Course, a course can have many activities assigned to it. An Activity has a specific ActivityType.
The classes are as below.
Activity
public class Activity implements Item, Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
#ManyToOne
#JoinColumn(name = "type_id", nullable = false)
private ActivityType type;
#ManyToOne
#JoinColumn(name = "course_id", nullable = false)
#JsonSerialize(using = CustomCourseSerializer.class)
private Course course;
...
}
Course
public class Course implements Item, Serializable {
...
#OneToMany(mappedBy = "course", fetch = FetchType.EAGER, targetEntity = Activity.class) //cascade = { CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH, CascadeType.REMOVE}
#Fetch(value = FetchMode.SUBSELECT)
private List<Activity> activities;
...
}
Activity Type (has no reference to Activity)
public class ActivityType implements Item, Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name", nullable = false, unique = true)
private String name;
...
}
Any ideas how can I solve this issue or at least debug it? Thank you.
Add orphanRemoval = true attribute in the #OneToMany annotation in your Course entity.
public class Course implements Item, Serializable {
...
#OneToMany(mappedBy = "course", fetch = FetchType.EAGER, targetEntity = Activity.class, orphanRemoval = true, cascade = CascadeType.REMOVE )
#Fetch(value = FetchMode.SUBSELECT)
private List<Activity> activities;
...
}
Try to delete reference to Activity from Course. It seems unnecessary to me
The scenario is the following
I have 2 tables, Company and Activity. A company can have one or more activities. One of these activities is a "primary" activity, and all others become secondary.
To handle this, I created 2 entities (Activity, Company) and a third entity for the join table, which is CompanyActivity
I used this tutorial as a starting point
Below my code (getters and setters omitted)
Company.java
#Entity
#Table(name = "T_COMPANY")
public class Company {
#Id
#Column(name = "COM_ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "company")
private List<CompanyActivity> activities = new ArrayList<>();
}
Activity.java
#Entity
#Table(name = "T_ACTIVITY")
public class Activity {
#Id
#Column(name = "ACT_ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String code;
private String description;
private boolean availableOnline;
}
CompanyActivity.java
#Entity
#Table(name = "T_COMPANY_ACTIVITY")
public class CompanyActivity {
#Id
#Column(name = "COM_ACT_ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "COM_ID")
private Company company;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "ACT_ID")
private Activity activity;
private boolean primary;
}
Adding activities for a company works without a problem. The children collection contains the newly added activities, and there is always one marked as primary as expected.
The problem happens when updating a company.
When I add a new activity, all previous existing activities are persisted again.
When I remove an activity, it is not removed from the table.
I'm using this code to update a company' activities
company.getActivities().clear();
company.getActivities().addAll(newActivities);
company = repository.save(company);
In this code, newActivities have the new activities that should be considered (this collection does not have the previous ones, I just replace them all)
I tried adding orphanRemoval=true to the #OneToMany(cascade = CascadeType.ALL, mappedBy = "company") on Company, but this deletes the activity type when no other company is using it, which is wrong as they should be available always.
Can you please help me sync the activities collection on Company without removing elements from Activity table ?
Thanks a lot!
I solved it. Here are the steps I followed.
First, I changed my Join table entity cascade types as follows
#Entity
#Table(name = "T_COMPANY_ACTIVITY")
public class CompanyActivity {
#Id
#Column(name = "COM_ACT_ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "COM_ID")
private Company company;
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
#JoinColumn(name = "ACT_ID")
private Activity activity;
private boolean primary;
}
Then, I added again the "orphanRemoval" property to Company mapping, and changed my CascadeTypes too, as follows
#OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "empresa", orphanRemoval = true)
private List<CompanyActivity> activities = new ArrayList<>();
With these changes, my mapping works as expected with the same code I used to replace the relationships.
company.getActivities().clear();
company.getActivities().addAll(newActivities);
company = repository.save(company);
Thanks :)
The way you created your entities is not correct. You don't need to create an entity for your join table (CompanyActivity/T_COMPANY_ACTIVITY). Instead you should be using the #JoinTable on your activities entity. Something like below:
#OneToMany(cascade = CascadeType.ALL, mappedBy = "company")
#JoinTable(
name = "T_COMPANY_ACTIVITY",
joinColumns = #JoinColumn(name = "COM_ID"),
inverseJoinColumns = #JoinColumn(name = "ACT_ID")
)
private List<CompanyActivity> activities = new ArrayList<>();
for more detailed explanation on how One-to-Many/Many-to-One with Join tables work here: http://www.codejava.net/frameworks/hibernate/hibernate-one-to-many-association-on-join-table-annotations-example
#Entity
public class Conference {
#ManyToOne
#JoinColumn(name = "host_id")
#JsonManagedReference
private User host;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(name = "PARTICIPANT_CONFERENCE",
joinColumns = #JoinColumn(name = "CONFERENCE_ID_FRK"),
inverseJoinColumns = #JoinColumn(name = "PARTICIPANT_ID_FRK")
)
#JsonManagedReference
private Set<User> participants;
}
#Entity
public class User {
#ManyToMany(
targetEntity = Conference.class,
fetch = FetchType.EAGER,
cascade = CascadeType.REMOVE,
mappedBy = "participants"
)
#JsonBackReference
private Set<Conference> conferenceSet;
#OneToMany(
targetEntity = Conference.class,
mappedBy = "host",
cascade = CascadeType.REMOVE,
fetch = FetchType.EAGER
)
#JsonBackReference
private Set<Conference> conferenceHostSet;
}
I used JPA and have a problem that OneTOMany field does not update in JPA.
If I added host in Conference, but User's host set did not update.
So, I tried to update User's host set, too.
Set<Conference> conferences = form.getHost().getConferenceHostSet();
conferences.add(conference);
userRepository.save(form.getHost().setConferenceSet(conferences));
It works well, but User's participant set also updated.
How can I update only host?
You have set cascading to only work for "remove" you need to set it up for all of the actions you want.