class Person {
private static final Date NULL_DATE = new Date(0)
String firstName
String lastName
Date signupDate = NULL_DATE
def beforeInsert() {
if (signupDate == NULL_DATE) {
signupDate = new Date()
}
}
}
8 Advanced GORM Features
Version: 2023.3.0-SNAPSHOT
Table of Contents
8 Advanced GORM Features
The following sections cover more advanced usages of GORM including caching, custom mapping and events.
8.1 Events and Auto Timestamping
GORM supports the registration of events as methods that get fired when certain events occurs such as deletes, inserts and updates. The following is a list of supported events:
-
beforeInsert
- Executed before an object is initially persisted to the database. If you return false, the insert will be cancelled. -
beforeUpdate
- Executed before an object is updated. If you return false, the update will be cancelled. -
beforeDelete
- Executed before an object is deleted. If you return false, the operation delete will be cancelled. -
beforeValidate
- Executed before an object is validated -
afterInsert
- Executed after an object is persisted to the database -
afterUpdate
- Executed after an object has been updated -
afterDelete
- Executed after an object has been deleted -
onLoad
- Executed when an object is loaded from the database
To add an event simply register the relevant method with your domain class.
Do not attempt to flush the session within an event (such as with obj.save(flush:true)). Since events are fired during flushing this will cause a StackOverflowError. |
The beforeInsert event
Fired before an object is saved to the database
The beforeUpdate event
Fired before an existing object is updated
class Person {
def securityService
String firstName
String lastName
String lastUpdatedBy
static constraints = {
lastUpdatedBy nullable: true
}
def beforeUpdate() {
lastUpdatedBy = securityService.currentAuthenticatedUsername()
}
}
The beforeDelete event
Fired before an object is deleted.
class Person {
String name
def beforeDelete() {
ActivityTrace.withNewSession {
new ActivityTrace(eventName: "Person Deleted", data: name).save()
}
}
}
Notice the usage of withNewSession
method above. Since events are triggered whilst Hibernate is flushing using persistence methods like save()
and delete()
won’t result in objects being saved unless you run your operations with a new Session
.
Fortunately the withNewSession
method lets you share the same transactional JDBC connection even though you’re using a different underlying Session
.
The beforeValidate event
Fired before an object is validated.
class Person {
String name
static constraints = {
name size: 5..45
}
def beforeValidate() {
name = name?.trim()
}
}
The beforeValidate
method is run before any validators are run.
Validation may run more often than you think. It is triggered by the validate() and save() methods as you’d expect, but it is also typically triggered just before the view is rendered as well. So when writing beforeValidate() implementations, make sure that they can handle being called multiple times with the same property values.
|
GORM supports an overloaded version of beforeValidate
which accepts a List
parameter which may include
the names of the properties which are about to be validated. This version of beforeValidate
will be called
when the validate
method has been invoked and passed a List
of property names as an argument.
class Person {
String name
String town
Integer age
static constraints = {
name size: 5..45
age range: 4..99
}
def beforeValidate(List propertiesBeingValidated) {
// do pre validation work based on propertiesBeingValidated
}
}
def p = new Person(name: 'Jacob Brown', age: 10)
p.validate(['age', 'name'])
Note that when validate is triggered indirectly because of a call to the save method that
the validate method is being invoked with no arguments, not a List that includes all of
the property names.
|
Either or both versions of beforeValidate
may be defined in a domain class. GORM will
prefer the List
version if a List
is passed to validate
but will fall back on the
no-arg version if the List
version does not exist. Likewise, GORM will prefer the
no-arg version if no arguments are passed to validate
but will fall back on the
List
version if the no-arg version does not exist. In that case, null
is passed to beforeValidate
.
The onLoad/beforeLoad event
Fired immediately before an object is loaded from the database:
class Person {
String name
Date dateCreated
Date lastUpdated
def onLoad() {
log.debug "Loading ${id}"
}
}
beforeLoad()
is effectively a synonym for onLoad()
, so only declare one or the other.
The afterLoad event
Fired immediately after an object is loaded from the database:
class Person {
String name
Date dateCreated
Date lastUpdated
def afterLoad() {
name = "I'm loaded"
}
}
Custom Event Listeners
To register a custom event listener you need to subclass AbstractPersistenceEventListener
(in package org.grails.datastore.mapping.engine.event) and implement the methods onPersistenceEvent
and supportsEventType
. You also must provide a reference to the datastore to the listener. The simplest possible implementation can be seen below:
public MyPersistenceListener(final Datastore datastore) {
super(datastore)
}
@Override
protected void onPersistenceEvent(final AbstractPersistenceEvent event) {
switch(event.eventType) {
case PreInsert:
println "PRE INSERT \${event.entityObject}"
break
case PostInsert:
println "POST INSERT \${event.entityObject}"
break
case PreUpdate:
println "PRE UPDATE \${event.entityObject}"
break;
case PostUpdate:
println "POST UPDATE \${event.entityObject}"
break;
case PreDelete:
println "PRE DELETE \${event.entityObject}"
break;
case PostDelete:
println "POST DELETE \${event.entityObject}"
break;
case PreLoad:
println "PRE LOAD \${event.entityObject}"
break;
case PostLoad:
println "POST LOAD \${event.entityObject}"
break;
}
}
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return true
}
The AbstractPersistenceEvent
class has many subclasses (PreInsertEvent
, PostInsertEvent
etc.) that provide further information specific to the event. A cancel()
method is also provided on the event which allows you to veto an insert, update or delete operation.
Once you have created your event listener you need to register it. If you are using Spring this can be done via the ApplicationContext
:
HibernateDatastore datastore = applicationContext.getBean(HibernateDatastore)
applicationContext.addApplicationListener new MyPersistenceListener(datastore)
If you are not using Spring then you can register the event listener using the getApplicationEventPublisher()
method:
HibernateDatastore datastore = ... // get a reference to the datastore
datastore.getApplicationEventPublisher()
.addApplicationListener new MyPersistenceListener(datastore)
Hibernate Events
It is generally encouraged to use the non-Hibernate specific API described above, but if you need access to more detailed Hibernate events then you can define custom Hibernate-specific event listeners.
You can also register event handler classes in an application’s app/conf/spring/resources.groovy
or in the doWithSpring
closure in a plugin descriptor by registering a Spring bean named hibernateEventListeners
. This bean has one property, listenerMap
which specifies the listeners to register for various Hibernate events.
The values of the Map are instances of classes that implement one or more Hibernate listener interfaces. You can use one class that implements all of the required interfaces, or one concrete class per interface, or any combination. The valid Map keys and corresponding interfaces are listed here:
Name | Interface |
---|---|
auto-flush |
|
merge |
|
create |
|
create-onflush |
|
delete |
|
dirty-check |
|
evict |
|
flush |
|
flush-entity |
|
load |
|
load-collection |
|
lock |
|
refresh |
|
replicate |
|
save-update |
|
save |
|
update |
|
pre-load |
|
pre-update |
|
pre-delete |
|
pre-insert |
|
pre-collection-recreate |
|
pre-collection-remove |
|
pre-collection-update |
|
post-load |
|
post-update |
|
post-delete |
|
post-insert |
|
post-commit-update |
|
post-commit-delete |
|
post-commit-insert |
|
post-collection-recreate |
|
post-collection-remove |
|
post-collection-update |
For example, you could register a class AuditEventListener
which implements PostInsertEventListener
, PostUpdateEventListener
, and PostDeleteEventListener
using the following in an application:
beans = {
auditListener(AuditEventListener)
hibernateEventListeners(HibernateEventListeners) {
listenerMap = ['post-insert': auditListener,
'post-update': auditListener,
'post-delete': auditListener]
}
}
or use this in a plugin:
def doWithSpring = {
auditListener(AuditEventListener)
hibernateEventListeners(HibernateEventListeners) {
listenerMap = ['post-insert': auditListener,
'post-update': auditListener,
'post-delete': auditListener]
}
}
Automatic timestamping
If you define a dateCreated
property it will be set to the current date for you when you create new instances. Likewise, if you define a lastUpdated
property it will be automatically be updated for you when you change persistent instances.
If this is not the behaviour you want you can disable this feature with:
class Person {
Date dateCreated
Date lastUpdated
static mapping = {
autoTimestamp false
}
}
If you have nullable: false constraints on either dateCreated or lastUpdated , your domain instances will fail validation - probably not what you want. Omit constraints from these properties unless you disable automatic timestamping.
|
It is also possible to disable the automatic timestamping temporarily. This is most typically done in the case of a test where you need to define values for the dateCreated
or lastUpdated
in the past. It may also be useful for importing old data from other systems where you would like to keep the current values of the timestamps.
Timestamps can be temporarily disabled for all domains, a specified list of domains, or a single domain. To get started, you need to get a reference to the AutoTimestampEventListener
. If you already have access to the datastore, you can execute the getAutoTimestampEventListener
method. If you don’t have access to the datastore, inject the autoTimestampEventListener
bean.
Once you have a reference to the event listener, you can execute withoutDateCreated
, withoutLastUpdated
, or withoutTimestamps
. The withoutTimestamps
method will temporarily disable both dateCreated
and lastUpdated
.
Example:
//Only the dateCreated property handling will be disabled for only the Foo domain
autoTimestampEventListener.withoutDateCreated(Foo) {
new Foo(dateCreated: new Date() - 1).save(flush: true)
}
//Only the lastUpdated property handling will be disabled for only the Foo and Bar domains
autoTimestampEventListener.withoutLastUpdated(Foo, Bar) {
new Foo(lastUpdated: new Date() - 1, bar: new Bar(lastUpdated: new Date() + 1)).save(flush: true)
}
//All timestamp property handling will be disabled for all domains
autoTimestampEventListener.withoutTimestamps {
new Foo(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true)
new Bar(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true)
new FooBar(dateCreated: new Date() - 2, lastUpdated: new Date() - 1).save(flush: true)
}
Because the timestamp handling is only disabled for the duration of the closure, you must flush the session during the closure execution! |
8.2 Custom ORM Mapping
GORM domain classes can be mapped onto many legacy schemas with an Object Relational Mapping DSL (domain specific language). The following sections takes you through what is possible with the ORM DSL.
None of this is necessary if you are happy to stick to the conventions defined by GORM for table names, column names and so on. You only needs this functionality if you need to tailor the way GORM maps onto legacy schemas or configures caching |
Custom mappings are defined using a static mapping
block defined within your domain class:
class Person {
...
static mapping = {
version false
autoTimestamp false
}
}
You can also configure global mappings in application.groovy
(or an external config file) using this setting:
grails.gorm.default.mapping = {
version false
autoTimestamp false
}
It has the same syntax as the standard mapping
block but it applies to all your domain classes! You can then override these defaults within the mapping
block of a domain class.
8.2.1 Table and Column Names
Table names
The database table name which the class maps to can be customized using the table
method:
class Person {
...
static mapping = {
table 'people'
}
}
In this case the class would be mapped to a table called people
instead of the default name of person
.
Column names
It is also possible to customize the mapping for individual columns onto the database. For example to change the name you can do:
class Person {
String firstName
static mapping = {
table 'people'
firstName column: 'First_Name'
}
}
Here firstName
is a dynamic method within the mapping
Closure that has a single Map parameter. Since its name corresponds to a domain class persistent field, the parameter values (in this case just "column"
) are used to configure the mapping for that property.
Column type
GORM supports configuration of Hibernate types with the DSL using the type attribute. This includes specifying user types that implement the Hibernate org.hibernate.usertype.UserType interface, which allows complete customization of how a type is persisted. As an example if you had a PostCodeType
you could use it as follows:
class Address {
String number
String postCode
static mapping = {
postCode type: PostCodeType
}
}
Alternatively if you just wanted to map it to one of Hibernate’s basic types other than the default chosen by GORM you could use:
class Address {
String number
String postCode
static mapping = {
postCode type: 'text'
}
}
This would make the postCode
column map to the default large-text type for the database you’re using (for example TEXT or CLOB).
See the Hibernate documentation regarding Basic Types for further information.
Many-to-One/One-to-One Mappings
In the case of associations it is also possible to configure the foreign keys used to map associations. In the case of a many-to-one or one-to-one association this is exactly the same as any regular column. For example consider the following:
class Person {
String firstName
Address address
static mapping = {
table 'people'
firstName column: 'First_Name'
address column: 'Person_Address_Id'
}
}
By default the address
association would map to a foreign key column called address_id
. By using the above mapping we have changed the name of the foreign key column to Person_Adress_Id
.
One-to-Many Mapping
With a bidirectional one-to-many you can change the foreign key column used by changing the column name on the many side of the association as per the example in the previous section on one-to-one associations. However, with unidirectional associations the foreign key needs to be specified on the association itself. For example given a unidirectional one-to-many relationship between Person
and Address
the following code will change the foreign key in the address
table:
class Person {
String firstName
static hasMany = [addresses: Address]
static mapping = {
table 'people'
firstName column: 'First_Name'
addresses column: 'Person_Address_Id'
}
}
If you don’t want the column to be in the address
table, but instead some intermediate join table you can use the joinTable
parameter:
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']
}
}
Many-to-Many Mapping
GORM, by default maps a many-to-many association using a join table. For example consider this many-to-many association:
class Group {
...
static hasMany = [people: Person]
}
class Person {
...
static belongsTo = Group
static hasMany = [groups: Group]
}
In this case GORM will create a join table called group_person
containing foreign keys called person_id
and group_id
referencing the person
and group
tables. To change the column names you can specify a column within the mappings for each class.
class Group {
...
static mapping = {
people column: 'Group_Person_Id'
}
}
class Person {
...
static mapping = {
groups column: 'Group_Group_Id'
}
}
You can also specify the name of the join table to use:
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'
}
}
8.2.2 Caching Strategy
Setting up caching
Hibernate features a second-level cache with a customizable cache provider. This needs to be configured in the app/conf/application.yml
file as follows:
hibernate:
cache:
use_second_level_cache: true
provider_class: net.sf.ehcache.hibernate.EhCacheProvider
region:
factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory
You can customize any of these settings, for example to use a distributed caching mechanism.
For further reading on caching and in particular Hibernate’s second-level cache, refer to the Hibernate documentation on the subject. |
Caching instances
Call the cache
method in your mapping block to enable caching with the default settings:
class Person {
...
static mapping = {
table 'people'
cache true
}
}
This will configure a 'read-write' cache that includes both lazy and non-lazy properties. You can customize this further:
class Person {
...
static mapping = {
table 'people'
cache usage: 'read-only', include: 'non-lazy'
}
}
Caching associations
As well as the ability to use Hibernate’s second level cache to cache instances you can also cache collections (associations) of objects. For example:
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
}
This will enable a 'read-write' caching mechanism on the addresses
collection. You can also use:
cache: 'read-write' // or 'read-only' or 'transactional'
to further configure the cache usage.
Caching Queries
In order for the results of queries to be cached, you must enable caching in your mapping:
hibernate:
cache:
use_query_cache: true
To enable query caching for all queries created by dynamic finders, GORM etc. you can specify:
hibernate:
cache:
queries: true # This implicitly sets `use_query_cache=true`
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])
You can also cache criteria queries:
def people = Person.withCriteria {
like('firstName', 'Fr%')
cache true
}
Cache usages
Below is a description of the different cache settings and their usages:
-
read-only
- If your application needs to read but never modify instances of a persistent class, a read-only cache may be used. -
read-write
- If the application needs to update data, a read-write cache might be appropriate. -
nonstrict-read-write
- If the application only occasionally needs to update data (i.e. if it is very unlikely that two transactions would try to update the same item simultaneously) and strict transaction isolation is not required, anonstrict-read-write
cache might be appropriate. -
transactional
- Thetransactional
cache strategy provides support for fully transactional cache providers such as JBoss TreeCache. Such a cache may only be used in a JTA environment and you must specifyhibernate.transaction.manager_lookup_class
in theapp/conf/application.groovy
file’shibernate
config.
8.2.3 Inheritance Strategies
By default GORM classes use table-per-hierarchy
inheritance mapping. This has the disadvantage that columns cannot have a NOT-NULL
constraint applied to them at the database level. If you would prefer to use a table-per-subclass
inheritance strategy you can do so as follows:
class Payment {
Integer amount
static mapping = {
tablePerHierarchy false
}
}
class CreditCardPayment extends Payment {
String cardNumber
}
The mapping of the root Payment
class specifies that it will not be using table-per-hierarchy
mapping for all child classes.
8.2.4 Custom Database Identity
You can customize how GORM generates identifiers for the database using the DSL. By default GORM relies on the native database mechanism for generating ids. This is by far the best approach, but there are still many schemas that have different approaches to identity.
To deal with this Hibernate defines the concept of an id generator. You can customize the id generator and the column it maps to as follows:
class Person {
...
static mapping = {
table 'people'
version false
id generator: 'hilo',
params: [table: 'hi_value',
column: 'next_value',
max_lo: 100]
}
}
In this case we’re using one of Hibernate’s built in 'hilo' generators that uses a separate table to generate ids.
For more information on the different Hibernate generators refer to the Hibernate reference documentation |
Although you don’t typically specify the id
field (GORM adds it for you) you can still configure its mapping like the other properties. For example to customise the column for the id property you can do:
class Person {
...
static mapping = {
table 'people'
version false
id column: 'person_id'
}
}
8.2.5 Composite Primary Keys
GORM supports the concept of composite identifiers (identifiers composed from 2 or more properties). It is not an approach we recommend, but is available to you if you need it:
import org.apache.commons.lang.builder.HashCodeBuilder
class Person implements Serializable {
String firstName
String lastName
boolean equals(other) {
if (!(other instanceof Person)) {
return false
}
other.firstName == firstName && other.lastName == lastName
}
int hashCode() {
def builder = new HashCodeBuilder()
builder.append firstName
builder.append lastName
builder.toHashCode()
}
static mapping = {
id composite: ['firstName', 'lastName']
}
}
The above will create a composite id of the firstName
and lastName
properties of the Person class. To retrieve an instance by id you use a prototype of the object itself:
def p = Person.get(new Person(firstName: "Fred", lastName: "Flintstone"))
println p.firstName
Domain classes mapped with composite primary keys must implement the Serializable
interface and override the equals
and hashCode
methods, using the properties in the composite key for the calculations. The example above uses a HashCodeBuilder
for convenience but it’s fine to implement it yourself.
Another important consideration when using composite primary keys is associations. If for example you have a many-to-one association where the foreign keys are stored in the associated table then 2 columns will be present in the associated table.
For example consider the following domain class:
class Address {
Person person
}
In this case the address
table will have an additional two columns called person_first_name
and person_last_name
. If you wish the change the mapping of these columns then you can do so using the following technique:
class Address {
Person person
static mapping = {
columns {
person {
column name: "FirstName"
column name: "LastName"
}
}
}
}
8.2.6 Database Indices
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. With GORM’s DSL you can specify which columns are used in which indexes:
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'
}
}
Note that you cannot have any spaces in the value of the index
attribute; in this example index:'Name_Idx, Address_Index'
will cause an error.
8.2.7 Optimistic Locking and Versioning
As discussed in the section on Optimistic and Pessimistic Locking, by default GORM uses optimistic locking and automatically injects a version
property into every class which is in turn mapped to a version
column at the database level.
If you’re mapping to a legacy schema that doesn’t have version columns (or there’s some other reason why you don’t want/need this feature) you can disable this with the version
method:
class Person {
...
static mapping = {
table 'people'
version false
}
}
If you disable optimistic locking you are essentially on your own with regards to concurrent updates and are open to the risk of users losing data (due to data overriding) unless you use pessimistic locking |
Version columns types
By default GORM maps the version
property as a Long
that gets incremented by one each time an instance is updated. But Hibernate also supports using a Timestamp
, for example:
import java.sql.Timestamp
class Person {
...
Timestamp version
static mapping = {
table 'people'
}
}
There’s a slight risk that two updates occurring at nearly the same time on a fast server can end up with the same timestamp value but this risk is very low. One benefit of using a Timestamp
instead of a Long
is that you combine the optimistic locking and last-updated semantics into a single column.
8.2.8 Eager and Lazy Fetching
Lazy Collections
As discussed in the section on Eager and Lazy fetching, GORM collections are lazily loaded by default but you can change this behaviour with the ORM DSL. There are several options available to you, but the most common ones are:
-
lazy: false
-
fetch: 'join'
and they’re used like this:
class Person {
String firstName
Pet pet
static hasMany = [addresses: Address]
static mapping = {
addresses lazy: false
pet fetch: 'join'
}
}
class Address {
String street
String postCode
}
class Pet {
String name
}
The first option, lazy: false
, ensures that when a Person
instance is loaded, its addresses
collection is loaded at the same time with a second SELECT. The second option is basically the same, except the collection is loaded with a JOIN rather than another SELECT. Typically you want to reduce the number of queries, so fetch: 'join'
is the more appropriate option. On the other hand, it could feasibly be the more expensive approach if your domain model and data result in more and larger results than would otherwise be necessary.
For more advanced users, the other settings available are:
-
batchSize: N
-
lazy: false, batchSize: N
where N is an integer. These let you fetch results in batches, with one query per batch. As a simple example, consider this mapping for Person
:
class Person {
String firstName
Pet pet
static mapping = {
pet batchSize: 5
}
}
If a query returns multiple Person
instances, then when we access the first pet
property, Hibernate will fetch that Pet
plus the four next ones. You can get the same behaviour with eager loading by combining batchSize
with the lazy: false
option.
You can find out more about these options in the Hibernate user guide. Note that ORM DSL does not currently support the "subselect" fetching strategy.
Lazy Single-Ended Associations
In GORM, one-to-one and many-to-one associations are by default lazy. Non-lazy single ended associations can be problematic when you load many entities because each non-lazy association will result in an extra SELECT statement. If the associated entities also have non-lazy associations, the number of queries grows significantly!
Use the same technique as for lazy collections to make a one-to-one or many-to-one association non-lazy/eager:
class Person {
String firstName
}
class Address {
String street
String postCode
static belongsTo = [person: Person]
static mapping = {
person lazy: false
}
}
Here we configure GORM to load the associated Person
instance (through the person
property) whenever an Address
is loaded.
Lazy Associations and Proxies
Hibernate uses runtime-generated proxies to facilitate single-ended lazy associations; Hibernate dynamically subclasses the entity class to create the proxy.
Consider the previous example but with a lazily-loaded person
association: Hibernate will set the person
property to a proxy that is a subclass of Person
. When you call any of the getters (except for the id
property) or setters on that proxy, Hibernate will load the entity from the database.
Unfortunately this technique can produce surprising results. Consider the following example classes:
class Pet {
String name
}
class Dog extends Pet {
}
class Person {
String name
Pet pet
}
Proxies can have confusing behavior when combined with inheritance. Because the proxy is only a subclass of the parent class, any attempt to cast or access data on the subclass will fail. Assuming we have a single Person
instance with a Dog
as the pet
.
The code below will not fail because directly querying the Pet
table does not require the resulting objects to be proxies because they are not lazy.
def pet = Pet.get(1)
assert pet instanceof Dog
The following code will fail because the association is lazy and the pet
instance is a proxy.
def person = Person.get(1)
assert person.pet instanceof Dog
If the only goal is to check if the proxy is an instance of a class, there is one helper method available to do so that works with proxies. Take special care in using it though because it does cause a call to the database to retrieve the association data.
def person = Person.get(1)
assert person.pet.instanceOf(Dog)
There are a couple of ways to approach this issue. The first rule of thumb is that if it is known ahead of time that the association data is required, join the data in the query of the Person
. For example, the following assertion is true.
def person = Person.where { id == 1 }.join("pet").get()
assert person.pet instanceof Dog
In the above example the pet
association is no longer lazy because it is being retrieved along with the Person
and thus no proxies are necessary. There are cases when it makes sense for a proxy to be returned, mostly in the case where its impossible to know if the data will be used or not. For those cases in order to access properties of the subclasses, the proxy must be unwrapped. To unwrap a proxy inject an instance of ProxyHandler and pass the proxy to the unwrap
method.
def person = Person.get(1)
assert proxyHandler.unwrap(person.pet) instanceof Dog
For cases where dependency injection is impractical or not available, a helper method GrailsHibernateUtil.unwrapIfProxy(Object) can be used instead.
Unwrapping a proxy is different than initializing it. Initializing a proxy simply populates the underlying instance with data from the database, however unwrapping a returns the inner target.
8.2.9 Custom Cascade Behaviour
As described in the section on cascading updates, the primary mechanism to control the way updates and deletes cascade from one association to another is the static belongsTo property.
However, the ORM DSL gives you complete access to Hibernate’s transitive persistence capabilities using the cascade
attribute.
Valid settings for the cascade attribute include:
-
merge
- merges the state of a detached association -
save-update
- cascades only saves and updates to an association -
delete
- cascades only deletes to an association -
lock
- useful if a pessimistic lock should be cascaded to its associations -
refresh
- cascades refreshes to an association -
evict
- cascades evictions (equivalent todiscard()
in GORM) to associations if set -
all
- cascade all operations to associations -
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. Children are also deleted when the parent is.
To specify the cascade attribute simply define one or more (comma-separated) of the aforementioned settings as its value:
class Person {
String firstName
static hasMany = [addresses: Address]
static mapping = {
addresses cascade: "all-delete-orphan"
}
}
class Address {
String street
String postCode
}
8.2.10 Custom Hibernate Types
You saw in an earlier section that you can use composition (with the embedded
property) to break a table into multiple objects. You can achieve a similar effect with Hibernate’s custom user types. These are not domain classes themselves, but plain Java or Groovy classes. Each of these types also has a corresponding "meta-type" class that implements org.hibernate.usertype.UserType.
The Hibernate reference manual has some information on custom types, but here we will focus on how to map them in GORM. Let’s start by taking a look at a simple domain class that uses an old-fashioned (pre-Java 1.5) type-safe enum class:
class Book {
String title
String author
Rating rating
static mapping = {
rating type: RatingUserType
}
}
All we have done is declare the rating
field the enum type and set the property’s type in the custom mapping to the corresponding UserType
implementation. That’s all you have to do to start using your custom type. If you want, you can also use the other column settings such as "column" to change the column name and "index" to add it to an index.
Custom types aren’t limited to just a single column - they can be mapped to as many columns as you want. In such cases you explicitly define in the mapping what columns to use, since Hibernate can only use the property name for a single column. Fortunately, GORM lets you map multiple columns to a property using this syntax:
class Book {
String title
Name author
Rating rating
static mapping = {
author type: NameUserType, {
column name: "first_name"
column name: "last_name"
}
rating type: RatingUserType
}
}
The above example will create "first_name" and "last_name" columns for the author
property. 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, override the column’s SQL type using the sqlType
attribute:
class Book {
String title
Name author
Rating rating
static mapping = {
author 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.
8.2.11 Derived Properties
A derived property is one that takes its value from a SQL expression, often but not necessarily based on the value of one or more other persistent properties. Consider a Product class like this:
class Product {
Float price
Float taxRate
Float tax
}
If the tax
property is derived based on the value of price
and taxRate
properties then is probably no need to persist the tax
property. The SQL used to derive the value of a derived property may be expressed in the ORM DSL like this:
class Product {
Float price
Float taxRate
Float tax
static mapping = {
tax formula: 'PRICE * TAX_RATE'
}
}
Note that the formula expressed in the ORM DSL is SQL so references to other properties should relate to the persistence model not the object model, which is why the example refers to PRICE
and TAX_RATE
instead of price
and taxRate
.
With that in place, when a Product is retrieved with something like Product.get(42)
, the SQL that is generated to support that will look something like this:
select
product0_.id as id1_0_,
product0_.version as version1_0_,
product0_.price as price1_0_,
product0_.tax_rate as tax4_1_0_,
product0_.PRICE * product0_.TAX_RATE as formula1_0_
from
product product0_
where
product0_.id=?
Since the tax
property is derived at runtime and not stored in the database it might seem that the same effect could be achieved by adding a method like getTax()
to the Product
class that simply returns the product of the taxRate
and price
properties. With an approach like that you would give up the ability query the database based on the value of the tax
property. Using a derived property allows exactly that. To retrieve all Product
objects that have a tax
value greater than 21.12 you could execute a query like this:
Product.findAllByTaxGreaterThan(21.12)
Derived properties may be referenced in the Criteria API:
Product.withCriteria {
gt 'tax', 21.12f
}
The SQL that is generated to support either of those would look something like this:
select
this_.id as id1_0_,
this_.version as version1_0_,
this_.price as price1_0_,
this_.tax_rate as tax4_1_0_,
this_.PRICE * this_.TAX_RATE as formula1_0_
from
product this_
where
this_.PRICE * this_.TAX_RATE>?
Because the value of a derived property is generated in the database and depends on the execution of SQL code, derived properties may not have GORM constraints applied to them. If constraints are specified for a derived property, they will be ignored. |
8.2.12 Custom Naming Strategy
By default GORM uses Hibernate’s ImprovedNamingStrategy
to convert domain class Class and field names to SQL table and column names by converting from camel-cased Strings to ones that use underscores as word separators. You can customize these on a per-class basis in the mapping
closure but if there’s a consistent pattern you can specify a different NamingStrategy
class to use.
Configure the class name to be used in app/conf/application.groovy
in the hibernate
section, e.g.
dataSource {
pooled = true
dbCreate = "create-drop"
...
}
hibernate {
cache.use_second_level_cache = true
...
naming_strategy = com.myco.myproj.CustomNamingStrategy
}
You can also specify the name of the class and it will be loaded for you:
hibernate {
...
naming_strategy = 'com.myco.myproj.CustomNamingStrategy'
}
A third option is to provide an instance if there is some configuration required beyond calling the default constructor:
hibernate {
...
def strategy = new com.myco.myproj.CustomNamingStrategy()
// configure as needed
naming_strategy = strategy
}
You can use an existing class or write your own, for example one that prefixes table names and column names:
package com.myco.myproj
import org.hibernate.cfg.ImprovedNamingStrategy
import org.hibernate.util.StringHelper
class CustomNamingStrategy extends ImprovedNamingStrategy {
String classToTableName(String className) {
"table_" + StringHelper.unqualify(className)
}
String propertyToColumnName(String propertyName) {
"col_" + StringHelper.unqualify(propertyName)
}
}
8.3 Default Sort Order
You can sort objects using query arguments such as those found in the list method:
def airports = Airport.list(sort:'name')
However, you can also declare the default sort order for a collection in the mapping:
class Airport {
...
static mapping = {
sort "name"
}
}
The above means that all collections of Airport
instances will by default be sorted by the airport name. If you also want to change the sort order, use this syntax:
class Airport {
...
static mapping = {
sort name: "desc"
}
}
Finally, you can configure sorting at the association level:
class Airport {
...
static hasMany = [flights: Flight]
static mapping = {
flights sort: 'number', order: 'desc'
}
}
In this case, the flights
collection will always be sorted in descending order of flight number.
These mappings will not work for default unidirectional one-to-many or many-to-many relationships because they involve a join table. See this issue for more details. Consider using a SortedSet or queries with sort parameters to fetch the data you need.
|