Creating a Grails Multi Project
A web application and a domain plugin used by the web application
Why?
A multi project is useful in some cases. The first and foremost that springs to mind is the need to expose a web application to your normal Internet users using a browser as well as exposing a service providing, say, RSS feeds and JSON and XML data to machines connected to the Internet. All this will come from the same database.
That means both your web application and your service will need the same domain objects. One can of course create a monolithic application that can take care off all those things, but that is frowned upon nowadays. Not to mention that upkeep of such an intertwined bowl of spaghetti will not be easy. So, here's what we will do:
- create a plugin handling all the domain classes and services
- create a web application that will make use of this plugin just mentioned
Later, on your own, you can also create a rest-api application as part of the same project which will also use the same domain plugin.
What you will need
Obviously, you will need to know a reasonable amount about Grails. You will also need to have a working Grails installation on your computer. I used 3.2.4. Having a working Grails installation implies a working JDK installation. Other than that, you will need a terminal window (command window in Windows?) and a decent text editor. You can't go wrong with Atom. It has nearly 6,000 plugins. By the time you read this, maybe more. Among them, a Grails plugin. Look at the retro video on the Atom page. It's both entertaining and enlightening. And short. In the examples that follow, you will see references to Nano, a Linux terminal window text editor. That's used for quick, small updates or entries.
Let's get going
First, create the multi project root directory. In your termincal window do:
$ mkdir domain-example
Now go to your multi project root directory:
$ cd domain-example
Now a quick look at all the Grails profiles that can be used.
$ grails list-profiles
You will see:
| Available Profiles -------------------- * angular - A profile for creating applications using AngularJS * rest-api - Profile for REST API applications * base - The base profile extended by other profiles * angular2 - A profile for creating Grails applications with Angular 2 * plugin - Profile for plugins designed to work across all profiles * profile - A profile for creating new Grails profiles * react - A profile for creating Grails applications with a React frontend * rest-api-plugin - Profile for REST API plugins * web - Profile for Web applications * web-plugin - Profile for Plugins designed for Web applications * webpack - A profile for creating applications with node-based frontends using webpack
When we create a sub-project, we will always use the name of the profile as the first part of the project name. E.g. we will have a web-admin and a plugin-domain sub-projects. You will see later that it comes in handy in the root build.gradle file.
We are in the main or root project file. Let's do:
$ grails create-app web-admin --profile=web
You will get the following feedback:
| Application created at /home/you/working-directory/domain-example/web-admin
Still in the main or root project file, do:
$ grails create-app plugin-domain --profile=plugin
You will get the following feedback:
| Application created at /home/you/working-directory/domain-example/plugin-domain
Remove logback.groovy from domain-example/web-admin/grails-app/conf, otherwise Grails will complain that logback.groovy is on the classpath twice. It is also in the plugin-domain sub-project. All other sub-projects can source it there.
Now you have to do some house-keeping. First off, in the main or root project directory, you have to tell of all the sub-projects in your multi project. You do this in a file called settings.gradle. So, go:
$ echo "include 'web-admin', 'plugin-domain'" >> settings.gradle
If you now open the newly created settings.gradle, using cat or nano, you will see that one sentence in it.
You also have to tell web-admin to use plugin-domain. You do that in the buid.gradle file of web-admin. So, do this:
$ nano web-admin/build.gradle
Just above the dependencies, put in and save:
grails { plugins { compile project(':plugin-domain') } }dependencies { compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-starter-tomcat"
The next thing you have to do is to generate a Gradle wrapper. For that, you need a build.gradle file in the main or root directory of your project. You are still there in your terminal window. So, do this:
$ touch build.gradle
Now do:
$ nano build.gradle
and add the following to the top of your build.gradle file and save:
task wrapper(type: Wrapper) { gradleVersion = gradleWrapperVersion }
To supply the above mentioned gradleWrapperVersion, copy the file gradle.properties from any of the two sub-projects to the main project, like so:
$ cp web-admin/gradle.properties .
Note the above command ending on a full-stop one space removed from the last letter. You can now delete the gradle.properties files in the two sub-projects:
$ rm web-admin/gradle.properties
$ rm plugin-domain/gradle.properties
All you now have to do to create the Gradle wrapper is, in the root or main project directory, run:
$ gradle wrapper
Afterwards, you can look at what's in the main directory:
$ ls
In my terminal window, the plain files are white, directories blue and executable files yellow. The image below shows the output of the ls command above.
There is a final bit of house keeping to be done.
Keeping Things DRY
You will surely have noticed that there are three build.gradle files, one in the main or root project and one in each sub-project. If you open these files side by side in your trusty text editor, you will notice that there is much duplication in the two files of the sub-projects. All the duplicated content can be moved out of the sub-project build.gradle files into that of the root project. As a build.gradle file is a plain Groovy file, one can put conditional logic into the root build.gradle file to generate contents dependent on the type of sub-projects. This is where the naming convention of sub-projects talked about at the start comes in handy. Here are my three build.gradle files after neatness sets in:
The plugin-domain build.gradle file
version "0.1" group "plugin.domain" dependencies { profile "org.grails.profiles:plugin" provided "org.grails:grails-plugin-services" provided "org.grails:grails-plugin-domain-class" } // enable if you wish to package this plugin as a standalone application bootRepackage.enabled = false grailsPublish { // TODO: Provide values here user = 'user' key = 'key' githubSlug = 'foo/bar' license { name = 'Apache-2.0' } title = "My Plugin" desc = "Full plugin description" developers = [johndoe:"John Doe"] portalUser = "" portalPassword = "" }
The web-admin build.gradle file
version "0.1" group "web.admin" grails { plugins { compile project(':plugin-domain') } } dependencies { compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-starter-tomcat" compile "org.grails:grails-dependencies" compile "org.grails:grails-web-boot" compile "org.grails.plugins:cache" compile "org.grails.plugins:scaffolding" compile "org.grails.plugins:hibernate5" compile "org.hibernate:hibernate-core:5.1.2.Final" compile "org.hibernate:hibernate-ehcache:5.1.2.Final" profile "org.grails.profiles:web" runtime "com.bertramlabs.plugins:asset-pipeline-grails:2.11.6" runtime "com.h2database:h2" testCompile "org.grails.plugins:geb" testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" testRuntime "net.sourceforge.htmlunit:htmlunit:2.18" } assets { minifyJs = true minifyCss = true }
The root project build.gradle file
buildscript { ext { grailsVersion = project.grailsVersion } repositories { mavenLocal() maven { url "https://repo.grails.org/grails/core" } } dependencies { subprojects { project -> classpath "org.grails:grails-gradle-plugin:$grailsVersion" if ( project.name.startsWith('web') ) { classpath "com.bertramlabs.plugins:asset-pipeline-gradle:2.11.6" classpath "org.grails.plugins:hibernate5:6.0.4" } } } } ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion } task wrapper(type: Wrapper) { gradleVersion = gradleWrapperVersion } subprojects { project -> apply plugin:"eclipse" apply plugin:"idea" if ( project.name.startsWith('web') ) { apply plugin:"war" apply plugin:"org.grails.grails-web" apply plugin:"org.grails.grails-gsp" apply plugin:"asset-pipeline" } else if ( project.name.startsWith('plugin') ) { apply plugin:"org.grails.grails-plugin" apply plugin:"org.grails.grails-plugin-publish" } repositories { mavenLocal() maven { url "https://repo.grails.org/grails/core" } } dependencyManagement { imports { mavenBom "org.grails:grails-bom:$grailsVersion" } applyMavenExclusions false } dependencies { compile "org.springframework.boot:spring-boot-starter-logging" compile "org.springframework.boot:spring-boot-autoconfigure" compile "org.grails:grails-core" console "org.grails:grails-console" testCompile "org.grails:grails-plugin-testing" } bootRun { jvmArgs('-Dspring.output.ansi.enabled=always') } }
Note the subprojects closures in this build.gradle file. Certain things should just be rendered in the appropriate sub-project build.gradle files. If they are rendered in the root build.gradle file, your projects won't run. Also note the conditional logic inside these closures. You can see where the naming convention of the sub-project names comes in handy. If you have a web-plugin sub-project, put that condition first in the subprojects closure to be followed at any stage by a web sub-project in an if else condition.
Your Domain Classes/Objects
At long last. Change into your plugin-domain directory:
$ cd plugin-domain
Let's see what we can do here with Grails:
$ grails help
You will see that many of the Grails commands you expect are not there. For one thing, you can generate neither controllers nor views. See the output below:
Usage (optionals marked with *):' grails [environment]* [target] [arguments]*' | Examples: $ grails dev run-app $ grails create-app books | Available Commands (type grails help 'command-name' for more info): | Command Name Command Description ---------------------------------------------------------------------------------------------------- assemble Creates a JAR or WAR archive for production deployment bug-report Creates a zip file that can be attached to issue reports for the current project clean Cleans a Grails application's compiled sources compile Compiles a Grails application console Runs the Grails interactive console create-command Creates an Application Command create-domain-class Creates a Domain Class create-script Creates a Grails script create-service Creates a Service create-unit-test Creates a unit test dependency-report Prints out the Grails application's dependencies gradle Allows running of Gradle tasks help Prints help information for a specific command install Installs a Grails application or plugin into the local Maven cache list-plugins Lists available plugins from the Plugin Repository open Opens a file in the project package-plugin Packages the plugin into a JAR file plugin-info Prints information about the given plugin publish-plugin Publishes the plugin to the Grails central repository run-app Runs a Grails application run-command Executes Grails commands run-script Executes Groovy scripts in a Grails context shell Runs the Grails interactive shell stats Prints statistics about the project stop-app Stops the running Grails application test-app Runs the applications tests
Authentication and Authorisation
Few projects nowadays won't have the above. For us, that means Spring Security Core. Add both of the following to the dependencies section of the build.gradle file in the plugin-domain sub-project.
compile "org.grails.plugins:spring-security-core:3.1.1" compile "commons-lang:commons-lang:2.0"
The second dependency is usually not necessary with Spring Security Core. But having Spring Security Core live within this plugin, it is needed. Eventually, the UserRole domain class will be generated by the s2-quickstart script. Inside, the class will have this import statement:
import org.apache.commons.lang.builder.HashCodeBuilder
This import won't be found. That is what commons-lang provides.
To get Spring Security Core installed into the plugin-domain sub-project and the plugin plugin-domain in turn installed into the web-admin sub-project, change to the sub-project web-admin
directory:
$ cd ../web-admin
and enter:
$ grails run-app --verbose
Note that now with the Gradle wrapper installed, you can be in the multi-project root directory and do:
$ ./gradlew web-admin:bootRun
Either way, the output will show how the plugin-domain is installed in web-admin and the fact that the s2-quickstart script can now be run as well as the following near the end:
Grails application running at http://localhost:8080 in environment: development
Right click on the above url and select Open Link. When your browser opens up with the default Grails page, you will notice your pluginDomain under the Installed Plugins. You can now stop the Grails application.
At long last, let's create some domain classes. You remember, the domain classes are going to live in plugin-domain. So, if you're in the web-admin directory, do:
$ cd ../plugin-domain
Make very sure you're in the plugin-domain directory. Use the Linux pwd (present working directory) command and you should see $ plugin-domain.
The first domain classes we are going to generate are the Spring Security ones. Remember, you now have the grails s2-quickstart script available. Do:
$ grails s2-quickstart com.example SecUser SecRole
Once, in the distant past, I had a database complaining about either user or role being a reserved word and not fit for a table name. We are going to use the com.example package for all our classes to make things simple. As feedback, among others, you will see:
************************************************************ * Created security-related domain classes. Your * * grails-app/conf/application.groovy has been updated with * * the class names of the configured domain classes; * * please verify that the values are correct. * ************************************************************
Open plugin-domain/grails-app/conf/application.groovy in a text editor and add as indicated below:
// Added by the Spring Security Core plugin: grails.plugin.springsecurity.userLookup.userDomainClassName = 'com.example.SecUser' grails.plugin.springsecurity.userLookup.authorityJoinClassName = 'com.example.SecUserSecRole' grails.plugin.springsecurity.authority.className = 'com.example.SecRole' grails.plugin.springsecurity.controllerAnnotations.staticRules = [ [pattern: '/', access: ['permitAll']], [pattern: '/error', access: ['permitAll']], [pattern: '/index', access: ['permitAll']], [pattern: '/index.gsp', access: ['permitAll']], [pattern: '/shutdown', access: ['permitAll']],[pattern: '/dbconsole/**', access: ['permitAll']],[pattern: '/assets/**', access: ['permitAll']], [pattern: '/**/js/**', access: ['permitAll']], [pattern: '/**/css/**', access: ['permitAll']], [pattern: '/**/images/**', access: ['permitAll']], [pattern: '/**/favicon.ico', access: ['permitAll']] ] grails.plugin.springsecurity.filterChain.chainMap = [ [pattern: '/assets/**', filters: 'none'], [pattern: '/**/js/**', filters: 'none'], [pattern: '/**/css/**', filters: 'none'], [pattern: '/**/images/**', filters: 'none'], [pattern: '/**/favicon.ico', filters: 'none'], [pattern: '/**', filters: 'JOINED_FILTERS'] ]
The H2 database console is extremely handy for trouble shooting. All you have to do to use it is enter http://localhost:8080/dbconsole in your browser's address field when your application is running, then test the connection, then connect to the database.
Let's create the remaining domain classes. Still in the plugin-domain directory, do:
$ grails create-domain-class com.example.Book
$ grails create-domain-class com.example.Author
You will get the feedback that the two domain classes have been created in plugin-domain/grails-app/com/example/. Open them up in a text editor and change them to look as below:
Author
package com.example class Author { String name, country static hasMany = [books: Book] static belongsTo = Book static constraints = { name blank: false country blank: false } String toString(){ name } }
Book
package com.example class Book { String title, description, year static hasMany = [authors: Author] static constraints = { title blank: false description blank: false year matches: /\d{4}/ authors minSize: 1 } String toString(){ title } }
That's it for out domain classes. This example is not going to have a service. If you need a service, you know how to create it, also in your plugin-domain using:
$ grails create-service com.example.MyService
This service will be available in all your projects into which your plugin-domain is installed, just like the domain classes are.
We are now finished with plugin-domain. You can now change directory into web-admin:
$ cd ../web-admin
Make sure (pwd) you are in the right directory. We are going to create our controllers and views here.
Before we start to create controllers and views, there are two things we must do. First, we want the controllers to be generated a little differently from the way they usually are. So, issue the following command in your term window:
$ grails install-templates
The templates will be in web-admin/src/main/templates/scaffolding/. Open Controller.groovy in your text editor and change as follows:
import static org.springframework.http.HttpStatus.* import grails.transaction.Transactionalimport grails.plugin.springsecurity.annotation.Secured@Transactional(readOnly = true) class ${className}Controller { static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] def index(Integer max) { params.max = Math.min(max ?: 10, 100) respond ${className}.list(params), model:[${propertyName}Count: ${className}.count()] } def show(${className} ${propertyName}) { respond ${propertyName} } def create() {respond new ${className}()}
We are going to use Spring Security annotations in all our controllers. And you must remove the params argument from the create method, otherwise you'll get an exception. You can try it. For only two controllers, this editing of the Controller template is not so serious. But if you're going to have 20 controlers, believe me, it's better to do it once than 20 times.
The next thing you must do is to copy all the domain classes, in their correct directory structure, to web-admin/grails-app/domain/. Paste plugin-domain/grails/app/domain/com (just the com directory and everything it contains) into web-admin/grails-app/domain/. Even though plugin-domain is installed into web-admin and those domain classes are fully available to web-admin when the application is running, Grails can't see them from web-admin when one wants to do a grails generate-*. It's such an obvious shortcoming that I won't be surprised if this is not corrected in later editions of Grails. After the copying and pasting, in the web-admin directory, do:
$ grails generate-all *
All the controllers and views will be generated to where they should be in web-admin/grails-app/controllers/ and web-admin/grails-app/views/. You should now delete the com directory and everything in it in web-admin/grails-app/domain/.
We are now going to make a few changes to the controllers and views. We don't want to see the user's password in show and index. Unfortunately there's no easy way to prevent that in show. It can of course be done, but it takes too much effort for such a simple thing. In the index view it's easy. Change the SecUser index.gsp as shown below:
<div id="list-secUser" class="content scaffold-list" role="main"> <h1><g:message code="default.list.label" args="[entityName]" /></h1> <g:if test="${flash.message}"> <div class="message" role="status">${flash.message}</div> </g:if><f:table collection="${secUserList}" properties="['username', 'enabled', 'accountLocked', 'accountExpired', 'passwordExpired']" /><div class="pagination"> <g:paginate total="${secUserCount ?: 0}" /> </div> </div>
There's an even easier fix for excluding unwanted inputs from a form, as you will soon see. Hopefully, the Fields plugin will come up with a similar fix for show views. Often, one wants to exclude only one or two attributes from a long list of attributes.
When creating or updating an Author, we don't want to have to specify a book written by that author. We first want to create authors, then create books and in the process of creating or updating a book, specify the author or authors of the book. That means we must remove the multi-select on the author create and edit pages created by the Fields plugin. The solution is exactly the same in both cases. See below:
<fieldset class="form"><f:all bean="author" except="books" /></fieldset>
While we're busy with the views, let's put up login and logout links. Open web-admin/grails-app/views/layouts/main.gsp in a text editor and change as shown below:
<div class="navbar-collapse collapse" aria-expanded="false" style="height: 0.8px;"> <ul class="nav navbar-nav navbar-right"> <g:pageProperty name="page.nav" /><sec:ifLoggedIn> <li><g:link controller='logoff'>Logout</g:link></li> </sec:ifLoggedIn> <sec:ifNotLoggedIn> <li><g:link controller='login' action='auth'>Login</g:link></li> </sec:ifNotLoggedIn></ul> </div>
We also want the administrator to be able to create new users and assign them to roles. When the user is shown, we also want to see his roles. For that we have to work with the create, edit and show views in web-admin/grails-app/views/secUser. Make changes as shown below:
Create and Edit
<fieldset class="form"> <f:all bean="secUser"/><g:each in="${roleList}" var="role"> <div class="fieldcontain"> <label for="secRole"> <g:message code="${role.encodeAsHTML()}" default="${role?.authority?.encodeAsHTML()}" /> </label> <g:radio name="secRole" value="${role?.id}" checked="${secUser.id ? secUser.getAuthorities().contains(role) : false}" /> </div> </g:each></fieldset>
If you want a user to be able to have many roles, just replace the radio inputs with check-boxes. The controller code shown later will be able to deal with both cases.
Show
<f:display bean="secUser" /><g:if test="${secUser?.getAuthorities()}"> <ol class="property-list secUser"> <li class="fieldcontain"> <span id="roles-label" class="property-label"> <g:message code="secUser.roles.label" default="Roles" /> </span> <g:each var="role" in="${secUser?.getAuthorities()}"> <span class="property-value" aria-labelledby="roles-label"> ${role?.authority} </span> </g:each> </li> </ol> </g:if><g:form resource="${this.secUser}" method="DELETE"> <fieldset class="buttons"> <g:link class="edit" action="edit" resource="${this.secUser}"><g:message code="default.button.edit.label" default="Edit" /></g:link> </fieldset> </g:form>
Repent, the end is near!
It's been quite a long haul, hasn't it? Only the controllers to go. Oh, yes, and creating test data and users in web-admin/grails-app/init/web/admin/BootStrap.groovy. Leaving out the controllers for SecRole and SecUserSecRole, add persmission for everyone in the ROLE_ADMIN to use these controllers, as shown below:
import static org.springframework.http.HttpStatus.* import grails.transaction.Transactional import grails.plugin.springsecurity.annotation.Secured @Transactional(readOnly = true)@Secured(['ROLE_ADMIN'])class AuthorController {
Obviously, this goes for all the controllers, except those mentioned, which we are never going to use.
Now only the SecUser controller, of the controllers, to go. Below is what needs to be done.
package com.example import static org.springframework.http.HttpStatus.* import grails.transaction.Transactional import grails.plugin.springsecurity.annotation.Secured @Transactional(readOnly = true) @Secured(['ROLE_ADMIN']) class SecUserController { static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] def index(Integer max) { params.max = Math.min(max ?: 10, 100) respond SecUser.list(params), model:[secUserCount: SecUser.count()] } def show(SecUser secUser) { respond secUser } def create() {respond new SecUser(), model: [roleList: SecRole.listOrderByAuthority()]} @Transactional def save(SecUser secUser) { if (secUser == null) { transactionStatus.setRollbackOnly() notFound() return } if (secUser.hasErrors()) { transactionStatus.setRollbackOnly() respond secUser.errors, view:'create' return } secUser.save flush:truethis.addRoles(secUser)request.withFormat { form multipartForm { flash.message = message(code: 'default.created.message', args: [message(code: 'secUser.label', default: 'SecUser'), secUser.id]) redirect secUser } '*' { respond secUser, [status: CREATED] } } } def edit(SecUser secUser) {respond secUser, model: [roleList: SecRole.listOrderByAuthority()]} @Transactional def update(SecUser secUser) { if (secUser == null) { transactionStatus.setRollbackOnly() notFound() return } if (secUser.hasErrors()) { transactionStatus.setRollbackOnly() respond secUser.errors, view:'edit' return }if(doUpdate(secUser)){ request.withFormat { form multipartForm { flash.message = message(code: 'default.updated.message', args: [ message(code: 'SecUser.label', default: 'SecUser'), secUser.id ]) redirect secUser } '*'{ respond secUser, [status: OK] } } }else { respond secUser.errors, view:'edit', model: [roleList: SecRole.listOrderByAuthority()] } } @Transactional def delete(SecUser secUser) { if (secUser == null) { transactionStatus.setRollbackOnly() notFound() return }SecUserSecRole.removeAll(secUser) // remove all rolessecUser.delete flush:true request.withFormat { form multipartForm { flash.message = message(code: 'default.deleted.message', args: [message(code: 'secUser.label', default: 'SecUser'), secUser.id]) redirect action:"index", method:"GET" } '*'{ render status: NO_CONTENT } } }@Transactional private boolean doUpdate(secUser) { if (secUser.save(flush: true)) { SecUserSecRole.removeAll(secUser) // remove all roles addRoles(secUser) // add the new roles - which may be the old ones return true } else{ return false } } private void addRoles(secUser) { if(!secUser.isAttached()){ secUser.attach() } def roleIds = params.secRole if(roleIds instanceof java.lang.String){ def role = SecRole.get(Integer.parseInt(roleIds)) if(!role.isAttached()){ role.attach() } new SecUserSecRole(secUser: secUser, secRole: role).save(flush: true) } else{ roleIds.each{ def role = SecRole.get(Integer.parseInt(it)) if(!role.isAttached()){ role.attach() } new SecUserSecRole(secUser: secUser, secRole: role).save(flush: true) } } }protected void notFound() { request.withFormat { form multipartForm { flash.message = message(code: 'default.not.found.message', args: [message(code: 'secUser.label', default: 'SecUser'), params.id]) redirect action: "index", method: "GET" } '*'{ render status: NOT_FOUND } } } }
Below is what the fruit of your labour will look like. The create and edit views will be very similar, just no data in the form fields to start off with in the create view.
SecUser show

SecUser edit

Development users and data in web-admin/grails-app/init/web/adminBootStrap.groovy
The only caveat here is that the passwords won't be encoded unless you explicitly do it in BootStrap.groovy. For users created by a signed in administator in the running application, they will be. Without further ado, here is the code of BootStrap.groovy:
package web.admin import com.example.* import grails.util.Environment class BootStrap { def springSecurityService def init = { servletContext -> if(Environment.current == Environment.DEVELOPMENT) { createDevUsers() createDevData() } } def destroy = { } def createDevUsers() { def adminSecRole = new SecRole(authority: 'ROLE_ADMIN').save(failOnError:true) def authorSecRole = new SecRole(authority: 'ROLE_AUTHOR').save(failOnError:true) def admin = new SecUser(username: "admin", password: springSecurityService.encodePassword('strongPassword')).save(failOnError:true) def author = new SecUser(username: "author", password: springSecurityService.encodePassword('strongPassword')).save(failOnError:true) SecUserSecRole.create(admin, adminSecRole) SecUserSecRole.create(author, authorSecRole) } def createDevData() { def book1 = new Book(title: 'Snowflakes in hell', description: 'Weather conditions in hell', year: '1994').save(failOnError:true) def book2 = new Book(title: 'Honesty in parliament', description: 'This is all about wishful thinking', year: '2005').save(failOnError:true) def author1 = new Author(name: 'Jon Donne', country: 'Hingland').save(failOnError:true) def author2 = new Author(name: 'Koos van der Merwe', country: 'Azania').save(failOnError:true) author1.addToBooks(book1).save(failOnError:true) author2.addToBooks(book1).save(failOnError:true) author2.addToBooks(book2).save(failOnError:true) } }
Well, that's it. Below is an image of home page when you run the application. You can download the code and run it as is. Nothing to configure.

If you find what you learned on this page useful, please use the social media widgets at the bottom and pin, tweet, plus-one or whatever this page.
The functionality you have learned here is very useful. This is it. Write a note to let us know what you think of all this.
Use and empty line to separate paragraphs in the "Comment" text area.
Links and html markup are not allowed.