Infinite JSON in ManyToMany relationship mapped by Intermediary Table

866 views Asked by At

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:

  • 2 entities mapped by Many-To-Many relationship;
  • Many-To-Many implemented using an intermediary entity and One-To-Many + Many-To-One associations;
  • Getting recursion when showing my UserEntity;

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

1

There are 1 answers

0
Mike O. On BEST ANSWER

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.