如何在Spring控制器中使用JPA和Hibernate获取FetchType.LAZY关联。

180

我有一个Person类:

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(fetch = FetchType.LAZY)
    private List<Role> roles;
    // etc
}

具有惰性的多对多关系。

在我的控制器中,我有

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    PersonRepository personRepository;

    @RequestMapping("/get")
    public @ResponseBody Person getPerson() {
        Person person = personRepository.findOne(1L);
        return person;
    }
}

而PersonRepository只是这段代码,根据这个指南编写的。

public interface PersonRepository extends JpaRepository<Person, Long> {
}

然而,在这个控制器中,我实际上需要延迟数据。我该如何触发它的加载?

试图访问它将会失败,并显示以下信息:

无法初始化代理 - 没有Session:无法惰性地初始化角色集合 no.dusken.momus.model.Person.roles,或其他异常(取决于我尝试什么)。

如果需要,可以查看xml-description

谢谢。


你能否编写一个方法,用于创建一个查询以获取给定参数的“Person”对象?在该“Query”中,包括“fetch”子句并为该人员加载“Roles”。 - Rahul
7个回答

252

为了初始化懒加载的集合,您需要显式调用它(通常使用.size()方法进行调用)。在Hibernate中有专门的方法(Hibernate.initialize()),但JPA没有相应的等价物。当然,在会话可用时,您必须确保调用已完成,因此请在控制器方法上注释@Transactional。另一种选择是在控制器和存储库之间创建一个中间服务层,该层可以公开初始化延迟集合的方法。

更新:

请注意,以上解决方案很简单,但会导致向数据库发出两个不同的查询(一个用于用户,另一个用于其角色)。如果您想获得更好的性能,请将以下方法添加到您的Spring Data JPA存储库接口中:

public interface PersonRepository extends JpaRepository<Person, Long> {

    @Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.id = (:id)")
    public Person findByIdAndFetchRolesEagerly(@Param("id") Long id);

}

此方法将使用JPQL的fetch join子句,在单次数据库访问中急切地加载角色关联,因此可以缓解上述解决方案中两个不同查询所产生的性能损失。


10
有趣的事情要注意,如果你只是简单地使用 join 而没有使用 fetch,那么集合将会以 initialized = false 的状态返回;因此,在访问集合时仍然会发出第二个查询。使用 fetch 是确保关系完全加载并避免第二个查询的关键。 - FGreg
似乎同时使用fetch和join的问题在于,join谓词条件被忽略了,你最终会得到列表或映射中的所有内容。如果你想要所有东西,那么使用fetch,如果你想要特定的东西,那么使用join,但是,正如所说的,join将是空的。这违背了使用.LAZY加载的目的。 - K.Nicholas
非常感谢,我也遇到了同样的问题,没想到我可以将我的控制器注释为 @Transactional。 - Shady Mohamed Sherif
使用JOIN FETCH进行代码更新是无意义的(从面向对象的角度来看,响应是正确的,但在这种情况下,JPA API / JPQL设计是荒谬的)。例如,假设我们有一个拥有5个角色的人Joe。那么这个JOIN FETCH会检索到5个Joe实体?完全没有意义。期望的行为是,这段代码应该检索到一个Joe实体,并初始化具有5个角色实体的角色数组。我已经测试了这段代码,它确实检索到了5个Joe实体,因此我不能使用它。 - TomR
你能帮忙调整一下,改成 findByEmail 吗?我不知道怎样弄对。 - Marek Urbanowicz
findByIdAndFetchRolesEagerly solution works great. @MU for mails try sth like:@Query("SELECT p FROM Person p JOIN FETCH p.roles WHERE p.email = (:email)") public Person findByEmailAndFetchRolesEagerly(@Param("email") String email); - tryingHard

42

虽然这是一篇旧文章,请考虑使用@NamedEntityGraph(Javax Persistence)和@EntityGraph(Spring Data JPA)。 这个组合非常有效。

例如:

@Entity
@Table(name = "Employee", schema = "dbo", catalog = "ARCHO")
@NamedEntityGraph(name = "employeeAuthorities",
            attributeNodes = @NamedAttributeNode("employeeGroups"))
public class EmployeeEntity implements Serializable, UserDetails {
// your props
}

然后是以下的Spring仓库

@RepositoryRestResource(collectionResourceRel = "Employee", path = "Employee")
public interface IEmployeeRepository extends PagingAndSortingRepository<EmployeeEntity, String>           {

    @EntityGraph(value = "employeeAuthorities", type = EntityGraphType.LOAD)
    EmployeeEntity getByUsername(String userName);

}

1
请注意,@NamedEntityGraph 是 JPA 2.1 API 的一部分,在 Hibernate 4.3.0 版本之前并未实现。 - naXa stands with Ukraine
5
@EntityGraph(attributePaths = "employeeGroups") 可以直接用于Spring Data Repository的方法注释上,而不需要在你的 @Entity 上使用 @NamedEntityGraph - 这样可以减少代码量,在打开仓库时更易于理解。 - Desislav Kamenov

17

Spring Data JpaRepository

Spring Data JpaRepository定义了以下两个方法:

  • getOne,返回一个适合设置@ManyToOne@OneToOne父关联在持久化子实体时实体代理
  • findById,在运行SELECT语句从相关表中加载实体后,返回实体POJO。

然而,在您的情况下,您没有调用getOnefindById

Person person = personRepository.findOne(1L);

因此,我假设 findOne 方法是您在 PersonRepository 中定义的方法。然而,在您的情况下, findOne 方法并不是非常有用。由于您需要获取 Person 以及其 roles 集合,最好使用 findOneWithRoles 方法。

自定义Spring Data方法

您可以定义一个 PersonRepositoryCustom 接口,如下所示:

public interface PersonRepository
    extends JpaRepository<Person, Long>, PersonRepositoryCustom { 

}

public interface PersonRepositoryCustom {
    Person findOneWithRoles(Long id);
}

并像这样定义其实现:

public class PersonRepositoryImpl implements PersonRepositoryCustom {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public Person findOneWithRoles(Long id)() {
        return entityManager.createQuery("""
            select p 
            from Person p
            left join fetch p.roles
            where p.id = :id 
            """, Person.class)
        .setParameter("id", id)
        .getSingleResult();
    }
}

就是这样!


6
因为EntityGraphs的计划不像JPQL一样被缓存,这可能会对性能造成重大影响。 - Vlad Mihalcea
只是为了确认我是否理解你的意思。你是在谈论这篇精彩文章中提到的Hibernate QueryPlanCache吗?https://vladmihalcea.com/hibernate-query-plan-cache/ - Jeroen Vandevelde
5
好的,当我有时间时,我会写一篇关于此的文章。 - Vlad Mihalcea
1
JsonIgnore 在 Web 层中用于双向关联,不会影响您在数据访问层中查询数据的方式。 - Vlad Mihalcea
1
@Vlad 如果您像上面的一个答案(https://dev59.com/emUp5IYBdhLWcg3wTGXe#15360333)中所述使用`@Query`注释,是否会像您在上面的评论中描述的那样缓存查询? - Kaj Hejer
显示剩余4条评论

15

你有几个选项:

  • 按照R.J建议,在repository上编写一个方法,返回一个初始化好的实体。

更多工作,性能最佳。

  • 使用OpenEntityManagerInViewFilter来保持整个请求期间会话处于打开状态。

较少的工作量,在Web环境中通常被接受。

  • 在需要时使用辅助类来初始化实体。

较少的工作量,当OEMIV不是选项时(例如在Swing应用程序中),可以初始化任何实体,也可能对存储库实现有用。

对于最后一种选项,我编写了一个实用程序类JpaUtils来深度初始化实体。

例如:

@Transactional
public class RepositoryHelper {

    @PersistenceContext
    private EntityManager em;

    public void intialize(Object entity, int depth) {
        JpaUtils.initialize(em, entity, depth);
    }
}

由于我的所有请求都是简单的REST调用,没有渲染等操作,因此事务基本上就是我的整个请求。感谢您的输入。 - Matsemann
我该怎么做第一个?我知道如何编写查询,但不知道你说的是什么。你能给个例子吗?这会非常有帮助。 - Matsemann
zagyi在他的回答中提供了一个例子,无论如何感谢你指引我正确的方向。 - Matsemann
我不知道你的类应该叫什么名字!不完整的解决方案会浪费别人的时间。 - Shady Mohamed Sherif
使用OpenEntityManagerInViewFilter在整个请求期间保持会话处于打开状态并不是一个好主意。我会发起一个额外的请求来获取实体的所有集合。 - Yan Khonski

9

6

我认为你需要使用OpenSessionInViewFilter来在视图渲染期间保持会话的打开状态(但这并不是太好的做法)。


1
由于我没有使用JSP或其他任何东西,只是在制作REST-api,@Transactional对我来说就足够了。但在其他时候也会很有用。谢谢。 - Matsemann
@Matsemann 我知道现在有点晚了...但是你可以在控制器中使用OpenSessionInViewFilter,因为会话存在直到响应被编译... - Vishwas Shashidhar
@Matsemann 谢谢!Transactional-annotation 对我很有帮助!顺便说一下:如果你只注释 rest-class 的超类,它甚至也可以工作。 - desperateCoder

2
您可以像这样进行操作:
@Override
public FaqQuestions getFaqQuestionById(Long questionId) {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();
    FaqQuestions faqQuestions = null;
    try {
        faqQuestions = (FaqQuestions) session.get(FaqQuestions.class,
                questionId);
        Hibernate.initialize(faqQuestions.getFaqAnswers());

        tx.commit();
        faqQuestions.getFaqAnswers().size();
    } finally {
        session.close();
    }
    return faqQuestions;
}

只需在控制器中使用faqQuestions.getFaqAnswers().size(),您将获得懒初始化列表的大小,而无需获取列表本身。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接