Blog sobre desarrollo en Java/Jakarta EE, VueJS, DevOps y más..

lunes, 26 de octubre de 2020

Primefaces: Implementación de Lazy Loading

 

Primefaces dispone de un amplio conjunto de componentes que podemos utilizar en nuestros proyectos, uno de ellos es DataTable que se utiliza para mostrar datos en un formato tabular. Este componente, pone a disposición de los programadores funcionalidades muy interesantes tales como: paginación, búsqueda, filtrado, selección, ordenamiento, entre otras.

Para ello debemos implementar la consulta (query) utilizando Criteria API, dentro del EJB correspondiente, ya que ella nos permite construir una query en tiempo de ejecución.

Nuestra base de datos cuenta con una sola tabla llamada persona, cuya estructura es la siguiente:

Luego mediante JPA debemos mapear esta tabla a una clase java llamada Persona.java

@Entity
@Table(name = "persona")
public class Persona implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Integer id;
  @Column(name = "nombre_apellido")
  private String nombreApellido;
  @Column(name = "email")
  private String email;

  // Constructor

 // Setters/Getters

}

En la interface local PersonaFacadeLocal.java, debemos definir dos métodos, el primero de ellos llamado findAll, cuenta con los siguientes parámetros:

  • start - Indica la posición inicial del cojunto de datos
  • size -  Indica la cantidad de registros a mostrar
  • sortField - Campo por el que se desea ordenar la colección
  • sortOrder- Tipo de ordenamiento (ASC | DESC)
  • filters - Criterios de búsqueda

y el segundo llamado count con un solo parámetro de tipo Map, que sera el encargado de retornar la cantidad de registros encontrados en base a los filtros aplicados. 

PersonaFacadeLocal.java

List<Persona> findAll(int start, int size, String sortField, SortOrder sortOrder, Map<String, FilterMeta> filters);
 
int count(Map<String, FilterMeta> filters);

Luego se debe realizar la implementación de ambos métodos dentro del session bean PersonaFacade como se observa a continuación:

PersonaFacade.java

El método findAll comienza con la interfaces CriteriaBuilder y CriteriaQuery, ya que ellas nos permitiran construir una query dinámica de manera segura sin ser víctimas de la inyección SQL, ya que se valida su contrucción y los parámetros que recibe.

CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<Persona> criteriaQuery = criteriaBuilder.createQuery(Persona.class);
Root<Persona> root = criteriaQuery.from(Persona.class);
CriteriaQuery<Persona> select = criteriaQuery.select(root);
List<Predicate> listaPredicados = new ArrayList<>();
List<Persona> listaPersonas = new ArrayList<>(); 

En esta sección verificamos si existe o no algún criterio de ordenamiento y su tipo.

      if (sortField != null) {
        criteriaQuery.orderBy(sortOrder == SortOrder.ASCENDING
                ? criteriaBuilder.asc(root.get(sortField))
                : criteriaBuilder.desc(root.get(sortField)));
      }

Luego, verificamos si el usuario especificó algun criterio de búsqueda para filtrar los resultados. Recordemos que la tabla persona cuenta con los campos id, nombre_apellido, email, por lo que el usuario puede aplicar cualquiera de estos 3 criterios. En el caso de haya alguno se lo añade a una lista de predicados, caso contrario se ignoran.

      if (filters != null && filters.size() > 0) {

        // Capturmos los filtros
        Object id = filters.get("id").getFilterValue();
        String email = (String) filters.get("email").getFilterValue();
        String nombreApellido = (String) filters.get("nombreApellido").getFilterValue();

        // Filtro id
        if (id != null) {
          Predicate predicado = criteriaBuilder.equal(root.get("id"), id);
          listaPredicados.add(predicado);
        }

        // Filtro nombre y apellido
        if (nombreApellido != null) {
          Predicate predicado = criteriaBuilder.like(criteriaBuilder.lower(root.get("nombreApellido")), "%" + nombreApellido.toLowerCase() + "%");
          listaPredicados.add(predicado);
        }

        // Más filtros...

        // Añadimos predicados al criterio de búsqueda
        if (listaPredicados.size() > 0) {
          listaPredicados.forEach(x -> {
            criteriaQuery.where(listaPredicados.toArray(new Predicate[listaPredicados.size()]));
          });
        }
      } 

Es importante destacar que la búsqueda por id debe retornar una sola fila, y en los restantes casos el usuario puede ir escribiendo letra por letra y el filtro debe ir encontrando las coincidencias en "tiempo real", por lo que podría retornar más de un resultado o ninguno.

En el CDI Controller llamado por ejemplo PersonaDataController.java y con un scope de tipo vista, debemos realizar las siguientes tareas:

  • Inyectar una referencia a la interface PersonaFacadeLocal para poder utilizar sus métodos:
  @EJB
  PersonaFacadeLocal personaEJB;
  • Definir una variable llamada listaPersonas cuyo tipo de dato es LazyDataModel especialidada en Persona, con sus respectivos métodos set y get.
   private LazyDataModel<Persona> listaPersonas; 
  • Implementar los métodos init e iniciar acompañados de la anotación @PostConstruct, para que al momento en que el usuario acceda a la vista el listado este disponible.
  @PostConstruct
  public void init() {
    iniciar();
  }

private void iniciar() {
    listaPersonas = new LazyDataModel<Persona>() {
      @Override
      public List<Persona> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, FilterMeta> filterBy) {
        List<Persona> listaDePersonas = personaEJB.findAll(first, pageSize, sortField, sortOrder, filterBy);
        listaPersonas.setRowCount(personaEJB.count(filterBy));
        refreshTableState();
        return listaDePersonas;
      }

      @Override
      public Object getRowKey(Persona object) {
        return object.getId();
      }

      @Override
      public Persona getRowData(String rowKey) {
        Persona persona = null;
        try {
          persona = personaEJB.find(Integer.parseInt(rowKey));
        } catch (NumberFormatException e) {
          System.out.println("ocurrio un error : " + e.getLocalizedMessage());
        }
        return persona;
      }
    };
  }

El método iniciar() es muy interesante, ya que en su interior se instancia el objeto listaPersonas, y en su interior se sobreescriben los métodos load(), getRowKey(), getRowData().

El método load(), es el encargado de acceder a la base de datos para obtener el listado mediante el método findAll() y a su vez "setea" la cantidad de registros obtenidos mediante setRowCount. 

El metodo  getRowKey(), se encarga de retornar el campo que identifica de manera univoca a cada registro en el listado. 

Por su parte, el metodo getRowData(), tiene como objetivo buscar un objeto por su clave primaria y retornarlo al usuario.

Estos dos últimos método solo son necesarios en caso de que se desee implementar la selección de registros en el componente DataTable.

Finalmente en la vista (archivo index.xhtml), implementamos el componente DataTable de la siguiente manera:

  <p:dataTable id="tblPersonas"
                       value="#{personaDataController.listaPersonas}"
                       var="p"
                       paginator="true"
                       paginatorTemplate="{CurrentPageReport} {FirstPageLink} {PreviousPageLink} {PageLinks} {NextPageLink} {LastPageLink} {RowsPerPageDropdown}"
                       currentPageReportTemplate="{startRecord}-{endRecord} de {totalRecords} registros"
                       rowsPerPageTemplate="10,25,50,100"
                       rows="10"
                       rowKey="#{p.id}"
                       selectionMode="single"
                       selection="#{personaDataController.personaSeleccionada}"
                       lazy="true"
                       emptyMessage="Su búsqueda no arrojó resultados."
                       >
            <f:facet name="header">
              Listado de Clientes
            </f:facet>
            <p:column headerText="Cod."
                      field="id"
                      filterBy="#{p.id}"
                      sortable="true"
                      sortBy="#{p.id}"
                      style="text-align: center;"
                      >
              <h:outputText value="#{p.id}"/>
            </p:column>
            <p:column headerText="Nombre y Apellido"
                      field="nombreApellido"
                      filterBy="#{p.nombreApellido}"
                      sortable="true"
                      sortBy="#{p.nombreApellido}">
              <h:outputText value="#{p.nombreApellido}"/>
            </p:column>
            <p:column headerText="Email"
                      field="email"
                      filterBy="#{p.email}"
                      sortable="true"
                      sortBy="#{p.email}">
              <h:outputText value="#{p.email}"/>
            </p:column>
          </p:dataTable>
 

Para que esta implementación funcione, es muy importante que el valor del atributo lazy sea true (lazy=true), y que se haya definido el rowKey.

De este modo, nuestro componente DataTable divide el cojunto de registros en varias paginas cargando en la memoria del navegador solo 10 filas y permite que el usuario pueda ordenar de manera ascendente o descendente los registros y  filtrarlos de acuerdo a su criterio de búsqueda en tiempo de ejecución.

Seguramente haya otras formas de implementación, pero hasta el momento, a mi me funcionó de maravillas y no experimente ningún tipo de retraso en las operaciones  en una base de datos poblada con una cantidad enorme de registros.

Las sugerencias y/o comentarios siempre son bienvenidos, asi que estaré muy agradecido si alguien puede aportar su feedback. Al fin de cuentas, al conocimiento lo hacemos entre todos.

El proyecto completo se encuentra disponible en: https://github.com/Francisco-Castillo/primefaces-lazy-loading.git

Compartir:

0 comentarios:

Publicar un comentario

Acerca de mí

Mi foto
Capital, Santiago del Estero, Argentina