如何在java中设计JPA多态关系?

时间:2016-02-05 14:45:58

标签: java spring hibernate jpa orm

我正在设计产品目录。我想要一个类别树,其中产品只能连接到 LeafCategories ,可以有一个父类别。 我正在使用Spring Boot,Spring Data和Hibernate 4和H2数据库(现在)。

任务的基本实体是 AbstractCategory (是否有更好的方式来继承关系?)(省略了Getters和Setters,NamedEntity是带有String名称的 @MappedSuperclass 和长id)

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

类别实体 - 它们不是叶子,也不能连接到它们的产品:

@Entity
public class Category extends AbstractCategory {

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

LeafCategory 它可以用作产品实体的属性。

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

我有一个非常简单的类别的CrudRepository和 LeafCategory 的相同

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

当我从CategoryRepository加载一个类别并访问getSubcategories()时,我得到以下异常:

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

首先 - 我如何改进设计?第二个也是更具体的问题是为什么@Transactional没有保持会话开放?我知道我可以使用FetchType.EAGER,但它是一个递归结构 - 如果我对Hibernate的理解是正确的,那就意味着加载整个子树,我不想要那样。我也不想使用Hibernate.initialize

我没有数据库或hibernate的配置。我正在使用spring.boot中的devtools:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>

2 个答案:

答案 0 :(得分:1)

  

如何改进设计?

对我来说这看起来很合理。

  

为什么@Transactional没有保持会话开放?

您已将@Transactional放在存储库中。数据库会话仅在运行查询时打开,该查询返回其子类别标记为延迟加载的类别。然后,会话关闭(一旦存储库方法返回),并且您之后尝试访问子类别,此时不再有会话。如果您使用的是3层架构,请将@Transactional注释移到调用堆栈的上方 - 服务层(参见this post)。

由于存储库方法只运行一个查询,因此无需将它们标记为@Transactional - 它们无论如何都会在事务中运行。只有在运行多个查询或运行查询以及其他一些处理(可能会抛出异常并且您希望由于它而回滚查询)时才有@Transactional才有意义。这就是为什么,如果你想明确地将任何内容标记为@Transactional,那么它宁愿在服务层中。

答案 1 :(得分:1)

首先,您获得LazyInitializationException,因为Session已关闭,而且并非所有子项都已初始化。

即使您使用了EAGER(often a bad decision),您也只能在嵌套子树中获取单个级别。

您可以使用递归遍历所有子项并在从DAO方法返回结果之前强制进行初始化,这需要您为find方法提供自定义实现:

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);
    }
}