原文:http://www.hibernate.org/43.html
问题
在一个典型的(Web)应用中常见的一个问题是在主要的逻辑动作完成之后渲染页面,同时,因为逻辑动作的完成,Hibernate Session 已被关闭,数据库事务也已结束。如果你在你的 JSP 中(或者其它视图渲染机制)访问已被 Session 加载的 Detached Object 的话,你可能会遇到一个没有被初始化的未加载的 Collection 或 代理。这时,你将会得到一个 LazyInitializationException: Session has been closed,或类似的消息。当然,这是可预见的,毕竟,你已经结束了你的工作单元了。
一个解决方案是开启另一个工作单元去渲染视图。这很容易被实现,但这通常不是正确的方法。为一个完整的动作去渲染页面应当是在第一个单元的工作中,而不是在独立的单元中。这个解决方案,在一个动作的执行,通过 Session 进行数据访问,视图的渲染都是在一个虚拟机中完成的两层架构系统中,是通过保持 Session 的打开直至视图渲染完成来实现的。
使用拦截器
如果你为了自动化的 Hibernate Session Context 管理而使用 Hibernate 内建的支持(ThreadLocalSessionContext 和 JTASessionContext)来实现你的 Session 处理的话,请见 Sessions and transactions ,你已经有了一半的代码实现了。现在你只需要某种拦截器在试图渲染之后运行,然后去提交事务,关闭 Session。换句话说,在大多数应用中你需要做如下事情:当一个 Http 请求被处理的时候,一个新的 Session 和事务将开始。在响应被发回到客户端之前,所用的动作完成之后,事务被提交,Session 被关闭。
Servlet 容器中的标准拦截器是 ServletFilter。写一个自定义的 Filter 很平常,下面是 CaveatEmptor 的例子:
public class HibernateSessionRequestFilter implements Filter {
private static Log log = LogFactory.getLog(HibernateSessionRequestFilter.class);
private SessionFactory sf;
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
try {
log.debug("Starting a database transaction");
sf.getCurrentSession().beginTransaction();
// Call the next filter (continue request processing)
chain.doFilter(request, response);
// Commit and cleanup
log.debug("Committing the database transaction");
sf.getCurrentSession().getTransaction().commit();
} catch (StaleObjectStateException staleEx) {
log.error("This interceptor does not implement optimistic concurrency control!");
log.error("Your application will not work until you add compensation actions!");
// Rollback, close everything, possibly compensate for any permanent changes
// during the conversation, and finally restart business conversation. Maybe
// give the user of the application a chance to merge some of his work with
// fresh data... what you do here depends on your applications design.
throw staleEx;
} catch (Throwable ex) {
// Rollback only
ex.printStackTrace();
try {
if (sf.getCurrentSession().getTransaction().isActive()) {
log.debug("Trying to rollback database transaction after exception");
sf.getCurrentSession().getTransaction().rollback();
}
} catch (Throwable rbEx) {
log.error("Could not rollback transaction after exception!", rbEx);
}
// Let others handle it... maybe another interceptor for exceptions?
throw new ServletException(ex);
}
}
public void init(FilterConfig filterConfig) throws ServletException {
log.debug("Initializing filter...");
log.debug("Obtaining SessionFactory from static HibernateUtil singleton");
sf = HibernateUtil.getSessionFactory();
}
public void destroy() {}
}
如果你将这个 Filter 与自动的 Session Context 支持结合。DAO 代码将像下面这个简单平常:
public class ItemDAO {
Session currentSession;
public ItemDAO() {
currentSession = HibernateUtil.getSessionFactory().getCurrentSession();
}
public Item getItemById(Long itemId) {
return (Item) currentSession.load(Item.class, itemId);
}
}
或者,DAO 可以有一个以 Session 作为参数的构造器,这样的话,设置当前 Session 的责任就将移到 DAO 的调用者上(或者是工厂)。关于 DAO 的更多信息,请看泛型 DAO
现在你的应用中的 Controller 可以使用 DAO 了,并且不被 Session 和事务所打扰。例如,在你的 Servlet 中:
public String execute(HttpRequest request) {
Long itemId = request.getParameter(ITEM_ID);
ItemDAO dao = new ItemDAO();
request.setAttribute( RESULT, dao.getItemById(itemId) );
return "success";
}
使 Filter 对所有的 Http 请求生效,在 web.xml 添加如下配置:
<filter>
<filter-name>HibernateFilter</filter-name>
<filter-class>my.package.HibernateThreadFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HibernateFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
如果你不想为每一个 Http 请求开启一个 Hibernate Session (和一个数据库事务,其从连接池中得到一个数据库连接),将 Filter 映射到合适的 Url 模式上(例如,仅需要数据库访问的 Url)。或者,为 Filter 增加一个开关,如果 Session 和事务是必须的,基于某种任意的条件(例如,请求参数)
附加说明:因为 Session 在视图渲染之后被 flush,数据库异常可能会在输出成功生成之后产生。如果你使用纯 JSP 和 Servlet,页面渲染之后的输出将被放在一个 buffer 中,仅有 8kb 大小。如果 buffer 满了,它将被 flush 到客户的浏览器中!所以,你的用户可能在发生异常的情况下看到成功的页面(200 OK)。为了避免这个问题,不要将输出渲染进 Servlet 的 buffer 中,或者增大 buffer 的尺寸到一个安全的值。多数 Web 框架不将渲染后输出放进标准 Servlet 的buffer 中,而是它们自带的 buffer,以避免这个问题。
关于三层环境
毫无疑问,这种模式仅当你渲染视图时使用本地的 Session 这种情况下有效。在一个三层的环境中,视图可能在展现层虚拟机被渲染,而不是在拥有商业逻辑和数据访问层的 Service 虚拟机中被渲染。因此,保持 Session 和事务的开启并不是一个选择。这种情况下,你不得不发送合适量的数据到展现层虚拟机中,这样视图可以在 Session 关闭的情况下被构建。至于你是选择 Detached Object,或是 DTO,亦或使用 Command Pattern 混合两者,就取决于你的架构了。这些在 Hibernate in Action 均有讨论。
关于为长对话(long-conversation)扩展 Session 的模式
如果你喜欢让一个 Hibernate Session 跨越多个数据库事务,那你也不得不自力更生了,或者使用(Hibernate)内建的“应用管理”策略(使用 ManagedSessionContext)。
[img]
[/img]
为了启用“应用管理”当前 Session 策略,你要设置 hibernate.current_session_context_class 配置属性为 org.hibernate.context.ManagedSessionContext (or simply "managed" in Hibernate 3.2)。你现在可以 使用静态方法去 bind 和 unbind 当前的 Session,和控制 FlushMode 并手动 flush。前面见到的 Servlet filter 需要在 conversation 期间去控制 bind 和 flush。
public class HibernateSessionConversationFilter
implements Filter {
private static Log log = LogFactory.getLog(HibernateSessionConversationFilter.class);
private SessionFactory sf;
public static final String HIBERNATE_SESSION_KEY = "hibernateSession";
public static final String END_OF_CONVERSATION_FLAG = "endOfConversation";
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
org.hibernate.classic.Session currentSession;
// Try to get a Hibernate Session from the HttpSession
HttpSession httpSession =
((HttpServletRequest) request).getSession();
Session disconnectedSession =
(Session) httpSession.getAttribute(HIBERNATE_SESSION_KEY);
try {
// Start a new conversation or in the middle?
if (disconnectedSession == null) {
log.debug(">>> New conversation");
currentSession = sf.openSession();
currentSession.setFlushMode(FlushMode.NEVER);
} else {
log.debug("< Continuing conversation");
currentSession = (org.hibernate.classic.Session) disconnectedSession;
}
log.debug("Binding the current Session");
ManagedSessionContext.bind(currentSession);
log.debug("Starting a database transaction");
currentSession.beginTransaction();
log.debug("Processing the event");
chain.doFilter(request, response);
log.debug("Unbinding Session after processing");
currentSession = ManagedSessionContext.unbind(sf);
// End or continue the long-running conversation?
if (request.getAttribute(END_OF_CONVERSATION_FLAG) != null ||
request.getParameter(END_OF_CONVERSATION_FLAG) != null) {
log.debug("Flushing Session");
currentSession.flush();
log.debug("Committing the database transaction");
currentSession.getTransaction().commit();
log.debug("Closing the Session");
currentSession.close();
log.debug("Cleaning Session from HttpSession");
httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);
log.debug("<<< End of conversation");
} else {
log.debug("Committing database transaction");
currentSession.getTransaction().commit();
log.debug("Storing Session in the HttpSession");
httpSession.setAttribute(HIBERNATE_SESSION_KEY, currentSession);
log.debug("> Returning to user in conversation");
}
} catch (StaleObjectStateException staleEx) {
log.error("This interceptor does not implement optimistic concurrency control!");
log.error("Your application will not work until you add compensation actions!");
// Rollback, close everything, possibly compensate for any permanent changes
// during the conversation, and finally restart business conversation. Maybe
// give the user of the application a chance to merge some of his work with
// fresh data... what you do here depends on your applications design.
throw staleEx;
} catch (Throwable ex) {
// Rollback only
try {
if (sf.getCurrentSession().getTransaction().isActive()) {
log.debug("Trying to rollback database transaction after exception");
sf.getCurrentSession().getTransaction().rollback();
}
} catch (Throwable rbEx) {
log.error("Could not rollback transaction after exception!", rbEx);
} finally {
log.error("Cleanup after exception!");
// Cleanup
log.debug("Unbinding Session after exception");
currentSession = ManagedSessionContext.unbind(sf);
log.debug("Closing Session after exception");
currentSession.close();
log.debug("Removing Session from HttpSession");
httpSession.setAttribute(HIBERNATE_SESSION_KEY, null);
}
// Let others handle it... maybe another interceptor for exceptions?
throw new ServletException(ex);
}
}
public void init(FilterConfig filterConfig) throws ServletException {
log.debug("Initializing filter...");
log.debug("Obtaining SessionFactory from static HibernateUtil singleton");
sf = HibernateUtil.getSessionFactory();
}
public void destroy() {}
}
备注:1. org.hibernate.classic.Session 同普通的 Session 一样,只是多了一些 Hibernate 2 中的,现已不推荐的方法,用以代码移植。
2. 这里提到为 long-conversation 扩展 Session 的做法和 Seam 中的有很大的不同,不要将两者混淆。这里的做法也不是真正将 Hibernate Session 和 Conversation (比 Http Session 小,但比 Request 的 Context,详见 JBoss Seam 或 WebBeans 文档)整合。
这个 Filter 对于你的应用的其余部分是透明的,使用“当前” Session 的 DAO 和其它代码是不需要改变的。然而,在某一点 Conversation 是需要结束的,这时 Session 需要被 flush 和关闭。上面的例子使用了一个Request 范围的特殊标记。你可以在请求的处理过程中设置这个标记。可能你有其它的拦截器层应用在 Conversation。或者,是工作流引擎。
我不想写 Servlet Filter,它太老土了!
你是对的,Servlet Filter 不再是热门技术了。然而,它们在 Web 应用中作为 wrap-around 的拦截器工作的非常好(wrap-around,同 AOP 中的 around 含义相同),并且 Servlet Filter 本质上没有什么问题。就像已经提到的,如果你使用了自带拦截器的 Web 框架,你可能会发现它们更灵活。目前,很多 Web 容器也提供了自定义的拦截器。注意,如果这些 Web 容器没有实现 Java EE 规范的话,你的应用将不具有可移植性。最后,更灵活的方式是使用 AOP 拦截器。见 Session handling with AOP。
如何处理异常?
毫无疑问,一旦异常发生,事务将会回滚。Hibernate 的另一条规则是,当前 Session 必须被关闭,并立即被抛弃,它不能被重新使用。因此,从 Hibernate 的角度看,这是你在处理异常时全部要做的。当然,如果失败的话你可能想重试一些工作,或者你可能想显示自定义的错误。所有这些已超出了 Hibernate 的范围,你可以按照自己喜欢的方式实现。
我能在渲染视图前提交事务吗?
显然地,虽然没有在 Hibernate 文档中提及,一些开发者使用这种模式的变种去保持 Session 的开启直到视图渲染,但在视图渲染之前提交事务。然后,在视图渲染期间,未被加载的代理或集合被访问,并进行初始化。因为 Session 依然开启,这表面上是可行的。甚至 Session 可以被调用,并做一些事情。Session 为一个单独的操作得到数据库连接,并执行准备好的语句。
然而,这种访问是非事务的。你需要为此在 Hibernate 配置中启用 auto-commit 模式。如果你在 Hibernate 中启用 auto-commit 模式,Hibernate 就会将从连接池中去到数据库连接设置为 auto-commit 模式,然后在通过调用 close() 方法使 JDBC Connection 返回连接池。
同样需要注意的是,如果你忘记了在 Hibernate 配置中启用 auto-commit,非事务的访问也可能会工作。如果你不在 Hibernate 中启用 auto-commit 模式,Connection 可能以任意的默认模式从连接池中获得,并在没有提交或者回滚的情况下返回连接池。这个行为是没有被定义的。绝对不要这样做,因为 JDBC 规范并没有说有潜在的未完成的事务时,调用 close() 将会发生什么(是的,当 Hibernate 从数据库连接池中得到连接时,数据库事务可能将隐式地开始)
事实上,任何非事务数据访问(没有 JTA/EJB)被认为是一个反模式,因为这无法从事务中得到性能、可伸缩性以及其它方面的好处。
许多依赖 Hibernate 的框架也依赖于这个反模式,要避免它们。总是用清晰的事务边界将你的工作划分为组。你可以考虑使用在一个 Session 中的两个事务,一个用于执行事件,另一个用于渲染视图。
我能在一个 Session 中使用两个事务吗?
是的,这事实上是这种模式(Open Session In View)的一个更好的实现。在一个请求事件中,一个数据库事务用于数据的读写。第二个数据库事务仅用于在渲染视图期间读数据。在这点上没有对对象的修改。因此,数据库锁早在第一个事务时就被释放了,这使得应用有更好的可伸缩性,第二个事务可以被优化。要使用两阶段的事务,你需要比 Servlet Filter 更强大的拦截器 - AOP 是个很好的选择。JBoss Seam 使用了这种模式。
为什么 Hibernate 不在需要时就加载 Object?
每个月很多人都会有这种想法,为什么 Hibernate 不能在有需要的就开启一个新的数据库连接(更有效率的是开启一个 Session),然后加载集合或是初始化代理,而是选择抛出一个 LazyInitializationException。当然,这种想法,第一眼看上去可能是明智之举。但这种做法有很多的缺点,只有当你考虑特别的事务访问时才会发现。
如果 Hibernate 可以进行任意的数据库连接和事务,这种操作是开发人员不可知,并且也是在任何事务边界之外的,那还要事务边界做什么。当 Hibernate 开启了新的数据库连接去加载集合,但同时集合的拥有者却被删除了,这是将会发生什么?(注意,这种情况是不会发生在上面提到的两阶段的事务模式中的 - 单个 Session 可对实体可重复读。)当所有的对象都可以通过关联导航获取时为什么还要有 Service 层?这种方式将消耗多少内存?哪些对象要首先被清除掉?所有这些问题都是无解的,因为 Hibernate 是一个在线的事务处理服务(并包含一些批处理操作),并不是一个“在未定义的工作单元中从数据持久仓库取得对象”的服务。此外,对于 n+1 查询问题,我们是否需要 n+1 的事务和连接的问题?
这个问题的解决方案当然是正确的工作单元划分和设计,支撑其的拦截技术就像这里所展现的一样,并且/或者正确的抓取技术,使得特定工作单元所需的全部信息能够以最小的影响、最好的性能和伸缩性被获得。
这一切很难吗?能否做的更简单?
Hibernate 所能做的只是持久化服务,这里所做的事情,其责任在于应用程序架构和框架。EJB3 编程模型使得事务和持久化上下文的管理变得简单了,使用 Hibernate EntityManager(这是 JPA 的一个实现)得到其 API。可以在一个完整的 Java EE 的实现服务器去运行你的 EJB 们,也可以在一个轻量级嵌入式的 EJB 容器中运行,在你的 Java 环境中。JBoss Seam 有内建的自动上下文管理机制,包括持久化和对话(Conversation),实现这些仅需要你在代码中使用一些注释。