Reputation: 6629
I'd like to tell entity to join with itself for a field that it's not unique in both directions, but it looks like all examples in internet use a Join table or either are pretty old.
Person Denormalized Table:
PersonId (Pk) | RoleId | ParentRoleId
1 1 NULL
2 1 NULL
3 2 1
4 2 1
Person Entity (With mappings that seem to load an empty list):
@Column
private Long personId;
@Column
private Long roleId;
@Column
private Long parentRoleId;
@ManyToMany
@JoinColumn(name = "parentRoleId", referencedColumnName = "roleId", updatable = false, insertable = false)
private List<Person> personsWithParentRole;
@ManyToMany
@JoinColumn(name = "roleId", referencedColumnName = "parentRoleId", updatable = false, insertable = false)
private List<Person> personsWhoseRoleHasCurrentPersonRoleAsParent;
I would like to know if there's a way to map my case. I know it's not the best arch or the most performant, and that one can use a different approach, but please I am just wondering about that particular solution. This is a simplification from a most complex case.
Upvotes: 1
Views: 3533
Reputation: 3412
Disclaimer: this answer is not definitive. I write the answer for readibility sake and it is meant to be improved along the comment with OP. Furthermore, code is not tested.
for the answer, I'll avoid the join table and assume that table is designed as followed:
person
and role
with respective primary key columns PersonId
and RoleId
RoleId
and ParentRoleId
are foreign keys referring to the same role.RoleId
role
table (e.g. relationship between roles) are irrelevant for the questionEntities follow table structure. The Role entity would be a basic entity:
@Entity
public class Role{
// ---- JPA attributes
@Id
// ...
@Column(...)
private Long roleId;
@OneToMany(mappedBy = "role")
private List<Person> personsWithThisRoleAsPrimaryRole;
@OneToMany(mappedBy = "parentRole")
private List<Person> personsWithThisRoleAsParentRole;
// ---- Constructor
public Role(){
// your initialisation
// initialise list to avoid NullPointerException
this.personsWithThisRoleAsPrimaryRole = new ArrayList<>();
this.personsWithThisRoleAsParentRole = new ArrayList<>();
}
// getters & setters
}
The trick for bypassing the join table would be leveraging the @OneToMany
relationship with a transient attribute:
@Entity
public class Person{
// ---- JPA attributes
@Id
// ...
@Column(...)
private Long personId;
@ManyToOne
@JoinColumn(name = "RoleId")
private Role role;
@ManyToOne
@JoinColumn(name = "ParentRoleId")
private Role parentRole;
// ---- Transient attributes
@Transient
private List<Person> personsWithParentRole;
@Transient
private List<Person> personsWhoseRoleHasCurrentPersonRoleAsParent;
// ---- Constructor
public Person(){
// your initialisation
// initialise list to avoid NullPointerException
this.personsWithParentRole = new ArrayList<>();
this.personsWhoseRoleHasCurrentPersonRoleAsParent = new ArrayList<>();
}
@PostLoad
public void postLoad(){
// during JPA initialisation, role and parentRole have been defined
// if the value exist in the database. Consequently, we can fetch some
// interesting info:
if(role != null){
personsWithParentRole.addAll(role.getPersonsWithThisRoleAsParentRole());
}
if(parentRole != null){
personsWhoseRoleHasCurrentPersonRoleAsParent.addAll(parentRole.getPersonsWithThisRoleAsPrimaryRole());
}
}
// getters and setters for JPA attributes
// getters for transient attributes. It doesn't make sense to create the setters for the transient list here.
}
Transient attributes have to be used with care as I encountered many fancy problems. However, they are useful as you may fetch the persons list once. If you had something like:
public List<Person> getPersonsWithParentRole{
if(role != null){
return role.getPersonsWithThisRoleAsParentRole();
}
}
public List<Person> getPersonsWithParentRole{
if(parentRole != null){
return parentRole.getPersonsWithThisRoleAsPrimaryRole();
}
}
It should also work but performance wise, it may engender additional irrelevant calculations.
To see if it should work, let's do a paper+pen like draft:
Person table
Person | Role | ParentRoleId
------ | ---- | ------------
1 | 1 | null
2 | 1 | null
3 | 2 | 1
4 | 2 | 1
Role table
Role | Additional Columns
---- | ----------------
1 | ...
2 | ...
Entity-wise
Person entity without considering the @PostLoad
and transient lists:
Person | Role | ParentRoleId
------ | ---- | ------------
1 | 1 | null
2 | 1 | null
3 | 2 | 1
4 | 2 | 1
the role entities with the @OneToMany
relationships:
Role | PersonsWithThisRoleAsPrimaryRole | PersonsWithThisRoleAsParentRole
---- | -------------------------------- | -------------------------------
1 | [1, 2] | [3, 4]
2 | [3, 4] | [empty]
Consequently, after @postLoad
, you'll have:
Person | Role | ParentRoleId | PersonsWithParentRole | PersonsWhoseRoleHasCurrentPersonRoleAsParent
------ | ---- | ------------ | --------------------- | --------------------------------------------
1 | 1 | null | [3,4] | [empty]
2 | 1 | null | [3,4] | [empty]
3 | 2 | 1 | [empty] | [1, 2]
4 | 2 | 1 | [empty] | [1, 2]
/!\ Be careful about initialisation stuff (Lazy initialisation can be tricky) /!\
Hope this helps
Upvotes: 1
Reputation: 552
I think is definitly a bad idea to avoid a join table in your case. Your current solution is the best.
I think you need something like this:
public class Person {
@Id
private long id;
@JoinTable(name = "person_links", joinColumns = {
@JoinColumn(name = "subordinate", referencedColumnName = "id", nullable = false)}, inverseJoinColumns = {
@JoinColumn(name = "manager", referencedColumnName = "id", nullable = false)})
@ManyToMany
private List<Person>subordinates;
@ManyToMany(mappedBy = "subordinates")
private List<Person> managers;
}
Upvotes: 3