5.2 领域(Domain)建模 - Reference Documentation
Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith
Version: null
Table of Contents
5.2 领域(Domain)建模
When building Grails applications you have to consider the problem domain you are trying to solve. For example if you were building an Amazon-style bookstore you would be thinking about books, authors, customers and publishers to name a few.These are modeled in GORM as Groovy classes, so aBook class may have a title, a release date, an ISBN number and so on. The next few sections show how to model the domain in GORM.To create a domain class you run the create-domain-class command as follows:grails create-domain-class org.bookstore.Book
grails-app/domain/org/bookstore/Book.groovy:package org.bookstoreclass Book {
}book (the same name as the class). This behaviour is customizable through the ORM Domain Specific LanguageNow that you have a domain class you can define its properties as Java types. For example:package org.bookstoreclass Book { String title Date releaseDate String ISBN }
releaseDate maps onto a column release_date. The SQL types are auto-detected from the Java types, but can be customized with Constraints or the ORM DSL.
5.2.1 GORM中的关联
Relationships define how domain classes interact with each other. Unless specified explicitly at both ends, a relationship exists only in the direction it is defined.5.2.1.1 Many-to-one和one-to-one
A many-to-one relationship is the simplest kind, and is defined with a property of the type of another domain class. Consider this example:Example A
class Face {
Nose nose
}class Nose {
}Face to Nose. To make this relationship bidirectional define the other side as follows:Example B
class Face {
Nose nose
}class Nose {
static belongsTo = [face:Face]
}belongsTo setting to say that Nose "belongs to" Face. The result of this is that we can create a Face, attach a Nose instance to it and when we save or delete the Face instance, GORM will save or delete the Nose. In other words, saves and deletes will cascade from Face to the associated Nose:new Face(nose:new Nose()).save()
Face:new Nose(face:new Face()).save() // will cause an error
Face instance, the Nose will go too:def f = Face.get(1) f.delete() // both Face and Nose deleted
hasOne property on the owning side, e.g. Face:Example C
class Face {
static hasOne = [nose:Nose]
}class Nose {
Face face
}nose table inside a column called face_id. Also, hasOne only works with bidirectional relationships.Finally, it's a good idea to add a unique constraint on one side of the one-to-one relationship:class Face {
static hasOne = [nose:Nose] static constraints = {
nose unique: true
}
}class Nose {
Face face
}5.2.1.2 One-to-many
A one-to-many relationship is when one class, exampleAuthor, has many instances of a another class, example Book. With Grails you define such a relationship with the hasMany setting:class Author {
static hasMany = [books: Book] String name
}class Book {
String title
}The ORM DSL allows mapping unidirectional relationships using a foreign key association insteadGrails will automatically inject a property of type
java.util.Set into the domain class based on the hasMany setting. This can be used to iterate over the collection:def a = Author.get(1)for (book in a.books) {
println book.title
}The default fetch strategy used by Grails is "lazy", which means that the collection will be lazily initialized on first access. This can lead to the n+1 problem if you are not careful.If you need "eager" fetching you can use the ORM DSL or specify eager fetching as part of a queryThe default cascading behaviour is to cascade saves and updates, but not deletes unless a
belongsTo is also specified:class Author {
static hasMany = [books: Book] String name
}class Book {
static belongsTo = [author: Author]
String title
}mappedBy to specify which the collection is mapped: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 supports many-to-many relationships by defining ahasMany on both sides of the relationship and having a belongsTo on the owned side of the relationship:class Book {
static belongsTo = Author
static hasMany = [authors:Author]
String title
}class Author {
static hasMany = [books:Book]
String name
}Author, takes responsibility for persisting the relationship and is the only side that can cascade saves across.For example this will work and cascade saves:new Author(name:"Stephen King") .addToBooks(new Book(title:"The Stand")) .addToBooks(new Book(title:"The Shining")) .save()
Book and not the authors!new Book(name:"Groovy in Action") .addToAuthors(new Author(name:"Dierk Koenig")) .addToAuthors(new Author(name:"Guillaume Laforge")) .save()
Grails' Scaffolding feature does not currently support many-to-many relationship and hence you must write the code to manage the relationship yourself
5.2.1.4 基本的集合类型
As well as associations between different domain classes, GORM also supports mapping of basic collection types. For example, the following class creates anicknames association that is a Set of String instances:class Person {
static hasMany = [nicknames: String]
}joinTable argument:class Person { static hasMany = [nicknames: String] static mapping = {
hasMany joinTable: [name: 'bunch_o_nicknames',
key: 'person_id',
column: 'nickname',
type: "text"]
}
}--------------------------------------------- | person_id | nickname | --------------------------------------------- | 1 | Fred | ---------------------------------------------
5.2.2 GORM中的组合
As well as association, Grails supports the notion of composition. In this case instead of mapping classes onto separate tables a class can be "embedded" within the current table. For example:class Person {
Address homeAddress
Address workAddress
static embedded = ['homeAddress', 'workAddress']
}class Address {
String number
String code
}
If you define theAddressclass in a separate Groovy file in thegrails-app/domaindirectory you will also get anaddresstable. If you don't want this to happen use Groovy's ability to define multiple classes per file and include theAddressclass below thePersonclass in thegrails-app/domain/Person.groovyfile
5.2.3 GORM中的继承
GORM supports inheritance both from abstract base classes and concrete persistent GORM entities. For example:class Content {
String author
}class BlogEntry extends Content {
URL url
}class Book extends Content { String ISBN }
class PodCast extends Content { byte[] audioStream }
Content class and then various child classes with more specific behaviour.Considerations
At the database level Grails by default uses table-per-hierarchy mapping with a discriminator column calledclass so the parent class (Content) and its subclasses (BlogEntry, Book etc.), share the same table.Table-per-hierarchy mapping has a down side in that you cannot have non-nullable properties with inheritance mapping. An alternative is to use table-per-subclass which can be enabled with the ORM DSLHowever, excessive use of inheritance and table-per-subclass can result in poor query performance due to the use of outer join queries. In general our advice is if you're going to use inheritance, don't abuse it and don't make your inheritance hierarchy too deep.Polymorphic Queries
The upshot of inheritance is that you get the ability to polymorphically query. For example using the list method on theContent super class will return all subclasses of Content:def content = Content.list() // list all blog entries, books and podcasts
content = Content.findAllByAuthor('Joe Bloggs') // find all by authordef podCasts = PodCast.list() // list only podcasts5.2.4 集合、列表和映射
Sets of Objects
By default when you define a relationship with GORM it is ajava.util.Set which is an unordered collection that cannot contain duplicates. In other words when you have:class Author {
static hasMany = [books: Book]
}java.util.Set. Sets guarantee uniquenes but not order, which may not be what you want. To have custom ordering you configure the Set as a SortedSet:class Author { SortedSet books static hasMany = [books: Book]
}java.util.SortedSet implementation is used which means you must implement java.lang.Comparable in your Book class:class Book implements Comparable { String title Date releaseDate = new Date() int compareTo(obj) { releaseDate.compareTo(obj.releaseDate) } }
Lists of Objects
To keep objects in the order which they were added and to be able to reference them by index like an array you can define your collection type as aList:class Author { List books static hasMany = [books: Book]
}author.books[0] // get the first book
books_idx column where it saves the index of the elements in the collection to retain this order at the database level.When using a List, elements must be added to the collection before being saved, otherwise Hibernate will throw an exception (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()
Bags of Objects
If ordering and uniqueness aren't a concern (or if you manage these explicitly) then you can use the Hibernate Bag type to represent mapped collections.The only change required for this is to define the collection type as aCollection:class Author { Collection books static hasMany = [books: Book]
}Set or a List.Maps of Objects
If you want a simple map of string/value pairs GORM can map this with the following:class Author {
Map books // map of ISBN:book names
}def a = new Author()
a.books = ["1590597583":"Grails Book"]
a.save()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()hasMany property defines the type of the elements within the Map. The keys for the map must be strings.A Note on Collection Types and Performance
The JavaSet type doesn't allow duplicates. To ensure uniqueness when adding an entry to a Set association Hibernate has to load the entire associations from the database. If you have a large numbers of entries in the association this can be costly in terms of performance.The same behavior is required for List types, since Hibernate needs to load the entire association to maintain order. Therefore it is recommended that if you anticipate a large numbers of records in the association that you make the association bidirectional so that the link can be created on the inverse side. For example consider the following code:def book = new Book(title:"New Grails Book") def author = Author.get(1) book.author = author book.save()
Author with a large number of associated Book instances if you were to write code like the following you would see an impact on performance:def book = new Book(title:"New Grails Book") def author = Author.get(1) author.addToBooks(book) author.save()

