In my previous GORM related article, “Association Types in GORM“, I have described how to create different types of relationships using Grails ORM. In this article, I would like to talk about several advanced GORM features that may help you in the development process, such as:
- domain classes inheritance
- embedded data
- maps and lists storing
Domain classes inheritance
Polymorphic queries
Suppose that you have the following domain model in your project:
class Product {
String name
static hasMany = [reviews: Review]
}
class Review {
String text
static belongsTo = [product: Product]
}
Code language: JavaScript (javascript)
Users can leave reviews about some products. The desirable feature is to add optional ability to rate products, so you would be able to get either all the reviews or only rated ones. The simplest way that comes to mind first is to add a nullable field rating
to the Review
domain. And if you do this, you’ll be able to get the reviews for a product in the following way:
// Getting all the reviews for a product.
product.reviews
Review.findAllByProduct(product)
// Getting only rated reviews for a product.
product.reviews.findAll { it.rating }
Review.findAllByProductAndRatingIsNotNull(product)
Code language: JavaScript (javascript)
A bit cumbersome, right? To get rid of this bulkiness and make your model more elegant, you can use inheritance. Let’s create new domain class for rated reviews instead of adding new field:
class RatedReview extends Review {
Integer rating
static constraints {
rating(nullable: false)
}
}
class Product {
String name
static hasMany = [reviews: Review, ratedReviews: RatedReview]
}
Code language: JavaScript (javascript)
Additional columns rating
and class
appeared in the review
table. The class
column will contain the com.sysgears.examples.Review
or com.sysgears.examples.RatedReview
string, according to the saved review’s class. You can get all the reviews the same way as before and get rated only reviews using advantages of polymorphic queries:
product.ratedReviews
RatedReview.findAllByProduct(product)
Code language: CSS (css)
Looks more understandable and shorter now.
Inheritance strategies
GORM uses one table per hierarchy by default, that’s why the rated_review
table has not been created. You can change this by setting the tablePerHierarchy
property to false
in your root class:
static mapping = {
tablePerHierarchy false
}
Code language: JavaScript (javascript)
Hereby you have two separate tables – the review
and the rated_review
that store the following data:
+---------------------------------------------------------+ +----------------+ | review | | rated_review | +----+---------+------------+-----------------------------+ +----+-----------+ | id | version | product_id | text | | id | rating | +----+---------+------------+-----------------------------+ +----+-----------+ | 1 | 0 | 1 | Nice product! | | 2 | 5 | | 2 | 0 | 1 | Awesome! Wanna buy it more! | +----+-----------+ +----+---------+------------+-----------------------------+ |
The only restriction of this inheritance strategy is that you can’t use the ratedReviews
reference of the Product
domain anymore, because in this case GORM adds the product_id
field to the rated_review
table, but uses the same column of the review
table. You should delete the ratedReviews
reference, otherwise you’ll get an exception when saving rated reviews, saying that you didn’t specified a product id. But you are still able to get reviews of a product in the following way:
product.reviews
RatedReview.findAllByProduct(product)
Code language: CSS (css)
Note that GORM will use outer and inner joins to get reviews for the certain product: outer join for all the reviews, inner join for the rated ones. Outer joins may cause low query performance, particularly if the class hierarchy is too deep, so don’t abuse using this inheritance strategy. Here’s a table that illustrates different queries:
GORM query | Hibernate query |
product.reviews | select reviews0_.product_id as product3_1_, reviews0_.id as id1_, reviews0_.id as id8_0_, reviews0_.version as version8_0_, reviews0_.product_id as product3_8_0_, reviews0_.text as text8_0_, reviews0_1_.rating as rating9_0_, case when reviews0_1_.id is not null then 1 when reviews0_.id is not null then 0 end as clazz_0_ from review reviews0_ left outer join rated_review reviews0_1_ on reviews0_.id=reviews0_1_.id where reviews0_.product_id=? |
Review.findAllByProduct(product) | select this_.id as id8_0_, this_.version as version8_0_, this_.product_id as product3_8_0_, this_.text as text8_0_, this_1_.rating as rating9_0_, case when this_1_.id is not null then 1 when this_.id is not null then 0 end as clazz_0_ from review this_ left outer join rated_review this_1_ on this_.id=this_1_.id where this_.product_id=? |
RatedReview.findAllByProduct(product) | select this_.id as id8_0_, this_1_.version as version8_0_, this_1_.product_id as product3_8_0_, this_1_.text as text8_0_, this_.rating as rating9_0_ from rated_review this_ inner join review this_1_ on this_.id=this_1_.id where this_1_.product_id=? |
Embedded data
Another useful feature I’d like to talk about is embedded tables. It is a good decision if you want to keep the convenience of domain objects without creating an additional table in the database. For example, you have a User
class (keeps email, password, status, etc.) and want to add some profile info into it, such as nickname, birthday and height. It’s a logical solution to place these fields in a separate class named Profile
, isn’t it? But on the database level, you may want to keep these data in the same user
table. In order to do this, you can use the following approach:
User.groovy
class User {
String email
String password
UserStatus status
Profile profile
static embedded = ['profile']
}
class Profile {
String nickname
Date birthday
Integer height
}
Code language: JavaScript (javascript)
You can add all the necessary constraints to these classes, but both of them should be defined in the same file (User.groovy). Otherwise, you will have an empty profile
table in your database, although the embedding will still be working.
Three columns were added to the user
table: profile_nickname
, profile_birthday
and profile_height
. Finally, you can create users as follows:
new User(username: 'user@gmail.com', password: 'my_password', status: UserStatus.ACTIVE,
profile: new Profile(nickname: 'Flash', birthday: new Date(), height: 175)).save()
Code language: JavaScript (javascript)
Maps and lists storing
Sometimes, there is a need to store simple lists or maps in database. For instance, you may need to store some photo / video links and variable number of parameters for each of your products.
We will talk about the lists first. If you just add the List<String> links
line into the Product
class, nothing will happen and GORM will not save the list to the database, although the next code will be performed “successfully”:
new Product(name: 'Samsung E730', links: ['http://link1.com', 'http://link2.com'])
.save(failOnError: true)
Code language: JavaScript (javascript)
To get the list to be added to your database, you should use the hasMany
static property:
class Product {
String name
List links
static hasMany = [links: String]
}
Code language: JavaScript (javascript)
Check the database: there are no changes in the product
table, but there is a new table – product_links
with the product_id
, links_string
and links_idx
columns. Notice that if you remove the definition of a List
from the Product
class, GORM will save links as a Set
by default and the links_idx
column won’t appear. If you need an ordered set, you can use the SortedSet
instead of the List
.
If you want to store list of objects of another domain class, the elements should be added to the collection before being saved. This allows to avoid the HibernateException
throwing:
def review = new Review(text: 'Cool!')
product.addToReviews(review)
product.save()
Code language: JavaScript (javascript)
Now, let’s find a way to add the parameters
map to our products. It is easy enough:
class Product {
/* ... */
Map parameters
}
Code language: JavaScript (javascript)
As well as in the case with list, new table named product_parameters
will be created in the database with the following columns:
parameters
– stores product idparameters_idx
– map keyparameters_elt
– map value
So you can now add products as follows:
new Product(name: 'Samsung E730',
links: ['http://link1.com', 'http://link2.com'],
parameters: [
height: '90mm',
width: '45mm',
thickness: '23mm',
polyphony: '64-tone'
]).save()
Code language: JavaScript (javascript)