Ajax DataTables with the Play framework 1.2

07 February 2011

by PeterHilton

This article is a follow-on to Integrating Play framework with jQuery DataTables, which describes how to integrate DataTables with a Play framework 1.2 web application. This time, we show how to load the table data from the server using Ajax, rather than decorating an existing (populated) HTML table.

Architectural considerations

In the case that generating HTML on the server for the entire data set is a problem, the more complex alternative is to use Ajax to fetch the data in separate HTTP requests that do not fetch the entire data set. For example, each request could fetch only data for the 15 rows that are currently displayed.

However, before launching into what could be a premature optimisation, it is worth considering the numbers. This article uses the example of a table of time zones in two languages: this has four columns and 1216 rows, resulting in a 1.1 MB HTML document. This almost certainly too big. However, the generated HTML contains a lot of white-space; if you use a copy of the CRUD #{table} tag - ${play.path}/modules/crud/app/views/tags/crud/table.html - and remove the leading white-space on each line as well as line breaks inside table cells, then the HTML document size drops to 160 KB, which is not ‘too big’ in many cases.

Loading the entire table from a single Ajax request improves this situation further in two ways. First, the data is loaded from a separate asynchronous HTTP, which can happen partly in parallel, resulting in a faster overall page load. Second, the total size of the data is less: 2 KB of HTML and 69 KB of JSON - less than half the previous total. Again, this is still without paging, where you load even less data at a time, in separate requests.

Serve the table data in JSON format

At this point all of the functionality works. However, the CRUD application now serves the entire set of model data objects as an HTML document in one go, which means a single large data transfer. If you do not want to do this then you can take advantage of DataTables’ support for Ajax loading, so that you only load one or two pages of data at a time.

The first step is to make the table data available in JSON format, for Ajax calls. To do this, add a new crud-datatables/app/controllers/CrudJson.java controller to your Play module:

package controllers;

import java.lang.reflect.Field;
import java.util.List;

import play.Logger;
import play.db.Model;


public class CrudJson extends CRUD {

   /**
   * Controller method for DataTables, based on
   * {@link CRUD#list(int, String, String, String, String)}
   * with unused request parameters.
   */
   public static void listJson(int page, String search, String searchFields,
      String orderBy, String order) {
     ObjectType type = ObjectType.get(getControllerClass());
     notFoundIfNull(type);
     if (page < 1) {
      page = 1;
     }
     final List<Model> objects = type.findPage(page, search, searchFields,
      orderBy, order, (String) request.args.get("where"));
     renderJSON(new DataTablesSource(type, objects));
   }

   /**
   * JSON wrapper for a list of model objects, for use by DataTables.
   */
   private static class DataTablesSource {

      private final String[][] aaData;
      private Long iTotalRecords; // TODO: set to total records, before filtering
      private Long iTotalDisplayRecords; // TODO: set to total records, after filtering
      private Long sEcho; // TODO: set to pass-through request parameter

      public DataTablesSource(final ObjectType type, final List objects) {

         this.aaData = new String[objects.size()][type.getFields().size()];
         for (int column = 0; column < type.getFields().size(); column++) {
            final ObjectType.ObjectField objectField = type.getFields().get(column);
            for (int rowIndex = 0; rowIndex < objects.size(); rowIndex++) {
               try {
                  final Field field = type.entityClass.getField(objectField.name);
                  final String value = field.get(objects.get(rowIndex)).toString()
                  aaData[rowIndex][column] = value;
               }
               catch (Exception e) {
                  Logger.error("Could not read field " + objectField.name);
               }
            }
         }
      }
   }
}

To use the new controller, add a corresponding route for each CRUD controller in crud-datatables/conf/routes

#{crud.types}
GET /${type.controllerName}.json ${type.controllerClass.name.substring(12).replace('$','')}.listJson
#{/crud.types}

… and change the example application’s controller to use the new JSON controller as its super-class:

package controllers;

public class LocalisedTimeZones extends CrudJson {

}

The JSON-formatted data is now available from http://localhost:9000/localisedtimezones.json

Load the table from Ajax requests

To change the view template to load this JSON data we need to do two things: change the DataTables configuration, and remove the generated HTML table in crud-datatables-example/app/views/CRUD/list.html. First, replace the DataTables initialisation script:

<script type="text/javascript" charset="utf-8">
  $(document).ready(function() {
     $('#crudListTable table').dataTable( {
        "sAjaxSource": '${type.controllerName}.json',
        aoColumns: [
           #{list items:type.fields*.name, as:'field'}
              { sTitle: "${field}" },
           #{/list}
        ]
     });
  });
</script>

The call to the DataTables jQuery plug-in now includes two options: sAjaxSource specifies the relative URL of the JSON data, and aoColumns provides the column names, for which we use a Play template tag to generate a list of field names from the CRUD type.

Next, replace the crud.table template tag with an empty HTML table, which DataTables will populate:

<div id="crudListTable">
   <table></table>
</div>

The table now loads from a single Ajax request.

Add paging, sorting and search to the Ajax requests

To make paging, sorting and search work, we have to parse the relevant parameters from the Ajax HTTP request that data tables sends, and set the appropriate parameters in the call to the CRUD.ObjectType.findPage method. The relevant DataTables parameters, described in the server-side processing documentation, are:

  • iDisplayStart

  • iDisplayLength

  • iSortingCols

  • iSortCol_0

  • sSortDir_0

The last two sorting parameters’ names end with the zero-based column index. DataTables supports multi-column sorting, and sends additional ‘column’ and ‘direction’ request parameters whose name ends with an index for each sort column, e.g. iSortCol_1 and iSortCol_1 for a second sort column.

To parse these parameters into the form that the CRUD module needs, we can use a new inner class in the crud-datatables/app/controllers/CrudJson.java controller:

public static class DataTablesParameters {
  private final ObjectType type;
  public int page;
  public Integer pageSize;
  public String search;
  public String orderBy;
  public String order;

  public DataTablesParameters(final ObjectType type, final Request request) {
     this.type = type;

     // Paging
     final Integer startRow = request.params.get("iDisplayStart", Integer.class);
     this.pageSize = request.params.get("iDisplayLength", Integer.class);
     this.page = startRow == null ? 1 : (startRow / this.pageSize) + 1;

     // Sorting (first sort column only).
     final Integer sortingColumns = request.params.get("iSortingCols", Integer.class);
     if (sortingColumns != null) {
        final int sortColumnIndex = request.params.get("iSortCol_0", Integer.class);
        final String sortDirection = request.params.get("sSortDir_0");
        this.orderBy = type.getFields().get(sortColumnIndex).name;
        this.order = sortDirection == null ? null : sortDirection.toUpperCase();
     }

     // Search
     this.search = request.params.get("sSearch");
  }
}

We can now change the controller’s listJson method:

public static void listJson() {
   ObjectType type = ObjectType.get(getControllerClass());
   notFoundIfNull(type);

   final DataTablesParameters dtp = new DataTablesParameters(type, request);

   // Hack: set the page size for the whole application.
   if (dtp.pageSize != null) {
      Play.configuration.setProperty("crud.pageSize", String.valueOf(dtp.pageSize));
   }

   final Long totalCount = type.count(null, null, null);
   final List<Model> objects = type.findPage(dtp.page, dtp.search, null, dtp.orderBy, dtp.order, null);
   renderJSON(new DataTablesSource(type, totalCount, objects));
}

Note that we pass a null to the findPage method’s searchField argument, which means that the search string will search all fields, which is what we want.

Unfortunately, the CRUD.ObjectType.findPage method does not have a parameter for the page size, so this code includes a nasty hack to change the Play configuration’s crud.pageSize parameter. You do not want to do this for multiple users.

Finally, although DataTables supports multi-column sorting, we are only getting the first sort column because the findPage method’s orderBy and order arguments only work for a single column.

The fix for the last two issues is to construct your own JPA query, based on the CRUD.ObjectType.findPage method’s implementation. This is left as an exercise for the reader.

Next steps

The above examples are enough to get an idea of how you can integrate Play with DataTables’ JavaScript API. However, you can take this further by supporting additional DataTables features, such as:

  1. multi-column sorting (mentioned above)

  2. individual column filtering - ‘advanced search’

  3. pipelining data to fetch more than one page at a time, to reduce the number of Ajax requests.