如何在Spring Controller中获取与JPA和Hibernate的FetchType.LAZY关联


146

我有一个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> {
}

但是,在此控制器中,我实际上需要惰性数据。如何触发加载?

尝试访问它将会失败

无法延迟初始化角色集合:no.dusken.momus.model.Person.roles,无法初始化代理-没有会话

或其他例外情况,具体取决于我的尝试。

我的xml-description,如果需要的话。

谢谢。


您可以编写一个方法,该方法将创建查询以获取Person给定参数的对象吗?在那Query,包括fetch子句Roles也为人加载。
SudoRahul

Answers:


206

您必须对延迟集合进行显式调用才能对其进行初始化(通常的做法是.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子句在一次往返数据库中热切地加载角色关联,因此将减轻上述解决方案中两个不同查询引起的性能损失。


3
请注意,这是一个简单的解决方案,但是会导致对数据库进行两个不同的查询(一个查询给用户,另一个查询给它的角色)。如果要获得更好的性能,请尝试编写一种专用方法,该方法使用JPQL或其他标准建议的Criteria API,一步一步就能获取用户及其相关角色。
zagyi 2013年

我现在想举一个约瑟的答案的例子,不得不承认我不完全理解。
Matsemann

请在我更新的答案中检查所需查询方法的可能解决方案。
zagyi 2013年

7
有趣的是,如果您只是join不使用fetch,则该集合将与一起返回initialized = false;因此,一旦访问该集合,仍然会发出第二个查询。fetch确保关系完全加载并避免第二个查询的关键。
FGreg 2013年

似乎同时执行和获取以及联接的问题是联接谓词条件被忽略,最终您将所有内容都包含在列表或映射中。如果需要所有内容,则使用访存,如果需要特定的内容,则使用联接,但是正如前面所说的,联接将为空。这违反了使用.LAZY加载的目的。
K.Nicholas

37

尽管这是一篇过时的文章,但是请考虑使用@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
}

然后如下所示的春季回购

@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的一部分,在4.3.0之前的版本中未在Hibernate中实现。
naXa

2
@EntityGraph(attributePaths = "employeeGroups")可以直接在Spring Data Repository中使用,以对方法进行注释,而无需@NamedEntityGraph在@Entity上使用-更少的代码,打开存储库时易于理解。
Desislav Kamenov

13

你有一些选择

  • 根据RJ建议,在存储库上编写一个返回初始化实体的方法。

更多工作,最佳性能。

  • 使用OpenEntityManagerInViewFilter可以使整个请求的会话保持打开状态。

工作量少,通常在网络环境中可以接受。

  • 在需要时使用帮助器类初始化实体。

较少的工作,在未选择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 2013年

我该如何做第一个?我知道如何编写查询,但不知道该怎么做。你能举个例子吗?将会非常有帮助。
Matsemann

zagyi在回答中提供了一个例子,尽管如此,还是感谢您为我指明了正确的方向。
Matsemann

我不知道您的班级将如何称呼!未完成的解决方案浪费了其他时间
Shady Sherif 2015年

使用OpenEntityManagerInViewFilter可以使整个请求的会话保持打开状态。-糟糕的主意。我会额外要求为我的实体获取所有集合。
Yan Khonski


6

我认为您需要OpenSessionInViewFilter来在视图渲染期间保持会话打开(但这不是很好的做法)。


1
由于我没有使用JSP或其他任何东西,仅制作了REST-api,@ Transactional会对我有用。但是在其他时间会很有用。谢谢。
Matsemann

@Matsemann我知道现在已经晚了...但是您甚至可以在控制器中使用OpenSessionInViewFilter,并且会话将一直存在,直到编译响应为止...
Vishwas Shashidhar 2014年

@Matsemann谢谢!事务注释为我成功了!fyi:如果您仅注释其余类的超类,它甚至可以工作。
desperateCoder

3

春季数据 JpaRepository

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

  • getOne,它在保留子实体时返回适合于设置父或父关联的实体代理@ManyToOne@OneToOne
  • findById,它在运行SELECT语句后返回实体POJO,该SELECT语句从关联表中加载该实体

但是,就您而言,您没有呼叫getOnefindById

Person person = personRepository.findOne(1L);

因此,我假设该findOne方法是您在中定义的方法PersonRepository。但是,该findOne方法在您的情况下不是很有用。由于您需要获取Personis 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();
    }
}

而已!


您是否有理由自己编写查询,而不在@rakpan答案中使用EntityGraph之类的解决方案?这样会不会产生相同的结果?
Jeroen Vandevelde

使用EntityGraph的开销高于JPQL查询。从长远来看,最好编写查询。
Vlad Mihalcea

您能否详细说明开销(它来自何处,值得注意吗?)?原因我不明白如果它们都生成相同的查询,为什么会有更高的开销。
Jeroen Vandevelde

1
因为EntityGraphs计划没有像JPQL那样被缓存。这可能会严重影响性能。
Vlad Mihalcea

1
究竟。有空的时候我必须写一篇有关它的文章。
Vlad Mihalcea

1

您可以这样做:

@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(),就可以通过延迟初始化列表来获取大小,而无需获取列表本身。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.