【问题标题】:How to fetch FetchType.LAZY associations with JPA and Hibernate in a Spring Controller如何在 Spring Controller 中获取与 JPA 和 Hibernate 的 FetchType.LAZY 关联
【发布时间】:2013-02-27 20:27:48
【问题描述】:

我有一个 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就是这段代码,按照this guide写的

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

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

尝试访问它会失败

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

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

我的xml-description,以备不时之需。

谢谢。

【问题讨论】:

  • 你能写一个方法,它会创建一个查询来获取一个给定参数的Person对象吗?在该Query 中,包含fetch 子句并为该人加载Roles

标签: java spring hibernate jpa spring-data


【解决方案1】:

您必须对惰性集合进行显式调用才能对其进行初始化(通常的做法是为此目的调用.size())。在 Hibernate 中,有一个专门的方法(Hibernate.initialize()),但 JPA 没有类似的方法。当然,当会话仍然可用时,您必须确保调用已完成,因此请使用 @Transactional 注释您的控制器方法。另一种方法是在 Controller 和 Repository 之间创建一个中间服务层,它可以公开初始化惰性集合的方法。

更新:

请注意,上述解决方案很简单,但会导致对数据库的两个不同查询(一个针对用户,另一个针对其角色)。如果您想获得更好的性能,请将以下方法添加到您的 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 子句在单次往返数据库中急切地加载角色关联,因此将减轻上述解决方案中两个不同查询导致的性能损失。

【讨论】:

  • 请注意,这是一个简单的解决方案,但会导致对数据库的两个不同查询(一个针对用户,另一个针对其角色)。如果您想获得更好的性能,请尝试编写一个专用方法,使用 JPQL 或其他人建议的 Criteria API 在一个步骤中急切地获取用户及其相关角色。
  • 我现在要求举一个 Jose 回答的例子,不得不承认我完全不明白。
  • 请在我更新的答案中检查所需查询方法的可能解决方案。
  • 有趣的是,如果你只是join而没有fetch,则返回的集合将带有initialized = false;因此,一旦访问该集合,仍会发出第二个查询。 fetch 是确保完全加载关系并避免第二次查询的关键。
  • 似乎同时执行 fetch 和 join 的问题是连接谓词条件被忽略,您最终会得到列表或映射中的所有内容。如果你想要所有东西,那么使用 fetch,如果你想要特定的东西,那么使用 join,但是,如前所述,join 将是空的。这违背了使用 .LAZY 加载的目的。
【解决方案2】:

虽然这是一篇旧文章,但请考虑使用 @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);

}

【讨论】:

  • 请注意,@NamedEntityGraph 是 JPA 2.1 API 的一部分,在 Hibernate 4.3.0 版本之前没有实现。
  • @EntityGraph(attributePaths = "employeeGroups") 可以直接在 Spring Data Repository 中使用来注释方法,而无需在 @Entity 上使用 @NamedEntityGraph - 更少的代码,打开 repo 时易于理解。
【解决方案3】:

你有一些选择

  • 在存储库上编写一个方法,该方法返回 R.J 建议的初始化实体。

更多的工作,最好的表现。

  • 使用 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 调用,没有渲染等,事务基本上是我的整个请求。感谢您的意见。
  • 第一个怎么做?我知道如何编写查询,但不知道如何执行您所说的。你能举个例子吗?会很有帮助的。
  • zagyi 在他的回答中提供了一个例子,不过还是感谢您为我指明了正确的方向。
  • 我不知道你的课怎么称呼!未完成的解决方案浪费他人时间
  • 使用 OpenEntityManagerInViewFilter 为整个请求保持会话打开。- 坏主意。我会提出一个额外的请求来获取我的实体的所有集合。
【解决方案4】:

它只能在事务中延迟加载。所以你可以访问你的存储库中的集合,它有一个事务——或者I normally do is a get with association, or set fetchmode to eager.

【讨论】:

    【解决方案5】:

    弹簧数据JpaRepository

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

    但是,就您而言,您没有致电 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();
        }
    }
    

    就是这样!

    【讨论】:

    • 您自己编写查询并且没有使用@rakpan 答案中的 EntityGraph 之类的解决方案是否有原因?这不会产生相同的结果吗?
    • 因为 EntityGraphs 计划不像 JPQL 那样被缓存。这可能会对性能造成重大影响。
    • 没错。有时间我会写一篇关于它的文章。
    • Web层的双向关联需要JsonIgnore,所以不会影响你在数据访问层查询数据的方式。
    • @Vlad 如果您使用 @Query 注释(如上述 amswers 之一)(stackoverflow.com/a/15360333/924036),您是否会像您在上面的评论中描述的那样缓存查询?
    【解决方案6】:

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

    【讨论】:

    • 因为我没有使用 JSP 或任何东西,只是制作一个 REST-api,@Transactional 会为我做。但在其他时候会有用。谢谢。
    • @Matsemann 我知道现在已经很晚了...但是您甚至可以在控制器中使用 OpenSessionInViewFilter ,并且会话将存在直到编译响应...
    • @Matsemann 谢谢!事务性注释对我有用!仅供参考:如果您只是注释休息类的超类,它甚至可以工作。
    【解决方案7】:

    你也可以这样做:

    @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(),如果延迟初始化列表,您将获得大小,而无需获取列表本身。

    【讨论】:

      猜你喜欢
      • 2015-07-06
      • 2021-01-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-07
      • 1970-01-01
      • 2016-02-08
      • 2017-01-17
      相关资源
      最近更新 更多