简体   繁体   中英

Jpa ManyToMany Self Reference without auxiliar Join Table

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.

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;

}

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.


Database design

for the answer, I'll avoid the join table and assume that table is designed as followed:

  • there are two tables: person and role with respective primary key columns PersonId and RoleId
  • in the person table, RoleId and ParentRoleId are foreign keys referring to the same role.RoleId
  • other columns in role table (eg relationship between roles) are irrelevant for the question

JPA

Entities

Entities 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

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.

For your example

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM