Grails 对象关联映射 (GORM) 一
转自:http://justjavac.iteye.com/blog/701445
Domain 类是任何商业应用的核心。 他们保存事务处理的状态,也处理预期的行为。 他们通过关联联系在一起, one-to-one 或 one-to-many。
GORM 是 Grails对象关联映射 (GORM)的实现。在底层,它使用 Hibernate 3 (一个非常流行和灵活的开源ORM解决方案),但是因为Groovy天生的动态性,实际上,对动态类型和静态类型两者都支持,由于Grails的规约,只需要很少的配置涉及Grails domain 类的创建。
你同样可以在Java中编写 Grails domain 类。 请参阅在 Hibernate 集成上如果在Java中编写 Grails domain 类, 不过,它仍然使用动态持久方法。下面是GORM实战预览:
def book = Book.findByTitle("Groovy in Action")
book .addToAuthors(name:"Dierk Koenig") .addToAuthors(name:"Guillaume LaForge") .save()
5.1 快速入门指南
domain类可以使用 create-domain-class 命令来创建:
grails create-domain-class Person
这将在 grails-app/domain/Person.groovy
位置上创建类,如下:
class Person { }
如果在 DataSource 上设置dbCreate
属性为"update", "create" or "create-drop", Grails 会为你自动生成/修改数据表格。
你可以通过添加属性来自定义类:
class Person { String name Integer age Date lastVisit }
一旦你拥有一个 domain 类,可以尝试通过在 shell 或 console 上输入:
grails console
这会载入一个交互式GUI,便于你键入Groovy命令。
5.1.1 CRUD基础
尝试执行一些基础的 CRUD (Create/Read/Update/Delete) 操作。
Create
为了创建一个 domain 类,可以使用 Groovy new操作符, 设置它的属性并调用 save:
def p = new Person(name:"Fred", age:40, lastVisit:new Date()) p.save()
save 方法将使用底层的Hibernate ORM持久你的类到数据库中。
Read
Grails 会为你的domain类显式的添加一个隐式 id
属性,便于你检索:
def p = Person.get(1) assert 1 == p.id
get 方法通过你指定的数据库标识符,从db中读取 Person
对象。 你同样可以使用 read 方法加载一个只读状态对象:
def p = Person.read(1)
在这种情况下,底层的 Hibernate 引擎不会进行任何脏读检查,对象也不能被持久化。注意,假如你显式的调用save 方法,对象会回到 read-write 状态.
Update
更新一个实体, 设置一些属性,然后,只需再次调用 save:
def p = Person.get(1)
p.name = "Bob"
p.save()
Delete
删除一个实体使用 delete 方法:
def p = Person.get(1) p.delete()
5.2 GORM中进行Domain建模
当构建 Grails应用程序时,你必须考虑你要试图解决的问题域。 比如,你正在构建一个 Amazon 书店,你要考虑 books, authors, customers 和publishers 等等.
这些在GORM中被当做Groovy类 来进行建模,因此, Book
类可能拥有 title, release date,ISBN等等。 在后面章节将展示如何在GORM中进行domain建模。
创建domain类,你可以运行 create-domain-class ,如下:
grails create-domain-class Book
将会创建 grails-app/domain/Book.groovy
类:
class Book { }
如果你想使用 packages 你可以把 Book.groovy类移动到 domain 目录的子目录下,并按照Groovy (和 Java)的 packaging 规则添加正确的 package
。
上面的类将会自动映射到数据库中名为 book
的表格 (与类名相同). 可以通过 ORM Domain Specific Language定制上面的行为。
现在,你可以把这个domain类的属性定义成Java类型。 例如:
class Book { String title Date releaseDate String ISBN }
每个属性都会被映射到数据库的列,列名的规则是所有列名小写,通过下划线分隔。 比如 releaseDate
映射到release_date
列。 SQL类型会自动检测来自Java的类型 , 但可以通过 Constraints 或 ORM DSL定制。
5.2.1 GORM中的关联
关联定义了domain类之间的相互作用。除非在两端明确的指定,否则关联只存在被定义的一方。
5.2.1.1 One-to-one
one-to-one 关联是最简单的种类,它只是把它的一个属性的类型定义为其他domain类。 考虑下面的例子:
Example A
class Face { Nose nose } class Nose { }
在这种情况下, 拥有一个Face
到 Nose
的one-to-one单向关联。为了使它双向关联,需要定义另一端,如下:
Example B
class Face { Nose nose } class Nose { Face face }
这就是双向关联。不过, 在这种情况下,关联的双方并不能级联更新。
考虑下这样的变化:
Example C
class Face {
Nose nose
}
class Nose {
static belongsTo = [face:Face]
}
在这种情况下,我们使用 belongsTo
来设置Nose
"属于" Face。结果是,我们创建一个Face并save 它,数据库将 级联 更新/插入 Nose
:
new Face(nose:new Nose()).save()
上面的示例,face 和 nose都会被保存。注意,逆向 不为 true,并会因为一个临时的Face
导致一个错误:
new Nose(face:new Face()).save() // will cause an error
belongsTo
另一个重要的意义在于,假如你删除一个 Face
实体, Nose
也会被删除:
def f = Face.get(1) f.delete() // both Face and Nose deleted
如果没有belongsTo
,deletes 将不被级联,并会得到一个外键约束错误,除非你明确的删除Nose:
// error here without belongsTo def f = Face.get(1) f.delete()
// no error as we explicitly delete both def f = Face.get(1) f.nose.delete() f.delete()
你可以保持上面的关联为单向,为了保证级联保存/更新,可以像下面这样:
class Face {
Nose nose
}
class Nose {
static belongsTo = Face
}
注意,在这种情况下,我们没有在belongsTo
使用map语法声明和明确命名关联。Grails 会把它当做单向。.下面的图表概述了3个示例:
5.2.1.2 One-to-many
one-to-many 关联是,当你的一个类,比如 Author
,拥有许多其他类的实体,比如 Book
。 在Grails 中定义这样的关联可以使用 hasMany
:
class Author {
static hasMany = [ books : Book ]
String name } class Book { String title }
在这种情况下,拥有一个单向的one-to-many关联。 Grails 将默认使用一个连接表映射这样的关联。
ORM DSL 允许使用外键关联作为映射单向关联的替代
对于 hasMany
设置,Grails将自动注入一个java.util.Set
类型的属性到domain类。用于迭代集合:
def a = Author.get(1)
a.books.each { println it.title }
Grails中默认使用的fetch策略是 "lazy", 意思就是集合将被延迟初始化。 如果你不小心,这会导致 n+1 问题 。
默认的级联行为是级联保存和更新,但不删除,除非 belongsTo
被指定:
class Author {
static hasMany = [ books : Book ]
String name } class Book { static belongsTo = [author:Author] String title }
如果在one-to-many的多方拥有2个同类型的属性,必须使用mappedBy
指定哪个集合被映射:
class Airport { static hasMany = [flights:Flight] static mappedBy = [flights:"departureAirport"] } class Flight { Airport departureAirport Airport destinationAirport }
如果多方拥有多个集合被映射到不同的属性,也是一样的:
class Airport { static hasMany = [outboundFlights:Flight, inboundFlights:Flight] static mappedBy = [outboundFlights:"departureAirport", inboundFlights:"destinationAirport"] } class Flight { Airport departureAirport Airport destinationAirport }
5.2.1.3 Many-to-many
Grails支持many-to-many关联,通过在关联双方定义 hasMany
,并在关联拥有方定义 belongsTo
:
class Book { static belongsTo = Author static hasMany = [authors:Author] String title } class Author { static hasMany = [books:Book] String name }
Grails在数据库层使用一个连接表来映射many-to-many,在这种情况下,Author
负责持久化关联,并且是唯一可以级联保存另一端的一方 。
例如,下面这个可以进行正常级联保存工作:
new Author(name:"Stephen King") .addToBooks(new Book(title:"The Stand")) .addToBooks(new Book(title:"The Shining")) .save()
而下面这个只保存 Book
而不保存 authors!
new Book(name:"Groovy in Action") .addToAuthors(new Author(name:"Dierk Koenig")) .addToAuthors(new Author(name:"Guillaume Laforge")) .save()
这是所期待的行为,就像Hibernate,只有many-to-many的一方可以负责管理关联。
当前,Grails的Scaffolding 特性不支持many-to-many关联, 你必须自己编写关联的管理代码
5.2.1.4 集合类型基础
除了关联不同 domain 类外, GORM 同样支持映射基本的集合类型。比如,下面的类创建一个 nicknames
关联, 它是一个 String
的 Set
实体:
class Person { static hasMany = [nicknames:String] }
GORM 将使用一个链接表,来映射上面的关联。你可以使用joinTable
参数来改变各式各样的连接表映射:
class Person { static hasMany = [nicknames:String]
static mapping = { hasMany joinTable:[name:'bunch_o_nicknames', key:'person_id', column:'nickname', type:"text"] } }
上面的示例映射到表后看上去像这样:
bunch_o_nicknames Table
--------------------------------------------- | person_id | nickname | --------------------------------------------- | 1 | Fred | ---------------------------------------------
5.2.2 GORM中的组合
除了 association 之外, Grails 支持组合概念。在这种情况下,并不是把类映射到分离的表格,而是将这个类"embedded"到当前的表格内。 例如:
class Person { Address homeAddress Address workAddress static embedded = ['homeAddress', 'workAddress'] } class Address { String number String code }
所产生的映射看上去像这样:
如果你在grails-app/domain
目录中定义了一个单独的Address
类,你同样会得到一个表格。如果你不想这样,你可以 利用Groovy在单个文件定义多个类的能力,让grails-app/domain/Person.groovy
文件中的Person
类包含Address
类。
5.2.3 GORM中的继承
GORM 支持从抽象类的继承和具体持久化GORM实体的继承。例如:
class Content { String author } class BlogEntry extends Content { URL url } class Book extends Content { String ISBN } class PodCast extends Content { byte[] audioStream }
上面的示例,我们拥有一个 Content
父类和各式各样带有更多指定行为的子类。
注意事项
在数据库层, Grails默认使用一个类一个表格的映射附带一个名为class
的识别列,因此,父类 (Content
) 和它的子类(BlogEntry
, Book
等等.), 共享 相同的表格。
一个类一个表格的映射有个负面的影响,就是你 不能 有非空属性一起继承映射。 另一个选择是使用每个子类一个表格 ,你可以通过 ORM DSL启用。
不过, 过分使用继承与每个子类一个表格会带来糟糕的查询性能,因为,过分使用链接查询。总之,我们建议:假如你打算使用继承,不要滥用它,不要让你的继承层次太深。
多态性查询
继承的结果是你有能力进行多态查询。比如,在Content
使用 list 方法,超类将返回所有Content
子类:
def content = Content.list() // list all blog entries, books and pod casts content = Content.findAllByAuthor('Joe Bloggs') // find all by author
def podCasts = PodCast.list() // list only pod casts
5.2.4 Sets, Lists 和 Maps
Sets对象
默认情况下,在中 GORM定义一个 java.util.Set
映射,它是无序集合,不能包含重复元素。 换句话,当你有:
class Author {
static hasMany = [books:Book]
}
GORM会将books注入为 java.util.Set
类型。问题在于存取时,这个集合的无序的,可能不是你想要的。为了定制序列,你可以设置为 SortedSet
:
class Author {
SortedSet books
static hasMany = [books:Book]
}
在这种情况下,需要实现 java.util.SortedSet
,这意味着,你的Book类必须实现 java.lang.Comparable
:
class Book implements Comparable { String title Date releaseDate = new Date()
int compareTo(obj) { releaseDate.compareTo(obj.releaseDate) } }
上面的结果是,Author类的中的books集合将按Book的releasedate排序。
List对象
如果你只是想保持对象的顺序,添加它们和引用它们通过索引,就像array一样,你可以定义你的集合类型为List
:
class Author {
List books
static hasMany = [books:Book]
}
在这种情况下当你向books集合中添加一个新元素时,这个顺序将会保存在一个从0开始的列表索引中,因此你可以:
author.books[0] // get the first book
这种方法在数据库层的工作原理是:为了在数据库层保存这个顺序,Hibernate创建一个叫做books_idx
的列,它保存着该元素在集合中的索引.
当使用List
时,元素在保存之前必须先添加到集合中,否则Hibernate会抛出异常 (org.hibernate.HibernateException
: null index column for collection):
// This won't work!
def book = new Book(title: 'The Shining')
book.save()
author.addToBooks(book)
// Do it this way instead. def book = new Book(title: 'Misery') author.addToBooks(book) author.save()
映射(Maps)对象
如果你想要一个简单的 string/value 对map,GROM可以用下面方法来映射:
class Author { Map books // map of ISBN:book names }
def a = new Author() a.books = ["1590597583":"Grails Book"] a.save()
这种情况map的键和值都必须是字符串.
如果你想用一个对象的map,那么你可以这样做:
class Book {
Map authors
static hasMany = [authors:Author]
}
def a = new Author(name:"Stephen King")
def book = new Book() book.authors = [stephen:a] book.save()
static hasMany
属性定义了map中元素的类型,map中的key 必须 是字符串.
集合类型和性能
Java中的 Set
是一个不能有重复条目的集合类型. 为了确保添加到 Set
关联中的条目是唯一的,Hibernate 首先加载数据库中的全部关联. 如果你在关联中有大量的条目,那么这对性能来说是一个巨大的浪费.
这样做就需要 List
类型, 因为Hibernate需要加载全部关联以维持供应. 因此如果你希望大量的记录关联,那么你可以制作一个双向关联以便连接能在反面被建立。例如思考一下代码:
def book = new Book(title:"New Grails Book") def author = Author.get(1) book.author = author book.save()
在这个例子中关联链接被child (Book)创建,因此没有必要手动操作集合以使查询更少和高效代码。由于Author
有大量的关联的Book
实例,如果你写入像下面的代码,你可以看到性能的影响:
def book = new Book(title:"New Grails Book") def author = Author.get(1) author.addToBooks(book) author.save()
5.3 持久化基础
关于Grails要记住的很重要的一点就是,Grails的底层使用 Hibernate 来进行持久化. 如果您以前使用的是ActiveRecord 或者 iBatis 您可能会对Hibernate的"session"模型感到有点陌生.
本质上,Grails自动绑定Hibernate session到当前正在执行的请求上.这允许你像使用GORM的其他方法一样很自然地使用 save 和 delete 方法.
5.3.1 保存和更新
下面看一个使用 save 方法的例子:
def p = Person.get(1) p.save()
一个主要的不同是当你调用save的时候Hibernate不会执行任何SQL操作. Hibernate通常将SQL语句分批,最后执行他们.对你来说,这些一般都是由Grails自动完成的,它管理着你的Hibernate session.
也有一些特殊情况,有时候你可能想自己控制那些语句什么时候被执行,或者用Hibernate的术语来说,就是什么时候session被"flushed".要这样的话,你可以对save方法使用flush参数:
def p = Person.get(1)
p.save(flush:true)
请注意,在这种情况下,所有暂存的SQL语句包括以往的保存将同步到数据库。这也可以让您捕捉任何被抛出的异常,这在涉及乐观锁高度并发的情况下是很常用的:
def p = Person.get(1) try { p.save(flush:true) } catch(Exception e) { // deal with exception }
5.3.2 删除对象
下面是 delete 方法的一个例子:
def p = Person.get(1) p.delete()
默认情况下在执行delete以后Grails将使用事务写入, 如果你想在适当的时候删除,这时你可以使用flush
参数:
def p = Person.get(1)
p.delete(flush:true)
使用 flush
参数也允许您捕获在delete执行过程中抛出的任何异常. 一个普遍的错误就是违犯数据库的约束, 尽管这通常归结为一个编程或配置错误. 下面的例子显示了当您违犯了数据库约束时如何捕捉DataIntegrityViolationException
:
def p = Person.get(1)
try { p.delete(flush:true) } catch(org.springframework.dao.DataIntegrityViolationException e) { flash.message = "Could not delete person ${p.name}" redirect(action:"show", id:p.id) }
注意Grails没有提供 deleteAll
方法,因为删除数据是discouraged的,而且通常可以通过布尔标记/逻辑来避免.
如果你确实需要批量删除数据,你可以使用 executeUpdate 法来执行批量的DML语句:
Customer.executeUpdate("delete Customer c where c.name = :oldName", [oldName:"Fred"])
5.3.3 级联更新和删除
在使用GORM时,理解如何级联更新和删除是很重要的.需要记住的关键是 belongsTo
的设置控制着哪个类"拥有"这个关联.
无论是一对一,一对多还是多对多,如果你定义了 belongsTo
,更新和删除将会从拥有类到被它拥有的类(关联的另一方)级联操作.
如果你 没有 定义 belongsTo
那么就不能级联操作,你将不得不手动保存每个对象.
下面是一个例子:
class Airport { String name static hasMany = [flights:Flight] } class Flight { String number static belongsTo = [airport:Airport] }
如果我现在创建一个 Airport
对象,并向它添加一些 Flight
它可以保存这个 Airport
并级联保存每个flight,因此会保存整个对象图:
new Airport(name:"Gatwick") .addToFlights(new Flight(number:"BA3430")) .addToFlights(new Flight(number:"EZ0938")) .save()
相反的,如果稍后我删除了这个 Airport
所有跟它关联的 Flight
也都将会被删除:
def airport = Airport.findByName("Gatwick")
airport.delete()
然而,如果我将 belongsTo
去掉的话,上面的级联删除代码就了. 不能工作. 为了更好地理解, take a look at the summaries below that describe the default behaviour of GORM with regards to specific associations.
设置了belongsTo的双向一对多
class A { static hasMany = [bees:B] } class B { static belongsTo = [a:A] }
如果是双向一对多,在多的一端设置了belongsTo
,那么级联策略将设置一的一端为"ALL",多的一端为"NONE".
单向一对多
class A { static hasMany = [bees:B] }
class B { }
如果是在多的一端没有设置belongsTo
单向一对多关联,那么级联策略设置将为"SAVE-UPDATE".
没有设置belongsTo的双向一对多
class A { static hasMany = [bees:B] }
class B { A a }
如果是在多的一端没有设置belongsTo
的双向一对多关联,那么级联策略将为一的一端设置为"SAVE-UPDATE" 为多的一端设置为"NONE".
设置了belongsTo的单向一对一
class A { }
class B { static belongsTo = [a:A] }
如果是设置了belongsTo
的单向一对一关联,那么级联策略将为有关联的一端(A->B)设置为"ALL",定义了belongsTo
的一端(B->A)设置为"NONE".
请注意,如果您需要进一步的控制级联的行为,您可以参见 ORM DSL.
5.3.4 立即加载和延迟加载
在GORM中,关联默认是lazy的.最好的解释是例子:
class Airport { String name static hasMany = [flights:Flight] } class Flight { String number static belongsTo = [airport:Airport] }
上面的domain类和下面的代码:
def airport = Airport.findByName("Gatwick")
airport.flights.each {
println it.name
}
GORM GORM将会执行一个单独的SQL查询来抓取 Airport
实例,然后再用一个额外的for each查询逐条迭代flights
关联.换句话说,你得到了N+1条查询.
根据这个集合的使用频率,有时候这可能是最佳方案.因为你可以指定只有在特定的情况下才访问这个关联的逻辑.
配置立即加载
一个可选的方案是使用立即抓取,它可以按照下面的方法来指定:
class Airport { String name static hasMany = [flights:Flight] static mapping = { flight fetch:"join" } }
在这种情况下 Airport
实例对应的 flights
关联会被一次性全部加载进来(依赖于映射). 这样的好处是执行更少的查询,但是要小心使用,因为使用太多的eager关联可能会导致你将整个数据库加载进内存.
关联也可以用 ORM DSL 将关联声明为 non-lazy
使用批量加载Using Batch Fetching
虽然立即加载适合某些情况,它并不总是可取的,如果您所有操作都使用立即加载,那么您会将整个数据库加载到内存中,导致性能和内存的问题.替代立即加载是使用批量加载.实际上,您可以在"batches"中配置Hibernate延迟加载. 例如:
class Airport { String name static hasMany = [flights:Flight] static mapping = { flight batchSize:10 } }
在这种情况下,由于 batchSize
参数,当您迭代 flights
关联, Hibernate 加载10个批次的结果. 例如,如果您一个Airport
有30个s, 如果您没有配置批量加载,那么您在对Airport
的查询中只能一次查询出一个结果,那么要执行30
次查询以加载每个flight. 使用批量加载,您对Airport
查询一次将查询出10个Flight
,那么您只需查询3次. 换句话说, 批量加载是延迟加载策略的优化. 批量加载也可以配置在class级别:
class Flight {
…
static mapping = {
batchSize 10
}
}
5.3.5 悲观锁和乐观锁
乐观锁
默认的GORM类被配置为乐观锁。乐观锁实质上是Hibernate的一个特性,它在数据库里一个特别的 version 字段中保存了一个版本号.
version
列读取包含当前你所访问的持久化实例的版本状态的 version
属性:
def airport = Airport.get(10)
println airport.version
当你执行更新操作时,Hibernate将自动检查version属性和数据库中version列,如果他们不同,将会抛出一个StaleObjectException 异常,并且当前事物也会被回滚.
这是很有用的,因为它允许你不使用悲观锁(有一些性能上的损失)就可以获得一定的原子性。由此带来的负面影响是,如果你有一些高并发的写操作的话,你必须处理这个异常。这需要刷出(flushing)当前的session:
def airport = Airport.get(10)
try { airport.name = "Heathrow" airport.save(flush:true) } catch(org.springframework.dao.OptimisticLockingFailureException e) { // deal with exception }
你处理异常的方法取决于你的应用. 你可以尝试合并数据,或者返回给用户并让他们来处理冲突.
作为选择,如果它成了问题,你可以求助于悲观锁.
悲观锁
悲观锁等价于执行一个 SQL "SELECT * FOR UPDATE" 语句并锁定数据库中的一行. 这意味着其他的读操作将会被锁定直到这个锁放开.
在Grails中悲观锁通过 lock 方法执行:
def airport = Airport.get(10) airport.lock() // lock for update airport.name = "Heathrow" airport.save()
一旦当前事物被提交,Grails会自动的为你释放锁. 可是,在上述情况下我们做的事情是从正规的SELECT“升级”到SELECT ..FOR UPDATE同时其它线程也会在调用get()和lock()之间更新记录。
为了避免这个问题,你可以使用静态的lock 方法,就像get方法一样传入一个id:
def airport = Airport.lock(10) // lock for update airport.name = "Heathrow" airport.save()
这个只有 SELECT..FOR UPDATE 时候可以使用.
尽管Grails和Hibernate支持悲观所,但是在使用Grails内置默认的 HSQLDB 数据库时不支持。如果你想测试悲观锁,你需要一个支持悲观锁的数据库,例如MySQL.
你也可以使用lock 方法在查询中获得悲观锁。例如使用动态查询:
def airport = Airport.findByName("Heathrow", [lock:true])
或者使用criteria:
def airport = Airport.createCriteria().get {
eq('name', 'Heathrow')
lock true
}
5.4 GORM查询
GORM提供了从动态查询器到criteria到Hibernate面向对象查询语言HQL的一系列查询方式.
Groovy通过 GPath 操纵集合的能力, 和GORM的像sort,findAll等方法结合起来,形成了一个强大的组合.
但是,让我们从基础开始吧.
获取实例列表
如果你简单的需要获得给定类的所有实例,你可以使用 list 方法:
def books = Book.list()
list 方法支持分页参数:
def books = Book.list(offset:10, max:20)
也可以排序:
def books = Book.list(sort:"title", order:"asc")
这里,Here, the sort
参数是您想要查询的domain类中属性的名字,argument is the name of the domain class property that you wish to sort on, and the order
参数要么以argument is either asc
for asc结束ending or要么以desc
for desc结束ending.
根据数据库标识符取回
第二个取回的基本形式是根据数据库标识符取回,使用 get 方法:
def book = Book.get(23)
你也可以根据一个标识符的集合使用 getAll方法取得一个实例列表:
def books = Book.getAll(23, 93, 81)
5.4.1 动态查询器
GORM支持 动态查找器 的概念 . 动态查找器看起来像一个静态方法的调用,但是这些方法本身在代码中实际上并不存在.
而是在运行时基于一个给定类的属性,自动生成一个方法. 比如例子中的 Book
类:
class Book { String title Date releaseDate Author author } class Author { String name }
Book
类有一些属性,比如 title
, releaseDate
和 author
. 这些都可以按照"方法表达式"的格式被用于 findBy和 findAllBy 方法:
def book = Book.findByTitle("The Stand")
book = Book.findByTitleLike("Harry Pot%")
book = Book.findByReleaseDateBetween( firstDate, secondDate )
book = Book.findByReleaseDateGreaterThan( someDate )
book = Book.findByTitleLikeOrReleaseDateLessThan( "%Something%", someDate )
方法表达式
在GORM中一个方法表达式由前缀,比如 findBy 后面跟一个表达式组成,这个表达式由一个或多个属性组成。基本形式是:
Book.findBy([Property][Comparator][Boolean Operator])?[Property][Comparator]
用'?' 标记的部分是可选的. 每个后缀都会改变查询的性质。例如:
def book = Book.findByTitle("The Stand")
book = Book.findByTitleLike("Harry Pot%")
在上面的例子中,第一个查询等价于等于后面的值, 第二个因为增加了 Like
后缀, 它等价于SQL的 like
表达式.
可用的后缀包括:
InList
- list中给定的值LessThan
- 小于给定值LessThanEquals
- 小于或等于给定值GreaterThan
- 大于给定值GreaterThanEquals
- 大于或等于给定值Like
- 价于 SQL like 表达式Ilike
- 类似于Like
,但不是大小写敏感NotEqual
- 不等于Between
- 于两个值之间 (需要两个参数)IsNotNull
- 不为null的值 (不需要参数)IsNull
- 为null的值 (不需要参数)
你会发现最后三个方法标注了参数的个数,他们的示例如下:
def now = new Date()
def lastWeek = now - 7
def book = Book.findByReleaseDateBetween( lastWeek, now )
books = Book.findAllByReleaseDateIsNull() books = Book.findAllByReleaseDateIsNotNull()
布尔逻辑(AND/OR)
方法表达式也可以使用一个布尔操作符来组合两个criteria:
def books = Book.findAllByTitleLikeAndReleaseDateGreaterThan("%Java%", new Date()-30)
在这里我们在查询中间使用 And
来确保两个条件都满足, 但是同样地你也可以使用 Or
:
def books = Book.findAllByTitleLikeOrReleaseDateGreaterThan("%Java%", new Date()-30)
At the moment此时, 你最多只能用两个criteria做动态查询, 也就是说,该方法的名称只能含有一个布尔操作符. 如果你需要使用更多的, 你应该考虑使用 Criteria 或 HQL.
查询关联
关联也可以被用在查询中:
def author = Author.findByName("Stephen King")
def books = author ? Book.findAllByAuthor(author) : []
在这里如果 Author
实例不为null 我们在查询中用它取得给定 Author
的所有Book
实例.
分页和排序
跟 list 方法上可用的分页和排序参数一样,他们同样可以被提供为一个map用于动态查询器的最后一个参数:
def books = Book.findAllByTitleLike("Harry Pot%", [max:3, offset:2, sort:"title", order:"desc"])
5.4.2 条件查询
Criteria 是一种类型安全的、高级的查询方法,它使用Groovy builder构造强大复杂的查询.它是一种比使用StringBuffer好得多的选择.
Criteria可以通过 createCriteria 或者 withCriteria 方法来使用. builder使用Hibernate的Criteria API, builder上的节点对应Hibernate Criteria API中 Restrictions 类中的静态方法. 用法示例:
def c = Account.createCriteria() def results = c { like("holderFirstName", "Fred%") and { between("balance", 500, 1000) eq("branch", "London") } maxResults(10) order("holderLastName", "desc") }
逻辑与(Conjunctions)和逻辑或(Disjunctions)
如前面例子所演示的,你可以用 and { }
块来分组criteria到一个逻辑AND:
and { between("balance", 500, 1000) eq("branch", "London") }
逻辑OR也可以这么做:
or { between("balance", 500, 1000) eq("branch", "London") }
你也可以用逻辑NOT来否定:
not { between("balance", 500, 1000) eq("branch", "London") }
查询关联
关联可以通过使用一个跟关联属性同名的节点来查询. 比如我们说 Account
类有关联到多个 Transaction
对象:
class Account { … def hasMany = [transactions:Transaction] Set transactions … }
我们可以使用属性名 transaction
作为builder的一个节点来查询这个关联:
def c = Account.createCriteria()
def now = new Date()
def results = c.list {
transactions {
between('date',now-10, now)
}
}
上面的代码将会查找所有过去10天内执行过 transactions
的 Account
实例. 你也可以在逻辑块中嵌套关联查询:
def c = Account.createCriteria()
def now = new Date()
def results = c.list {
or {
between('created',now-10,now)
transactions {
between('date',now-10, now)
}
}
}
这里,我们将找出在最近10天内进行过交易或者最近10天内新创建的所有用户.
投影(Projections)查询
投影被用于定制查询结果. 要使用投影你需要在criteria builder树里定义一个"projections"节点. projections节点内可用的方法等同于 Hibernate 的 Projections 类中的方法:
def c = Account.createCriteria()
def numberOfBranches = c.get { projections { countDistinct('branch') } }
使用可滚动的结果
Y你可以通过调用scroll方法来使用Hibernate的 ScrollableResults 特性:
def results = crit.scroll { maxResults(10) } def f = results.first() def l = results.last() def n = results.next() def p = results.previous()
def future = results.scroll(10) def accountNumber = results.getLong('number')
下面引用的是Hibernate文档中关于ScrollableResults的描述:
结果集的迭代器(iterator)可以以任意步进的方式前后移动,而Query / ScrollableResults模式跟JDBC的PreparedStatement/ ResultSet也很像,其接口方法名的语意也跟ResultSet的类似.
不同于JDBC,结果列的编号是从0开始.
在Criteria实例中设置属性
如果在builder树内部的一个节点不匹配任何一项特定标准,它将尝试设置为Criteria对象自身的属性。因此允许完全访问这个类的所有属性。下面的例子是在Criteria Criteria实例上调用 setMaxResults
和 setFirstResult
:
import org.hibernate.FetchMode as FM … def results = c.list { maxResults(10) firstResult(50) fetchMode("aRelationship", FM.EAGER) }
立即加载的方式查询
在 Eager and Lazy Fetching立即加载和延迟加载 这节,我们讨论了如果指定特定的抓取方式来避免N+1查询的问题。这个criteria查询也可以做到:
def criteria = Task.createCriteria()
def tasks = criteria.list{
eq "assignee.id", task.assignee.id
join 'assignee'
join 'project'
order 'priority', 'asc'
}
注意这个 join
方法的用法. This method indicates the criteria API that a JOIN
query should be used to obtain the results.
方法引用
如果你调用一个没有方法名的builder,比如:
c { … }
默认的会列出所有结果,因此上面代码等价于:
c.list { … }
方法 描述
list | 这是默认的方法。它会返回所有匹配的行。 |
get | 返回唯一的结果集,比如,就一行。criteria已经规定好了,仅仅查询一行。这个方法更方便,免得使用一个limit来只取第一行使人迷惑。 |
scroll | 返回一个可滚动的结果集 |
listDistinct | 如果子查询或者关联被使用,有一个可能就是在结果集中多次出现同一行,这个方法允许只列出不同的条目,它等价于 CriteriaSpecification 类的DISTINCT_ROOT_ENTITY |