`
hax
  • 浏览: 951670 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Grails陷阱之二

    博客分类:
  • MISC
阅读更多
前篇: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中解决。
分享到:
评论
5 楼 tedeyang 2012-07-13  
hibernate这种框架迟早要进历史的垃圾堆啊
4 楼 wintersun 2009-05-02  
} else if (session.user && !session.user.attached) {   
                    log.debug "reattach ${session.user}"  
                    session.user.attach()   
                }


有点奇怪,如果我用“user.attached”就会报出以下错误

org.codehaus.groovy.runtime.InvokerInvocationException: groovy.lang.MissingPropertyException: No such property: attached for class: xxx


改用user.isAttached()就可以了。
3 楼 fcoffee 2009-03-26  
""注意,Hibernate提供了OSIV(Open Session In View)的Filter""

OSIV是Spring提供的, 不是hibernate
2 楼 hax 2009-03-19  
SSailYang 写道

感觉你 attach 一个 Model 的原因无非是因为 LazyInitializationException。如果 User 的 roles 要经常适用的话,比如是因为权限的操作,那可以用 join fetch 直接读取出来,这样也不用 attach 了。


lazy:false即相当于join fetch,即我写的第二种方法。如文中所述,这样只适合只读场合,如果role在运行时被改变了,采用第二种方式是不会立即起作用的,需要重新登陆,或提供强制刷新机制。更关键的问题是,这样的代码存在潜在的异常可能——比如你加了一段代码,访问了原先没有访问的某个lazy关联——而且这种异常是一般的单元测试无法测试出来的。所以attach是更好的做法。
1 楼 SSailYang 2009-03-19  
感觉你 attach 一个 Model 的原因无非是因为 LazyInitializationException。如果 User 的 roles 要经常适用的话,比如是因为权限的操作,那可以用 join fetch 直接读取出来,这样也不用 attach 了。

相关推荐

Global site tag (gtag.js) - Google Analytics