Sunday, March 1, 2015

jQuery Datatable Remote pagination with Spring MVC / SpringData


So I wanted to use the nice jQuery Datatable and integrate it with Spring-Boot backend using Spring MVC Restcontroller and Spring Data.


The jQuery datatable from http://datatables.net/ is a very nice fully functional javascript datatable with pagination, searching, sorting, etc. There are several customizations that make it very nice but also a little complicated to get setup depending on what you want. I wanted to have a "rest" endpoint provided by Spring MVC produce the results for the datatable.

The spring controller allowed me to pass in a "Pageable" org.springframework.data.domain.Pageble;. Which can be passed directly to the Spring Data repository without extracting the data and packaging it again for a query. Notice in the UserRepository.java there is no "userRespository.findAll(pageable);" as needed by line 46 in the UserController.java this is provided by PagingAndSortingRepository in SpringData. The "Page<User>" object gets directly returned in the response again eliminating an annoying unpackaging / repackaging scenario. The request URL looks very nasty. Because of all the fields the datatable adds on. I decided not to try and fight that.

http://localhost:8080/user/datatable.jquery?draw=2&columns%5B0%5D%5Bdata%5D=id&columns%5B0%5D%5Bname%5D=&columns%5B0%5D%5Bsearchable%5D=true&columns%5B0%5D%5Borderable%5D=true&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=firstName&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=true&columns%5B1%5D%5Borderable%5D=true&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B1%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B2%5D%5Bdata%5D=lastName&columns%5B2%5D%5Bname%5D=&columns%5B2%5D%5Bsearchable%5D=true&columns%5B2%5D%5Borderable%5D=true&columns%5B2%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B2%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B3%5D%5Bdata%5D=email&columns%5B3%5D%5Bname%5D=&columns%5B3%5D%5Bsearchable%5D=true&columns%5B3%5D%5Borderable%5D=true&columns%5B3%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B3%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B4%5D%5Bdata%5D=creationDate&columns%5B4%5D%5Bname%5D=&columns%5B4%5D%5Bsearchable%5D=true&columns%5B4%5D%5Borderable%5D=true&columns%5B4%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B4%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B5%5D%5Bdata%5D=&columns%5B5%5D%5Bname%5D=&columns%5B5%5D%5Bsearchable%5D=true&columns%5B5%5D%5Borderable%5D=true&columns%5B5%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B5%5D%5Bsearch%5D%5Bregex%5D=false&order%5B0%5D%5Bcolumn%5D=0&order%5B0%5D%5Bdir%5D=asc&start=5&length=5&search%5Bvalue%5D=&search%5Bregex%5D=false&page=1&size=5&sort=id%2Casc&_=1425242466260
This can be simplified to: (and still produce the same result).
http://localhost:8080/user/datatable.jquery?draw=2&page=1&size=5
The page and size fields make up the Pageable model object. On lines 10 thru 12 I configured the datatable to pass page, size, and sort to the the MVC controller. On the way out of the @RestController I package the response the way the datatable expects it in fields named "data", "draw", "recordsTotal", and "recordsFiltered".

Lastly I wanted view, edit, and delete links in the right column. This would be ugly code configured inline with the datatable so on line 28 I add small reference tags "<a class='dt-edit'></a>" to be used later.



User.java (Entity Bean)

package com.jot.model;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;

/**
 * @author Paul Mefford
 * @since 9/1/14
 *
 */
@Data //lombok 
@Entity
@Table(name = "user")
@EqualsAndHashCode(exclude = {"addresses"})
public class User  implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "user_id")
    private Long id;

    @Column(name = "user_name")
    private String username;
    @Column(name = "password")
    private String password;
    @Column(name = "firstname")
    private String firstName;
    @Column(name = "lastname")
    private String lastName;
    @Column(name = "user_type")
    private String userType;
    @Column(name = "email")
    private String email;
    @Column(name = "phone1")
    private String phone1;
    @Column(name = "phone2")
    private String phone2;
    @Column(name = "status")
    private String status;
    @Column(name = "creation_date")
    @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    private DateTime creationDate;
    @Column(name = "security_role")
    private Integer role;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "userId")
    private List<useraddress> addresses = Collections.EMPTY_LIST;

}


UserRepository.java (Spring Data)

package com.jot.repository;

import com.jot.model.Company;
import com.jot.model.User;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.List;

/**
 * @author Paul Mefford
 * @since 9/6/14
 */

@Repository
public interface UserRespository extends PagingAndSortingRepository {
    User findByUsername(String username);
}


UserController.java (Spring MVC RestController)

package com.jot.web;

import com.google.common.collect.Lists;
import com.jot.model.LinkedUsers;
import com.jot.model.User;
import com.jot.repository.LinkedUserRepository;
import com.jot.repository.UserRespository;
import com.jot.services.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestWrapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import javax.mail.MessagingException;
import javax.validation.Valid;
import java.security.Principal;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * @author Paul Mefford
 * @since 9/6/14
 */
@RequestMapping("/user")
@Controller
public class UserController {
    Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private UserRespository userRespository;

    @RequestMapping("datatable.jquery")
    public @ResponseBody Map
    datatable(Model model, Pageable pageable, @RequestParam("draw") Integer draw , @RequestParam(value = "search", defaultValue = "") String search){
        Map data = new HashMap<>();
        Page page = userRespository.findAll(pageable);
        data.put("data",page.getContent());
        data.put("draw",draw);
        data.put("recordsTotal",page.getTotalElements());
        data.put("recordsFiltered",page.getTotalElements());
        return data;
    }

}

HTML (Thymeleaf template engine)

<table class="display datatable" id="userTable" cellspacing="0" width="100%">
                <thead>
                <tr>
                    <th>Id</th>
                    <th>First Name</th>
                    <th>Last Name</th>
                    <th>Email</th>
                    <th>Created</th>
                    <th width="10%">&nbsp;</th>
                </tr>
                </thead>

                <tfoot>
                <tr>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th>&nbsp;</th>
                    <th width="10%">
                        <a th:href="@{'/user?form'}">
                            <img src="../../../images/add.png" th:src="@{/images/add.png}" style="float: right"/>
                        </a>
                    </th>
                </tr>
                </tfoot>

                <tbody>


                </tbody>

            </table>


JavaScript

<script>
        //<![CDATA[

        $(document).ready(function() {
            $('#userTable').dataTable( {
                "ajax": {
                    "url":"user/datatable.jquery",
                    "data":function(d) {
                        var table = $('#userTable').DataTable()
                        d.page = (table != undefined)?table.page.info().page:0
                        d.size = (table != undefined)?table.page.info().length:5
                        d.sort = d.columns[d.order[0].column].data + ',' + d.order[0].dir
                    }
                },
                "searching":false,
                "processing": true,
                "serverSide": true,
                "lengthMenu": [[5, 10, 15,30,50,75,100], [5, 10, 15,30,50,75,100]],
                "columns": [
                    { "data": "id" },
                    { "data": "firstName" },
                    { "data": "lastName" },
                    { "data": "email" },
                    { "data": "creationDate" },
                    { "": "" }
                ],
                "columnDefs": [
                    { "data": null, "targets": -1, "defaultContent":"<h4><a class='dt-view'></a><a class='dt-edit'></a><a class='dt-delete'></a></h4>" }
                ],
                "pagingType": "full_numbers"

            } );

        } );

        //]]>
    </script>


Add Links / Icons to last Column


$("#userTable").on('draw.dt',function(){
            $(".dt-view").each(function(){
                $(this).addClass('text-success').append("<span class='glyphicon glyphicon-search' aria-hidden='true'></span>");
                $(this).on('click',function(){
                    var table = $('#userTable').DataTable();
                    var data = table.row( $(this).parents('tr') ).data();
                    window.location = 'user/'+data.id;
                });
            });

            $(".dt-edit").each(function(){
                $(this).addClass('text-default').append("<span class='glyphicon glyphicon-edit' aria-hidden='true'></span>");
                $(this).on('click',function(){
                    var table = $('#userTable').DataTable();
                    var data = table.row( $(this).parents('tr') ).data();
                    var path =  'user/'+data.id+'/update';
                    $("<form action='"+path+"'></form>").appendTo('body').submit();
                });
            });

            $(".dt-delete").each(function(){
                $(this).addClass('text-danger').append("<span class='glyphicon glyphicon-remove' aria-hidden='true'></span>");
                $(this).on('click',function(){
                    var table = $('#userTable').DataTable();
                    var data = table.row( $(this).parents('tr') ).data();
                    var path =  'user/'+data.id+'/delete';
                    $("<form action='"+path+"'></form>").appendTo('body').submit();
                    $.simplyToast('danger', data.firstName+ ' has been deleted');
                });
            });
        });

No comments: