Grails陷阱之二

前篇:[url=http://hax.iteye.com/blog/347558]Grails陷阱之一[/url]

Grails陷阱之二:[b]跨request使用Domain Class实例需要重新attach[/b]

其实这并不能说是Grails的陷阱,而是Grails所依赖的Hibernate设计使然,不过初学者(比如我)可能对此没有概念,因此拿来说一下。

代码非常简单,如下:
[code]
if (session.user.canDo(actionName)) {
...
}
[/code]
这段代码从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通过[url=http://www.hibernate.org/280.html]动态代理[/url]来读取以便支持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。
[code]
if (User.read(session.userId).canDo(actionName)) {
...
}
[/code]
我见到的类似的方式还有
[code]
session.user = User.read(session.user.id)
if (session.user.canDo(actionName)) {
...
}
[/code]
当然这样的代码太别扭,而且只适用User.read(即只读)的情形,如果你之前对user对象做过修改(比如loginCount++)并尚未保存,则重新读取会丢失原对象上的修改。

2. 不使用lazy load,所有关联都一次读入。
[code]
class User {
...
static hasMany = [roles:Role]
static mapping = {
roles lazy:false
}
}
[/code]
[code]
注意,调用时所有可能读到的关联都必须改成不是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
}
}
[/code]


方案1的问题就是它只适用于只读情况,如果需要跨若干个request修改持久化对象就不行了。当然你可以把所有修改存放在web session中,然后一次性修改和提交,但是这无谓增加了代码复杂度。方案2也只适用于只读情况,还要求你小心处理所有的关联。而且在许多情况下不宜一次性读入所有相关数据(否则干嘛要lazy加载)。幸好,我们有方案3。

3. 重新attach
[code]
if (!session.user.attached) session.user.attach()
if (session.user.canDo(actionName)) {
...
}
[/code]
Grails的domain class上的attach方法,可以把一个持久化对象重新attach到当前的Hibernate Session中。

类似的方式还有调用domainInstance.refresh(),但它会强制重新读取数据库,所以通常不应使用。

还有domainInstance = domainInstance.merge(),它相当于detached版本的save()。注意merge返回一个新对象,所以要重新给引用赋值。


就我自己的案例来说,由于user对象是每次都要用的,所以我最后使用了Grails filters来重新attach:

[code]
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()
}
}
}
}
}
[/code]

其他:

不过你还是有可能会在layout gsp中碰到类似的问题(我暂时还没遇到),据说是grails和sitemesh的整合bug,将在grails 1.2中解决。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值