1

I am designing a product catalogue. I would like to have a category tree, where products can be connected only to LeafCategories, that can have one parent Category. I am using Spring Boot, Spring Data and Hibernate 4 and H2 database(for now).

Base entity for the task is AbstractCategory (Is it there a better way to inherit relationships ?) (Getters and Setters omitted, NamedEntity is a @MappedSuperclass with String name and Long id)

public abstract class AbstractCategory extends NamedEntity{
    @ManyToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "parentId")
    Category parent;
}

Category entities - they are not leafs and cannot have Products connected to them:

@Entity
public class Category extends AbstractCategory {

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "parent")
    Collection<AbstractCategory> subcategories;
}

LeafCategory it can be used as a property for my Product entity.

@Entity
public class LeafCategory extends AbstractCategory {
    @OneToMany(cascade = CascadeType.PERSIST, mappedBy = "category")
    Collection<Product> products;
}

I have a very simple CrudRepository for Category and an identical for LeafCategory

@Repository
@Transactional
public interface CategoryRepository extends CrudRepository<Category, Long> {}

When I load a Category from CategoryRepository and access getSubcategories() I get following exception:

Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: uj.jg.domain.products.Category.subcategories,  could not initialize proxy - no Session

First of all - How can I improve the design? Second and more concrete question is why is @Transactional not keeping a session open? I know I could just use FetchType.EAGER, but it's a recursive structure - if my understanding of Hibernate is correct it would mean loading the whole subtree, and I don't want that. I don't want to use Hibernate.initialize either.

I do not have any config for database or hibernate. I am using devtools from spring.boot:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
Vlad Mihalcea
  • 103,297
  • 39
  • 432
  • 788
Jan Grz
  • 1,206
  • 12
  • 18

2 Answers2

1

How can I improve the design?

It looks reasonable to me.

Why is @Transactional not keeping a session open?

You have placed @Transactional on the repository. The DB session is open only for the time of running the query, which returns the categories with their subcategories marked as lazy-loaded. Then, the session is closed (once the repository method returns) and you are trying to access the subcategories after that, when there's no session any more. Move the @Transactional annotation higher up the call stack - to the service layer if you are using a 3-layer architecture (cf. this post).

Since the repository methods run just a single query, there's no need to mark them as @Transactional - they will run within a transaction anyway. It only makes sense to have @Transactional when you run several queries or run a query and some other processing (which could throw an exception and you'd want the query to be rolled back because of it). That's why, again, if you want to explicitly mark anything as @Transactional it would rather be in the service layer.

Community
  • 1
  • 1
Adam Michalik
  • 9,448
  • 11
  • 55
  • 89
1

First of all, you get the LazyInitializationException because the Session got closed and not all children were initialized.

Even if you used EAGER (which is often a bad decision), you'd only fetch a single level in your nested children tree.

You can use recursion to traverse all children and force their initialization before returning the result from the DAO method, which requires you to provide a custom implementation for the find method:

public Category findOne(Long id) {
    Category category = entityManager.find(Category.class, id);
    fetchChildren(category);
    return category;
}

public void fetchChildren(Category category) {
    for (Category _category : category.getSubcategories()) {
        fetchChildren(_category);
    }
}
Vlad Mihalcea
  • 103,297
  • 39
  • 432
  • 788