Table of Contents

Domain

In Grails or Spring, Domain is an plain old Java object (POJO) that do the mapping between your app, and the database. In Grails by conversion, we put the domain objects under the directory /grails-ap/domain/YOUR_PACKAGE_NAME/, and Grails will do the rest for us.

Create a Domain Object

There are two ways to do so, and they are listed as follow:

In Grails Console

In Grails console, type create-domain-class YOUR_DOMAIN_NAME, and a domain skeleton would be created.

grails> create-domain-class Book
| Created grails-app/domain/hello/Book.groovy
| Created src/test/groovy/hello/BookSpec.groovy

An domain skeleton created by the command.

class Book {
    static constraints = {
    }
}

In Intellij IDEA Ultimate

Right click the domain directory in the file viewer, select New, and than select Grails Domain Class. Input the name of your domain class, and a class skeleton like the above will be created.

The Mapping

Basic

Let's focus on creating a POJO first. Assume our book object has a title, an author, and a release date. We would do:

class Book {
    String title
    String author
    @BindingFormat('yyyy-MM-dd')
    Date releaseDate
    static constraints = {
    }
}

Just like how we create a POJO. The @BindingFormat('yyyy-MM-dd') is usually when we try to map a HTTP request form data to this object. Nothing special. Run the app, and goto the path http://localhost:8080/dbconsole/. The default JDBC URL should be jdbc:h2:mem:devDb (set inside application.yml). You should able to see the newly created domain has a corresponding table in your database. Note that we did not added ID, and VERSION fields, grails added these for us. The VERSION field are useful for hibernate optimistic locking, or some sort of version console for race condition in your UI for multiple users.

Creating Instance, and Save it

Just creating the Jave object, and call save() on it.

class BookController {
    def index() { 
        render("index")
    }

    def add(){
        Book book = new Book(title: "ABC", author: "Tom Lee", releaseDate: "2010-03-01")
        book.save()
        render("Book is created")
    }
}

Save might Fail Silently

NOTE that the save() function fail silently. Instead of crashing or throwing exception, it ignore it, and continue to run your code. To illustrate it, try to remove title field for the above code, and restart the app, and rerun this code.

Book book = new Book(author: "Tom Lee", releaseDate: "2010-03-01")

You will still see the page said Book is created, but in the dbconsole, you see nothing is created.

There are two ways to solve this problem, and it is personal choice of which one to user:

Validate It First

def add() {
    Book book = new Book(author: "Tom Lee", releaseDate: "2010-03-01")
    if (book.validate()) {
        book.save()
        render("Book is created")
    } else {
        render("Book cannot be created with errors:<br>" + book.errors)
    }
}

try catch, failOnError:true

def add() {
    Book book = new Book(author: "Tom Lee", releaseDate: "2010-03-01")
    try {
        book.save(failOnError: true)
        render("Book is created")
    } catch (Exception e) {
        render("Book cannot be created with errors:<br>" + book.errors)
    }
}

The Error Output

Readable by a programmer, but sure non-readable by a regular user.

Book cannot be created with errors:
org.grails.datastore.mapping.validation.ValidationErrors: 1 errors Field error in object 'hello2.Book' on field 'title': rejected value [null]; codes [hello2.Book.title.nullable.error.hello2.Book.title,hello2.Book.title.nullable.error.title,hello2.Book.title.nullable.error.java.lang.String,hello2.Book.title.nullable.error,book.title.nullable.error.hello2.Book.title,book.title.nullable.error.title,book.title.nullable.error.java.lang.String,book.title.nullable.error,hello2.Book.title.nullable.hello2.Book.title,hello2.Book.title.nullable.title,hello2.Book.title.nullable.java.lang.String,hello2.Book.title.nullable,book.title.nullable.hello2.Book.title,book.title.nullable.title,book.title.nullable.java.lang.String,book.title.nullable,nullable.hello2.Book.title,nullable.title,nullable.java.lang.String,nullable]; arguments [title,class hello2.Book]; default message [Property [{0}] of class [{1}] cannot be null]

Auto Timestamp

By adding dateCreated, and lastUpdated, Grails will automatically update the date the object created, and updated in the database.

class Book {
    String title
    String author
    @BindingFormat('yyyy-MM-dd')
    Date releaseDate

    Date dateCreated
    Date lastUpdated
    static constraints = {
    }
}

The Constraints

You might notice there is a static constraints = {} in the domain. This is use to set the constraints on how to map the fields in the database. If we put nothing there, by default, Grails assumes every field is NON-NULL. Remember we were not able to save() when we omitted the author field? It is because the database require this field to be non-null.

Now lets us add an ISBN field in the domain. As we know, it is possible that book does not have an ISBN. So our constraints will look like:

class Book {
    String title
    String author
    @BindingFormat('yyyy-MM-dd')
    Date releaseDate
    String isbn

    Date dateCreated
    Date lastUpdated

    static constraints = {
        isbn nullable: true, blank: true, maxSize: 13, unique: true
    }
}

This tells the database that the isbn field can be null or blank (empty string), and it length contains 13 chars, and it is a unique field, meaning this value is unique across the whole table.

Actually it would be a good idea to constraint all fields so that we know what things are going to be set in the database other than guess what Grails does by convention, so we do:

class Book {
    String title
    String author
    @BindingFormat('yyyy-MM-dd')
    Date releaseDate
    String isbn

    Date dateCreated
    Date lastUpdated

    static constraints = {
        title nullable: false, blank: false, maxSize: 256
        author nullable: false, blank: false, maxSize: 256
        releaseDate nullable: false
        isbn nullable: true, blank: true, maxSize: 13, unique: true
    }
}

Now when we save a book object, title, author, release date must be set, and isbn is optional. But if we set the isbn, it might not exist in the database, or we will not able to save it.

What Field Can be Use in a Domain

This is depending on which database we use. For H2, the result as follow. You can of course change the size of byte[], String in the static constraints{} block.

class WhatField {
    Boolean typeBoolean
    Byte typeByte
    Character typeChar
    String typeString
    Integer typeInteger
    Short typeShort
    Long typeLong
    Float  typeFloat
    Double typeDouble
    Date typeDate
    byte[] typaAttachment
    Book typeBook
    List<Book> typeListOfBooks

    static constraints = {
    }
}

Multiple Domains

Now we rewrite the domains, and have

class Author {
    String name
    String address

    static constraints = {
        name  nullable: false, blank: false, maxSize: 255
        address nullable: false, blank: false, maxSize: 512
    }
}

class Book {
    String title
    Author author
    @BindingFormat('yyyy-MM-dd')
    Date releaseDate
    String isbn

    Date dateCreated
    Date lastUpdated

    static constraints = {
        title nullable: false, blank: false, maxSize: 256
        author nullable: false
        releaseDate nullable: false
        isbn nullable: true, blank: true, maxSize: 13, unique: true
    }
}

To save a book with an author, we can do:

def add() {
    Author author = new Author(name: "Tom Lee", address: "Some where out there, NY, USA")
    Book book = new Book(title: "ABC", author: author, releaseDate: "2010-03-01")
    try {
        book.save(failOnError: true)
        render("Book is created")
    } catch (Exception e) {
        render("Book cannot be created with errors:<br>" + book.errors)
    }
}

Get Domain Data From Database

We can use GORM to get the data from the database. After called the add(), we can call show() by http://127.0.0.1/book/show. The dynamic finder findBy…() or findAllBy…() functions are provided by GORM. It is a huge topic, and we will cover more in Mastering GORM Dynamic Finders.

class BookController {

    def index() {
        render("index")
    }

    def add() {
        Author author = new Author(name: "Tom Lee", address: "Some where out there, NY, USA")
        Book book = new Book(title: "ABC", author: author, releaseDate: "2010-03-01")
        try {
            book.save(failOnError: true)
            render("Book is created")
        } catch (Exception e) {
            render("Book cannot be created with errors:<br>" + book.errors)
        }
    }

    def show(){
        Book book = Book.findById(1)
        render("Title:${book.title} <br>" +
                "Release Date: ${book.releaseDate}<br>" +
                "ISBN:${book.isbn}<br>" +
                "Author: ${book.author.name}, ${book.author.address}")
    }
}

The result should be

Title:ABC
Release Date: 2010-03-01 00:00:00.0
ISBN:null
Author: Tom Lee, Some where out there, NY, USA

Domain Contains List of Other Domain

class Book {
    String title
    List<Author> authors
    @BindingFormat('yyyy-MM-dd')
    Date releaseDate
    String isbn

    Date dateCreated
    Date lastUpdated

    static constraints = {
        title nullable: false, blank: false, maxSize: 256
        releaseDate nullable: false
        isbn nullable: true, blank: true, maxSize: 13, unique: true
    }
}

class Author {
    String name
    String address

    static constraints = {
        name  nullable: false, blank: false, maxSize: 255
        address nullable: false, blank: false, maxSize: 512
    }
}

Now we can create this book object in the controller like:

class BookController {

    def index() {
        render("index")
    }

    def add() {
        List<Author> authors = []
        Author author1 = new Author(name: "Tom Lee", address: "Some where out there, NY, USA")
        Author author2 = new Author(name: "Jack Kovsky", address: "Some where out there, Russia")
        authors.add(author1)
        authors.add(author2)
        Book book = new Book(title: "ABC", releaseDate: "2010-03-01", isbn: "1234567890123")
        book.authors = authors
        try {
            book.save(failOnError: true)
            render("Book is created")
        } catch (Exception e) {
            render("Book cannot be created with errors:<br>" + book.errors)
        }
    }

    def show(){
        Book book = Book.findById(1)
        StringBuilder sb = new StringBuilder()
        sb.append("Title:${book.title} <br>" +
                "Release Date: ${book.releaseDate}<br>" +
                "ISBN:${book.isbn}<br>")
        List<Author> authors = book.getAuthors()
        authors.each {
            sb.append("Author: ${it.name}, ${it.address}<br>")
        }
        render(sb.toString())
    }
}

The result:

Title:ABC
Release Date: 2010-03-01 00:00:00.0
ISBN:1234567890123
Author: Tom Lee, Some where out there, NY, USA
Author: Jack Kovsky, Some where out there, Russia

In the database, three tables were created. The AUTHOR, BOOK, and a table that links these two tables called BOOK_AUTHOR

Thing We Skipped Here

If want to know more, goto the GORM hibernate manual.