为什么在视图中打开Hibernate会话被认为是不良实践?

125

你采用什么样的备选策略来避免LazyLoadExceptions呢?

我知道在视图中开启会话存在以下问题:

  • 运行在不同jvm的分层应用程序
  • 事务只有在最后才提交,而你很可能希望在此之前获得结果。

但是,如果你知道你的应用程序在单个VM上运行,为什么不使用一个在视图中开启会话的策略来减轻痛苦呢?


13
OSIV被认为是一种不好的实践吗?由谁认为? - Johannes Brodwall
4
那么 - 有哪些好的替代品? - David Rabinowitz
7
这段文本来自Seam开发者: 这个实现存在几个问题,其中最严重的问题是,在我们提交事务之前,我们永远无法确定事务是否成功,但是当“在视图中打开会话”事务被提交时,视图已经完全渲染,而已经渲染的响应可能已经被发送到客户端。那么我们如何通知用户他们的交易不成功呢? - darpet
这是链接:http://www.redhat.com/docs/manuals/jboss/jboss-eap-4.2/doc/seam/Seam_Reference_Guide/Seam_and_ObjectRelational_Mapping-Seam_managed_transactions.html - darpet
2
请参阅此博客文章,了解其优缺点以及我的个人经验 - http://blog.jhades.org/open-session-in-view-pattern-pros-and-cons/ - Angular University
我看到一个问题,但不确定OSIV是否导致了这个问题,在我的应用程序中,当使用多节点服务器和负载均衡器时,我会遇到延迟初始化错误。相同的代码在一个服务器上正常工作,但是当我添加另一个服务器时,它开始随机出现延迟初始化错误。 - mksmanjit
10个回答

57

在视图中打开会话采用了一种不好的数据获取方法。它强制持久化上下文保持打开状态,以便视图层可以触发代理初始化,而不是让业务层决定如何最好地获取视图层所需的所有关联。

enter image description here


  • OpenSessionInViewFilter 调用底层 SessionFactoryopenSession 方法并获取一个新的 Session
  • SessionTransactionSynchronizationManager 绑定。
  • OpenSessionInViewFilter 调用 javax.servlet.FilterChain 对象引用的 doFilter,并继续处理请求。
  • 调用 DispatcherServlet,它将 HTTP 请求路由到底层的 PostController
  • PostController 调用 PostService 来获取 Post 实体列表。
  • PostService 开启一个新事务,HibernateTransactionManager 重用了由 OpenSessionInViewFilter 打开的同一 Session
  • PostDAO 获取 Post 实体列表,而不初始化任何延迟关联。
  • PostService 提交底层事务,但是 Session 没有关闭,因为它是外部打开的。
  • DispatcherServlet 开始呈现 UI,进而导航延迟关联并触发它们的初始化。
  • OpenSessionInViewFilter 可以关闭 Session,底层数据库连接也将被释放。
乍一看,这似乎不是一个可怕的事情,但是,一旦你从数据库的角度来看待它,一系列缺陷就开始变得更加明显。
服务层打开并关闭了一个数据库事务,但之后没有明确的事务正在进行。因此,从UI渲染阶段发出的每个附加语句都在自动提交模式下执行。自动提交会对数据库服务器造成压力,因为每个语句必须将事务日志刷新到磁盘上,从而在数据库端引起大量I/O流量。一种优化方法是将连接标记为只读,这将允许数据库服务器避免写入事务日志。
现在已经没有关注点分离了,因为语句既由服务层生成,也由UI渲染进程生成。编写集成测试 (断言所生成的语句数量) 需要通过所有层(Web、服务、DAO),同时将应用程序部署在Web容器上。即使使用内存数据库(例如HSQLDB)和轻量级Web服务器(例如Jetty),这些集成测试的执行速度也比分层更慢,而后端集成测试使用数据库,而前端集成测试则完全模拟服务层。
UI层仅限于导航关联,这可能会引发N+1查询问题。虽然Hibernate提供@BatchSize批量获取关联和FetchMode.SUBSELECT处理此场景,但注释会影响默认获取计划,因此它们将应用于每个业务用例。因此,数据访问层查询更加适合,因为它可以根据当前用例数据获取要求进行定制。
最后,数据库连接可能在UI渲染阶段保持(取决于连接释放模式),这会增加连接租赁时间并限制整体事务吞吐量,因为数据库连接池拥塞。连接保持时间越长,其他并发请求就越需要等待从池中获取连接。
因此,要么保持连接时间过长,要么在单个HTTP请求中获取/释放多个连接,从而对底层连接池施加压力并限制可扩展性。

Spring Boot

不幸的是,在Spring Boot中默认启用了Open Session in View

因此,请确保在application.properties配置文件中有以下条目:

spring.jpa.open-in-view=false

这将禁用OSIV,因此您可以正确处理LazyInitializationException,通过在EntityManager打开时获取所有所需的关联。请注意保留HTML标签。

3
在自动提交模式下,使用Open Session in View是可行的,但这不是Hibernate开发人员原本预期的方式。因此,尽管Open Session in View有其缺点,但自动提交不是其中之一,因为您可以简单地关闭它并仍然使用它。 - stefan.m
2
会话仍然保持打开状态,但事务不是。将事务跨越整个过程也不是最优的,因为它会增加长度并且锁定时间比必要的要长。想象一下,如果视图抛出RuntimeException会发生什么。UI渲染失败后,事务会回滚吗? - Vlad Mihalcea
3
虽然我同意OSIV不是最理想的解决方案,但你提出的解决方法抵消了像Hibernate这样的ORM的好处。ORM的目的是加速开发人员的体验,要求开发人员在获取链接属性时返回编写JPA查询,这正好相反。Spring在这里做得很对,通过默认启用OSIV并包括日志记录来通知开发人员已配置完成。 - Johnny.Minty
3
你完全误解了。即使Hibernate可以生成CRUD语句,也不意味着应用程序开发人员不应该使用查询。事实上,JPA和SQL查询不是例外,而是规则。Spring是一个很棒的框架,但默认启用OSIV会造成伤害。 - Vlad Mihalcea
1
@VladMihalcea 这是来自官方Hibernate文档的一句话:“Hibernate的设计目标是通过消除手动编写SQL和JDBC进行数据处理的需要,从而减轻开发人员在95%的常见数据持久化相关编程任务中的负担。”现在,你说JPA和SQL查询不是例外,而是规则。我认为这两个陈述是矛盾的。顺便说一下,我没有反对你的答案,你已经很好地列出了细节。不过,我认为他们应该将文档中的95%更正为70%之类的数字 :) - sanemain
显示剩余6条评论

47

因为在视图层中发送可能未初始化的代理对象,特别是集合对象,会从性能和理解角度带来麻烦。

理解:

使用OSIV会将与数据访问层相关的问题“污染”到视图层。

视图层不能处理可能发生的HibernateException(如延迟加载),但数据访问层可以。

性能:

OSIV倾向于把适当的实体加载隐藏起来-您往往不会注意到您的集合或实体已被延迟初始化(也许是N+1)。更方便,但控制力较弱。


更新:请参阅The OpenSessionInView反模式,以获取有关此主题的更大讨论。作者列出了三个重要观点:

  1. 每次延迟初始化都会生成一个查询,这意味着每个实体将需要N + 1个查询,其中N是延迟关联的数量。如果您的屏幕显示表格数据,则读取Hibernate日志表明您没有按照正确的方式操作。
  2. 这完全破坏了分层架构,因为您在表示层中会与数据库交互。这是一个概念上的问题,所以我可以接受,但有个附带条件
  3. 最后但并非最不重要的,如果在获取会话时发生异常,则将在编写页面时发生异常:您无法向用户呈现干净的错误页面,唯一能做的就是在页面正文中编写错误消息。

15
好的,它会以Hibernate异常的形式“污染”视图层。但就性能而言,我认为问题与访问返回DTO的服务层相似。如果遇到性能问题,则应通过更智能的查询或更轻量级的DTO来优化该特定问题。如果您必须开发过多的服务方法来处理可能在视图中需要的情况,那么您也会“污染”服务层,不是吗? - HeDinges
1
一个区别是它延迟了Hibernate会话的关闭。您将等待JSP被渲染/编写/等等,这会使对象在内存中保留更长时间。如果您需要在会话提交时写入数据,这可能是一个问题。 - Robert Munteanu
10
除了使用DTO,没有其他替代方案可以说OSIV会影响性能。但是,如果使用DTO,则始终会存在较低的性能,因为任何视图使用的数据都必须加载,即使对于不需要该数据的视图也是如此。 - Johannes Brodwall
13
我认为污染是双向的。如果我需要急切地加载数据,逻辑层(或更糟的是数据访问层)需要知道一个对象将以哪种方式显示。更改视图可能会导致加载不需要的内容或丢失所需的对象。Hibernate异常是一个错误,与任何其他意外异常一样有害。但性能是个问题。性能和可伸缩性问题将迫使您在数据访问层中投入更多思考和工作,并可能强制更早关闭会话。 - Jens Schauder
2
@JensSchauder “改变视图,你最终会加载不需要的东西或者缺失需要的对象”。这正是问题所在。如果你改变了视图,最好加载一些不需要的东西(因为你更可能会急切地获取它们),或者找出缺失的对象,因为你会得到懒加载异常,而不是让视图懒加载它,这将导致N+1问题,而你甚至不知道它正在发生。所以,在我看来,让服务层(和你)知道发送了什么比让视图懒加载并且你对此一无所知要好。 - Jeshurun
1
那个OSIV反模式链接上有一些非常好的反驳论点;任何阅读此文的人也应该阅读这些内容。 - Jason C

24
  • 可以在服务层提交事务 - 事务与OSIV无关。它是Session保持开启,而不是事务 - 运行。

  • 如果您的应用程序层分布在多台计算机上,则基本上无法使用OSIV - 您必须在将对象发送到远程之前初始化所需的所有内容。

  • OSIV是一种不错且透明的方式(即 - 您的代码不知道这是发生了)来利用延迟加载的性能优势。


2
关于第一个要点,至少对于JBoss维基上的原始OSIV,这是不正确的,它还处理了请求周围的事务划分。 - Pascal Thivent
@PascalThivent 你认为是哪一部分让你这么想的? - Sanghyun Lee

14

我不认为Open Session In View被视为一种不良实践;是什么给你留下了这样的印象?

开放式会话视图是处理Hibernate会话的一种简单方法。因为它很简单,所以有时候过于简单化。如果您需要对事务进行细粒度控制,例如在一个请求中有多个事务,那么开放式会话视图并不总是一个好的选择。

正如其他人所指出的,OSIV存在一些权衡取舍--您更容易遇到N+1问题,因为您不太可能意识到您正在启动哪些事务。与此同时,这意味着您无需更改服务层来适应视图中的小变化。


5
如果您正在使用像Spring这样的控制反转(IoC)容器,您可能想要了解一下bean scoping。本质上,我告诉Spring给我一个Hibernate Session对象,其生命周期跨越整个请求(即,在HTTP请求的开始和结束时创建和销毁)。我不必担心LazyLoadException或关闭会话,因为IoC容器为我管理它们。
正如提到的那样,您将不得不考虑N+1 SELECT性能问题。您始终可以在性能成为问题的地方配置您的Hibernate实体以进行急切的连接加载。
bean范围解决方案并非仅适用于Spring。我知道PicoContainer提供了相同的功能,我相信其他成熟的IoC容器也提供类似的功能。

1
你是否有指向Hibernate会话实际实现的指针,可以通过请求范围的bean在视图中使用? - Marvo

4

根据我的经验,OSIV并不是那么糟糕。 我所做的唯一安排是使用两个不同的事务: - 第一个在“服务层”中打开,在这里我有“业务逻辑” - 第二个在视图渲染之前打开。


3

2
通常情况下,如果您在提供答案,最好不要仅仅提供链接。或许可以提供一两句话或列出要点来简述。提供链接是没问题的,但您需要提供额外的价值。否则,您可能只需发表评论并在那里放置链接。 - DWright
这个答案中的链接值得一读,它提供了关于何时使用OSIV和何时不使用的良好指导。 - ams

1

我对Hibernate有点生疏,但我认为在一个Hibernate会话中可以有多个事务。因此,您的事务边界不必与会话的开始/停止事件相同。

在我看来,OSIV主要是有用的,因为我们可以避免每次请求需要进行数据库访问时编写启动“持久化上下文”(即会话)的代码。

在您的服务层中,您可能需要调用具有不同事务需求的方法,例如“Required, New Required”等。这些方法唯一需要的是某人(即OSIV过滤器)已经启动了持久化上下文,以便他们所关心的只是 - “嘿,给我这个线程的Hibernate会话..我需要做一些数据库操作。”


1

这并不能太有帮助,但你可以在这里查看我的主题: * Hibernate Cache1 OutOfMemory with OpenSessionInView

由于OpenSessionInView和大量加载的实体,我遇到了一些OutOfMemory问题,因为它们停留在Hibernate缓存级别1中,并且没有被垃圾回收(我每页加载500个项目,但所有实体都停留在缓存中)。


1
如果您将大量东西加载到L1缓存中,那么您的问题不是OSIV,而是您设计了一些愚蠢的东西。 - lscoughlin

0

太长不看

之前的回答对于我理解真正实际避免使用OSIV的原因来说有些不方便。

OSIV是一种反模式,那么实际常见的原因是什么呢?

OSIV反模式是指在整个请求-响应周期中保持数据库会话处于打开状态(在大多数情况下,人们使用基于线程的每个请求模型),这意味着会话在请求生命周期期间保持打开状态。 OSIV的目的是允许在视图中进行实体关联的延迟加载,并在需要访问延迟加载实体时按需使用会话。

  • 并发性:当会话保持打开时,可能会导致潜在的并发问题。如果多个请求同时处理,它们可能会共享同一个会话,导致行为不可预测和数据完整性问题
  • 数据库性能影响:将数据库会话保持打开时间超过必要时间可能会导致资源使用增加和性能下降。
  • 事务生命周期管理:在OSIV中,事务边界经常丢失,因为会话在整个请求-响应周期内保持打开状态。这使得有效地管理事务变得困难,并可能导致未提交或长时间运行的事务问题,从而影响数据源连接限制,最终所有事务都可能处于“idle in transaction”状态。

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