spring-boothibernatejpamany-to-manyinfinite-recursion

Infinite JSON in ManyToMany relationship mapped by Intermediary Table


I have 2 entities that relate to one another. These 2 entities should map to each other in a Many-To-Many relationship, however, I need to also have a timestamp of their respective relationship (when it happened), so I am trying to map them using an intermediary table.

Initially, the relationship was One-To-Many, but I realized that I actually need a Many-To-Many as the business logic requires this. The structure is still the same, as in there is a Parent-Child relationship, but this time, a child should have multiple parents as well.

My BaseEntity is an abstract class that contains the fields present in all the other entities:

@Data
@MappedSuperclass
public abstract class BaseEntity {

    @Id
    @Min(100)
    @Max(Integer.MAX_VALUE)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;

    @CreationTimestamp
    @Column(name = "Created_At", updatable = false)
    protected ZonedDateTime createdDate;

    @UpdateTimestamp
    @Column(name = "Updated_At")
    protected ZonedDateTime updatedDate;

    @NotNull
    @Column(name = "Is_Active")
    protected Boolean active = true;
}

Then I have my 2 entities that should relate in a Many-To-Many style. This is my first entity and should be the parent:

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "User")
@EqualsAndHashCode(callSuper = true)
@TypeDefs( {
               @TypeDef(name = "json", typeClass = JsonStringType.class),
               @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
           })
public class UserEntity extends BaseEntity {

    @NotBlank
    @Column(name = "User_Name", columnDefinition = "varchar(255) default 'N/A'")
    private String userName;

    @Nullable
    @JoinColumn(name = "User_Id")
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<UserRole> roleList = new ArrayList<>();
}

My second entity is considered the child entity:

@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "Role")
@Where(clause = "is_active = true")
@EqualsAndHashCode(callSuper = true)
public class RoleEntity extends BaseEntity {

    @NotBlank
    @Column(name = "Name")
    private String name;

    @JsonIgnore
    @JoinColumn(name = "Role_Id")
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<UserRole> userList = new ArrayList<>();
}

I also have my intermediary entity:

@Data
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Where(clause = "is_active = true")
@EqualsAndHashCode(callSuper = true)
@Table(name = "User_Role", uniqueConstraints= @UniqueConstraint(columnNames={"User_Id", "Role_Id"}))
public class UserRole extends BaseEntity {

    // Adding @JsonIgnore here will only cause an error
    @JoinColumn(name = "User_Id")
    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false, targetEntity = UserEntity.class)
    private UserEntity user;

    @JoinColumn(name = "Role_Id")
    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false, targetEntity = RoleEntity.class)
    private RoleEntity role;
}

Problem now is that when I try to get my UserEntity, I get infinite recursion.

So far I've tried using @JsonIgnore, @JsonManagedReference, @JsonBackReference and it did not work or I simply don't know where or how to use them properly.

Recap:

Update: I managed to get this fixed using a different approach described in my answer to this question.


Solution

  • I fixed this by implementing a Composite Key structure and just using the @JsonIgnore annotation:

    @Getter
    @Setter
    @Embeddable
    @EqualsAndHashCode
    @NoArgsConstructor
    @AllArgsConstructor
    public class UserRoleKey implements Serializable {
        @Column(name = "User_Id")
        Long userId;
    
        @Column(name = "Role_Id")
        Long roleId;
    }
    

    This gets to be used in the intermediary entity, which now doesn't use my BaseEntity anymore.

    @Data
    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Table(name = "User_Role", uniqueConstraints= @UniqueConstraint(columnNames={"User_Id", "Role_Id"}))
    public class UserRole {
    
        @JsonIgnore
        @EmbeddedId
        private UserRoleKey id;
    
        @JsonIgnore
        @MapsId("userId")
        @JoinColumn(name = "User_Id")
        @ManyToOne(optional = false, targetEntity = UserEntity.class)
        private UserEntity user;
    
        @MapsId("roleId")
        @JoinColumn(name = "Role_Id")
        @ManyToOne(optional = false, targetEntity = RoleEntity.class)
        private RoleEntity role;
    
        @CreationTimestamp
        @Column(name = "Created_At", updatable = false)
        private ZonedDateTime createdDate;
    }
    

    Now, for my two entities, I have this definition:

    UserEntity class (definition of the role):

    @Nullable
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user", orphanRemoval = true)
    private List<UserRole> roleList = new ArrayList<>();
    

    RoleEntity class (definition of the user)

    @Nullable
    @JsonIgnore
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "role", orphanRemoval = true)
    private List<UserRole> userList = new ArrayList<>();
    

    This seems to be working and no longer returns an infinite JSON recursion.