'Advanced GORM features: inheritance, embedded data, maps and lists storing' post illustration

Advanced GORM features: inheritance, embedded data, maps and lists storing

avatar

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:

1
2
3
4
5
6
7
8
9
class Product {
    String name
    static hasMany = [reviews: Review]
}

class Review {
    String text
    static belongsTo = [product: Product]
}

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:

1
2
3
4
5
6
7
// 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)

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:

1
2
3
4
5
6
7
8
9
10
11
class RatedReview extends Review {
    Integer rating
    static constraints {
        rating(nullable: false)
    }
}

class Product {
    String name
    static hasMany = [reviews: Review, ratedReviews: RatedReview]
}

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:

1
2
product.ratedReviews
RatedReview.findAllByProduct(product)

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:

Review.groovy
1
2
3
static mapping = {
    tablePerHierarchy false
}

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:

1
2
product.reviews
RatedReview.findAllByProduct(product)

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class User {
    String email
    String password
    UserStatus status
    Profile profile

    static embedded = ['profile']
}

class Profile {
    String nickname
    Date birthday
    Integer height
}

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:

1
2
new User(username: 'user@gmail.com', password: 'my_password', status: UserStatus.ACTIVE,
         profile: new Profile(nickname: 'Flash', birthday: new Date(), height: 175)).save()

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":

1
2
new Product(name: 'Samsung E730', links: ['http://link1.com', 'http://link2.com'])
    .save(failOnError: true)

To get the list to be added to your database, you should use the hasMany static property:

1
2
3
4
5
6
class Product {
    String name
    List links

    static hasMany = [links: String]
}

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:

1
2
3
def review = new Review(text: 'Cool!')
product.addToReviews(review)
product.save()

Now, let's find a way to add the parameters map to our products. It is easy enough:

1
2
3
4
class Product {
    /* ... */
    Map parameters
}

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 id
  • parameters_idx - map key
  • parameters_elt - map value

So you can now add products as follows:

1
2
3
4
5
6
7
8
new Product(name: 'Samsung E730',
            links: ['http://link1.com', 'http://link2.com'],
            parameters: [
                height: '90mm',
                width: '45mm',
                thickness: '23mm',
                polyphony: '64-tone'
            ]).save()

If you're looking for a developer or considering starting a new project,
we are always ready to help!