前面我们已经谈过关系建模,本篇我们将谈谈其他的建模问题,如Composition、继承、集合,以及GORM持久化的基础内容。
在关系建模中,我们已经知道如何建立one-to-many关系,其中要用到hasMany,缺省情况下,GORM会用集合类型Set来对应many,在数据库中则单独地对应一张表,其中有一个指向one的外键。集合除了可以应用到对象上,它同样还可以应用到基本类型,如String:
class Person { static hasMany = [nicknames:String] }
以上的规则同样适用于它,many端对应的表为(person_id, nickname)。你当然也同样可以对其进行自定义,同样也是使用ORM DSL:
static mapping = { hasMany joinTable:[name:'bunch_o_nicknames', key:'person_id', column:'nickname', type:"text"] }
不要以为这种方式很罕见,实际上,在最新的 Shiro Plugin中,它为你产生的权限Domain Class中就使用了以上的映射方式。
除了支持对基本类型使用集合,Hibernate同样支持将一张表映射成多个对象,如Composition:
class Person { Address homeAddress Address workAddress static embedded = ['homeAddress', 'workAddress'] } class Address { String number String code }
以上两个对象所对应的表为这两个对象属性的并集,其中的Address又被称为“值对象”,它没有自己的生命周期,由所属的Person实例决定。这里需要注意的是这些类要定义在同一文件中。
对于继承,Hibernate支持:
GORM缺省使用table-per-hierarchy,这种方式是另一种多对象对应一个表的情况,这种继承方式同样可通过ORM DSL可以使用其他方式。至于多态查询,理解起来也并不困难,即使用不同的子类进行查询,如xxx.list(),就列出不同子类的列表;而使用父类,则列出全部。子类在此充当了条件判断符的作用。这些内容并不复杂,详细请参见文档。
除了缺省使用的Set,GORM还支持其他的集合类型:SortedSet、列表和Map,只需要显示声明其类型即可:
class Author { SortedSet books static hasMany = [books:Book] }
不同的集合类型具有不同的含义,产生的数据库结构也不一样:
在清楚这些集合的含义之后,了解它们对性能的影响就简单了:如果从包含集合的主表进行增加,一旦存在大量子表,性能必然下降。原因是:为了保持集合语义,Hibernate必须进行额外的查询操作。不信,你可以将SQL打印开关打开,然后创建包含Set和List的Domain Class,然后在Grails Console中保存这些Domain Class试试。
对于集合类型,Hibernate推荐使用bag,它的语义是:无序、可重复,这样Hibernate就不用再进行以上那些操作。遗憾的是。GORM目前并不支持Bag……
因此,典型的做法是:如果可预见有大量子表元素,那么就在子表上设置belongsTo,从子表端设置关系。据此可见,双向关联有很大的灵活性。
缺省情况下,Flush的时机是由Hibernate决定的,手工控制的方法则是在相应的方法中传入(flush:true),如p.save(flush:true)。
级联更新和删除:由定义belongsTo的另一端控制,它对级联策略的影响总结如下:
关联缺省都是Lazy取,在某些情况下,就会导致n+1查询的情况。改进的方法是Eager取:
class Airport { String name static hasMany = [flights:Flight] //或static mapping = { // flight fetch:"join" //} static fetchMode = [flights:"eager"] }
但请注意,以上只适合在关联对象数目很小的情况下,由于Hibernate对此会使用左外联结,一旦数量很大,性能影响很大。
对于以上例子,还有另一种优化方式,即分批取。缺省情况下,每批的大小是1,这才是导致n+1查询的元凶。每批的大小既可以设在关联属性上:
class Airport { String name static hasMany = [flights:Flight] static mapping = { flight batchSize:10 } }
也可以设置在对象上:
class Flight { … static mapping = { batchSize 10 } }
除了实例级别的更新,GORM也支持批量更新,实际就是使用executeUpdate执行HQL,如Customer.executeUpdate(...)。
对于锁,一般是两种:乐观锁和悲观锁。乐观锁是在数据提交时才锁住数据,发挥作用,并发性高;悲观锁则是从一开始就锁住数据,直到提交完毕,并发性不高。
Hibernate是通过Version来支持乐观锁的,因此GORM也是使用这种方式,同时乐观锁是GORM的缺省锁形式。对于每一个Domain Class,GORM会缺省的添加一个version属性和id属性。对于乐观锁的使用:
def airport = Airport.get(10) try { airport.name = "Heathrow" airport.save(flush:true) } catch(org.springframework.dao.OptimisticLockingFailureException e) { // deal with exception }
乐观锁虽然效率高,但如果高并发场合要频繁的处理异常,这有可能抵消其本身的好处。此时,使用悲观锁似乎更合适。悲观锁的含义非常类似“select ... for update”,在GORM中是通过lock来获得的:
def airport = Airport.get(10) airport.lock() // 获得锁 airport.name = "Heathrow" airport.save() //释放
但是,以上方式在get和lock之间还是给别人有可趁之机,这里则一开始就锁住:Airport.lock(10) 。除了这两种方法,我们还可以在查询中使用,如:
def airport = Airport.findByName("Heathrow", [lock:true]) def airport = Airport.createCriteria().get { eq('name', 'Heathrow') lock true } |