Grails 对象关联映射 (GORM) 二

商宏爽
2023-12-01
Groovy 的多行字符串对HQL查询无效

 

分页和排序

使用HQL查询的时候你也可以进行分页和排序。要做的只是简单指定分页和排序参数作为一个散列在方法的末尾调用:

 

def results =
      Book.findAll("from Book as b where b.title like 'Lord of the%' order by b.title asc",
                   [max:10, offset:20])

5.5 高级GORM特性

接下来的章节覆盖更多高级的GORM使用 包括 缓存、定制映射和事件。

5.5.1 事件和自动实现时间戳

GORM支持事件注册,只需要将事件作为一个闭包即可,当某个事件触发,比如删除,插入,更新。The following is a list of supported events下面就是所支持事件的列表:

  • beforeInsert - 对象持久到数据之前执行
  • beforeUpdate - 对象被更新之前执行
  • beforeDelete - 对象被删除之前执行
  • afterInsert - 对象持久到数据库之后执行
  • afterUpdate - 对象被更新之后执行
  • afterDelete - 对象被删除之后执行
  • onLoad - 对象从数据库中加载之后执行

为了添加一个事件需要在你的领域类中添加相关的闭包。

 

事件类型

 

beforeInsert事件

当一个对象保存到数据库之前触发

 

class Person {
   Date dateCreated

def beforeInsert = { dateCreated = new Date() } }

 

beforeUpdate事件

当一个对象被更新之前触发

 

class Person {
   Date dateCreated
   Date lastUpdated

def beforeInsert = { dateCreated = new Date() } def beforeUpdate = { lastUpdated = new Date() } }

 

beforeDelete事件

当一个对象被删除以后触发.

 

class Person {
   String name
   Date dateCreated
   Date lastUpdated

def beforeDelete = { new ActivityTrace(eventName:"Person Deleted",data:name).save() } }

 

onLoad事件

当一个对象被加载之后触发:

 

class Person {
   String name
   Date dateCreated
   Date lastUpdated

def onLoad = { name = "I'm loaded" } }

 

自动时间戳

上面的例子演示了使用事件来更新一个 lastUpdateddateCreated 属性来跟踪对象的更新。事实上,这些设置不是必须的。通过简单的定义一个 lastUpdateddateCreated 属性,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块,此块包含的方法调用匹配每一个属性名称 (in this case firstName). 接下来使用命名的 column, 来指定字段名称的映射.

 

列类型

GORM还可以通过DSL的type属性来支持Hibernate类型. 包括特定Hibernate的 org.hibernate.usertype.UserType 的子类, which allows complete customization of how a type is persisted. 比如,有一个 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类型.

See the Hibernate documentation regarding Basic Types for further information.

 

一对一映射

在关联中,你也有机会改变外键映射联系,在一对一的关系中,对列的操作跟其他常规的列操作并无二异,例子如下:

 

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.

 

一对多映射

在一个双向的一对多关系中,你可以象前节中的一对一关系中那样改变外键列,只需要在多的一端中改变列名即可。然而,在单向关联中,外键需要在关联自身中(即一的一端-译者注)指定。比如,给定一个单向一对多联系 PersonAddress 下面的代码会改变 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_idgroup_id 对应 persongroup 表. 假如你需要改变列名,你可以为每个类指定一个列映射.

 

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 documentation 相关文档.

 

缓存实例

假如要在映射代码块中启用缺省的缓存,可以通过调用 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'

更多配置请参考缓存用法.

 

Caching Queries

You can cache queries such as dynamic finders and criteria. To do so using a dynamic finder you can pass the cache argument:

 

def person = Person.findByFirstName("Fred", cache:true)

 

Note that in order for the results of the query to be cached, you still need to enable caching in your mapping as discussed in the previous section.

You can also cache criteria queries:

 

def people = Person.withCriteria {
        like('firstName', 'Fr%')
        cache true
}

 

缓存用法

下面是不同缓存设置和他们的使用方法:

  • read-only - 假如你的应用程序需要读但是从不需要更改持久化实例,只读缓存或许适用.
  • read-write - 假如你的应用程序需要更新数据,读-写缓存或许是合适的.
  • nonstrict-read-write - 假如你的应用程序仅偶尔需要更新数据(也就是说,如果这是极不可能两笔交易,将尝试更新同一项目同时)并且时进行) ,并严格交易隔离,是不是需要一个nonstrict-read-write可能是适宜的.
  • transactional - transactional 缓存策略提供支持对全事务缓存提供比如JBoss的TreeCache. 这个缓存或许仅仅使用在一个JTA环境,同时你必须在 grails-app/conf/DataSource.groovy 文件中的 hibernate 配置中 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文档

注意,如果你仅仅想定制列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类的 firstNamelastName 属性来创建一个复合id。当你后面需要通过id取一个实例时,你必须用这个对象的原型:

 

def p = Person.get(new Person(firstName:"Fred", lastName:"Flintstone"))
println p.firstName

5.5.2.6 数据库索引

To get the best performance out of your queries it is often necessary to tailor the table index definitions. How you tailor them is domain specific and a matter of monitoring usage patterns of your queries. 为得到最好的查询性能,通常你需要调整表的索引定义。如何调整它们是跟特定领域和要查询的用法模式相关的。使用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
  }
}

 

如果你关闭了乐观锁 你将自己负责并发更新并且存在用户丢失数据的风险 (due to data overriding) 除非你使用 悲观锁

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
}

这里我们设置 Personaddress属性为延迟加载.

 

5.5.2.9 自定义级联行为

正如 级联更新 这节描述的,控制更新和删除的主要机制是从关联一端到 belongsTo 静态属性的一端。

然而,通过cascade属性,ORM DSL可以让你访问Hibernate的 transitive persistence 能力。

有效级联属性的设置包括:

  • create - 创建从关联端到另一端的级联
  • merge - 合并 detached 联合
  • save-update - 只级联保存和更新
  • delete - 只级联删除
  • lock - 关联的悲观锁是否被级联
  • refresh - 级联refreshes
  • evict - cascades evictions (equivalent to discard() in GORM) to associations if set
  • all - 级联所有操作
  • delete-orphan - Applies only to one-to-many associations and indicates that when a child is removed from an association then it should be automatically deleted

 

获得级联样式更好的理解和用法的介绍,请阅读Hibernate文档的 transitive persistence章节

使用上述的值定义一个或多个级联属性(逗号分隔):

 

class Person {
  String firstName
  static hasMany = [addresses:Address]
  static mapping = {
      addresses cascade:"all,delete-orphan"
  }
}
class Address {
  String street
  String postCode
}

 

5.5.2.10 自定义Hibernate的类型

在较早的章节看到可以(通过 embedded 属性) 把一个表分成多个对象。 你也可以通过Hibernate的自定义用户类型实现相同的效果。这不是领域类本身,而是java或者groovy类。所有这些类型都有一个继承自org.hibernate.usertype.UserType org.hibernate.usertype.UserType 的"meta-type"类。

Hibernate参考手册 有一些自定义类型资料,在这里我们将重点放在如何在Grails中映射。让我们看一个使用老式的(Java 1.5以前)枚举类型安全的领域类:

 

class Book {
  String title
  String author
  Rating rating

static mapping = { rating type: RatingUserType } }

我们所要做的是声明 rating 的枚举类型和在自定义映射UserType中设置属性的类型。这是你想使用自定义类型所必须做的。你也可以使用其他列的设置,比如使用"column"来改变列名和使用"index"把它添加到index。

自定义类型不局限于只是一个列,他们可以映射到多列。在这种情况下,你必须在映射中明确地定义那列使用,因为Hibernate只能为一列使用属性名。 幸运的是,Grails可以为属性映射多列:

 

class Book {
  String title
  Name author
  Rating rating

static mapping = { name type: NameUserType, { column name: "first_name" column name: "last_name" } rating type: RatingUserType } }

上面的例子将为author属性创建"first_name"和"last_name"列。You'll be pleased to know that you can also use some of the normal column/property mapping attributes in the column definitions. For example:

 

column name: "first_name", index: "my_idx", unique: true

The column definitions do not support the following attributes: type, cascade, lazy, cache, and joinTable.

One thing to bear in mind with custom types is that they define the SQL types for the corresponding database columns. That helps take the burden of configuring them yourself, but what happens if you have a legacy database that uses a different SQL type for one of the columns? In that case, you need to override column's SQL type using the sqlType attribute:

 

class Book {
  String title
  Name author
  Rating rating

static mapping = { name type: NameUserType, { column name: "first_name", sqlType: "text" column name: "last_name", sqlType: "text" } rating type: RatingUserType, sqlType: "text" } }

Mind you, the SQL type you specify needs to still work with the custom type. So overriding a default of "varchar" with "text" is fine, but overriding "text" with "yes_no" isn't going to work.

5.5.3 缺省排序

你可以使用像 list 方法中的参数来排序对象:

 

def airports = Airport.list(sort:'name')

当然,你也可以定义一个排序的声明:

 

class Airport {
        …
        static mapping = {
                sort "name"
        }
}

必要的话你可以配置这个排序:

 

class Airport {
        …
        static mapping = {
                sort name:"desc"
        }
}

另外,您也可以在关联中配置排序:

 

class Airport {
        …
        static hasMany = [flights:Flight]
        static mapping = {
                flights sort:'number'
        }
}

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 接口来达到这个目的.

The withTransaction 方法为你处理begin/commit/rollback代码块作用域内的逻辑。

5.7 GORM和约束

尽管约束是 验证 章节的内容, 但是在此涉及到约束也是很重要的,因为一些约束会影响到数据库的生成。

Grails通过使用领域类的约束来影响数据库表字段(领域类所对于的属性)的生成,还是可行的。

考虑下面的例子,假如我们有一个域模型如下的属性.

 

String name
String description

默认情况下,在MySql数据库中,Grails将会定义这个列为...

 

column name | data type 
 description | varchar(255)

但是,在业务规则中,要求这个领域类的description属性能够容纳1000个字符,在这种情况下,如果我们是使用SQL脚本,那么我们定义的这个列可能是:

 

column name | data type 
 description | TEXT

现在我们又想要在基于应用程序的进行验证,_要求在持久化任何记录之前_,确保不能超过1000个字符。在Grails中,我们可以通过constraints. 来完成,我们将在领域类中新增如下的约束声明.

 

static constraints = {
        description(maxSize:1000)
}

这个约束条件将会提供我们所需的基于应用程序的验证并且也将生成上述示例所示的数据库信息。下面是影响数据库生成的其他约束的描述。

 

影响字符串类型属性的约束

如果 maxSize 或者 size 约束被定义, Grails将根据约束的值设置列的最大长度.

通常, 不建议在同一个的领域类中组合使用这些约束. 但是, 如果你非要同时定义 maxSizesize 约束的话, Grails将设置列的长度为 maxSize 约束和size上限约束的最少值. (Grails使用两者的最少值,因此任何超过最少值的长度将导致验证错误.)

如果定义了inList约束 (maxSizesize 未定义), 字段最大长度将取决于列表(list)中最长字符串的的长度. 以"Java"、"Groovy"和"C++"为例, Grails将设置字段的长度为6("Groovy"的最长含有6个字符).

 

影响数值类型属性的约束

如果定义了 maxmin 或者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
 类似资料: