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
- static mapping{…} block
- transient variable
- One to One mapping
- One to many mapping
- Many to many mapping
- Functions [beforeInsert, beforeUpdate, beforeDelete, beforeValidate, afterInsert, afterUpdate, afterDelete, onLoad]
- and more!
If want to know more, goto the GORM hibernate manual.