6

I have a Spring Boot application using Java and Thymeleaf. In it, I have a page with a JQuery DataTable. This table has thousands of rows in it, and so while I currently just put it all on the page and let JQuery handle it entirely, I want to switch to using ServerSide processing for the paging, sorting, etc.

I am set up to do this on the back-end, where I can pass a start and end row number and sort information dynamically to my SQL query. However, my issue is that I just can't figure out exactly how DataTables is trying to notify me via controller when the user clicks the 'next page' button, or a sort button. Where/how can my controller pick up this information, so that I can plug it into my SQL query and return what is necessary?

So, lets say I have an example object, like a "Person".

public class Person {

    private static String PERSON_IMAGE = "<img th:src=\"@{/images/personimage.png}\" alt=\"icon\"/>";
    private static String PERSON_IMAGE_2 = "<img th:src=\"@{/images/personimage2.png}\" alt=\"icon\"/>";

    private String name;
    private String socialSecurity;
    private String birthdate;
    private String gender;
    private String personImage;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSocialSecurity() {
        return socialSecurity;
    }

    public void setSocialSecurity(String socialSecurity) {
        this.socialSecurity = socialSecurity;
    }

    public String getBirthdate() {
        return birthdate;
    }

    public void setBirthdate(String birthdate) {
        this.birthdate = birthdate;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getPersonImage() {
        if(null != birthdate) {
            return PERSON_IMAGE;
        } else {
            return PERSON_IMAGE_2;
        }

    }
}

So, on my Thymeleaf HTML page, I used to have the following: (Version 1)

<table id="myDataTable" class="dataTable display compact">
    <thead>
        <tr>
            <th>Name</th>
            <th>Social Security</th>                                        
            <th>Birthdate</th>
            <th>Gender</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="person : ${peopleList}">
            <td th:attr="data-order=${person.name}"><a th:href="|javascript:openPersonDetail('${person.socialSecurity}');|"><span th:text="${person.name}">name</span></a></td>
            <td th:text="${person.socialSecurity}">socialSecurity</td>                      
            <td class="dt-body-center" th:text="${person.birthdate}">birthdate</td>
            <td class="dt-body-center" th:text="${person.gender}">gender</td>
        </tr>
    </tbody>
</table>

My page controller was very simple, and just passed in a full list of people, as follows: (Version 1)

@GetMapping("/ApplicationName/MyDataTablePage")
public String myDataTablePage(Model model) {

    SearchCriteria searchCriteria = new SearchCriteria();
    searchCriteria.setOrderByString("name");
    searchCriteria.setUsePaging(false);
    searchCriteria.setFIRST_NUMBER(null);
    searchCriteria.setLAST_NUMBER(null);

    model.addAttribute("peopleList", myMapperService.getPeople(searchCriteria)); //returns an ArrayList<Person>

    return "/pages/MyDataTablePage";
}

And finally, my Javascript used to have the following: (Version 1)

$(document).ready(function() {
    $('#myDataTable').DataTable({
        "destroy" : true,
        "scrollY" : 300,
        "scrollCollapse" : true,
        "paging" : true,
        "autoWidth" : true,
        "ordering" : true,
        "searching" : false,
        "order" : [ [ 0, 'asc' ] ],
        "pageLength" : 20,
        "lengthChange" : false,
        "pagingType" : "full_numbers",
        "dom" : '<"top"ip>rt<"bottom"fl><"clear">'
    });
});

This worked, but I wanted to switch the DataTable to use serverside processing, and so the first step was to (1) change the main page controller to stop appending the list of People to the model, (2) to create a new controller which would return my ArrayList as JSON for the DataTable to call by itself, (3) to change the html of the main page to no longer provide any tbody tag to the DataTable, and (4) to change the javascript where the table is created to put the data in by itself.

=============================================================

That led me to version 2 of the following elements.

My changed Thymeleaf HTML page: (Version 2)

<table id="myDataTable" class="dataTable display compact">
    <thead>
        <tr>
            <th>Name</th>
            <th>Social Security</th>                                        
            <th>Birthdate</th>
            <th>Gender</th>
        </tr>
    </thead>
</table>

My changed page controller: (Version 2)

@GetMapping("/ApplicationName/MyDataTablePage")
public String myDataTablePage(Model model) {

    return "/pages/MyDataTablePage";
}

My new DataTables controller:

@RequestMapping(path = "/ApplicationName/Data/Person", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public List<Person> getPersonData() {
    System.out.println("I was called!");

    SearchCriteria searchCriteria = new SearchCriteria();
    searchCriteria.setOrderByString("name");
    searchCriteria.setUsePaging(false);
    searchCriteria.setFIRST_NUMBER(null);
    searchCriteria.setLAST_NUMBER(null);

    return myMapperService.getPeople(searchCriteria); //returns an ArrayList<Person>
}

My changed (much more complicated) Javascript: (Version 2)

$(document).ready(function () {
     $('#myDataTable').DataTable({ 
         'destroy' : true,
         'serverSide' : true,
         'sAjaxSource': '/ApplicationName/Data/Person',
         'sAjaxDataProp': '',
         'order': [ [ 0, 'asc' ] ],
         'columns': 
         [ 
            {  'data': 'name',
                'render': function(data, type, row, meta){ 
                    if(type === 'display'){ 
                        data = '<a href="javascript:openPersonDetail(&apos;'+ row.socialSecurity +'&apos;);">' + data + '</a>' 
                    }  
                    return data; 
                } 
            } ,
            { 'data': 'socialSecurity'} ,
            { 'data': 'birthdate'} ,
            { 'data': 'gender'} 
         ],
         'scrollY' : 300,
         'scrollCollapse' : true,
         'paging' : true,
         'autoWidth' : true,
         'ordering' : true,
         'searching' : false,
         'pageLength' : 20,
         'lengthChange' : false,
         'pagingType' : 'full_numbers',
         'dom' : '<"top"ip>rt<"bottom"fl><"clear">' 
     }); 
 });

=============================================================

And that works, too. Especially if I remove the "'serverSide' : true," configuration - the DataTable works exactly like it used to before I made the above changes, but now it calls my new Data JSON controller instead of using a model attribute.

Problem:

However, with the "'serverSide' : true," configuration added to the DataTable, each time I go to a new 'page' of the table, it just recalls my "/Clinic/Detail/Data/Patient" controller each time.

But, I need DataTables to pass my controller information about what page the user has selected, or what column the user tried to sort on. I also need to pass the table (dynamically) the total number of rows there are (count) so that DataTables can properly set up the page numbers for the user.

Is DataTables sending some additional parameters to my controller that I can pick up? Something like a @PathVariable("whatever")?

I feel like I am so close to getting this working, but I'm stuck at this point.

I would really appreciate any help!

4
  • When you specify serverside: true, additional parameters are included in the draw request, see the docs. If you look in the network panel of your browser tools you'll be able to see this when the datatable loads. The parameters are what your controller needs to use. Commented Apr 15, 2019 at 14:09
  • @markpsmith Thanks for your comment. Yeah, I am on track to getting this working now, though it seems like for whatever reason my application wants to use the older (I think) version of these variables, even though I am pulling in the webjars for datatables <version>1.10.19</version>. In any case, once I get it working entirely I plan to post an answer to my own question. Assuming all works out... Commented Apr 15, 2019 at 16:04
  • @markpsmith Since you seem to know what you're talking about, maybe you can help me with another question? I've been going through the documentation on DataTables and can't find it. Is there a way to add a custom sort parameter string into the columns list, sort of like the following example.... 'columns': [ { 'data' : 'name', 'customSortableParamer' : 'FIRST_NAME' }, { 'data' : 'gender' } ], and have that value returned to me in the request parameters with the user sorts? Commented Apr 15, 2019 at 19:12
  • Could you add which versions of DataTable, Spring-Boot, Java, Thymeleaf and so on you used for this? Commented Aug 28, 2019 at 13:45

2 Answers 2

5

I found 2 solutions:

  1. Look at this post: Spring Boot + Bootstrap + Thymeleaf Datatable
<script>
    $('#example').DataTable({
        "processing": true,
        "serverSide": true,
        "ajax": {
            "url": "/employees",
            "type": "POST",
            "dataType": "json",
            "contentType": "application/json",
            "data": function (d) {
                return JSON.stringify(d);
            }
        },
        "columns": [
            {"data": "name", "width": "20%"},
            {"data": "position","width": "20%"},
            {"data": "office", "width": "20%"},
            {"data": "start_date", "width": "20%"},
            {"data": "salary", "width": "20%"}
        ]
    });
</script>
  1. Look at this github project: spring-data-jpa-datatables
Sign up to request clarification or add additional context in comments.

Comments

2

I have managed to solve my issue.

My HTML for the table remained the same, as follows:

<table id="myDataTable" class="dataTable display compact">
    <thead>
        <tr>
            <th>Name</th>
            <th>Social Security</th>                                        
            <th>Birthdate</th>
            <th>Gender</th>
        </tr>
    </thead>
</table>

My Controller for the page remained the same, as follows:

@GetMapping("/ApplicationName/MyDataTablePage")
public String myDataTablePage(Model model) {

    return "/pages/MyDataTablePage";
}

My Controller for the Table which retrieves the data needed to change. First, it needed to get the @RequestParam that datatables was sending, which included all the parameters that I needed regarding how many records I needed to show, which record I needed to sort at, and which cell was being sorted on. Second, instead of just returning a List<Person>, it needed to return a well-formed JSON object with certain parameters that Datatable uses to decide how many pages to show on screen, as well as the data List<Person> itself.

@RequestMapping(path = "/ApplicationName/Data/Person", method = RequestMethod.GET, produces = "application/json")
@ResponseBody
public TableBean getPersonData(@RequestParam Map<String, String> allRequestParams) {

    TableParameters tableParameters = myMapperService
            .getTableParameters(allRequestParams);

    return myMapperService.mapTableBean(tableParameters); //returns an TableBean which contains the ArrayList<Person>
}

In order for this to work, I needed to make a few new classes and methods. First, I made a TableParameters bean which holds all of the parameters for my Data Table:

public class TableParameters implements Serializable {
    private static final long serialVersionUID = 1L;

    private Integer iDisplayStart;

    private Integer iDisplayLength;

    private Integer iColumns;

    private String sSearch;

    private Boolean bRegex;

    private Integer iSortingCols;

    private Integer sEcho;

    private Integer sortedColumnNumber;

    private String sortedColumnName;

    private String sortedColumnDirection;


    // ... getters and setters

}

And then I needed to make a method that would pull these variables off of the Request Parms and map them to the new bean. This tells me what to use in my SQL as far as how to sort, and which numbers to start on and return.

private static String iDisplayLength = "iDisplayLength";
private static String iDisplayStart = "iDisplayStart";
private static String iColumns = "iColumns";
private static String sSearch = "sSearch";
private static String bRegex = "bRegex";
private static String iSortingCols = "iSortingCols";
private static String sEcho = "sEcho";
private static String iSortCol_0 = "iSortCol_0";
private static String sSortDir_0 = "sSortDir_0";

public TableParameters getTableParameters(Map<String, String> allRequestParams) {

    TableParameters finalBean = new TableParameters();

    if (null != allRequestParams.get(bRegex)) {
        finalBean.setbRegex(new Boolean(allRequestParams.get(bRegex)));
    }
    if (null != allRequestParams.get(iColumns)) {
        finalBean.setiColumns(new Integer(allRequestParams.get(iColumns)));
    }
    if (null != allRequestParams.get(iDisplayLength)) {
        finalBean.setiDisplayLength(new Integer(allRequestParams.get(iDisplayLength)));
    }
    if (null != allRequestParams.get(iDisplayStart)) {
        finalBean.setiDisplayStart(new Integer(allRequestParams.get(iDisplayStart)));
    }
    if (null != allRequestParams.get(iSortingCols)) {
        finalBean.setiSortingCols(new Integer(allRequestParams.get(iSortingCols)));
    }
    if (null != allRequestParams.get(sEcho)) {
        try {
            finalBean.setsEcho(new Integer(allRequestParams.get(sEcho)));
        } catch (Exception e) {
            // ignore
        }
    }
    if (null != allRequestParams.get(sSearch)) {
        finalBean.setsSearch(allRequestParams.get(sSearch));
    }

    int numberOfColumnsSortedOn = 0;
    if (allRequestParams.containsKey(iSortingCols) && null != allRequestParams.get(iSortingCols)) {
        numberOfColumnsSortedOn = new Integer(allRequestParams.get("iSortingCols")).intValue();
    }

    if (numberOfColumnsSortedOn > 0) {
        if (null != allRequestParams.get(iSortCol_0)) {
            finalBean.setSortedColumnNumber(new Integer(allRequestParams.get(iSortCol_0)));
        }
        if (null != allRequestParams.get(sSortDir_0)) {
            finalBean.setSortedColumnDirection(allRequestParams.get(sSortDir_0));
        }
        String keyForName = "mDataProp_" + finalBean.getSortedColumnNumber();
        if (null != allRequestParams.get(keyForName)) {
            finalBean.setSortedColumnName(allRequestParams.get(keyForName).toUpperCase());
        }
    }
    return finalBean;
}

Similarly, the Return json object TableBean needed to be created which has, not lonly a List<Person> on it, but also the necessary count information that datatable uses to show page numbers to the user. This is what is now returned in the table controller method, instead of just the basic list.

public class TableBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private int iTotalRecords;

    private int iTotalDisplayRecords;

    private String sEcho;

    private List data;

    public TableBean(int iTotalRecords, int iTotalDisplayRecords, String sEcho, List data) {
        super();
        this.iTotalRecords = iTotalRecords;
        this.iTotalDisplayRecords = iTotalDisplayRecords;
        this.sEcho = sEcho;
        this.data = data;
    }

    // ... getters and setters
}

And the mapTableBean method that pulls it all together by actually calling the database to get the records.

public TableBean mapTableBean(TableParameters tableParameters) {
    int iTotalRecords = database.getCount();
    int iTotalDisplayRecords = iTotalRecords;
    if (null != tableParameters.getsEcho()){
        String sEcho = tableParameters.getsEcho().toString(); 
    }
    List data = database.search(tableParameters.getiDisplayStart(), tableParameters.getiDisplayLength(),
            tableParameters.getSortedColumnName(),
            tableParameters.getSortedColumnDirection().equalsIgnoreCase("asc"));

    return new TableBean(iTotalRecords, iTotalDisplayRecords, sEcho, data);
}

Finally, the Javascript had to be altered so that DataTables knows to look at the variable titled 'data' for its list of Beans shown in the table itself, by adding the 'sAjaxDataProp': 'data', configuration.

$(document).ready(function () {
     $('#myDataTable').DataTable({ 
         'destroy' : true,
         'serverSide' : true,
         'sAjaxSource': '/ApplicationName/Data/Person',
         'sAjaxDataProp': 'data',
         'order': [ [ 0, 'asc' ] ],
         'columns': 
         [ 
            {  'data': 'name',
                'render': function(data, type, row, meta){ 
                    if(type === 'display'){ 
                        data = '<a href="javascript:openPersonDetail(&apos;'+ row.socialSecurity +'&apos;);">' + data + '</a>' 
                    }  
                    return data; 
                } 
            } ,
            { 'data': 'socialSecurity'} ,
            { 'data': 'birthdate'} ,
            { 'data': 'gender'} 
         ],
         'scrollY' : 300,
         'scrollCollapse' : true,
         'paging' : true,
         'autoWidth' : true,
         'ordering' : true,
         'searching' : false,
         'pageLength' : 20,
         'lengthChange' : false,
         'pagingType' : 'full_numbers',
         'dom' : '<"top"ip>rt<"bottom"fl><"clear">' 
     }); 
 });

And voila, now the server side processing is functional.

7 Comments

@Ruhshan It has been a very long time since I implemented this, but let's see what I can remember.
@Ruhshan I believe I made an abstract class called 'Database' with a number of methods on it including a 'search' method which took all of the shown pass-in values and returned a List<T>.
@Ruhshan I keep thinking 'enter' will give me a newline, but it keeps making new comments. Sorry. ANYWAY.... Then I made a concrete class which extended that abstract class for anything that I wanted to use with my datatables. So, in this example, I made a 'PersonDatabase' or something which had to fulfill the search method. So anyway, I would look at the passed in sort name, and I would create an 'order by clause' of asc or desc based on that column's name in the actual DB2 database. All of my DB2 calls were set up to use MyBatis, and so I set that string as the order by clause.....
@Ruhshan And then figured out from the passed in number values which row number I needed to start on and which row I needed to end on, and also added THAT as criteria to my database call (I had a custom MyBatis mapper to handle those values). Then I used MyBatis to call the database with a custom SQL statement that selected all results, ordered them by the order by criteria, and then returned only #s x through y of those results. The rest of that 'search' method then just mapped and returned those results from the DB.
@Ruhshan As for how the custom SQL was written in mybatis - I used to do that a lot. It's something like SELECT * FROM (SELECT FAKE.*, ROW_NUMBER() OVER ( [insert order by clause here]) AS RANK FROM ( [insert normal select statement for ALL records here]) AS FAKE) WHERE RANK BETWEEN [insert first number here] and [insert second number here]
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.