Open Session In View

原文:[url=http://www.hibernate.org/43.html]http://www.hibernate.org/43.html[/url]

[b]问题[/b]

在一个典型的(Web)应用中常见的一个问题是在主要的逻辑动作完成之后渲染页面,同时,因为逻辑动作的完成,Hibernate Session 已被关闭,数据库事务也已结束。如果你在你的 JSP 中(或者其它视图渲染机制)访问已被 Session 加载的 Detached Object 的话,你可能会遇到一个没有被初始化的未加载的 Collection 或 代理。这时,你将会得到一个 LazyInitializationException: Session has been closed,或类似的消息。当然,这是可预见的,毕竟,你已经结束了你的工作单元了。

一个解决方案是开启另一个工作单元去渲染视图。这很容易被实现,但这通常不是正确的方法。为一个完整的动作去渲染页面应当是在第一个单元的工作中,而不是在独立的单元中。这个解决方案,在一个动作的执行,通过 Session 进行数据访问,视图的渲染都是在一个虚拟机中完成的两层架构系统中,是通过保持 Session 的打开直至视图渲染完成来实现的。

[b]使用拦截器[/b]

如果你为了自动化的 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 的更多信息,请看[url=http://www.hibernate.org/328.html]泛型 DAO[/url]

现在你的应用中的 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,以避免这个问题。

[b]关于三层环境[/b]

毫无疑问,这种模式仅当你渲染视图时使用本地的 Session 这种情况下有效。在一个三层的环境中,视图可能在展现层虚拟机被渲染,而不是在拥有商业逻辑和数据访问层的 Service 虚拟机中被渲染。因此,保持 Session 和事务的开启并不是一个选择。这种情况下,你不得不发送合适量的数据到展现层虚拟机中,这样视图可以在 Session 关闭的情况下被构建。至于你是选择 Detached Object,或是 DTO,亦或使用 Command Pattern 混合两者,就取决于你的架构了。这些在 Hibernate in Action 均有讨论。


[b]关于为长对话(long-conversation)扩展 Session 的模式[/b]

如果你喜欢让一个 Hibernate Session 跨越多个数据库事务,那你也不得不自力更生了,或者使用(Hibernate)内建的“应用管理”策略(使用 ManagedSessionContext)。
[img]http://www.hibernate.org/hib_images/community/session_conversation.png[/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() {}

}

[i]备注:1. org.hibernate.classic.Session 同普通的 Session 一样,只是多了一些 Hibernate 2 中的,现已不推荐的方法,用以代码移植。
2. 这里提到为 long-conversation 扩展 Session 的做法和 Seam 中的有很大的不同,不要将两者混淆。这里的做法也不是真正将 Hibernate Session 和 Conversation (比 Http Session 小,但比 Request 的 Context,详见 JBoss Seam 或 WebBeans 文档)整合。[/i]

这个 Filter 对于你的应用的其余部分是透明的,使用“当前” Session 的 DAO 和其它代码是不需要改变的。然而,在某一点 Conversation 是需要结束的,这时 Session 需要被 flush 和关闭。上面的例子使用了一个Request 范围的特殊标记。你可以在请求的处理过程中设置这个标记。可能你有其它的拦截器层应用在 Conversation。或者,是工作流引擎。

[b]我不想写 Servlet Filter,它太老土了![/b]

你是对的,Servlet Filter 不再是热门技术了。然而,它们在 Web 应用中作为 wrap-around 的拦截器工作的非常好(wrap-around,同 AOP 中的 around 含义相同),并且 Servlet Filter 本质上没有什么问题。就像已经提到的,如果你使用了自带拦截器的 Web 框架,你可能会发现它们更灵活。目前,很多 Web 容器也提供了自定义的拦截器。注意,如果这些 Web 容器没有实现 Java EE 规范的话,你的应用将不具有可移植性。最后,更灵活的方式是使用 AOP 拦截器。见 [url=http://www.hibernate.org/391.html]Session handling with AOP[/url]。

[b]如何处理异常?[/b]

毫无疑问,一旦异常发生,事务将会回滚。Hibernate 的另一条规则是,当前 Session 必须被关闭,并立即被抛弃,它不能被重新使用。因此,从 Hibernate 的角度看,这是你在处理异常时全部要做的。当然,如果失败的话你可能想重试一些工作,或者你可能想显示自定义的错误。所有这些已超出了 Hibernate 的范围,你可以按照自己喜欢的方式实现。

[b]我能在渲染视图前提交事务吗?[/b]

显然地,虽然没有在 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 中的两个事务,一个用于执行事件,另一个用于渲染视图。

[b]我能在一个 Session 中使用两个事务吗?[/b]

是的,这事实上是这种模式(Open Session In View)的一个更好的实现。在一个请求事件中,一个数据库事务用于数据的读写。第二个数据库事务仅用于在渲染视图期间读数据。在这点上没有对对象的修改。因此,数据库锁早在第一个事务时就被释放了,这使得应用有更好的可伸缩性,第二个事务可以被优化。要使用两阶段的事务,你需要比 Servlet Filter 更强大的拦截器 - AOP 是个很好的选择。JBoss Seam 使用了这种模式。

[b]为什么 Hibernate 不在需要时就加载 Object?[/b]

每个月很多人都会有这种想法,为什么 Hibernate 不能在有需要的就开启一个新的数据库连接(更有效率的是开启一个 Session),然后加载集合或是初始化代理,而是选择抛出一个 LazyInitializationException。当然,这种想法,第一眼看上去可能是明智之举。但这种做法有很多的缺点,只有当你考虑特别的事务访问时才会发现。

如果 Hibernate 可以进行任意的数据库连接和事务,这种操作是开发人员不可知,并且也是在任何事务边界之外的,那还要事务边界做什么。当 Hibernate 开启了新的数据库连接去加载集合,但同时集合的拥有者却被删除了,这是将会发生什么?(注意,这种情况是不会发生在上面提到的两阶段的事务模式中的 - 单个 Session 可对实体可重复读。)当所有的对象都可以通过关联导航获取时为什么还要有 Service 层?这种方式将消耗多少内存?哪些对象要首先被清除掉?所有这些问题都是无解的,因为 Hibernate 是一个在线的事务处理服务(并包含一些批处理操作),并不是一个“在未定义的工作单元中从数据持久仓库取得对象”的服务。此外,对于 n+1 查询问题,我们是否需要 n+1 的事务和连接的问题?

这个问题的解决方案当然是正确的工作单元划分和设计,支撑其的拦截技术就像这里所展现的一样,并且/或者正确的抓取技术,使得特定工作单元所需的全部信息能够以最小的影响、最好的性能和伸缩性被获得。

[b]这一切很难吗?能否做的更简单?[/b]

Hibernate 所能做的只是持久化服务,这里所做的事情,其责任在于应用程序架构和框架。EJB3 编程模型使得事务和持久化上下文的管理变得简单了,使用 Hibernate EntityManager(这是 JPA 的一个实现)得到其 API。可以在一个完整的 Java EE 的实现服务器去运行你的 EJB 们,也可以在一个轻量级嵌入式的 EJB 容器中运行,在你的 Java 环境中。JBoss Seam 有内建的自动上下文管理机制,包括持久化和对话(Conversation),实现这些仅需要你在代码中使用一些注释。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值