前篇:
Grails陷阱之一
Grails陷阱之二:
跨request使用Domain Class实例需要重新attach
其实这并不能说是Grails的陷阱,而是Grails所依赖的Hibernate设计使然,不过初学者(比如我)可能对此没有概念,因此拿来说一下。
代码非常简单,如下:
if (session.user.canDo(actionName)) {
...
}
这段代码从session中取出user(假设user登陆之后,你把user对象保存在HTTP session中),然后调用上面的canDo方法,检测user是否有权限执行某个action。canDo方法会读取user.roles(一对多)。
扔出异常如下:
Grails Runtime Exception
Error Details
Error 500: could not initialize proxy - no Session
Servlet: grails
URI: /test/grails/admin/index.dispatch
Exception Message: could not initialize proxy - no Session
Caused by: could not initialize proxy - no Session
Class: AuthFilters
At Line: [40]
注意,这个异常仅会在你的代码(比如canDo)访问一对一或一对多的关联对象时产生。
熟悉hibernate的同志可能一眼就看出是什么问题了。在访问关联对象时,Hibernate通过
动态代理来读取以便支持lazy加载,而延迟加载的相关操作同所有持久化操作一样,需要在一个session内(是Hibernate的持久化Session,不要和Web容器管理的HTTP Session混淆哦),而当一个持久化对象被保存到HTTP Session中,然后在下一个request中被拿出来时,当然之前的Hibernate的Session早结束了。
注意,Hibernate提供了OSIV(Open Session In View)的Filter,可以配置在web.xml中,会针对每个HTTP request,开启一个新的Hibernate session,然后在request结束时关闭。
如果没有OSIV,你也会遇到类似的异常,但是这和这里讨论的并不是一个问题——我为什么对这个陷阱印象深刻,就是因为我一度把这两种情况搞混了。
Grails已经内置了OSIV,虽然它可能存在一些bug(见后文),但是这里的问题并非如此。这里的问题是,在新一次request中的Hibernate session,已经不是创建user对象当初的那个Hibernate session了。
解决方案:
1. 不在session中保存domain class的实例(即持久化对象),而是保存它的id。
if (User.read(session.userId).canDo(actionName)) {
...
}
我见到的类似的方式还有
session.user = User.read(session.user.id)
if (session.user.canDo(actionName)) {
...
}
当然这样的代码太别扭,而且只适用User.read(即只读)的情形,如果你之前对user对象做过修改(比如loginCount++)并尚未保存,则重新读取会丢失原对象上的修改。
2. 不使用lazy load,所有关联都一次读入。
class User {
...
static hasMany = [roles:Role]
static mapping = {
roles lazy:false
}
}
注意,调用时所有可能读到的关联都必须改成不是lazy的的,比如假设你的canDo方法,还要检查每个roles上的permission,则permissions关联也要禁用lazy加载:
class User {
...
static hasMany = [roles:Role]
static mapping = {
roles lazy:false
}
boolean canDo(String action) {
roles.any {
it.permissions.contains('*') ||
it.permissions.contains(action)
}
}
}
class Role {
...
static hasMany = [permissions:String]
static mapping = {
permissions lazy:false
}
}
方案1的问题就是它只适用于只读情况,如果需要跨若干个request修改持久化对象就不行了。当然你可以把所有修改存放在web session中,然后一次性修改和提交,但是这无谓增加了代码复杂度。方案2也只适用于只读情况,还要求你小心处理所有的关联。而且在许多情况下不宜一次性读入所有相关数据(否则干嘛要lazy加载)。幸好,我们有方案3。
3. 重新attach
if (!session.user.attached) session.user.attach()
if (session.user.canDo(actionName)) {
...
}
Grails的domain class上的attach方法,可以把一个持久化对象重新attach到当前的Hibernate Session中。
类似的方式还有调用domainInstance.refresh(),但它会强制重新读取数据库,所以通常不应使用。
还有domainInstance = domainInstance.merge(),它相当于detached版本的save()。注意merge返回一个新对象,所以要重新给引用赋值。
就我自己的案例来说,由于user对象是每次都要用的,所以我最后使用了Grails filters来重新attach:
class AuthFilters {
def filters = {
login(controller:'*', action:'*') {
before = {
if (controllerName && controllerName != 'auth' && !session.user) {
redirect(controller:'auth', action:'login',
params:[url:request.forwardURI])
return false
} else if (session.user && !session.user.attached) {
log.debug "reattach ${session.user}"
session.user.attach()
}
}
}
}
}
其他:
不过你还是有可能会在layout gsp中碰到类似的问题(我暂时还没遇到),据说是grails和sitemesh的整合bug,将在grails 1.2中解决。
分享到:
相关推荐
Grails Grails Grails Grails Grails
Grails开发之(Rest教程).pdf
Grails开发之(Rest教程).docx
grails-2
grails 中文第二版
Grails权威指南Grails权威指南Grails权威指南Grails权威指南Grails权威指南Grails权威指南
[Apress] Grails 2 权威指南 (英文版) [Apress] The Definitive Guide to Grails 2 (E-Book) ☆ 出版信息:☆ [作者信息] Jeff Scott Brown, Graeme Rocher [出版机构] Apress [出版日期] 2013年01月23日 ...
详细介绍grails框架的奥秘,英文版你值得拥有
在学习任何东西之前,最重要的是培养兴趣,Groovy世界最耀眼的技术之一--Grails相信大家早已耳闻,我将通过Grails实战系列文章 向您展现Grails的迷人风采,使您感受到Grails的魅力,以至疯狂地爱上Grails,并坠入...
Grails项目的应用越来越多,而对于初学者来说,在Eclipse下搭建Grails项目是一个难题,这个文档将教会你如何搭建Grails项目,希望对你有所帮助。
Grails入门指南中文pdf -- 针对grails1.0.4更新,附加idea8 开发grails的流程
Grails1.1中文文档
grails+Xfire webservice
grails-2.1.zip.001
Grails权威指南第二版 Grails是一个搭建在动态语言 Groovy 之上的开源 MVC 快速 Web 开发框架。使用 Grails 可以提高 Web 开发的效率,降低 Web 开发的复杂度。 本书由Grails项目负责人Graeme Keith Rocher编写,极...
Grails从入门指南(第二版)
详细讲解grails开发环境配置。 详细讲解grails连接mysql数据库,crud开发
Grails 中文 参考手册