本文转自Grails中文参考手册
领域类是任何商业应用的核心,它们保存这这些商业过程的状态并且实现相应的行为,它们还通过一对一或者一对多的关系相互联系在一起。
GORM是Grails的对象关系映射(ORM)的实现,实际上它使用的是Hibernate3(非常流行和灵活的开源ORM解决方案),但因为有Groovy的动态特性支持,因此GORM既支持动态类型也支持静态类型,再加上Grails的规约,现在创建Grails的领域类只需要更少的配置就可以了。
你也可以用Java类写Grails的领域类。集成Hibernate的相关章节介绍了如何使用Java来写Grails领域类,但又不失动态持久方法的优势。以下是GORM实践的预览:
def book = Book.findByTitle("Groovy in Action")
book .addToAuthors(name:"Dierk Koenig") .addToAuthors(name:"Guillaume LaForge") .save()
5.1 快速指南
用create-domain-class 命令创建一个domain类:
grails create-domain-class Person
这将在 grails-app/domain/Person.groovy
中创建下面的类:
class Person {
}
如果你在 数据源中将
dbCreate
属性设置为"update", "create" 或 "create-drop",Grails会自动生成或修改数据库中的表。
你可以通过添加属性来自定义类:
class Person {
String name
Integer age
Date lastVisit
}
一旦你有了一个domain类,你可以通过shell或console 中操纵它:
grails console
这将会为你载入一个可以输入Groovy命令的交互式图形界面。
5.1.1 基本的CRUD
尝试执行一些基本的CRUD (Create/Read/Update/Delete)操作。
用Groovy的new 操作符创建一个domain类的实例,设置它的属性,然后调用save:
def p = new Person(name:"Fred", age:40, lastVisit:new Date())
p.save()
save 方法将会使用底层的Hibernate ORM将你的实例持久化到数据库。
Grails自动在你的domain类中添加了一个隐含的id
属性,你可以用它来取回对象:
def p = Person.get(1)
assert 1 == p.id
这里使用get方法,它会根据数据库标识符从数据库中取回一个Person
对象。
要更新一个实例,只要设置一下属性,然后也是简单的调用一下save:
def p = Person.get(1)
p.name = "Bob"
p.save()
使用delete来删除一个实例:
def p = Person.get(1)
p.delete()
5.2 在GORM中进行领域建模
当构建一个Grials应用的时候,你必须事先考虑需要解决的问题域(problem domain)。比如,如果你要构建一个Amazon 书店,你就要考虑书,作者,顾客和出版商等等。
这些在GORM中是作为Groovy类来建模的,因此Book
类要有title,release date,ISBN number等属性。在下面的几节将演示在GORM中如何为领域建模。
要创建一个领域类,运行 create-domain-class:
grails create-domain-class Book
生成了grails-app/domain/Book.groovy
:
class Book {
}
如果你想使用包,你可以把Book.groovy类移入domain目录下的一个子目录,并根据Groovy(也是Java)的包规则添加一个合适的
package
声明。
上面的类将会在数据库中自动映射一个 book
(跟类同名)的表。映射规则可以通过ORM的DSL来自定义。
现在,你有了一个领域类,你可以把它的属性定义为Java语言的类型.比如:
class Book {
String title
Date releaseDate
String ISBN
}
每个属性映射到数据库中的一个列,列名的规则命名规则是全部小写并用下划线分隔。比如releaseDate
映射为列release_date
上。SQL类型是根据Java类型来自动检测的,你也可以用约束 或者ORM DSL自定义映射类型。
5.2.1 GORM中的关联
关联定义了domain类之间如何互相交互。除非在两端都显式的指定,否则一个关联只存在于定义它的那端。
5.2.1.1 一对一
一对一的关联是最简单的关联。它只要定义一个属性的类型为另一个domain类就可以了。看下面的例子:
class Face {
Nose nose
}
class Nose {
}
这样我们就有了一个从 Face
到 Nose
的单向关联。要把这个关联变为双向的,只要在另一端定义一下:
class Face {
Nose nose
}
class Nose {
Face face
}
这样就是双向关联了。但是,这种情况下,关联的双方并不能级联更新。
看下面的变化:
class Face {
Nose nose
}
class Nose {
static belongsTo = [face:Face]
}
在这里,我们使用belongsTo
表示Nose
"属于" Face。其结果就是我们创建并保存它时,数据库可以_级联_更新/插入Nose
:
new Face(nose:new Nose()).save()
上面这个例子会将face和nose都保存。注意反向的操作并不可行,它会导致一个对于临时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
的话,将不会级联删除,你会得到一个外键约束的错误,除非你手工去删除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
声明中使用映射语法明确地声明这个关联,Grails将认为它是一个单向关联。下面是对这三个例子的总结:
5.2.1.2 一对多
一对多的关系是指,当一个类(比如Author
)拥有另一个类(Book
)的多个实例这种情况。在Grails中,你可以使用hasMany
来定义这种关联:
class Author {
static hasMany = [ books : Book ]
String name } class Book { String title }
这样我们有了一个一对多的单向关联。Grails在数据库级别将默认使用外键映射来映射这种关联。
ORM DSL 允许使用连接表作为代替来映射单向关联。
Grails 将会根据hasMany
设置为domain类自动注入一个类型为java.util.Set的属性。这个可以被用来遍历集合:
def a = Author.get(1)
a.books.each { println it.title }
Grails使用的默认抓取策略是"lazy",这意味这集合将会被延迟初始化。如果你不小心的话,这可能导致 n+1 问题 .如果你需要"eager"抓取,你可以使用ORM DSL 或者指定立即抓取作为查询的一部分。
默认的级联行为是级联保存和更新,但不级联删除,除非你也指定了 belongsTo
:
class Author {
static hasMany = [ books : Book ]
String name } class Book { static belongsTo = [author:Author] String title }
如果你有两个相同类型的属性,他们都是一对多关系的多方,你必须用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 多对多
Grails支持多对多关联,这种关联需要在关联的两方都定义hasMany
,并在关联的被拥有方定义belongsTo
:
class Book {
static belongsTo = Author
static hasMany = [authors:Author]
String title
}
class Author {
static hasMany = [books:Book]
String name
}
Grials在数据库层使用连接表来映射多对多关联。关联的拥有方,在这里是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中一样,多对多关联中只有一方可以管理关联。
Grails的 脚手架 特性当前还不支持多对多关联,因此你必须自己写代码来管理关联。
5.2.2 GORM的组合
跟关联一样,Grails支持组合的概念。在这种情况下,要映射到一个独立的表的类,可以嵌入到当前表。比如:
class Person {
Address homeAddress
Address workAddress
static embedded = ['homeAddress', 'workAddress']
}
class Address {
String number
String code
}
映射的结果看起来像下面这样:
如果你在grails-app/domain
目录下一个单独的Groovy文件中定义了Address
类,你还是会得到一个address
表。如果你不想这样,你可以使用Groovy可以在一个文件中定义多个类的特性,在grails-app/domain/Person.groovy
文件中在Person
类的下面定义Address
类。
5.2.3 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默认使用每个类分层结构一个表(uses table-per-hierarchy)的映射策略,有一个辨别标识(discriminator)列叫class
,因此父类Content
和它的子类(BlogEntry
, Book
等等.)共享同一个表。
每个类分层结构一个表(uses table-per-hierarchy)有一个副作用,就是你继承映射中的属性不能有非空约束。一个选择是使用每个子类一个表(table-per-subclass)映射,它可以通过ORM DSL设置。
但是,过度的使用继承和每个子类一个表(table-per-subclass)的映射策略,会导致过度使用连接查询,使得查询性能很差。通常我们建议保持简单,只有在你确实需要继承的时候才用它。
使用继承的好处是你获得了多态查询的能力,比如在超类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 集合、列表和映射
对象集合(Set)
当你在GROM中定义一个关联的时候,默认地使用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实例将按他们的发布日期排序.
如果你只是想能够简单的保持对象按照他们被加进去的顺序排序,并能像数组一样按照索引来引用他们,你可以定义你的集合类型为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()
如果你想要一个简单的 string/value 对map,GROM可以用下面方法来映射:
class Author {
Map books // my 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 必须 是字符串
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()
Hibernate的一个主要的不同在于当你调用 save 时它不需要马上执行任何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 方法也允许通过flush
参数来控制flushing.
def p = Person.get(1)
p.delete(flush:true)
注意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
去掉的话,上面的级联删除代码就不能工作了.
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将会执行一个单独的SQL查询来抓取Airport
实例,然后再用一个额外的查询逐条迭代flights
关联.换句话说,你得到了N+1条查询.
根据这个集合的使用频率,有时候这可能是最佳方案.因为你可以指定只有在特定的情况下才访问这个关联的逻辑.
一个可选的方案是使用立即抓取,它可以按照下面的方法来指定:
class Airport {
String name
static hasMany = [flights:Flight]
static fetchMode = [flights:"eager"]
}
在这种情况下,Airport
实例对应的flights
关联会被一次性全部加载进来(依赖于映射).这样的好处是执行更少的查询,但是要小心使用,因为使用太多的eager关联可能会导致你将整个数据库加载进内存.
关联也可以用 ORM DSL 将关联声明为 non-lazy
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会自动的为你释放锁。
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:"asc", order:"title")
第二个取回的基本形式是根据数据库标识符取回,使用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][Suffix]*[Boolean Operator]*[Property][Suffix]
用*标记的部分是可选的。每个后缀都会改变查询的性质。例如:
def book = Book.findByTitle("The Stand")
book = Book.findByTitleLike("Harry Pot%")
在上面的例子中,第一个查询等价于等于后面的值。第二个因为增加了 Like
后缀,它等价于SQL的like
表达式。
可用的后缀包括:
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 )
同样的isNull
和 isNotNull
不需要参数:
def books = Book.findAllByReleaseDateIsNull()
方法表达式也可以使用一个布尔操作符来组合两个criteria:
def books =
Book
.findAllByTitleLikeAndReleaseDateGreaterThan("%Java%", new Date()-30)
在这里我们在查询中间使用 And
来确保两个条件都满足,但是同样地你也可以使用 Or
:
def books =
Book
.findAllByTitleLikeOrReleaseDateGreaterThan("%Java%", new Date()-30)
显然这种情况下方法名会变得相当长,这时候你应该考虑使用 条件查询.
关联也可以被用在查询中:
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:"asc",
order:"title"])
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")
}
如前面例子所演示的,你可以用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)
}
}
The above code will find all the Account
instances that have performedtransactions
within the last 10 days.上面的代码将会查找所有过去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天内新创建的所有用户。
投影被用于定制查询结果。要使用投影你需要在criteria builder树里定义一个"projections"节点。projections节点内可用的方法等同于 Hibernate 的Projections 类中的方法.
def c = Account.createCriteria()
def numberOfBranches = c.get { projections { countDistinct('branch') } }
你可以通过调用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开始.
如果在builder树内部的一个节点不匹配任何一项特定标准,它将尝试设置为Criteria对象自身的属性。因此允许完全访问这个类的所有属性。下面的例子是在Criteria实例上调用setMaxResults
和setFirstResult
:
import org.hibernate.FetchMode as FM
....
def results = c.list {
maxResults(10)
firstResult(50)
fetchMode("aRelationship", FM.EAGER)
}
在 立即加载和延迟加载 这节,我们讨论了如果指定特定的抓取方式来避免N+1查询的问题。这个criteria查询也可以做到:
import org.hibernate.FetchMode as FM
// ......
def criteria = Task.createCriteria() def tasks = criteria.list{ eq("assignee.id", task.assignee.id) fetchMode('assignee', FM.EAGER) fetchMode('project', FM.EAGER) order('priority', 'asc') }
如果你调用一个没有方法名的builder,比如:
c { … }
默认的会列出所有结果,因此上面代码等价于:
c.list { … }
方法 | 描述 |
---|---|
list | 这是默认的方法。它会返回所有匹配的行。 |
get | 返回唯一的结果集,比如,就一行。criteria已经规定好了,仅仅查询一行。这个方法更方便,免得使用一个limit来只取第一行使人迷惑。 |
scroll | 返回一个可滚动的结果集 |
listDistinct | 如果子查询或者关联被使用,有一个可能就是在结果集中多次出现同一行,这个方法允许只列出不同的条目,它等价于CriteriaSpecification 类的DISTINCT_ROOT_ENTITY 。 |
5.4.3 Hibernate查询语言
GORM也支持Hibernate的查询语言HQL,在Hibernate文档中的 Chapter 14. HQL: The Hibernate Query Language ,可以找到它非常完整的参考手册。
GORM提供了一些使用HQL的方法,包括find,findAll 和 executeQuery。下面是一个查询的例子:
def results =
Book.findAll("from Book as b where b.title like 'Lord of the%'")
上面的例子中传递给查询的值是硬编码的,但是,你可以同样地使用位置参数:
def results =
Book.findAll("from Book as b where b.title like ?", ["The Shi%"])
或者甚至使用命名参数:
def results =
Book.findAll("from Book as b where b.title like :search or b.author like :search", [search:"The Shi%"])
如果你需要将查询分割到多行你可以使用一个行连接符:
def results = Book.findAll("""\\
from Book as b, \\
Author as a \\
where b.author = a and a.surname = ?""", ['Smith'])
Groovy 的多行字符串对HQL查询无效
使用HQL查询的时候你也可以进行分页和排序。要做的只是简单指定分页和排序参数作为一个散列在方法的末尾调用:
def results =
Book.findAll("from Book as b where b.title like 'Lord of the%'",
[max:10, offset:20, sort:"asc", order:"title"])
5.5 高级GORM特性
接下来的章节覆盖更多高级的GORM使用 包括 缓存、定制映射和事件
5.5.1 事件和自动实现时间戳
GORM supports the registration of events as closures that get fired when certain events occurs such as deletes, inserts and updates. To add an event simply register the relevant closure with your domain class.GORM支持事件注册,只需要将事件作为一个闭包即可,当某个事件触发,比如删除,插入,更新。为了添加一个事件需要在你的领域类中添加相关的闭包。
事件类型
触发当一个对象保存到数据库之前
class Person {
Date dateCreated
def beforeInsert = { dateCreated = new Date() } }
更新前事件
class Person {
Date dateCreated
Date lastUpdated
def beforeInsert = { dateCreated = new Date() } def beforeUpdate = { lastUpdated = new Date() } }
触发但一个对象被删除
class Person {
String name
Date dateCreated
Date lastUpdated
def beforeDelete = { new ActivityTrace(eventName:"Person Deleted",data:name).save() } }
触发当一个对象从数据库取出
class Person {
String name
Date dateCreated
Date lastUpdated
def onLoad = { name = "I'm loaded" } }
lastUpdated
和
dateCreated
属性来跟踪对象的更新。事实上,这些设置不是必须的。通过简单的定义一个
lastUpdated
和
dateCreated
属性 ,GORM会自动的为你更新。
如果,这些行为不是你需要的,可以屏蔽这些功能。如下设置
class Person {
Date dateCreated
Date lastUpdated
static mapping = {
autoTimestamp false
}
}
5.5.2 自定义ORM映射
Grails 的域对象可以映射到许多遗留的模型通过 关系对象映射域语言。接下来的部分将带你领略它是可能的通过ORM DSL
这是必要的,如果你高兴地坚持以约定来定义GORM对应的表,列名等。你只需要这个功能,如果你需要定制GORM 映射到遗留模型或进行缓存
自定义映射是使用静态的mapping
块定义在你的域类中的:
class Person {
..
static mapping = {
} }
5.5.2.1 表名和列名
表名
类映射到数据库的表名可以通过使用table
关键字来定制
class Person {
..
static mapping = {
table 'people'
}
}
在上面的例子中,类会映射到people
表来代替默认的person
表
同样,也是可能的定制某个列到数据库。比如说,你想改变列名例子如下
class Person {
String firstName
static mapping = {
table 'people'
firstName column:'First_Name'
}
}
在这个例子中,你定义了一个column
块,此块包含的方法调用匹配每一个属性名称(本例子中是 firstName
),接下来使用命名的column
来指定字段名称的映射
GORM supports configuration of Hibernate types via the DSL using the type attribute. This includes specifing user types that subclass the Hibernateorg.hibernate.types.UserType class. As an exampleGORM还可以通过DSL的type属性来支持Hibernate类型,包括特定Hibernate的org.hibernate.types.UserType的子类。比如,有一个PostCodeType,你可以象下面这样使用:
class Address {
String number
String postCode
static mapping = {
postCode type:PostCodeType
}
}
另外如果你想将它映射到Hibernate的基本类型而不是Grails的默认类型,可以参考下面代码:
class Address {
String number
String postCode
static mapping = {
postCode type:'text'
}
}
上面的例子将使postCode列映射到数据库的SQL TEXT或者CLOB类型
在关联中,你也有机会改变外键映射联系,在一对一的关系中,对列的操作跟其他常规的列操作并无二异,例子如下
class Person {
String firstName
Address address
static mapping = {
table 'people'
firstName column:'First_Name'
address column:'Person_Adress_Id'
}
}
默认情况下,address
将映射到一个名称为address_id 的外键。但是使用上面的映射,我们改变外键列为Person_Adress_Id
在一个双向的一对多关系中,你可以象前节中的一对一关系中那样改变外键列,只需要在多的一端中改变列名即可。然而,在单向关联中,外键需要在关联自身中(即一的一端-译者注)指定。比如,给定一个单向一对多联系Person
和Address
,下面的代码会改变address
表中外键。
class Person {
String firstName
static hasMany = [addresses:Address]
static mapping = {
table 'people'
firstName column:'First_Name'
addresses column:'Person_Address_Id'
}
}
如果你不想在address
表中有这个列,可以通过中间关联表来完成,只需要使用joinTable参数即可:
class Person {
String firstName
static hasMany = [addresses:Address]
static mapping = {
table 'people'
firstName column:'First_Name'
addresses joinTable:[name:'Person_Addresses', key:'Person_Id', column:'Address_Id']
}
}
默认情况下,Grails中多对多的映射是通过中间表来完成的,以下面的多对多关联为例:
class Group {
…
static hasMany = [people:Person]
}
class Person {
…
static belongsTo = Group
static hasMany = [groups:Group]
}
在上面的例子中,Grails将会创建一个group_person
表包含外键person_id
和group_id
对应person
和group
表。假如你需要改变列名,你可以为每个类指定一个列映射
class Group {
…
static mapping = {
people column:'Group_Person_Id'
}
}
class Person {
…
static mapping = {
groups column:'Group_Group_Id'
}
}
你也可以指定中间表的名称
class Group {
…
static mapping = {
people column:'Group_Person_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
}
}
class Person {
…
static mapping = {
groups column:'Group_Group_Id',joinTable:'PERSON_GROUP_ASSOCIATIONS'
}
}
5.5.2.2 缓存策略
设置缓存
Hibernate 本身提供了自定义二级缓存的特性,这就需要在grails-app/conf/DataSource.groovy
文件中配置:
hibernate {
cache.use_second_level_cache=true
cache.use_query_cache=true
cache.provider_class='org.hibernate.cache.EhCacheProvider'
}
当然,你也可以按你所需来定制设置。比如,你想使用分布式缓存机制
想了解更多Hibernate的二级缓存,参考 Hibernate相关文档
假如要在映射代码块中启用缺省的缓存,可以通过调用cache
方法实现:
class Person {
..
static mapping = {
table 'people'
cache true
}
}
上面的例子中将配置一个读-写(read-write)缓存包括lazy和non-lazy属性。假如你想定制这些特性,你可以如下所示:
class Person {
..
static mapping = {
table 'people'
cache usage:'read-only', include:'non-lazy'
}
}
就像使用Hibernate的二级缓存来缓存实例一样,你也可以来缓存集合(关联),比如:
class Person {
String firstName
static hasMany = [addresses:Address]
static mapping = {
table 'people'
version false
addresses column:'Address', cache:true
}
}
class Address {
String number
String postCode
}
上面的例子中,我们在addresses集合启用了一个读-写缓存,你也可以使用
cache:'read-write' // or 'read-only' or 'transactional'
更多配置请参考缓存用法
下面是不同缓存设置和他们的使用方法
read-only
- 假如你的应用程序需要读但是从不需要更改持久化实例,只读缓存或许适用read-write
- 假如你的应用程序需要更新数据,读-写缓存或许是合适的nonstrict-read-write
- 假如你的应用程序仅偶尔需要更新数据(也就是说,如果这是极不可能两笔交易,将尝试更新同一项目同时)并且时进行) ,并严格交易隔离,是不是需要一个非严格-读写缓存可能是适宜的transactional
- transactional
缓存策略提供支持对全事务缓存提供比如JBoss的TreeCache。这个缓存或许仅仅使用在一个JTA环境,同时你必须在grails-app/conf/DataSource.groovy
文件中配置hibernate.transaction.manager_lookup_class
5.5.2.3 继承策略
默认情况下GORM 类使用table-per-hierarchy
来映射继承的。这就有一个缺点就是在数据库层面,列不能有NOT-NULL
的约束。如果你更喜欢table-per-subclass
,你可以使用下面方法
class Payment {
Long id
Long version
Integer amount
static mapping = { tablePerHierarchy false } } class CreditCardPayment extends Payment { String cardNumber }
在祖先Payment类的映射设置中,指定了在所有的子类中,不使用table-per-hierarchy
映射。
5.5.2.4 自定义数据库标识符
你可以通过DSL来定制GORM生成数据库标识,缺省情况下GORM将根据原生数据库机制来生成ids,这是迄今为止最好的方法,但是仍存在许多模式,不同的方法来生成标识。
为此,Hibernate特地定义了id生成器的概念,你可以自定义它要映射的id生成器和列,如下:
class Person {
..
static mapping = {
table 'people'
version false
id generator:'hilo', params:[table:'hi_value',column:'next_value',max_lo:100]
}
}
在上面的例子中,我们使用了Hibernate内置的'hilo'生成器,此生成器通过一个独立的表来生成ids。此外还有许多不同的生成器可以配置,具体参考Hibernate在这个主题上的相关文档。
想了解更多不同的Hibernate生成器请参考 Hibernate文档
注意,如果你仅仅想定制列id,你可以这样
class Person {
..
static mapping = {
table 'people'
version false
id column:'person_id'
}
}
5.5.2.5 复合主键
GORM支持复合标识(复合主键--译者注)概念(标识由两个或者更多属性组成)。这不是我们建议的方法,但是如果你想这么做,这也是可能的:
class Person {
String firstName
String lastName
static mapping = { id composite:['firstName', 'lastName'] } }
上面的代码将通过Person
类的firstName
和lastName
属性来创建一个复合id。当你后面需要通过id取一个实例时,你必须用这个对象的原型
def p = Person.get(new Person(firstName:"Fred", lastName:"Flintstone"))
println p.firstName
5.5.2.6 数据库索引
为得到最好的查询性能,通常你需要调整表的索引定义。如何调整它们是跟特定领域和要查询的用法模式相关的。使用GORM的DSL你可以指定那个列需要索引
class Person {
String firstName
String address
static mapping = {
table 'people'
version false
id column:'person_id'
firstName column:'First_Name', index:'Name_Idx'
address column:'Address', index:'Name_Idx, Address_Index'
}
}
5.5.2.7 乐观锁和版本定义
就像在乐观锁和悲观锁部分讨论的,默认情况下,GORM使用乐观锁和在每一个类中自动注入一个version
属性,此属性将映射数据库中的一个version
列
如果你映射的是一个遗留数据库(已经存在的数据库--译者注),这将是一个问题,因此可以通过如下方法来关闭这个功能:
class Person {
..
static mapping = {
table 'people'
version false
}
}
如果你关闭了乐观锁,你将自己负责并发更新并且存在用户丢失数据的风险(除非你使用 悲观锁)。
5.5.2.8 立即加载和延迟加载
延迟加载集合
就像在立即加载和延迟加载部分讨论的,默认情况下,GORM 集合使用延迟加载的并且可以通过fetchMode
来配置 。但如果你更喜欢把你所有的映射都集中在mappings
代码块中,你也可以使用ORM的DSL来配置获取模式:
class Person {
String firstName
static hasMany = [addresses:Address]
static mapping = {
addresses lazy:false
}
}
class Address {
String street
String postCode
}
在GORM中,one-to-one和many-to-one关联缺省是非延迟加载的。这在有很多实体(数据库记录-译者注)的时候,会产生性能问题,尤其是关联查询是以新的SELECT语句执行的时候,此时你应该将one-to-one和many-to-one关联的延迟加载象集合那样进行设置:
class Person {
String firstName
static belongsTo = [address:Address]
static mapping = {
address lazy:true // lazily fetch the address
}
}
class Address {
String street
String postCode
}
这里我们设置 Person
的address
属性为延迟加载
5.6 事务编程
Grails是构建在Spring的基础上的,所以使用Spring的事务来抽象处理事务编程,但GORM类通过withTransaction方法使得处理更简单,方法的第一个参数是Spring的TransactionStatus对象
典型的使用场景如下:
def transferFunds = {
Account.withTransaction { status ->
def source = Account.get(params.from)
def dest = Account.get(params.to)
def amount = params.amount.toInteger() if(source.active) { source.balance -= amount if(dest.active) { dest.amount += amount } else { status.setRollbackOnly() } }
}
}
在上面的例子中,如果目的账户没有处于活动状态,系统将回滚事务,同时如果有任何异常抛出在事务的处理过程中也将会自动回滚。
假如你不想回滚整个事务,你也可以使用"save points"来回滚一个事务到一个特定的点。你可以通过使用Spring的SavePointManager接口来达到这个目的。
withTransaction方法为你处理begin/commit/rollback代码块作用域内的逻辑。
5.7 GORM和约束
尽管约束是验证章节的内容,但是在此涉及到约束也是很重要的,因为一些约束会影响到数据库的生成。
Grails通过使用领域类的约束来影响数据库表字段(领域类所对于的属性)的生成,还是可行的。
考虑下面的例子,假如我们有一个域模型如下的属性:
String description
默认情况下,在MySql数据库中,Grails将会定义这个列为
column name | data type
description | varchar(255)
但是,在业务规则中,要求这个领域类的description
属性能够容纳1000个字符,在这种情况下,如果我们是使用SQL脚本,那么我们定义的这个列可能是:
column name | data type
description | TEXT
现在我们又想要在基于应用程序的进行验证,_要求在持久化任何记录之前_,确保不能超过1000个字符。在Grails中,我们可以通过约束来完成,我们将在领域类中新增如下的约束声明:
static constraints = {
description(maxSize:1000)
}
这个约束条件将会提供我们所需的基于应用程序的验证并且也将生成上述示例所示的数据库信息。下面是影响数据库生成的其他约束的描述。
如果maxSize
或者size
约束被定义,Grails将根据约束的值设置列的最大长度。
通常,不建议在同一个的领域类中组合使用这些约束。但是,如果你非要同时定义maxSize
和size
约束的话,Grails将设置列的长度为maxSize
约束和size
上限约束的最少值(Grails使用两者的最少值,因此任何超过最少值的长度将导致验证错误)
如果定义了inList约束(maxSize
和size
未定义)的话,字段最大长度将取决于列表(list)中最长字符串的的长度。以"Java"、"Groovy"和"C++"为例,Grails将设置字段的长度为6("Groovy"的最长含有6个字符)。
如果定义了约束max
、min
或者range
,Grails将基于约束的值尝试着设置列的精度(设置的结果很大程度上依赖于Hibernate跟底层数据库系统的交互程度)。
通常来说,不建议在同一领域类的属性上组合成双的min/max和range约束,但是如果这些约束同时被定义了,那么Grails将使用约束值中的最少精度值(Grails取两者的最少值,是因为任意超过最少精度的长度将会导致一个验证错误)。
如果定义了scale约束,那么Grails会试图使用基于约束的值来设置列的标度(scale)。此规则仅仅应用于浮点数值(比如,java.lang.Float,java.Lang.Double, java.lang.BigDecimal及其相关的子类),设置的结果同样也是很大程度上依赖于Hibernate跟底层数据库系统的交互程度。
约束定义着数值的最小/最大值,Grails使用数字的最大值来设置其精度。切记仅仅指定min/max约束中的一个,是不会影响到数据库的生成的(因为可能会是很大的负值,比如当max是100),除非指定的约束值要比Hibernate默认的精度(当前是19)更高,比如:
someFloatValue(max:1000000, scale:3)
将产生:
someFloatValue DECIMAL(19, 3) // precision is default
但是
someFloatValue(max:12345678901234567890, scale:5)
将产生:
someFloatValue DECIMAL(25, 5) // precision = digits in max + scale
和
someFloatValue(max:100, min:-100000)
将产生:
someFloatValue DECIMAL(8, 2) // precision = digits in min + default scale