Paginacion: DataTables + Spring

Les cuento que ando un poco alterado llevo 4 tasas de cafe ya termine mi pega y aun traigo energias...

Asi que me pondre a compartir un poco de mi experiencia con estas dos cosas datatables y spring.

En esta ocacion lo que tenemos es una paginacion y tabulacion de datos. Para ello como dice el titulo sera con DataTables y un framework que veo que se usa mucho Spring. Como acceso a datos se utiliza un ORM Hibernate


El ejemplo lo llevare a cabo con una tabulacion de dos columnas (ingrediente y su categoria), las cuales a nivel datos son la union de dos tablas. Realizaremos el ordenamiento por ambas columnas, paginacion y busqueda por texto en ambas columnas.

Caracteristicas:
Comunicacion: Peticiones ajax datatables ya realiza por ti el render de los resultados, genera la peticion al servidor y nosotros solo la procesamos.
Tipo de peticion: GET
Respuesta: Json que se define en la documentacion de datatables
Referencia: DataTables Server-Side

Receta:

Primero definiremos el contenido de nuestro JSP o sistema de templates que utilicen:

<body>
    <table id="ingredientes">
        <thead>
              <tr><th>Ingrediente</th><th>Categoria</th></tr>
        </thead>
        <tbody>
        </tbody>
    </table>
    <!-- Agregamos javascript -->
    <script type="text/javascript" src="//code.jquery.com/jquery-1.9.1.js"/>
    <script type="text/javascript" src="//ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.min.js" />
    <script type="text/javascript">
          $('#ingredientes').dataTable(
                {
                "aoColumns" :
                   [
                           //Indicamos que las columnas pueden reordenarse
                           {"bSortable": true},
                           {"bSortable": true}
                    ],
                     //Indicamos que se puede salvar el ultimo estado, por si cerramos el tab por equivocacion
                    "bStateSave": true,
                      //Indicamos que se procesara el filtrado del lado del servidor, asi como la paginacion y el ordenamiento
                    "bServerSide": true,
                      //Indicamos que la fuente de la peticion ajax
                    "sAjaxSource": "http://host/ingredientes/filtrar",
                      // Aqui indicamos que aparezcan los numeros de la paginacion si no solo aparecera el next o prev
                    "sPaginationType": "full_numbers"
                }
          );
    </script>
</body>

Bien con esto tenemos ya configurado nuestro paginador de lado del cliente, ahora veamos que necesitamos en nuestro servicio.


Dentro de los parametros que nos llegara al servidor, los mas importantes seran los siguientes:
  • sHecho: Indica la secuencia de peticiones al servidor. En la documentacion recomiendan un parseo a un entero para poder detectar mas facilmente XSS
  • iColumns: Indica el numero de columnas a procesar
  • iDisplayLenght: Indica la cantidad de registros por pagina (hablando del paginador)
  • iDisplayStart: Indica el offset del paginador
  • sSearch: Cadena de filtrado para busquedas
  • iSortingCols: Indica el numero de columna a la cual se realiza el sorting
  • iSortCol_(int): Numero de Columna que realiza el ordenamiento, dado mis pruebas el unico parametro que cambio es el indice 0 y ese es el que tomaremos. Por lo tanto el dato que nos interza es iSortCol_0
  • sSortDir_(int): Direccion de ordenamiento - "desc" or "asc". Al igual que para iSortCol_(int), solo nos intereza el indice 0. sSortDir_0

Nota:Los demas parametros segun la implementacion requerida se le daran uso, para mi caso y mi implementacion no se requieren.

Bien ahora lo que realizaremos es declarar tres POJOs que me ayudan a mantener esos parametros y los datos del response:

IngredientesRequest:

public final class IngredientesRequest {
   
    private int echo;
   
    private int columns;
   
    private int displayStart;
   
    private int displayLength;
   
    private String search;
   
    private int sortingCols;

    private ColumnFilter columnFilter;
   
    //Getters y Setters  
}

ColumnFilter:

public class ColumnFilter {
   
    private Integer sortCol;
    private String sortDir;
    //Setter getters y constructor con las dos propiedades

IngredientesResponse:

public final class IngredientesResponse {

    private int sEcho;
   
    private int iTotalRecords;
   
    private int iTotalDisplayRecords;
   
    private String[][] aaData;

    /**
     * @param sEcho
     *            An unaltered copy of sEcho sent from the client side. This
     *            parameter will change with each draw (it is basically a draw
     *            count) - so it is important that this is implemented. Note
     *            that it strongly recommended for security reasons that you
     *            'cast' this parameter to an integer in order to prevent Cross
     *            Site Scripting (XSS) attacks.
     * @param iTotalRecords
     *            Total records, before filtering (i.e. the total number of
     *            records in the database)
     */

    public IngredientesResponse(int sEcho, int iTotalRecords) {
        this.sEcho = sEcho;
        this.iTotalRecords = iTotalRecords;
    }
   
    public IngredientesResponse setData (int iColumns, List<Ingrediente> found, int countFound) {
        //Se crea un arreglo 2D con las medidas de los encontrado y el numero de columnas de la tabla
        aaData = new String[found.size()][iColumns];
       
        iTotalDisplayRecords = countFound;
       
        for (int index = 0; index < found.size(); index++) {
            Ingrediente ingrediente = found.get(index);
            aaData[index] = new String[] {
                    ingrediente.getNombre(),
                    ingrediente.getCategoria().getNombre()
            };
        }
       
        return this;
    }

    /**
     * @return the sEcho
     */

    public final int getsEcho() {
        return sEcho;
    }

    /**
     * @return the iTotalRecords
     */

    public final int getiTotalRecords() {
        return iTotalRecords;
    }

    /**
     * @return the iTotalDisplayRecords
     */

    public final int getiTotalDisplayRecords() {
        return iTotalDisplayRecords;
    }

    /**
     * @return the aaData
     */

    public final String[][] getAaData() {
        return aaData;
    }

Bueno con estas tres clases cumplimos con los requisitos de respuesta de datatables y tomamos algunos parametros de interes.
Por el lado de los datos tenemos lo siguiente:

Ingrediente:

@Entity
@Table(name = "INGREDIENTES")
public class Ingrediente {

    @Id
    @GeneratedValue(generator = "ingrediente_seq_gen", strategy = GenerationType.AUTO)
    @SequenceGenerator(name="ingrediente_seq_gen", sequenceName="ingrediente_seq")
    @Column(name = "id")
    private Integer id;

    @Column(name = "nombre", nullable = false)
    private String nombre;

    @ManyToOne
    @JoinColumn(name = "categoria_id", nullable = true)
    private Categoria categoria;
   
    //Getters y Setters

Categoria:

@Entity
@Table(name = "CATEGORIAS")
public class Categoria {
    @Id
    @GeneratedValue(generator = "categoria_seq_gen", strategy = GenerationType.AUTO)
    @SequenceGenerator(name="categoria_seq_gen", sequenceName="categoria_seq")
    @Column(name = "id")
    private Integer id;

    @Column(name = "nombre", nullable = false)
    private String nombre;
   
    //Getters y Setters
}

En este punto solo tenemos dos partes Datos y Cliente... nos falta nuestra logica de busqueda y el controlador

En nuestra logica de busqueda meteremos hibernate (por que? Por que regularmente ya muchos tendran Hibernate como ORM, en mi experiencia prefiero MyBatis me hizo la vida mas facil en estos casos)

Bueno esto no se trata de ver que es mejor o no asi que vamos por nuestro manipulador de datos:

IngredientesHelper: Lo llamo helper por que un DAO desde mi semantica ya esta hecho (trabajo que realiza el ORM, pero bueno tampoco esta bajo discucion).

Nota:Esta clase hereda de una clase que tiene acceso al manager asi que solo lo utilizo.

    /**
     * Retorna la cantidad de registros sin filtrar.
     */

    @Override
    public int contarTodo() {

        CriteriaBuilder qb = getEntityManager().getCriteriaBuilder();

        CriteriaQuery<Long> cq = qb.createQuery(Long.class);

        cq.select(
            qb.count(
                cq.from(Ingredient.class)
            )
        );

        return getEntityManager()
                     .createQuery(cq)
                     .getSingleResult()
                     .intValue();
    }

    /**
      * Retorna la cantidad de registros aplicando el filtro de busqueda proveniente del datatable
      */

    @SuppressWarnings("unchecked")
    @Override
    public int contarFiltrado(IngredientesRequest request) {

        EntityManager em = getEntityManager();
        CriteriaBuilder cb = em.getCriteriaBuilder();

        CriteriaQuery<Long> cq = cb.createQuery(Long.class);
       
        Root<Ingrediente> ingrediente = cq.from(Ingrediente.class);

        EntityType<Ingrediente> ingrediente_ = ingrediente.getModel();
       
        //Indicamos que en la busqueda se integre la tabla de categorias
        Join<Ingrediente, Categoria> categoria =
             (Join<Ingrediente, Categoria>) ingrediente.join(ingrediente_.getSingularAttribute("categoria"));

        EntityType<Categoria> categoria_ = (EntityType<Categoria>) categoria.getModel();
       
        // Creamos la sentencia del select
        cq.select(
          cb.count(ingrediente)
        );
       
        // Y generamos las expresiones para el where
        Expression<String> ingredienteExp =
            (Expression<String>) ingrediente.get(ingrediente_.getSingularAttribute("nombre"));

        Expression<String> categoriaExp =
            (Expression<String>) categoria.get(categoria_.getSingularAttribute("nombre"));
       
        if (StringUtils.isNotBlank(request.getSearch())) {
            cq.where(
                cb.or(
                    cb.like(ingredienteExp, request.getSearch()),
                    cb.like(categoriaExp, request.getSearch())
                )
             );
        }
       
        return em.createQuery(cq).getSingleResult().intValue();
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Ingredient> filtrar(IngredientesRequest request) {

        EntityManager em = getEntityManager();
        CriteriaBuilder cb = em.getCriteriaBuilder();

        CriteriaQuery<Ingrediente> cq = cb.createQuery(Ingrediente.class);
       
        Root<Ingrediente> ingrediente = cq.from(Ingrediente.class);
        EntityType<Ingrediente> ingrediente_ = ingrediente.getModel();
       
        Join<Ingrediente, Categoria> categoria =
               (Join<Ingrediente, Categoria>) ingrediente.join(ingrediente_.getSingularAttribute("categoria"));

        EntityType<Categoria> categoria_ = (EntityType<Categoria>) categoria.getModel();
       
        cq.select(ingrediente);
       
        ColumnFilter filter = request.getColumnFilter();
       
        //Averiguamos que columna es la que se ordena y asignamos la expresion de orden
        Expression<?> exp = null;
        switch (filter.getSortCol()) {
            case 0:
                exp = (Expression<String>) ingrediente.get(ingrediente_.getSingularAttribute("nombre"));
                break;
            case 1:
                exp = (Expression<String>) categoria.get(categoria_.getSingularAttribute("nombre"));;
        }
       
        //Y luego averiguamos el orden que tomara
        Order order = null;
        if (filter.getSortDir().equals("asc")) {
            order = cb.asc(exp);
        } else {
            order = cb.desc(exp);
        }
       
        //Aplicamos el filtrado de busqueda a ambas columnas
        if (StringUtils.isNotBlank(request.getSearch())) {
            cq.where(
                cb.or(
                    cb.like(ingredientExp, request.getSearch()),
                    cb.like(categoriaExp, request.getSearch())
                )
             );
        }
       
        //Aplicamos el orden
        cq.orderBy(order);
       
        //Generamos el query
        TypedQuery<Ingredient> q = em.createQuery(cq);
       
        //E indicamos el inicio del query
        q.setFirstResult(request.getDisplayStart());

        // Y su offset
        q.setMaxResults(request.getDisplayLength());
       
        return q.getResultList();
    }

Bueno ya que tenemos como sacar nuestra informacion de la DB segun los parametro de busqueda y ordenacion, vamos por nuestro controlador.

IngredientesController:

@Controller
public class IngredientesController {

@RequestMapping(value = INGREDIENT_FIND_PATH, method = { RequestMethod.GET })
    @ResponseBody
    public IngredientesResponse index(
            @RequestParam(value = "sEcho") int sEcho,
            @RequestParam(value = "iColumns") int iColumns,
            @RequestParam(value = "sColumns") String sColumns,
            @RequestParam(value = "iDisplayStart") int iDisplayStart,
            @RequestParam(value = "iDisplayLength") int iDisplayLength,
            @RequestParam(value = "mDataProp_0") String mDataProp_0,
            @RequestParam(value = "mDataProp_1") String mDataProp_1,
            @RequestParam(value = "mDataProp_2") String mDataProp_2,
            @RequestParam(value = "sSearch") String sSearch,
            @RequestParam(value = "bRegex") boolean bRegex,
            @RequestParam(value = "sSearch_0") String sSearch_0,
            @RequestParam(value = "bRegex_0") boolean bRegex_0,
            @RequestParam(value = "bSearchable_0") boolean bSearchable_0,
            @RequestParam(value = "sSearch_1") String sSearch_1,
            @RequestParam(value = "bRegex_1") boolean bRegex_1,
            @RequestParam(value = "bSearchable_1") boolean bSearchable_1,
            @RequestParam(value = "sSearch_2") String sSearch_2,
            @RequestParam(value = "bRegex_2") boolean bRegex_2,
            @RequestParam(value = "bSearchable_2") boolean bSearchable_2,
            @RequestParam(value = "iSortCol_0") int iSortCol_0,
            @RequestParam(value = "sSortDir_0") String sSortDir_0,
            @RequestParam(value = "iSortingCols") int iSortingCols,
            @RequestParam(value = "bSortable_0") boolean bSortable_0,
            @RequestParam(value = "bSortable_1") boolean bSortable_1,
            @RequestParam(value = "bSortable_2") boolean bSortable_2,
           
            HttpServletResponse response) {
       
        IngredientesRequest request = new IngredientesRequest(sEcho);
       
        request.setColumns(iColumns);
        request.setDisplayLength(iDisplayLength);
        request.setDisplayStart(iDisplayStart);
        request.setRegex(bRegex);
        request.setSearch(sSearch);
        request.setSortingCols(iSortingCols);
        request.setColumnFilter(new ColumnFilter(iSortCol_0, sSortDir_0));
       
        int total = ingredientesHelper.contarTodo();
       
        int found = ingredientesHelper.contarFiltrado(request);
        List<Ingrediente> ingredientes = ingredientesHelper.filtrar(request);
       
        IngredientesResponse iResponse = new IngredientesResponse(request.getEcho(), total);
       
        return iResponse.setData(request.getColumns(), ingredientes, found);
    }

    public static final String INGREDIENT_PATH = BASE_PATH + "/ingredientes";
    public static final String INGREDIENT_FIND_PATH = INGREDIENT_PATH + "/filtrar";
   
    @Autowired(required = true)
    private IngredientesHelper ingredientesHelper;
}

Y bueno parece ser que es todo, con la anotacion @ResponseBody el POJO que almacena la respuesta segun lo pedido por datatables se traduce aun JSon como no se magia negra de Spring :s, pero bueno funciona :)

Bueno cualquier duda o pregunta pueden googlear o hacerlo por este mismo canal :) y espero que les sea de ayuda :)

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

agregar parametro

Hola, ¿Como podría agregar un parámetro dentro de la llamada ajax? quisiera crear un formulario que se reenvié con datos de filtro en el ajax de esta consulta permitiendo que la búsqueda sea customizada.

Imagen de arterzatij

Con el API de busqueda

Con el API de busqueda puedes agregar un formulario, siempre y cuando este formulario tenga que ver con las columnas que estas utilizando en la tabla. Ya que de otra forma no tiene sentido la busqueda.

https://datatables.net/reference/api/column().search()