Grails GORM查询总结

GORM绝对是Grails框架的一大亮点。GORM基于Hibernate的ORM之上做二次封装,既有Hibernate强大的功能,又有使用简便的特点。本篇主要总结和类比在项目开发中用到的GORM查询方式。

GORM支持的查询方式

GORM底层使用Hibernate,所以支持Hibernate的所有查询方式,此外,还支持Dynamic finder这种简便的查询。先对GORM支持的查询方式做一个表(参考来源: http://tatiyants.com/how-and-when-to-use-various-gorm-querying-options/):

dynamic finder where clause criteria HQL SQL
simple queries x x x x x
complex filters x x x x
associations x x x x
property comparisons x x x x
some subqueries x x x x
eager fetches w/ complex filters x x x
projections x x x
queries with arbitrary return sets x x
highly complex queries (like self joins) x
some database specific features x
performance-optimized queries x

从上表我们就能大致看出在什么样的场景下应该用什么类型的查询,每种查询方式具体的使用方式GORM参考官方文档。这里对这些查询方式简单做个总结

Dynamic finder

相信很多人第一眼对GORM的这个特性所惊艳到,可以用一种类似于静态方法的调用方式,就可以从数据库查询结果。它的优点和缺点一样明显,使用极为简单,功能也极为简陋,基本只能实现类似于:

SELECT * FROM domain WHERE condtions LIMIT x OFFSET y

这种方式的查询。只适合于单一Domain,查询条件固定的场景,也没有太多要说的,非常容易使用。

Book.findByName('bookName')
User.findAllByEnabled(true)
Persion.findAllByAgeBetween(10, 20)

Criteria

这个功能可以说是Hibernate的一个亮点,也是一个难点。它非常适合构造结构化查询的SQL,当查询条件不固定的时候,不需要在StringBuilder中编写大量的判断条件拼接的SQL,使得代码整洁度和可读性都大大提高。同时由于针对结构化查询的条件加了很多额外的方法,使得这个玩意对新手并不那么友好,有一定的上手门槛。另外也有一些查询上的限制,即使对SQL较为熟悉的用户,在写一个Criteria查询的时候可能也要想半天与调试一会。

Grails创建一个Criteria的语法有两种: Domain.createCriteria()Domain.withCriteria(Closure),后者可以看做是Domain.createCriteria().list(Closure)的别名。此外,由于Groovy的动态语言特性,所以Grails支持通过DSL的形式定义Criteria查询规则,而不需要像Hibernate那样写一堆.addXXX()的方法,比如:

User.createCriteria().list(params) {
    if (params.dateCreated) {
        gt('dateCreated', params.dateCreated)
    }
    if (params.status) {
        eq('status', params.status)
    }
}

list(Map params)支持的参数详见: http://docs.grails.org/latest/ref/Domain%20Classes/list.html#_description

代表要查询User这个Domain,但是dateCreatedstatus都是可选的查询条件,可能有也可能没有,通过Criteria的DSL可以很方便的定义这些查询条件,如果用HQL或者SQL写的话,是没有那么方便的,需要写判断条件,然后根据条件拼接对应的where语句,会让代码很冗长,而且四处拼接字符串也会让代码很难懂,一眼看不出来产生的SQL长什么样子。

此外,还可以加入projections,这样返回的就不是整个Domain对象了,而是Domain中指定的field或者field的聚合。相当于原本SQL中的SELECT * FROM domain变成了SELECT column1, count(column2), sum(column3),..., FROM domain。比如我要查询符合条件的用户数量以及平均年龄,可以这么写:

User.createCriteria().get {
    if (params.dateCreated) {
        gt('dateCreated', params.dateCreated)
    }
    if (params.status) {
        eq('status', params.status)
    }
    projections {
        count()
        avg('age')
    }
}

这样最终的结果就返回[count, avg]。如果Criteria的查询方式为list,并且传递有maxoffset参数的话,Grails会自动封装成一个PagedResultList对象,这个类中不但会包含符合条件的List,而且还会带有一个totalCount属性,便于分页查询,比如:

PagedResultList result = User.createCriteria().list([max: 10, offset: 10]) {
    if (params.dateCreated) {
        gt('dateCreated', params.dateCreated)
    }
    if (params.status) {
        eq('status', params.status)
    }
}

List<User> = result.resultList
int totalCount = result.totalCount

如果开启了Hibernate的DEBUG及TRACE级别的日志,会发现这里其实执行了两条SQL语句,一条是按照where条件查询出符合条件的结果集,另一条是去掉order by之后的SELECT count(*) FROM domain WHERE conditions。也就是说,有分页查询的需求时,不需要自己写两条查询语句查询count + list了,写一个查询条件就行了,通过PagedResultList在Grails内部就会给你产生两条这样的SQL语句,减少了代码量。

此外还有一个更令人称道的特性,就是Criteria的DSL定义放在了groovy闭包中,因此可以利用闭包的动态delegate特性,复用查询条件。当需要复用Criteria的查询条件时,这个特性会变的特别好用。

还是上面那个例子,比如我们需要在用户查询的详情页面中返回符合查询条件的用户列表(支持分页),在dashboard统计页面中,返回用户数和平均年龄的统计就行了,我们会发现这个where条件是完全一样的,因此可以考虑复用:

Closure criteria = { Map params = [:] ->
    if (params.dateCreated) {
        gt('dateCreated', params.dateCreated)
    }
    if (params.status) {
        eq('status', params.status)
    }
}

PagedResultList getUserList(Map params = [:]) {
    User.createCriteria().list(params) {
        criteria.delegate = delegate
        criteria(params)
    }
}

List<Integer> getUserSummary(Map params = [:]) {
    User.createCriteria().get {
        criteria.delegate = delegate
        criteria(params)
        projections {
            count()
            avg('age')
        }
    }
|

这样在复用查询条件的时候,会让代码大大缩短,而且便于集中维护查询条件,而不是需要增加支持的查询条件的时候,所有调用的方法全改一遍。

NOTE: 直接这么使用时不支持并发的,如果这个闭包被同时delegate,并且使用的参数不一致,那么在GORM底层就会抛出java.util.ConcurrentModificationException,类似于这样:

java.util.ConcurrentModificationException: null
      at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
      at java.util.ArrayList$Itr.next(ArrayList.java:859)
      at org.hibernate.loader.criteria.CriteriaQueryTranslator.getQueryParameters(CriteriaQueryTranslator.java:328)
      at org.hibernate.loader.criteria.CriteriaLoader.list(CriteriaLoader.java:109)
      at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1787)
      at org.hibernate.internal.CriteriaImpl.list(CriteriaImpl.java:363)
      at org.grails.orm.hibernate.query.AbstractHibernateCriteriaBuilder.invokeMethod(AbstractHibernateCriteriaBuilder.java:1700)
      at org.codehaus.groovy.runtime.InvokerHelper.invokePogoMethod(InvokerHelper.java:931)
      at org.codehaus.groovy.runtime.InvokerHelper.invokeMethod(InvokerHelper.java:908)
      at org.grails.datastore.gorm.GormStaticApi$_withCriteria_closure11.doCall(GormStaticApi.groovy:384)
      at sun.reflect.GeneratedMethodAccessor633.invoke(Unknown Source)
      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
      at java.lang.reflect.Method.invoke(Method.java:498)
      at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
      at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
      at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
      at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1022)
      at groovy.lang.Closure.call(Closure.java:414)
      at org.codehaus.groovy.runtime.ConvertedClosure.invokeCustom(ConvertedClosure.java:54)
      at org.codehaus.groovy.runtime.ConversionHandler.invoke(ConversionHandler.java:124)
      at com.sun.proxy.$Proxy111.doInSession(Unknown Source)
      at org.grails.datastore.mapping.core.DatastoreUtils.execute(DatastoreUtils.java:319)
...

此时,应考虑将这个闭包放到一个函数中,每次调用的时候返回一个新的闭包对象,这样就可以避免这个问题,比如:

Closure criteria() {
   { Map params = [:] ->
       if (params.dateCreated) {
           gt('dateCreated', params.dateCreated)
       }
       if (params.status) {
           eq('status', params.status)
       }
   }
}

说了这么多Criteria的优点,也说说它的限制吧:

  • 查询的结果最终要绑定到Domain实例,或者返回Domain实例中的部分field或部分filed的聚合结果

所以不支持join结果。也就是说你需要SELECT domain1.c1, domain2.c2 FROM domain1 LEFT JOIN domain2 ON condtions这样的结果集的时候,是不能用Criteria实现的。

  • 跨Domain的查询条件必须是Domain的外键

比如定义了UserUserRole两个Domain:

class User {
}

class UserRole {
    User user
    Role role
}

想查询角色下包含了哪些用户(一对多),如果用Criteria,只能这么查:

UserRole.withCriteria {
    role {
        eq('name', roleName)
    }
    projections {
        property('user')
    }
}

想用User去join UserRole是不可以的:

User.withCriteria {
    join('UserRole')  // invalid
}

因此在join上会有不小的限制,而HQL中就自由的多。

Detached Criteria

Detached Criteria的作用是仅构建查询条件,不执行查询,仅当调用了执行查询语句的方法(list, count, exists等等),才会真正执行查询。当一个查询条件可能要经过多道手续才能最终确认的时候,这个特性就比较有用了。另外在find and update这种情况下也比较有用:

def criteria = new DetachedCriteria(Person).build {
    eq 'lastName', 'Simpson'
}
def bartQuery = criteria.build {
    eq 'firstName', 'Bart'
}

def results = bartQuery.list(max:4, sort:"firstName")

这里的DSL和Criteria的DSL是完全一样的,此外,还多了deleteAllupdateAll(Map)这样的方法。

where clause

where clause本质上返回的是一个Detached Criteria,所以理论上具有Detached Criteria的功能与限制,但是并不支持eager fetches这样的特性。本质上来看where clause查询可以看做是一个简化版本的Detached Criteria。

def query = Pet.where {
    year(birthDate) == 2011
}

和Criteria一样,在闭包中定义查询条件,但是where clause可以使用groovy的条件判断语法,而不是Criteria DSL的条件判断函数。比如下面这样的例子:

def results = Person.where {
    age > where { age > 18 }.avg('age')
}

很明显groovy的条件判断语句更灵活,不再局限于Criteria的DSL。最后必须提醒一下,where(Closure)本质上返回的是Detached Criteria,因此必须调用Detached Criteria的查询方法才会真正执行查询,如:

result.list()

HQL

用过Hibernate的用户对这个东西应该都不陌生,融合了SQL和Java对象的特性,并且可以支持join结果:

User.executeQuery('''
   FROM User user
   LEFT JOIN LoginHistory history
   ON history.user.id = user.id
   WHERE history.dateCreated >= LocalDateTime.now().minusDays(1)
''')

比如上面的结果可以返回[[User, LoginHistory], [User, LoginHistory], [],...[]]这样的join之后的结果,甚至这两个Domain可以没有直接的外键关联,因此需要复杂join的需求的时候,HQL会比Criteria方便的多,并且性能上也会比Criteria占优势。不过HQL对于查询条件不固定的需求就不那么友好了,同样需要很麻烦的拼接String。还需要注意的HQL支持的SQL标准很少,是所有SQL的子集,比如不支持postgresql的json query,不支持sql 99标准的CTE等等。

HQL的返回结果就不要求一定绑定Domain了,因此跨Domain的查询会比Criteria更灵活。

实际项目开发中,JOIN的操作应尽可能减少,防止未来出现大表JOIN这种严重拖慢性能的隐患。

SQL

这个其实最没啥要说的,熟悉RDB的用户对这玩意应该都会用,但是GORM的文档和Grails官方文档对怎么使用SQL却提之甚少。这里简单说一下怎么用SQL:

Book.withSession { Session session ->
    session.clear()
}

从任意一个Domain中通过withSession方法拿到Hibernate Session对象,然后通过Session的api就可以执行SQL了.

User.withSession { Session session ->
    session.createSQLQuery('SELECT * FROM user WHERE id = :id')
        .setParameter('id', userId)
        .addEntity(User)
}

我们只是用User中拿过来了Hibernate Session,这玩意非常底层了,并不会帮我们自动做ORM绑定,因此如果希望绑定到Domain中,必须加上.addEntity(Domain)方式进行绑定,当然也可以最后调用.list()让闭包直接返回List<ResultSet>结果。

使用SQL就可以使用数据库的全套特性了,是最自由的。同样这也将跟数据库耦合更紧密,你可能不能再切换底层数据库了。当需要数据库独有特性的时候,只能通过SQL解决了。比如写一个超长的SQL查询,使用CTE等高级特性。

不管怎样,在实际开发中还是应该尽可能避免直接使用这么底层的玩意,也应该尽可能减少直接使用SQL。

总结

本篇内容我们对Grails的GORM查询方式做了小结,对比了下各种查询方式的优缺点,实际项目中需要根据实际场景选择合适的使用方式。

虽然在ORM框架中应该尽可能避免使用底层的SQL,因为这会在一定程度上破坏框架的封装性,并且使用不当也会有SQL注入的风险。但是作为开发者,实际最应该熟练掌握的反而是最底层的SQL。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容

  • 本文包括:1、Hibernate 的查询方式2、HQL (Hibernate Query Language) 查询...
    廖少少阅读 2,649评论 0 15
  • 一、Hibernate 检索 hibernate 提供5种检索数据的方式导航对象图检索方式: 根据已经加载的对象...
    luweicheng24阅读 376评论 0 0
  • 这部分主要是开源Java EE框架方面的内容,包括Hibernate、MyBatis、Spring、Spring ...
    杂货铺老板阅读 1,343评论 0 2
  • 不折腾 人生在世,一切顺其自然。如果为了刻意追求长寿、健康,就去各种养生、食补,比如你非常不喜欢吃枸杞,偏偏要泡枸...
    仙人掌_娟阅读 309评论 0 0
  • 闹钟响,起床,小儿意外醒了。我忙着穿衣梳洗,他抱着爸爸絮絮叨叨:“天黑黑的怎么也要上班啊?妈妈辛苦了!”“爸爸的爸...
    四字真言阅读 615评论 3 4