Repository Rest Resource can't embed self-referencing object

Viewed 1825

I have two entities, a Store and a ProductCategory, that I persist and would like to retrieve from a JpaRepository annotated with @RepositoryRestResource. Stores have a collection of product categories, and product categories have a parent/child relationship with themselves. Furthermore, I'd like to embed the product categories hierarchy when a GET request is made to the Store's REST repository.

The entities look as follows:

Store:

@Entity
@Table(name = "store")
@EqualsAndHashCode(exclude="categories", callSuper = true)
@ToString(exclude="categories", callSuper = true)
public class Store extends BaseEntity {

    @Getter
    @Setter
    @Column(name = "name")
    private String name;

    @OneToMany(targetEntity = ProductCategory.class, cascade = CascadeType.ALL, mappedBy = "store")
    @Getter
    private List<ProductCategory> categories = new ArrayList<>();

    public void setCategories(List<ProductCategory> categories) {
        for (ProductCategory category : categories) {
            if (category.getStore() == null || !category.getStore().equals(this)) {
                category.setStore(this);
            }
        }
        this.categories = categories;

    }

}

ProductCategory.java:

@Entity
@Table(name = "product_category")
@EqualsAndHashCode(exclude={"store", "children", "parent"}, callSuper = true)
@ToString(exclude={"store", "children", "parent"}, callSuper = true)
public class ProductCategory extends BaseEntity {

    @Getter @Setter
    @Column(name="name")
    private String name;

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="store_id", nullable = true)
    @Getter
    @JsonIgnore
    @RestResource(exported = false)
    private Store store;

    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="parent", nullable = true)
    @Getter
    @JsonIgnore
    @RestResource(exported = false, path = "parent")
    private ProductCategory parent;

    @OneToMany(targetEntity = ProductCategory.class, cascade=CascadeType.ALL, mappedBy="parent")
    @Getter
    @RestResource(rel = "children", path = "children")
    private List<ProductCategory> children = new ArrayList<>();

    public void setStore(Store store) {
        this.store = store;
        if(!store.getCategories().contains(this)) {
            store.getCategories().add(this);
        }
    }

    public void setParent(ProductCategory parent) {
        this.parent = parent;
        if(!parent.getChildren().contains(this)) {
            parent.getChildren().add(this);
        }
    }

    public void setChildren(List<ProductCategory> children) {
        for(ProductCategory child : children) {
            if(child.getParent() == null || !child.getParent().equals(this)) {
                child.setParent(this);
            }
        }
        this.children = children;
    }

}

BaseEntity provides a unique identifier for the other entities. Each entity has their own repository:

StoreRepository:

@RepositoryRestResource(collectionResourceRel = "store", path = "store")
public interface StoreRepository extends JpaRepository<Store, Long>, JpaSpecificationExecutor<Store> {

    Store findById(@Param("id") Long id);
    Optional<Store> findFirstByName(@Param("name") String name);
}

ProductCategoryRepository:

@RepositoryRestResource(collectionResourceRel = "productCategory", path = "productCategory", exported = false)
public interface ProductCategoryRepository extends JpaRepository<ProductCategory, Long>, JpaSpecificationExecutor<ProductCategory> {    
    ProductCategory findById(@Param("id") Long id);
    List<ProductCategory> findByName(@Param("name") String name);
}

The result I'd like to receive as JSON after querying for a particular store would look like:

{
   id: 0,
   name: 'Pet Store',
   categories: [
       {
          id: 0,
          name: 'Mammal', 
          children: [
             {
                id: 2,
                name: 'Dog',
                children: [
                    {
                       id: 4,
                       name: 'Shiba',
                       children: []
                    },
                    {
                       id: 5,
                       name: 'Husky',
                       children: []
                    }
                ]
             },
             {
                id: 3,
                name: 'Cat',
                children: []
             }
          ]
       },
       {
          id: 1,
          name: 'Bird',
          children: []
       }
    ]
}

HATEOS relationships were removed for brevity. While the objects are created and read from the database just fine, when an attempt is made to retrieve a store through a GET request against the REST resource the following error is produced:

restGetStoreProductsEmbedded(com.synthapp.embeddedproject.com.repo.StoreRepositoryTest): Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Infinite recursion (StackOverflowError) (through reference chain: org.springframework.hateoas.PagedResources["_embedded"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: org.springframework.hateoas.PagedResources["_embedded"])

I have attempted to resolve this through the following methods:

  • Not exporting the ProductCategory repo
  • Removing the ProductCategoryRespository entirely for automatic serialization instead of using exported = false. It really isn't required that I be able to issue database save requests to this repository, but would be nice,
  • Removing the bi-directional relationships. The GitHub I reference at the end of this posting includes this example.
  • Using @JsonManagedReference and @JsonBackReference instead of @JsonIgnore, and with @JsonIgnore
  • A projection on the Store repository that includes the categories relationship. This doesn't produce an error, but it also doesn't provided the entire hierarchy
  • Removing the "exported = false" from the ProductCategory repo and applying an InlineChildren projection to the ProductCategory repository. A GET against the api/productCategory?projection=inlineChildren endpoint causes an error about multiple association links with same relationship "children".
  • An InlineChildren projection on the ProductCategory as above, but without the bi-directional relationships. This doesn't error, but it also only produces the first two tiers of the ProductCategory hierarchy.

A project implementing this example with an in memory database can be found at my GitHub

1 Answers
Related