`
biomedinfo
  • 浏览: 25121 次
  • 性别: 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
分享到:
评论

相关推荐

    The definitive Guide To Grails学习笔记

    《The definitive Guide To Grails学习笔记》是一份深入探讨Grails框架的重要资源,它源于经典书籍《The Definitive Guide to Grails》的精华总结。Grails是一种基于Groovy语言的开源Web应用框架,旨在提高开发效率...

    The definitive guide to grails 2 英文版 书 代码

    《The Definitive Guide to Grails 2》是Grails框架深入学习的重要参考资料,由业界专家撰写,旨在为开发者提供全面、详尽的Grails 2技术指导。这本书结合了理论与实践,不仅介绍了Grails的基本概念,还涵盖了高级...

    the definitive guide to grails 2

    《Grails 2 的终极指南》是一本深入探讨Grails框架精髓的专业书籍,该书以英文撰写,旨在为读者提供全面、深入的Grails框架学习资料。Grails框架基于Groovy语言,是一种高度动态、敏捷的Java应用开发框架,它简化了...

    The Definitive Guide to Grails 2nd Edition

    The Definitive Guide to Grails 2nd Edition.pdf

    The definitive guide to grails_2 源代码

    《The Definitive Guide to Grails 2》是关于Grails框架的一本权威指南,它为读者提供了深入理解和使用Grails 2开发Web应用程序所需的知识。Grails是一种基于Groovy语言的开源全栈式Web应用框架,它借鉴了Ruby on ...

    The Definitive Guide to Django 2nd Edition

    《The Definitive Guide to Django 2nd Edition》是一本深度解析Django框架的权威指南,旨在帮助初学者和有经验的开发者全面掌握Django的使用。这本书分为两个主要部分,确保读者能够从基础到高级逐步提升自己的技能...

    The Definitive Guide to Spring Batch, 2nd Edition.epub

    The Definitive Guide to Spring Batch takes you from the “Hello, World!” of batch processing to complex scenarios demonstrating cloud native techniques for developing batch applications to be run on...

    The Definitive Guide to SQLite

    And because SQLite's databases are completely file based, privileges are granted at the operating system level, allowing for easy and fast user management., The Definitive Guide to SQLite is the ...

    <The Definitive Guide to MySQL 5>

    这本书的第三版详细介绍了MySQL 5版本的各项功能,帮助读者掌握这一强大的数据库系统。 在书中,你可以了解到以下关键知识点: 1. **MySQL安装与配置**:包括在不同操作系统上的安装步骤、配置文件的解释以及...

    The Definitive Guide to Grails Second Edition (Apress 2009)

    ### Grails第二版终极指南(Apress 2009)关键知识点解析 #### 一、Grails概述 - **Grails**是一款基于Groovy语言的高性能、全栈式的Java Web应用开发框架,它极大地简化了Java Web开发过程,使得开发者能够更高效...

    The Definitive Guide to Java Swing Third Edition

    ### 《Java Swing 终极指南》第三版关键知识点概览 #### 一、书籍基本信息与版权信息 - **书名**:《Java Swing 终极指南》第三版 - **作者**:John Zukowski - **出版社**:本书由Springer-Verlag New York, Inc....

    The Definitive Guide to Windows Installer

    The Definitive Guide to Windows Installer Introduction Chapter 1 - Installations Past, Present, and Future Chapter 2 - Building an Msi File: Visual Studio and Orca Chapter 3 - COM in the ...

    The Definitive Guide to Django - Web Development Done Right(2nd) 无水印pdf

    The Definitive Guide to Django - Web Development Done Right(2nd) 英文无水印pdf 第2版 pdf所有页面使用FoxitReader和PDF-XChangeViewer测试都可以打开 本资源转载自网络,如有侵权,请联系上传者或csdn删除 ...

    The Definitive Guide to GCC, Second Edition

    The Definitive Guide to GCC, Second Edition has been revised to reflect the changes made in the most recent major GCC release, version 4. Providing in-depth information on GCC’s enormous array of ...

    The Definitive Guide to Jython-Python for the Java Platform

    ### 关于《The Definitive Guide to Jython—Python for the Java Platform》的知识点解析 #### 一、Jython简介 Jython 是一种开放源代码的实现方式,它将 Python 这种高级、动态且面向对象的脚本语言无缝集成到 ...

    The Definitive Guide to HTML5 epub

    The Definitive Guide to HTML5 英文epub 本资源转载自网络,如有侵权,请联系上传者或csdn删除 本资源转载自网络,如有侵权,请联系上传者或csdn删除

    802.11详解 802.11 Wireless Networks- The Definitive Guide

    Wireless Networks: The Definitive Guide 这个比直接看802.11 协议要舒服一些。理解更方便。 802.11® Wireless Networks: The Definitive Guide By Matthew Gast Publisher : O'Reilly Pub Date : April 2002 ISBN...

Global site tag (gtag.js) - Google Analytics