GORM绝对是Grails框架的一大亮点。GORM基于Hibernate的ORM之上作二次封装,既有Hibernate强大的功能,又有使用简便的特色。本篇主要总结和类比在项目开发中用到的GORM查询方式。html
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
相信不少人第一眼对GORM的这个特性所惊艳到,能够用一种相似于静态方法的调用方式,就能够从数据库查询结果。它的优势和缺点同样明显,使用极为简单,功能也极为简陋,基本只能实现相似于:github
SELECT * FROM domain WHERE condtions LIMIT x OFFSET y
这种方式的查询。只适合于单一Domain,查询条件固定的场景,也没有太多要说的,很是容易使用。sql
Book.findByName('bookName') User.findAllByEnabled(true) Persion.findAllByAgeBetween(10, 20)
这个功能能够说是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,可是dateCreated
和status
都是可选的查询条件,可能有也可能没有,经过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
,而且传递有max
和offset
参数的话,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的优势,也说说它的限制吧:
因此不支持join结果。也就是说你须要SELECT domain1.c1, domain2.c2 FROM domain1 LEFT JOIN domain2 ON condtions
这样的结果集的时候,是不能用Criteria实现的。
好比定义了User
和UserRole
两个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的做用是仅构建查询条件,不执行查询,仅当调用了执行查询语句的方法(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是彻底同样的,此外,还多了deleteAll
和updateAll(Map)
这样的方法。
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()
用过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这种严重拖慢性能的隐患。
这个其实最没啥要说的,熟悉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。