Grails GORM查詢總結

GORM絕對是Grails框架的一大亮點。GORM基於Hibernate的ORM之上作二次封裝,既有Hibernate強大的功能,又有使用簡便的特色。本篇主要總結和類比在項目開發中用到的GORM查詢方式。html

GORM支持的查詢方式

GORM底層使用Hibernate,因此支持Hibernate的全部查詢方式,此外,還支持Dynamic finder這種簡便的查詢。先對GORM支持的查詢方式作一個表(參考來源: http://tatiyants.com/how-and-...:java

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參考官方文檔。這裏對這些查詢方式簡單作個總結git

Dynamic finder

相信不少人第一眼對GORM的這個特性所驚豔到,能夠用一種相似於靜態方法的調用方式,就能夠從數據庫查詢結果。它的優勢和缺點同樣明顯,使用極爲簡單,功能也極爲簡陋,基本只能實現相似於:github

SELECT * FROM domain WHERE condtions LIMIT x OFFSET y

這種方式的查詢。只適合於單一Domain,查詢條件固定的場景,也沒有太多要說的,很是容易使用。sql

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()的方法,好比:json

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...

表明要查詢User這個Domain,可是dateCreatedstatus都是可選的查詢條件,可能有也可能沒有,經過Criteria的DSL能夠很方便的定義這些查詢條件,若是用HQL或者SQL寫的話,是沒有那麼方便的,須要寫判斷條件,而後根據條件拼接對應的where語句,會讓代碼很冗長,並且四處拼接字符串也會讓代碼很難懂,一眼看不出來產生的SQL長什麼樣子。api

此外,還能夠加入projections,這樣返回的就不是整個Domain對象了,而是Domain中指定的field或者field的聚合。至關於本來SQL中的SELECT * FROM domain變成了SELECT column1, count(column2), sum(column3),..., FROM domain。好比我要查詢符合條件的用戶數量以及平均年齡,能夠這麼寫:session

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。

相關文章
相關標籤/搜索