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