skip to Main Content

How to build a simple CRM with Grails 4

Purpose

In this article, using the Grails Framework, I will show you how to build a fully functional CRUD application to maintain a fictitious Customer database.

The primary goal is to show you how quickly it can be to build a simple CRM with Grails 4 based on industry-standard libraries such as Spring Boot and Hibernate, but thanks to Groovy and Grails, we can produce a working system in a fraction of the time it would take otherwise.

We will build a database of ‘Customers’ and provide the user with the following functions:

  • An initial welcome screen.
  • A searchable, sortable, and paginated list of customers with links to both edit existing and create new customers.
  • Create, Edit and Delete functionality.

Before beginning this exercise, you should download the full source code for this project from our Github Repository for your reference. I will show you the most important portions of the code, for brevity, however, I’ll leave out things like import statements.

It is beyond the scope of this tutorial to cover other common requirements such as Security, Test Cases, Caching and Performance tuning, etc. but these all would make for a great exercise to introduce once you have completed this exercise.

H2 will provide the app with an in-memory database. I will provide a SQL file with 1000 fictitious customer records that will automatically be loaded at application startup. For project creation and screenshots of code, I will be using IntelliJ Idea.

Setup

When you create a new project within IntelliJ Idea, one available option is the ‘Application Forge’. This provides us with a convenient way to establish new projects based on multiple versions of Grails without having to download the individual SDKs. An added bonus is you can also see the state of the latest Grails version without having to check on their website.

For this exercise, I have chosen version 4.0.1 as per the image below:

Creating a new Grails application from the Application Forge within IntelliJ

If for any reason, you are not using IntelliJ, you can always access the Application Forge Online to generate your project for you. For those familiar with Spring Initializr, this is very similar.

As you can see I’ve only selected hibernate5 as the optional feature.

Hit Next, and in the next window enter your project name and where you will save it, and hit the finish button. Your application should be ready to begin working on momentarily.

The Data Domain

As we’re focused on build a simple CRM, the absolute minimum we are going to need to create is the Customer domain.

Create a package, e.g. com.yourcompanyname.crm in the grails-app/domain folder, and create a new file called Customer.groovy

All we need to do is define some relevant fields relating to our customers. The beauty of Grails and Groovy is we can keep our code in the class super clean.

We don’t need getters or setters. Nor do we need to define an Id field & column as this is provided automatically by GORM.

We only need to write and maintain code that we actually NEED. This is such a time saver both for the developer, and further down the line in the future when some other person might be taking over the project.

Less clutter = less time reading = greater productivity

We will also add some minimal validation rules to our constraints to ensure a name is a mandatory requirement.

    String firstName
    String lastName
    String emailAddress
    String address
    String city
    String country
    String phoneNumber

    static constraints = {
        firstName blank: false
        lastName blank: false
        emailAddress nullable: true
        address nullable: true
        city nullable: true
        country nullable: true
        phoneNumber nullable: true
    }

Notice, as we’re using groovy we could have omitted the String type declarations and simply entered “def firstName” etc. However, for added clarity, I personally prefer to specify property/field types whenever I can.

It doesn’t save me any time writing def rather than String, or Integer, but it can make it ambiguous what the data type is.

Using mockaroo, I created 1000 fictitious customer records in SQL format. You will find these records in the accompanying Github Repository within src/main/resources/import.sql, or which you can download directly here.

Placing this file on the root of the classpath, i.e. within src/main/resources will cause Hibernate to notice the file at application startup and automatically execute the SQL statements contained within.

Furthermore, this can be a useful tool for the development environment, as this only works if our database scheme generation policy is set to create or create-drop.

Service Layer

Thanks to GORM data Services, there is very little code we need to write to provide basic CRUD functionality for our new Customer Domain.

We’re going to allow GORM to provide the necessary functionality for the get, delete and save services.

Within the Services folder, again create a package and within the package create a CustomerService.groovy file.

@Service(Customer)
interface CustomerService {

    Customer get(Serializable id)

    void delete(Serializable id)

    Customer save(Customer customer)
}

By using the @Service annotation, all necessary functionality is automatically provided, we just need to reference the service from our controllers.

However, one function is missing. We still don’t have the necessary code to populate our list data, this is going to be responsible for searching, sorting and paging our results. We will do this directly from a controller.

The Customer Controller

The customer controller is the C in MVC. We have our Model, we now need to respond to user requests.

Within the Controllers folders, create the appropriate package and create a new file named CustomerController.groovy

We don’t need to define any request mappings anywhere, thanks to Grails convention over configuration, the naming of our controller dictates our URL patterns.

The following code makes up our entire controller:

class CustomerController {

    CustomerService customerService

    def index() {}

    def data_for_datatable() {
        int draw = params.int("draw")
        int length = params.int("length")
        int start = params.int("start")

        String dataTableOrderColumnIdx = params["order[0][column]"]
        String dataTableOrderColumnName = "columns[" + dataTableOrderColumnIdx + "][data]"

        String sortName = params[dataTableOrderColumnName] ?: "id"
        String sortDir = params["order[0][dir]"] ?: "asc"

        String queryString = params["search[value]"]

        PagedResultList criteriaResult = Customer.createCriteria().list([max: length, offset:start]) {
            readOnly true
            or {
                ilike('firstName', '%' + queryString + '%')
                ilike('lastName', '%' + queryString + '%')
                ilike('emailAddress', '%' + queryString + '%')
                ilike('city', '%' + queryString + '%')
                ilike('phoneNumber', '%' + queryString + '%')
            }
            order sortName, sortDir
        }

        Map dataTableResults = [
                draw: draw,
                recordsTotal: criteriaResult.totalCount,
                recordsFiltered: criteriaResult.totalCount,
                data: criteriaResult
        ]

        render dataTableResults as JSON
    }

    def create() {
        respond new Customer(params)
    }

    def save(Customer customerInstance) {
        try {
            customerService.save(customerInstance)
        } catch (ValidationException e) {
            respond customerInstance, view:'create'
            return
        }

        flash.message = "Customer created successfully"

        redirect(action: "index")

    }

    def edit(Long id) {
        respond customerService.get(id)
    }

    def update(Customer customer) {

        try {
            customerService.save(customer)
        } catch (ValidationException e) {
            respond customer.errors, view:'edit'
            return
        }

        flash.message = "Customer updated successfully!"

        redirect(action: "index")
    }

    def delete(Long id) {
        try {
            customerService.delete(id)

            flash.message = "Customer Deleted"
        } catch (Exception ex) {
            flash.message = "Could not delete customer"
        }

        redirect action:'index'
    }
}

We start with a reference to our CustomerService, to provide the necessary CRUD functionality we previously defined.

Secondly is a stub method for our index page, so any calls to ‘/customer’ will be resolved through a customer/index.gsp view. No code is required here as we won’t be providing data to our view.

The Datatable within the index page will make an ajax call to the ‘data_for_datatable’ method within our controller requesting data, passing through relevant parameters.

Here we initially read the parameters sent by Datatable to determine the pagination parameters (length & start), followed by the sort column name and direction. The queryString String variable is set to any search query the user may have provided.

We then set up a Hibernate Criteria query. The results are efficiently filtered, paginated, and sorted at the database level.

Finally, we establish a Map object in the format that our Datatable JavaScript component is expecting and return the results as JSON.

The remaining functions in our controller are stubs for the Create and Edit Pages, or the actions wrapping around our Customer Service CRUD functionality.

We utilise Flash scope to set any relevant messages to pass back to the user upon success or failure of any action.

With our controller complete, all that is left are the Views.

The View Layer

Our views are based on Groovy Server Pages or GSP’s. These allow us to embed Groovy code within our HTML markup.

For decoration, responsiveness, and consistency between browsers we will use the Twitter Bootstrap UI framework along with jQuery. We’ll also use the Datatables JavaScript component to provide a rich list component that fits nicely within the Bootstrap UI.

Home Page

Create an index.gsp in the source of the views folder. This will form the home page and paste the following code:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Simple CRM - Customer Management made Simple</title>
</head>
<body>
<nav class="navbar navbar-dark bg-dark fixed-top">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="/">Simple CRM</a>
        </div>
    </div>
</nav>
<div class="container" style="margin-top:80px">
    <div>
        <h1>Welcome to simple crm</h1>
        <h2>Customer Management made Simple</h2>
    </div>
    <p class="mt-5"><a href="/customer" class="btn btn-primary btn-block">Manage Customers</a></p>
</div>
<footer class="footer navbar-dark bg-dark fixed-bottom">
    <div class="container">
        <div class="row">
            <div class="col-md-4"></div>
            <div class="col-md-4">
                <p class="text-center text-muted">©
                    <span>${formatDate(date: new Date(), format:'yyyy')}</span>
                    <a href="https://www.tucanoo.com">Tucanoo Solutions Ltd.</a>
                </p>
            </div>
        </div>
    </div>
</footer>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
        integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
        integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
        crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
        integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
        crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
      integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</body>
</html>

This forms the basis of all subsequent pages, but for this purpose simply acts as a splash screen to show the minimal code required.

Create a new folder named ‘customer’ within the views folder.

Here we will create the necessary views for the create.gsp, edit.gsp, and index.gsp views.

The Customer List (customer/index.gsp)

Our customer index page will allow the user to immediately see, in grid form a paginated list of the customer table.

A search input is provided to the user, allowing a query to be entered with searching taking place on keypress. Additionally, the column headers can be clicked on to influence the sort order and direction of the list. That’s quite a lot of functionality with very little code.

Any messages are conveyed back to the user through the use of flash messages within the following block:

<g:if test="${flash.message}">
    <div class="alert alert-info">
        <h3>${flash.message}</h3>
    </div>
</g:if>

Our table definition sets up the column structure for the Datatable component:

<table id="customerTable" class="table table-striped table-bordered" style="width:100%">
            <thead>
            <tr>
                <th>Id</th>
                <th>First Name</th>
                <th>Last Name</th>
                <th>Email</th>
                <th>City</th>
                <th>Country</th>
                <th>Phone</th>
            </tr>
            </thead>
</table>

Followed by the call to Datatables with a Javascript call. Here we define the Ajax endpoint within our customer controller and the column definitions. Notice we provide the ‘render’ functions to allow these cells to be clicked on, this will take the user to the ‘edit’ page.

<script>
  var url = '/customer/data_for_datatable';

  $(document).ready(function () {

    $('#customerTable').DataTable({
      "ajax": url,
      "processing": true,
      "serverSide": true,
      "columns": [
        {
          "data": "id",
          "render": function (data, type, row, meta) {
            return '<a href="/customer/edit/' + row.id + '">' + data + '</a>';
          }
        },
        {
          "data": "firstName",
          "render": function (data, type, row, meta) {
            return '<a href="/customer/edit/' + row.id + '">' + data + '</a>';
          }
        },
        {
          "data": "lastName",
          "render": function (data, type, row, meta) {
            return '<a href="/customer/edit/' + row.id + '">' + data + '</a>';
          }
        },
        {"data": "emailAddress"},
        {"data": "city"},
        {"data": "country"},
        {"data": "phoneNumber"}
      ]
    });
  });
</script>

Create and Edit Pages

Our create and edit pages are both very similar in markup. The primary function of each page is to present the user with a form reflecting the Customer fields, initially empty for the create page and pre-filled for the edit page.

At the beginning of each form, if errors are present on the backed customer bean, then we list the errors.

<form action="/customer/save" class="form" method="post">

            <g:hasErrors bean="${this.customer}">
                <div class="alert alert-danger">
                <ul>
                    <g:eachError bean="${this.customer}" var="error">
                        <li><g:message error="${error}"/></li>
                    </g:eachError>
                </ul>
                </div>
            </g:hasErrors>

            <div class="row">
                <div class="form-group col-6">
                    <label>First Name</label>
                    <input class="form-control" name="firstName" value="${customer?.firstName}"/>
                </div>
                <div class="form-group col-6">
                    <label>Last Name</label>
                    <input class="form-control" name="lastName" value="${customer?.lastName}"/>
                </div>
            </div>

<!-- REST OF FORM OMITTED FOR BREVITY -->

There is one last thing to take care of. In the case of input error, we should provide the user with a friendlier warning message than the defaults.

To do this, within the i18n folder, create a new file named customer.properties. This allows us to specify and override default error messages as well as provide internationalised strings if we need to support multiple languages.

To override the validation message of a Null value against our Customer fields, paste the following into customer.properties:

customer.firstName.nullable=First Name cannot be blank
customer.lastName.nullable=Last Name cannot be blank

Summary

You should now be able to run a working, albeit simplified, customer management system. We really have only touched the surface of what is possible. And this is by no means a demonstration on proper practices, rather a demonstration on how quickly rapid prototypes and proof of concept applications can be put together.

You can, of course, use this as a starter project to further your own education and practice by filling in some otherwise missing functionality.

For example, you could attempt the following:

  • Utilise Reusable Layouts.
    Refactor our views to use a common layout and not repeat code as we have. Consider the header and footer sections, and the customer form.
  • A Read-Only customer view
    It’s typically more common than not to first show a read-only view of our Customers when selected from the list screen, before going to an Edit view.
  • Test Test Test.
    Introduce unit testing for our Domain objects and integration tests for our service. Tip: Rather than creating a new blank groovy file for the Domain object, if you use IntelliJ to Create a new Domain class, it will also generate sample test code based on Spock.
  • Configure Logging.
    In this example, we didn’t touch the logging configuration, but you could ensure any errors are correctly logged to an error file, or even emailed for production environments.
  • Security
    Introduce a login screen and ensure your Customer controller is locked down against any unauthorised access. The Spring Security plugin makes this a breeze.
  • Real Database
    Of course, we wouldn’t want our customers to be stored in memory forever. Investigate introducing a real database and see how else you could load up sample data at startup. Tip: Bootstrap.groovy is a good place for data initialisation.

Don’t forget all the code for this project is available on Github.

I hope this has helped to show you just how much more productive you can be developing web applications with the Grails framework than would be otherwise.

This Post Has 0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Back To Top