`
biomedinfo
  • 浏览: 24852 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

《The Definitive Guide to Grails》 学习笔记七 (对应第10章)

阅读更多

 



1. GORM的基础知识回顾:

object get(id), List getAll(List idList), 和 object read(id)的差别是,get(包括getAll)方法从持久层返回的对象是可修改状态,而read方法返回的对象是只读的。
list()方法有一种动态形式是listOrderBy*,例如listOrderByDateCreated()。
save()方法可以用于新增对象,也可以用于更新对象,Hibernate会自动判断并相应地产生SQL INSERT或SQL UPDATE语句,但是在一些旧版本的数据库下,偶尔会出现混淆,使Hibernate在新增对象时发出UPDATE语句,为了避免这个问题,可以在 save()方法里显式地传输insert参数,如: album.save(insert:true)

对于一对多的关系,缺省是一个java.util.Set类,不允许重复,也没有顺序,如果需要增加这些特性,可以在domain类中进行定义,如使用 SortedSet(需要实现Comparable接口,定义compareTo()方法),例如:

class Album {
    .....
    SortedSet songs
}

class Song implements Comparable {

    ...
    int compareTo(o) {
        if(this.trackNumber > o.trackNumber)
            return 1
        elseif(this.trackNumber < o.trackNumber)
            return -1
        return 0
    }
}
或者也可以直接在domain类中利用mapping属性来声明排序,如:
class Song {    //这里不需要实现Comparable接口
......
static mapping = {
    sort: "trackNumber"    //原书缺少了这个冒号,应该是个笔误?
    }
}
如果只需要针对某个关联实现排序,可以在sort前定义需要实现排序的关联名:
static mapping = {
    songs sort: "trackNumber"
}

另一种实现排序的方法是使用另一种集合类型,如java.util.List,List允许重复并保持了对象被存入时的顺序。为了支持List关 联,Hibernate使用了一个包含List中每个item的index的特殊的index字段。例如:

class Album {
    ...
    List songs
}
因为List是有序的,可以直接用序号进行索引,例如: println album.songs[0]

GORM也支持Map关联,把上例中的List改为Map,用一个String而不是Integer来访问数据入口,grails同样为它产生一个 index字段。两者的区别是,List的index字段是一个描述它在List中位置的数值,而Map对应的index字段保存的是键值。


GORM通过动态方法addTo*和removeFrom*来对关联进行添加和删除操作,并且两个方法都返回当前处理的实例,从而支持连续的方法调用。例 如:

new Album(title:"Odelay", artist: beck).addToSongs(title:"devil's Haircut", artist:beck).addToSongs(title:"Hotwax",artist:beck).addToSongs(oldSong).save()

当在GORM中对某个实例进行保存、更新或删除操作时,这个操作可以级联到任何关联对象。缺省的级联操作是由belongsTo属性定义的,如果删除一个 实例,则所有belongsTo这个对象的其他对象都会被删除,而如果在关联中没有belongsTo定义,则只有保存和更新会进行级联,而不会进行级联 删除。通过mapping属性可以自定义级联方式,如:

class Album {
    ...
    static mapping = {
        songs cascade: 'save-update'
    }
}
此外还有一个特殊的级联方式delete-orphan,用来删除已经被去除关联关系的子对象。

2. 关于查询:SQL方式在Java这种面向对象的架构下是不可取的,Hibernate提供了一种比较优雅的Java API用于存取数据库中的数据,但是只有ORM做到了更高层次的对数据存取逻辑进行彻底抽象化,隐藏了与Hibernate交互的细节。

动态查找:
前面已经以ListOrderBy*为例说明了动态查找的强大功能,其实这只是GORM优势的冰山一角,完整的查找还可以支持诸如And, Or, 和Not的组合,例如:
Album.findByTitleAndGenre("Beck","Alternative")
此外,还可以使用GreaterThan, LessThan, Like, InList, IsNull和Between等。除了findBy*以外,GORM还提供了findAllBy(返回一个List)和countBy*(返回一个计 数),这样在典型的Java应用中必不可少的DAO(Data Access Object)层就基本可以退出历史舞台了。DAO做的几件事情是:
* 定义数据存取逻辑的接口,其签名几乎和GORM的动态查找完全一样;
* 利用Java类实现该接口
* 使用Spring或其他IoC容器连接诸如数据源或Hibernate session的dependencies
由此我们不难看出,数据存取逻辑部分是大量重复的,这严重违背了我们信奉的DRY原则。现在我们有了GORM,谁还会想着DAO呢?

条件查询:

和条件查询能够实现的功能相比,动态查找还是太小儿科了。GORM的条件使用了一个builder语法,通过Groovy的builder支持进行查询。 那么,什么是Groovy的builder呢?它基本上可以说是一个层次型的方法调用和闭包,适合用来产生树形结构,例如XML文档或图形用户界面 (GUI),当然用在构建查询特别是动态的查询上也是完美的,取代很容易出错的StringBuffer方式再合适不过了。它提供的方法包括get, list, scroll, count,具体使用例如:
def c = Album.createCriteria()
def results = c.list {
    eq('genre', 'Alternative')
    between('dateCreated', new Date()-30, new Date())
}
上述例子列出最近30天产生的genre是'Alternative'的唱片,在闭包中嵌套的方法调用会被自动转化为Hibernate的 org.hibernate.criterion.Restrictions类中的方法调用。但是,这和动态查找的def results = Album.listByGenreEqualAndDateCreatedBetween('Alternative', new Date()-30, new Date())有何优势可言呢?嗯,上面的例子确实不够有说服力,因为闭包的威力还没有表现出来。先看看闭包的特点,它是一段代码,可以被赋值给一个变 量,并且,在闭包内部还可以引用变量。把这两件事放到一起,就产生了一个非常强大的、可复用的动态查询机制。例如,你可以用一个map把要查询的属性保存 在key中,把查询的参数放在value中(类似于controller中的params),例如:
def today = new Date()
def queryMap = [genre: 'Alternative', dateCreated:[today-10, today] ]    //注意这里的today-10,grails自动处理Date实例-Integer实例
def query = {                                   //query变量被赋予一个闭包
    queryMap.each { key, value ->    //内置的GDK方法each,遍历每个map元素,并把其中的key和value作为参数传递给闭包
        if(value instanceof List) {    //instanceof 操作符,检查前面的value是否是后面List类的一个实例
            between(key, *value)    //*操作符,能够把一个List或者array拆开,把其中的每个值传递给目标
        }
        else {
            like(key, value)
        }
    }
}

def criteria = Album.createCriteria()


println(criteria.count(query))    //计数

println(cirteria.get(query))        //查到一条记录
criteria.list(query).each { println it }    //输出每一条记录
def scrollable = criteria.scroll(query)    //返回Hibernate的org.hibernate.ScrollableResults类的实例,类似于JDBC的 java.sql.ResultSet实例(但index从0开始)
def next = scrollable.next()
while(next) {
    println(scrollable.getString('title'))
    next = scrollable.next()
}

查询关联:到目前位置查询的都是单个类,如果需要查找多个关联类,grails的条件builder也能够胜任。它允许使用嵌套的条件方法调用,方法名要 匹配属性名,作为参数的闭包包含了嵌套的与关联类相关的条件调用。例如,当我们需要查找所有包含'shake' 这个词的唱片,可以这样做:

def criteria = Album.withCriteria {
    songs {
                ilike('title', '%shake%')
    }
}
前面举例的条件定义方式都可以被嵌套在songs嵌套方法内部,从而形成功能强大的动态关联查询。

投影(projection)查询:投影可以使条件查询的结果通过某种方式进行组织和汇总。类似于SQL的count, distinct, sum命令。通过条件查询可以声明一个projections方法,该方法以闭包的形式出现,映射到Hibernate的 org.hibernate.criterion.Projections类,而不是criteria类。例如:

def criteria = Album.createCriteria()
def count = criteria.get {
    projections {
        countDistinct('name')
    }
    songs {
        eq('genre', 'Alternative')
    }
}

example查询:除了条件查询外,还可以通过传递一个类的实例给find/findAll方法来进行查询,例如:

def album = Album.find( new Album(title: 'Odelay') )
看上去让人觉得grails很神奇,但这么做有什么实际意义呢?况且这种方法只能用于find或findAll,不能用于其他的方法如Like, Between或者GreaterThan等等,为啥grails要提供这么一个奇怪的用法?书上说这样在结合Groovy隐性的JavaBeans的 constructor使用方面比较有意思,我看了半天还请教了大师,才明白这样可以比较简单地添加多个查询项,而不局限于findBy*的两个参数,而且语句也比较直观。

3. HQL和SQL:HQL是比较灵活的面向对象查询方式,GORM也为它提供了一些内建的方法。HQL和SQL在语法上大同小异,GORM还提供了三个方 法:find, findAll和executeQuery,每个方法都接收一个字符串作为HQL的查询语句,例如:

def allAlbums = Album.findAll('from com.g2one.gtunes.Album')
此外,?作为占位符可以用来支持第二个参数,如:
def album = Album.find('from Album as a where a.title = ?', ['Odelay'])
如果用第二个参数不理想,还可以使用命名的参数,如:
def album = Album.find( 'from Album as a where a.title = :theTitle', [theTitle: 'Odelay'])
这样传递给find方法的就不是一个List,而是一个Map,key和用:标示的参数匹配。
find和findAll是针对某一个类进行查询,如果有更加灵活的HQL查询命令,可以使用executeQuery,如:
def songs = Album.executeQuery('select elements(b.songs) from Album as a')
HQL可以用于更加灵活的查询,比如使用join, 汇总函数和子查询等,详情可见Hibernate相关技术文档。

4. 分页:前面看到的list()等方法都可以使用max, offset等参数来进行分页,例如:

def results = Album.list(max:10, offset: 20)
在查询中也是一样,可以把包括max, offset以及sort, order等任何参数放到一个map里作为参数传递给findAllBy*等方法,如:
def results = Album.findAllByGenre("Alternative", [max:10, offset:20])
在view里,可以使用<g:paginate>标签,只需要给它传递一个total参数,就能够自动产生上页、当前页、下页等链接,如:
<g:paginate total = "${Album.count()}"/>
如果是对其他controller的action进行分页,可以在其中声明:
<g:paginate controller="album" action="list" total="${Album.count()}"/>
还可以通过prev和next属性修改缺省的"Previous"和"Next"链接:
<g:paginate prev="Back" next="Forward" total=${Album.count()}"/>
如果要求i18n(国际化),可以使用<g:message>标签,作为一个方法被调用,从message bundles里产生文本:
<g:paginate prev="${message(code:'back.button.text')}"
                    next="${message(code:'next.button.text')}"
                    total=${Album.count()}"/>

5. 配置GORM:GORM有很多参数可以配置,在Hibernate中的选项在GORM中都可用,比较有用的有SQL日志,

在DataSource.groovy中设定
logSql = true
Hibernate提交的所有的SQL语句都会被输出到控制台,但是只能看到那些statements,看不到实际的value。如果要看value,可 以在config.groovy设置一个特殊的log4j日志:
log4j = {
    .....
    logger {
        trace "org.hibernate.SQL", "org.hibernate.type"
    }
}
对于不同的数据库类型,Hibernate采用了方言(dialect)进行区分,方言是自动通过JDBC的metadata来判别的,但对于某些不支持 JDBC metadata的数据库,必须显式声明其方言,这是通过在DataSource.groovy中设置dialect来完成的。例如对InnoDB,定义 MySQL5InnoDBDialect类:
dataSource {
    .....
    dialect = org.hibernate.dialect.MySQL5InnoDBDialect
}
上述的logsql和dialect实际上是Hibernate SessionFactory中的hibernate.show_sql和hibernate.dialect属性,如果你熟悉Hibernate的配置 模型,可以在DataSource.groovy中直接定义一个hibernate的块,实际上Hibernate的第二级cache就是这么预先配置 的:
hibernate {
    cache.use_second_level_cache = true
    cache.use_query_cache = true
    cache.provider_class = 'com.opensymphony.oscache.hibernate.OSCacheProvider'
}


6. GORM语义:从前面的介绍不难得到一个印象:GORM很容易用,有了它,就不用再考虑数据库了。但是这么想是错误的,大师教导我们,知其然还要知其所以 然,使用ORM工具很重要的一点就是了解它是如何工作的,否则,你的应用很容易出现性能问题和功能问题。ORM一般都试图让开发人员脱离底层数据库的复杂 性,不幸的是,如果开发人员不认真考虑诸如lazy fetching, eager fetching, locking策略和caching这些方面的问题,应用的表现可能很难令人满意。

经常有人拿GORM和ActiveRecord in Rails比较,认为两者很相似。其实他们是不同的,最主要的差异之一就是GORM有持久化context的概念(session),Session属于 org.hibernate.Seesion类,本质上是一个容器,保存了所有已知的持久化domain类的实例的索引,在hibernate行家的眼 中,数据都是对象,至于怎么确保对象的状态和数据库同步(synchronized)的问题是交给hibernate来处理的。
同步的过程是从对Session对象进行flush()方法调用来触发的,但是这和grails有什么关系呢?在domain类部分我们从来没有提到过什 么flush()方法,实际上,GORM对Session对象的处理是对开发者透明的,所以有可能整个grails应用从来没有直接和Hibernate Session对象交互的必要。但是,如果有些开发者对session model不熟悉,有可能出现一些意外情况,如:
def album1 = Album.get(1)
def album2 = Album.get(1)
assertFalse album1.is(album2)    //这里用了is而不是=操作符,因为=在Groovy里等同于Java的equals(Object),记住在Groovy里,什么都是Object
album2在这里没有触发任何的SQL,实际上,最后一个assert是failed。这是因为Session对象实际上是一个Hibernate的第 一级cache。再看保存的代码:
def album = new Album(...)
album.save()
是不是GORM就会马上执行一个SQL INSERT命令呢?答案是不一定,取决于后台的数据库。GORM缺省会使用Hibernate的native identity generation 策略,自动选择最合适的产生对象id的方式。例如,在Oracle里,Hibernate使用sequence生成器提供id,在save()时就不需要 SQL INSERT,仅仅增加内存里的sequence就行了,在Session被flush的时候才真正地去执行SQL INSERT;而在MySQL里使用identity策略,就需要马上执行SQL INSERT,因为需要数据库来产生id。两个例子有个共同点,那就是Session负责同步对象在数据库中的状态,对象本身不需要考虑同步的问题。
总之,Hibernate实现了被称为transactional write-behind的策略,任何对持久化对象进行的变更不一定马上被持久化,甚至调用save()方法也未必进行持久化。这样做的好处是 Hibernate可以很好的优化和批量组合需要执行的SQL,减轻网络的负荷,并且减少数据库lock的频率。

7. Session管理和flush:上面的策略固然很好,但是这样可能对于某些开发者来说失去了对数据的控制。GORM提供了对Session flush的控制,在save()或delete()方法里传递一个flush参数即可,如: album.save(flush:true),这样在save()后就会立刻对Session对象调用flush()。但是,要注意Session对象 负责所有持久层实例的处理,其他数据的变化也同时被持久化了。

看到这里,不明真相的一小撮围观群众们不禁要问:Session到底是怎么工作的?说来话长,当grails应用接收到一个request时,在 controller的action执行之前,grails会默默地为当前线程绑定一个新的Hibernate Session,然后GORM的动态方法(如get)就通过Session来进行数据存取,在action完成后,如果没有异常抛出,Session就会 被flush,从而执行必要的SQL把Session的状态与后台数据库进行同步。
但是,到这里Session还没有被关闭,它在开始view渲染之前被设置为只读模式直到view渲染完成才被关闭。这又是为什么呢?众所周知,一旦 Session被关闭,所有在其中存放的持久化实例都会被剥离,如果这时view试图访问某些未被初始化的级联对象(也就是lazy模式的级联),因为级 联数据无法取得,就会出现org.hibernate.LazyInitializationException。
此外,在这个阶段Session被设置为只读,是为了避免在view渲染过程中对Session进行不必要的flush动作,view不应该做 controller做的事情。这就是标准的Session生命周期。但是,这对于flow是个例外。在flow中Hibernate Session的范围就不仅仅是request,而是flow scope。当flow开始的时候,一个新的Session被建立并绑定到flow scope,每次flow从view返回执行,就使用同一个Session进行数据存取,这时,所有对Session进行操作的GORM方法也绑定到 flow scope;最后,当flow终结的时候,Session被flush,所有的数据变更都被commit到数据库中。

既然Session是一个持久化实例的cache,那么它就是要消耗内存的。一个使用GORM的常见错误是查询了大量的对象却不周期性地清空 Session,这样就会使Session变得越来越大,最后导致应用系统的性能下降甚至出现out of memory错误。在这种情况下,有必要手工管理Session,那么怎么取得Session对象呢?

第一种方法是通过dependency注入得到一个Hibernate SessionFactory对象的reference,SessionFactory提供了currentSession()方法:
def sessionFactory
...
def index = {
    def session = sessionFactory.currentSession()
}
另一种方法是使用withSession方法,该方法对任何domain类都可用,后面跟一个closure,closure的第一个参数就是 Session对象,例如:
def index = {
    Album.withSession { session ->
        ....
    }
}
取得Session对象后,就可以使用clear()方法来清空Session对象中保存的内容,这些内容会在一定时间被gc,释放相应的内存。例如:
def index = {
    Album.withSession { session ->
        def allAlbums = Album.list()
        for(album in all Albums) {
            def songs = Song.findaAllByAlbum(album)
            //对这个songs List中的Song实例进行处理
            ......
            session.clear()
        }
    }
}
不过,有时候不应该清空全部的Session内容,特别是涉及到在我们前面提到的lazy级联数据的时候,如何部分清除Session中的内容呢?这就可 以使用discard()方法:
songs*.discard()
discard()方法可以用来清除Session中的单个对象或者利用通配符*来清除一组对象,这样提供了Session清理的一种灵活性。

自动化的Session flush:GORM缺省在一下三种事件发生时自动flush Session:

* 进行query查询时
* controller action完成且无异常抛出的情况下
* 在transaction进行commit操作之前
请注意第一条,这意味着什么呢?请看下面的例子:
def album = album.get(1)
album.title = "Change It"
def otherAlbums = Album.findAllWhereTitleLike("%Change%")
assert otherAlbums.contains(album)
assert返回的是true。有人要说了,没有save()怎么就能查到了?这就是因为在Hibernate环境下,当你加载一个Album实例的时 候,它立刻被Hibernate管理,因为第一条原则,所以在findAll的时候,Session就被flush()了。总的来说,Hibernate 的Session会缓存所有对持久层数据的变更,但只在迫不得已的时候才将其保存到数据库里。对于自动flush,这个时候可能是transaction 的终点或者Query的起点。

自动flush的行为模式可能和你的预期不同,导致你无法理解。例如:

def album = album.get(1)
album.title = "Change It"
这里改变了album对象的一个属性,但是开发者忘记了写save()方法,这个变更最后会被保存到数据库中吗?答案可能出乎你预料:会。因为 Hibernate自动对包含在Session里的持久化实例进行dirty checking并把其中的任何变更都flush到数据库中。看起来很智能,但是等一下,之前我们学到的validate()岂不是就不会被调用了,这样 不合法的数据也会被保存到数据库中去,这还得了?
这个问题问得很好,而且这个分析是完全正确的。所以在持久化对象时,一定要记得加入save()方法调用,这样不仅会自动加载validate()方法, 还会在validate()出错的时候把目标对象及其级联对象设置为只读;如果该对象不需要被更新,那么应该用read()而不是get()方法,这样返 回的对象就是只读的。
有的人看了这些,会觉得flush的问题太过繁琐,还不如自己控制Session被flush的条件,那么应该考虑修改 DataSource.groovy中的缺省FlushMode参数(auto),可以通过声明hibernate.flush.mode设置来进 行:  hibernate.flush.mode = "manual",这样只有当你在save()或delete()中插入flush:true参数时,才会进行更新数据库的操作。此外的 FlushMode参数还包括commit,只在transaction被commit的时候才flush。

8. GORM中的transaction:不管是否有transaction的划界声明,所有在Hibernate和数据库之间的通讯都在数据库 transaction的context中。Session本身是lazy的,只会在迫不得已的情况下初始化数据库transaction,具体 说,Session在request到来时就被打开并绑定到当前的线程,而transaction只有在Session第一次和数据库通讯的时候才会被初 始化,在这个时候,Session关联到一个JDBC Connection对象,其autoCommit属性被设为false,从而初始化一个transaction,这个Connection只有在 Session关闭的时候才会被释放。由此可见,在grails里,transaction覆盖了所有的数据库操作。

既然如此,是不是任何错误都可以被roll back呢?事实上,如果在Session被flush的时候,又没有明确的交易划界定义,数据的变更会被永久地commit到数据库中,无法roll back,特别是在flush不受控的情况(例如query,见前面的例子),这会导致数据的一致性混乱。例如:
def save = {
    def album = Album.get(params.id)
    album.title = "Changed Title"
    album.save(flush:true)
    .....
    // 出错啦!
    throw new Exception("Oh, my god")
}
有两个办法可以解决这一类问题:一是采用service来处理交易逻辑(见下一章),二是使用withTransaction方法来划定交易的边界:
def save = {
 Album.withTransaction { 
    def album = Album.get(params.id)
    album.title = "Changed Title"
    album.save(flush:true)
    .....
    // 出错啦!
    throw new Exception("Oh, my god")
  }
}
划定边界后,grails实际上是使用了Spring的PlatformTransactionManager抽象层,这样在抛出异常时,在边界内的所有 数据变更都可以被roll back。具体做法是,在withTransaction方法的第一个参数是一个Spring TransactionStatus对象,通过它的setRollbackOnly()方法可以roll back交易:
def save = {
 Album.withTransaction {  status ->
    def album = Album.get(params.id)
    album.title = "Changed Title"
    album.save(flush:true)
    .....
    // 出错啦!   
    if(hasSomethingGoneWrong()) {
        status.setRollbackOnly()
    }   
  }
}
此外,如果数据库兼容JDBC 3.0,还可以使用保存点(save point)进行回滚,这样可以回滚到预先设定的保存点,不必每次都把整个交易全部回滚。
def save = {
 Album.withTransaction {  status ->
    def album = Album.get(params.id)
    album.title = "Changed Title"
    album.save(flush:true)

    def savepoint = status.createSavepoint()    //保存点
    .....
    // 出错啦!   
    if(hasSomethingGoneWrong()) {
        status.rollbackToSavepoint(savepoint)    //回滚到保存点
    //继续
    }   
  }
}

9. Detached对象:Domain对象的生命周期是这样的:在对象被save()之前,处于transient状态,等同于常规的不需要持久化的 Java对象;当调用save()方法时,该对象就进入持久化状态,被赋予一个id和其他属性,例如对级联对象的lazy load;当对象被discard()或者Session被清空,而这个对象还在系统中,例如在HttpSession中保存,则进入detatched 状态,此时该对象不再被任何的Session所管理;

detatched状态有什么涵义呢?比较典型的就是当对象在HttpSession中处于Detatched状态,而且存在未经初始化的lazy load级联关系,系统会抛出LazyInitializationException异常,解决这个问题的一个办法是用attch()方法把 detatched状态下的对象重新关联到当前线程的Session里,如album.attach()。但是这样做也要当心,如果在当前线程的 Session里已经load了具有相同id的对象,会产生org.hibernate.NonUniqueObjectException异常,所以需 要先通过isAttached()方法检查:
if(!album.isAttached()) {
    album.attach()
}
如果应用中大量存在重新关联detatched对象的情况,那么必须考虑为所有存在detatched对象的domain类重新实现equals()和 hashCode()方法。为什么呢?看下面的例子:
def album1 = Album.get(1)
album.discard()
def album2 = Album.get(1)
assert album1 == album2      //this assertion will fail
上述的断言是失败的,因为在Java中,缺省的equals()和hashCode()方法利用对象相等来比较实例,而当一个实例是 detatched,Hibernate会丢失它所有的相关信息,这样就会被认为是另一个实例。那么hashCode又有什么关系呢?当对象被放入集合的 时候,Set是用hashCode来判断两个对象是否重复,但是上面的两个Album实例却会返回不同的两个hashCode(尽管它们在数据库中对应的 id是相同的),从而造成重复。
有人大概会提出用数据库id来产生哈希码,但是这样对transient对象持久化时产生的哈希码是随时间而不同的,这和哈希码的原则(hashCode 的实现必须在任何时间都对一个对象返回相同的整数)不符。grails推荐的方法是使用business key,即采用一些具有唯一性的典型的逻辑属性,例如在Album中的Artist.name和title,实现的例子如下:
class Album {
    ...
    boolean equals(o) {
        if(this.is(o)) return true
        if(!(o instanceof Album) return false
        return this.title = o.title && this.artist?.name = o.artist?.name
    }
   
    int hashCode() {
        this.title.hasCode() + this.artist?.name?.hashCode() ?: 0
    }
}
再考虑一个复杂问题:假设已经有一个detatched的对象保存在HttpSession中,另外又出现了一个逻辑上相等(id相同)的实例,应该怎么 处理?答案是discard掉HttpSession中的实例:
def index = {
    def album = session.album    //此session非彼session,是HttpSession而不是Hibernate Session
    if(album.isAttached()) {            //看到这里我怎么也看不明白,应该是if(
!album.isAttached()) { 才对啊?经过某大师核对,认为我的看法是对的。
        album = Album.get(album.id)
        session.album = album        //对于detatched的对象,通过hibernate重载,并覆盖到HttpSession里,从而保持数据一致性
    }
}
某大师认为,这种把object直接保存到HttpSession中的做法是不值得提倡的,这样会导致HttpSession变得很大,其实只需要在其中 保留一个索引即可。

这本书喜欢把问题一步步地搞得越来越复杂,这里继续考虑下去,如果在HttpSession里的对象已经发生了改变怎么办?这时这个对象显然比刚从 Hibernate里load出来的那个对象要新。这个嘛,就可以用merge()方法合并了。

merge()方法接收一个实例,如果在Session中不存在,则load一个与其逻辑相等的永久化实例,然后把接收的实例的状态合并到被load的持 久化实例中。merge()方法在完成这些工作后返回一个新的实例,其中包含了merge后的状态。例如:
def index = {
    def album = session.album
    album = album.merge(album)
    render album.title
}

10. GORM性能调优:除了前面讲到的缓存数据(Session)机制外,GORM还提供了一系列的性能优化手段,为了尝试下面提到的方法,可以在 DataSource.groovy中把logsql设为true来查看不同设置的效果。

Eager vs. Lazy级联:GORM缺省是lazy,当级联对象被访问时,Hibernate会对每个级联对象的记录提交一个SQL SELECT请求,这就是N+1问题。例如:
def albums = Album.list()        //一个SQL SELECT
for(album in albums) {             //这种for的方式值得注意
    println album.artist.name    //如果album里有N个artist,会执行N次SQL SELECT
}

这里的每次级联SQL SELECT都产生进程间通讯,如果N的数值比较大,势必会拖累应用的性能。那么把缺省级联机制改成eager如何?eager方式使用SQL JOIN,这样在查询时所有的级联数据就被一次性读入了,如果这样做,需要在Domain类中修改mapping属性:
class Album {
    ....
    static mapping = {
        artist fetch: 'join'
    }
}
但是这样也未必是理想的做法,很可能导致要把整个数据库都load到内存里。lazy级联显然还是最合适的缺省配置,如果只需要级联对象的id,那么就无 需额外的SQL SELECT了。对于前面的例子,需要输出级联对象的内部属性,join是解决之道,这时可以在list()方法中显式声明对级联对象的读取方式:
def albums = Album.list(fetch: [artist: 'join'])
这样就不会有N+1的SQL SEELCT,而是一个SQL SELECT ...  INNER JOIN ...。
其他的查询也可以采用这种方式来优化性能:
动态finder: def albums = Album.findAllByGenre("Alternative", [fetch: [artist: 'join']])
条件查询: def albums = Album.withCriteria {
                        ....
                        join 'artist'
                 }
HQL:  def albums = Album.findAll("from Album as a inner join a.artist as artist")

批量抓取:SQL JOIN也不一定都是经济的,取决于JOIN的数量和关联到的数据量,另外一种对lzay方式的优化手段是批量抓取。在批量抓取方式 下,Hibernate不再是对每个查询请求生成一个SQL SELECT,二是根据设定的批量数对一批请求进行一次SELECT,例如:

class Song {
    ....
    static mapping = {
        batchSize 10
    }
}
比如一个Album里有23首song,在lazy缺省方式下,查询所有的song名会产生1次对Album,23次对Song的SQL SELECT,在上面的batchSize设定后,Hibernate会把对Song的SQL SELECT组合成3次,每次查到的记录数是10,10,和3。
另外,也可以对级联关系设置batchSize,例如Album有15个实例被load的时候,为了取得其中级联的songs,Hibernate会为每 个Album实例发出一个SQL SELECT,这样共发出15个SELECT,如果在Album类里对songs属性设定batchSize,如:
class Album {
    ....
    static mapping = {
        songs batchSize: 5
    }
}
这样对应15个Album实例,发出的对songs级联的查询只需要15/5=3个。

以上的讨论都是着眼于减少对数据库的访问次数,此外还有一些缓存技术是着眼于尽可能减少对数据库的直接访问。

缓存:Session是一级缓存,它保存被load的持久化实体,防止重复的对同一实体进行数据库存取。此外,Hibernate也提供了一些其他的缓 存,包括二级缓存和查询缓存。
* 二级缓存
一级缓存是在Session scope保存涉及的持久化实例,而二级缓存只保存属性值及/或外键,但是是在SessionFactory的整个生命过程中有效。 SessionFactory是组建每个Session的特殊对象,因此二级缓存实际上是存在于整个application scope。二级缓存的例子如下:
9 -> ["Odelay", 1994, "Alternative", 9.99,  [34,35,36], 4]
可以看出,二级缓存以包含多维数组的map形式来保存数据,这样做Hibernate就无须要求每个Domain类实现Serializable或其他持 久化接口;对级联数据只保存id,可以避免级联对象出现过时数据。上述细节不需要花费太多精力考虑,开发者要做的是明确一个cache的提供者,缺省情况 grails设置的是OSCache,但grails也配套了Ehcache(推荐用于生产环境),在DataSource.groovy中可以修改相应 的设置:
hibernate {
    cache.use_second_level_cache = true  
    cache.use_query_cache = true
    cache.provider_class = 'com.opensymphony.oscache.hibernate.OSCacheProvider'
}
甚至也可以设置分布式的cache例如Oracle Coherence或者Terracotta,但是要当心数据过时的问题,因为缓存结果并不一定反映数据库中数据的当前状态。
缺省情况下,所有的持久化类都没有激活二级缓存,缓存的方式需要对数据进行设定,主要分为如下4种模式:
read-only:如果数据生成后不会修改,就可以采用这种方式,甚至对分布式数据缓存也适用;
nonstrict-read-write:如果数据以读取为主,偶尔有修改,就可以采用这种方式,它不保证两个或多个交易不会同时修改持久化实例;
read-write:如果数据经常会被修改就采用这种方式,当对象被更新时,Hibernate会自动刷新二级缓存中的数据。不过还是不能排除出现幽灵 读取(过时数据),如果需要交易控制,那么应该设定为transactional;
transactional:提供完整的交易控制,不会出现脏数据,但是需要确认提供的cache提供者支持这个特性,例如JBoss TreeCache。
这样,就可以在Domain类中以mapping描述cache的模式:
class Album {
    .....
    static mapping {
        cache true
        songs chache: 'read-only'
    }
}
对于查询结果,如果已经存在二级缓存中,Hibernate就会自动将其调入,否则才到数据库中去读取数据。

查询缓存:如果有一些查询公式被频繁使用,返回相同的结果,就需要激活hibernate.cache.use_query_cache中的设置,并且, 查询缓存是与二级缓存配合工作的,设定cahche模式的时候要确认二级缓存可用,否则无法缓存查询结果。这样的缓存也需要在查询中确定:

def albums = Album.list(cache: true)
def albums = Album.findAllByGenre("Alternative", [cache: true])
def albums = Album.withCriteria {
    ....
    cache = true
}

11. 加锁策略:既然grails运行在多线程的servlet容器中,并发就是在对域实例进行持久化时需要考虑的问题。GORM的缺省方式是通过 version提供乐观锁,而不是通过SELECT FOR ...... UPDATE来锁定数据,具体说,每个GORM产生的table都包含一个version字段,当一个域实例被保存时,version就被加1;在对持久 化实例进行更新时,Hibernate会发出一个SQL SELECT来检查当前数据的version,如果数据库中的version和当前要更新的实例不符,会抛出 org.hibernate.StaleObjectException,并被打包在Spring的 org.springframework.dao.OptimisticLockingFailureException抛出。

上述问题的涵义是,如果应用是高并发的,就需要处理这种版本冲突的问题,这样的好处是数据表的列从来不会被锁定,有利于性能提高;但是,对于 org.hibernate.StaleObjectException异常该如何处理呢?这就和Domain模型有密切关系,从技术上说,既可以用 merge()方法来把修改的内容同步到数据库中;或者也可以把错误信息反馈给用户,让他确定是否手工合并。合并的代码是:
catch(OptimisticLockingFailureException e) {
    album= Album.merge(album)
    ....
}
如果不打算选用乐观锁,比如因为需要映射到已有的数据库中,那么可以在DataSource.groovy中取消乐观锁:
static mapping = {
    version false
}
如果应用的访问负载不大,也可以考虑悲观锁,它使用SELECT FOR ...... UPDATE,于是其他线程就不能访问此数据,直到更新被commit。这样做需要比较慎重,否则应用的性能会明显下降。悲观锁的实现是利用一个 lock()方法,传递实例的id并得到一个lock,例如:
def update= {
    def album = Album.lock(params.id)
    ....
}
如果已取得存在的持久化实例的id,可以直接调用lock()方法来实现悲观锁:
def update = {
    def album = Album.get(params.id)
    album.lock()
    ....
}
对于上述情况,在album.lock()完成之前,如果有另一个并发用户更新了这行数据,则还会出现 OptimisticLockingFailureException。

12. 事件的自动时间戳:GORM提供了一批内置的事件钩子,每个事件定义为Domain类的一个闭包属性,事件包括:

onLoad/beforeLoad
beforeInsert
beforeUpdate
beforeDelete
afterInsert
afterUpdate
afterDelete
这些事件对于执行类似于audit日志和追踪等任务很有用,例如:
class Album {
    ....
    transient onLoad = {
        new AuditLogEvent(type: "read", data:title).save()
    }
    ....
    transient beforeSave = {
        new AuditLogEvent(type: "save", data: title).save()
    }
}
GORM也支持自动时间戳。基本上只要Domain类中包含了dateCreated/lastUupdate属性,GORM会在每次实例保存或更新时自 动生成这些值。如果想自行管理这些时间戳,可以在mapping中取消自动时间戳:
class Album {
    ....
    static mapping = {
        autoTimestamp false
    }
}

------------------------------

后记:总算看完GORM了,这一章是看得最慢最吃力的,不过也是学到东西最多的,付出和收获总是成正比啊!
0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics