Java JPA:如何跨 HTTP 请求保持会话活动?

Posted

技术标签:

【中文标题】Java JPA:如何跨 HTTP 请求保持会话活动?【英文标题】:Java JPA: How to keep sessions alive across HTTP requests? 【发布时间】:2013-01-29 13:17:18 【问题描述】:

在 Web 应用程序中,我使用 JPA 实体将我的域对象持久保存(和检索)到(和从)底层数据库。 在 Web 应用程序的整个运行时间内,这些 JPA 实体在内存缓存结构中保持“热”(想想Map<UniqueID, Entity>

所以我正在向我的 Web 应用程序发出请求,从存储库中加载了一个实体。该实体被放入内存缓存结构中。在这个请求的整个生命周期中,我可以愉快地访问这个实体的任何字段。在第一次请求期间,延迟加载与其他实体的关系也可以正常工作,即使在我的 View 中也是如此:我成功地使用了 Open-Session-in-View 模式(通过 Spring 的 OpenEntityManagerInViewInterceptor )。

第一个请求已结束。

我正在对我的 Web 应用程序执行下一个请求。此请求要求另一个实体。 这个实体已经在内存缓存结构中,所以它是从那里加载的。从这个实体中,我尝试访问一个应该延迟加载与其他实体的关系的字段。不幸的是,这导致了令人讨厌的org.hibernate.LazyInitializationException: could not initialize proxy - no Session(我使用 Hibernate 作为我的底层 JPA 实现)

据我了解,此异常源于以下事实:在第一个请求结束后,JPA/Hibernate 已结束任何 JPA 会话,但我的内存缓存结构中的实体仍期望这些会话中的任何一个存在;在下一个请求触发延迟加载实体机制的那一刻,延迟加载机制找不到任何不再存在的会话。

我的问题有什么解决方案?

【问题讨论】:

您能否确保在请求结束之前加载您需要的所有内容,或者在缓存之前复制/克隆到新对象?一般来说,从性能的角度来看,最好尽可能缩短数据库事务。 @tofarr 在我的域模型中,JPA 实体类指向其他实体。所以基本上,想象我的实体类是Person,方法aPerson.getFriends()返回Set<Person>;域模型实例成为网络图。我需要在连续的 HTTP 请求中从Person“跳跃”到Person。如果我加载了所有可能的朋友,朋友的朋友和朋友的朋友的朋友和 (...),那么我需要在我的数据库中加载每个 Person 以确保我可以访问所有数据。 【参考方案1】:
    一种解决方案是在第二个请求开始时使用Session.update() 将实体重新附加到会话。 另一种解决方案是在 Hibernate 中使用 second level cache 而不是您自己的解决方案。它应该比任何国产缓存机制都可靠得多。

【讨论】:

【参考方案2】:

基本上,您不能在 HTTP 请求之间保持会话处于活动状态,因为这意味着在请求之间保持事务打开。

我认为唯一的解决方案(除了自己检测已加载和未加载的内容)是在将其放入缓存之前获取整个实体。恕我直言,您不应该将部分加载的对象放入缓存中。如果您不想第一次加载整个对象,您可以为对象关系使用单独的缓存。

如果你愿意,你也可以考虑按照@Adam 的建议启用 Hibernate 缓存,但我认为它不适用于女士加载字段。

【讨论】:

在 JPA 上下文中,“会话”和“事务”是两个独立的概念,不是吗?据我了解,可以在不影响事务语义的情况下保持 JPA 会话处于活动状态。所以我想说“这意味着在请求之间保持交易开放”这句话并不总是正确的。 ....关于预取技术:我的实体非常递归(想想person.getFriends().getFriends()...,所以有一个大实体图)。如此急切地“以防万一”获取所有实体可能意味着需要加载成千上万个对象。 你说得对,会话更像是与数据库的连接,确实可以跨越多个事务。我不确定关闭事务后托管对象会发生什么,因为实体管理器不再知道数据库对象的状态。请注意会话不是线程安全的。 Hibernate 的二级缓存值得一试,它通常应该透明地处理延迟加载的集合。但是我不确定是否可以自己缓存关系(我最近尝试使用简单的 OneToMany,但无法缓存它)【参考方案3】:

您收到此异常是因为您的对象已与当前会话分离。在评估它之前,您必须将此对象重新附加到当前会话

session.update(object);

您可以阅读详情here

【讨论】:

session.update(...) 似乎是特定于 Hibernate 的。但是我使用的是 JPA,因此我需要相应的 JPA 机制来执行此操作。现在问题也出现了,我应该在何时何地重新连接实体 - 最好是在抛出 LazyInitializationException 的地点和时间? (这也许可以在有问题的实体方法中,在一个 catch 块中完成)。最后,我需要获取“that JPA session manager”才能调用 session.update(this); 如何使用 Spring 将“that JPA session manager”注入我的实体? 好的,在我刚才评论的基础上,我成功地使用建议的“会话更新”解决方案解决了问题:... 1. 使用EntityManagerFactory emf... 2. 注入我的有问题的实体。在有问题的实体内部,每次访问可能延迟加载的字段时,都用 try-catch 块包围该字段... 3. public Object problematicMethod() try ... catch (LazyInitializationException e) JpaFacebookUser throwAway = emf.createEntityManager().merge(this); return throwAway.problematicMethod(); ...... 显然,这更多的是一种技巧,而不是一种优雅的解决方案。 @Abdull 是的,它远非优雅的解决方案,它是一个 ugly hack。如果您使用 JPA,那么我建议您使用上面描述的二级缓存或重构代码以使其与 EntityManager.merge() 而不是 Session.update() 一起使用,但无论如何它都会从 DB 重新加载数据

以上是关于Java JPA:如何跨 HTTP 请求保持会话活动?的主要内容,如果未能解决你的问题,请参考以下文章

会话保持之cookie session token

Python模块-requests

Ajax请求如何保持同一会话session

绕过cookies进行登录并封装请求方法

会话保持

如何跨单个项目的不同 Spring Boot 微服务实现 JPA 审计?