I have created a hierachy structure or a tree structure for a shopping site using springboot. My problem is how to query such a structure when looking for a specific product and its parents:
id, category_name, parent_id
'1', 'Electronics', NULL
'2', 'Gaming', NULL
'3', 'Home Audio', '1'
'4', 'Console', '2'
'5', 'Sony', '4'
'6', 'Karaoke', '3'
This is what i have done, any pointers on what i need to do on the entity to achieve this structure and also how i can query it i.e
It is also important to note that i am using postgres database
Category Entity
@Entity
@Table(name = "categories")
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String categoryName;
@ManyToOne
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Category> children = new ArrayList<Category>();
// Getter and setter removed for readability
}
Product Entity
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Long id;
@Version
@Column(name = "version")
private Integer version;
private String name;
private int quantity;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "products_categories", joinColumns = {
@JoinColumn(name = "product_id", referencedColumnName = "id") }, inverseJoinColumns = {
@JoinColumn(name = "category_id", referencedColumnName = "id") })
private List<Category> categories;
// getters and setter omitted for readability
}
Category Service
public class CategoryService {
@Autowired
private CategoryRepository categoryRepository;
public void addCategory(Category category) {
categoryRepository.save(category);
}
}
Product Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public void addProduct(Product product) {
productRepository.save(product);
}
}
You can use a recursive query to select the parents of a given category.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface CategoryRepository extends JpaRepository<Category, Long> {
@Query(
value = "WITH RECURSIVE ancestors(id, parent_id, category_name, lvl) AS ("
+ " SELECT cat.id, cat.parent_id, cat.category_name, 1 AS lvl "
+ " FROM categories cat "
+ " WHERE cat.id = :categoryId "
+ " UNION ALL "
+ " SELECT parent.id, parent.parent_id, parent.category_name, child.lvl + 1 AS lvl "
+ " FROM categories parent "
+ " JOIN ancestors child "
+ " ON parent.id = child.parent_id "
+ " )"
+ "SELECT category_name from ancestors ORDER BY lvl DESC"
, nativeQuery = true)
List<String> findAncestry(@Param("categoryId") Long categoryId);
}
Following is a test that creates the category hierarchy and queries it:
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest
public class HierarchyTest {
@Autowired
CategoryRepository categoryRepository;
@Test
public void testCategoryRepository() {
String[] electronicsCategoryNames = {"Electronics", "Home Audio", "Karaoke"};
String[] gamingCategoryNames = {"Gaming", "Console", "Sony"};
Category karaoke = createCategories(electronicsCategoryNames);
Category sony = createCategories(gamingCategoryNames);
findAncestry(karaoke, electronicsCategoryNames);
findAncestry(sony, gamingCategoryNames);
}
Category createCategories(String[] categoryNames) {
Category parent = null;
for (String categoryName : categoryNames) {
Category category = new Category();
category.setCategoryName(categoryName);
category.setParent(parent);
parent = categoryRepository.save(category);
}
Assert.assertNotNull("category.id", parent.getId());
return parent;
}
void findAncestry(Category category, String[] categoryNames) {
List<String> ancestry = categoryRepository.findAncestry(category.getId());
Assert.assertEquals("ancestry.size", categoryNames.length, ancestry.size());
for (int i = 0; i < categoryNames.length; i++) {
Assert.assertEquals("ancestor " + i, categoryNames[i], ancestry.get(i));
}
}
}
Note that this was tested on H2 and PostgreSQL databases. You might need to modify the native query according to the actual database you are using.
PostgreSQL has excellent documentation on how this works, see:
https://www.postgresql.org/docs/11/queries-with.html
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With