« Atrás

RichFaces, trabajando con tablas (2)

Este artículo surge como continuación de "RichFaces, trabajando con tablas (1)". Describe los pasos a seguir para trabajar con tablas paginadas mediante componentes RichFaces que sean capaces de guardar en memoria SÓLO los objetos que se están mostrando en el navegador. Se presenta un nuevo modelo de datos con ese objetivo, nuevas clases de la especificación JavaServer Faces y otras específicas de RichFaces. Este artículo nos enseña a optimizar el diseño de tablas paginadas.

Introducción

Se quiere trabajar con tablas paginadas que NO tengan que guardar en memoria TODOS los objetos de la tabla, estén siendo mostrados en pantalla o no. De esta forma se optimiza la memoria necesaria para la aplicación.

El modelo presentado en el artículo "RichFaces, trabajando con tablas (1)" maneja un conjunto de datos mediante una lista Java (se modela mediante la interface List). De tal forma que, independiente de la paginación que se esté aplicando, todos los objetos de la tabla se encuentran cargados en memoria. Esto, en muchas ocasiones, no es viable.

La solución que se plantea en este artículo permite almacenar en memoria SÓLO los objetos que se están utilizando en cada momento. Como ya se ha comentado anteriormente, no se trabaja con una implementación de la interface List. El modelo va a evolucionar para trabajar de una forma más sofisticada:

<rich:dataTable value="#{modeloImpl}"...>
  ...
</rich:dataTable>

El modelo será una clase que hereda de SerializableDataModel. Se modifica, a continuación, el ejemplo inicial descrito en "RichFaces, trabajando con tablas (1)" para que no tengan que cargarse todos los objetos generados como resultado de la consulta en memoria.

Además, vamos a hacer que el MBean utilizado para esta página NO tenga que encontrarse en el alcance de sesión:

...
  public class MBean2 {

      private DataModel facturas;
      ...
      public DataModel getFacturas() {
          return facturas;
      }
      public void setFacturas(DataModel facturas) {
          this.facturas = facturas;
      }
  }


Pasamos a inyectar el modelo mediante el fichero de configuración de JavaServer Faces:

...
<managed-bean>
  <managed-bean-name>paginacion2</managed-bean-name>
  <managed-bean-class>
      es.ematiz.paginacion.MBean2
  </managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
  <managed-property>
    <property-name>facturas</property-name>
    <value>#{modeloFacturas}</value>
  </managed-property>
</managed-bean>
...

Nota: el MBean asociado al modelo de datos llamado modeloFacturas será definido posteriormente.

Es importante NO olvidar que vamos a tener que implementar una clase que herede de SerializableDataModel. Para entender de forma adecuada cómo hacerlo, se describen a continuación las clases que se van a manejar:

  • La clase DataModel: clase perteneciente a la especificación JavaServer Faces que permite definir modelos de datos avanzados.
  • La clase ExtendedDataModel: es una clase abstracta que define modelos de datos avanzados.
  • La clase SerializableDataModel: el modelo de datos será una clase que hereda de ésta.

La clase DataModel

Esta clase es una abstracción de datos que puede utilizarse como fuente de datos para distintos componentes JSF que soportan procesado de filas de sus componentes hijos.
La colección de datos subyacente de una instancia de DataModel se modela como una colección de objetos fila a la que se puede acceder mediante un índice.

Métodos:

  • getRowCount(): número de filas de objetos representados por la instancia DataModel.
  • getRowData(): devuelve el objeto representado por la fila seleccionada por el índice de filas.
  • getRowIndex(): devuelve el índice de filas.
  • ...

Se recomienda analizar de forma detallada la clase mediante documentación oficial.

La clase ExtendedDataModel

Es una extensión abstracta de DataModel definida por RichFaces para manejar estructuras de datos complejas.

- La clave o identificador para cada uno de los objetos no tiene que ser un índice entero. Puede ser cualquier tipo de objeto. Para modelar esta idea se deben implementar los métodos siguientes adaptándolos a nuestras necesidades:

public abstract void setRowKey(java.lang.Object key)
public abstract java.lang.Object getRowKey()

- El método walk() será el responsable de generar los objetos que va a manejar el componente JSF en la renderización.

public abstract void walk( FacesContext context, DataVisitor visitor, Range range, 
                           Object argument) throws java.io.IOException

Este método itera sobre el modelo mediante el patrón visitor para un rango dado.
Cada vez que el ciclo de vida JSF necesita recuperar el modelo de datos ejecuta este método, es decir, en una situación normal se ejecuta tres veces: fase de aplicación de la petición, fase de validación y fase de actualización del modelo.

- El método getSerializableModel(): un componente de iteración puede soportar operaciones de guardado de datos en las fases de decodificación/validación y actualización para evitar llamadas innecesarias al modelo original. Por ejemplo, para evitar peticiones a la base de datos hasta que todos los datos están validados.
Este método se ejecuta una vez en el ciclo de vida estándar JSF, en la fase de renderización.

La clase SerializableDataModel

Hereda de ExtendedDataModel añadiendo un nuevo método update() que sólo se ejecuta cuando se lanza una acción dentro del componente JSF asociado en la fase de actualización del modelo:

Diseño del modelo de datos

Una vez descritas las tres clases importantes que nos permitirán crear el modelo de datos, pasamos a construirlo para nuestro ejemplo de Facturas.

Como ya se ha comentado el modelo de datos hereda de la clase SerializableDataModel:

...
public class FacturaDataModel extends SerializableDataModel{

// Atributos del modelo de datos
private static final long serialVersionUID = 1L;

// Clave o identificador de cada factura.
private Integer currentPk;
// Mapa que almacena id/Factura para los objetos que están manejando
// la tabla en este momento.
private Map wrappedData =
new HashMap();
// Lista de los identificadores que está manejando la tabla
// en cada caso.
private List wrappedKeys = null;
private FacturaDataProvider dataProvider;
private boolean detached = false;
...


- El atributo currentPk representa a la clave primaria del objeto asociado a la tabla que se está manejando.
Para el caso de Factura, la clave es de tipo Integer, es por eso que currentPK es de tipo Integer. Si la clave primaria del objeto manejado por la tabla fuera de otro tipo, deberíamos cambiar también el tipo asociado a este atributo. Por ejemplo, si fuera una cadena de caracteres sería:

private String currentPk;


- El atributo wrappedData es un mapa que debe almacenar pares de datos: identificador único del objeto y los objetos que maneja la tabla en este momento.
- El atributo wrappedKeys almacena los identificadores únicos de cada uno de los objetos que maneja la tabla.

private Map wrappedData =
new HashMap();
private List wrappedKeys = null;


- El atributo detached es un booleano que vamos a utilizar para indicar cuándo hay que recuperar el modelo de datos de la base de datos y cuando NO se debe hacer. Este atributo nos permite diferenciar entre peticiones que renderizan la tabla paginada y peticiones POSTBACK. MUY IMPORTANTE a la hora de optimizar el acceso a la base de datos.

- El atributo dataProvider es el objeto Java que realmente se responsabiliza del acceso a base de datos.
Posteriormente, se describe el proceso para crear esta clase.

El diseño del modelo de datos implica la implementación los métodos anteriormente comentados:

  • Método walk.
  • Método getSerializableModel.
  • Método update.
  • Método getRowCount.
  • Método getRowData.

El método walk()

public void walk(FacesContext context, DataVisitor visitor,
                 Range range,
                 Object argument) throws IOException {
  // Paso 1: Se recupera el rango de datos con el que trabaja la tabla
  // paginada.
  int firstRow = ((SequenceRange)range).getFirstRow();
  int numberOfRows = ((SequenceRange)range).getRows();
       
  // Se comprueba si el modelo esta serializado
  if (detached) {
    // Paso 2.1: Si el modelo está serializado NO se accede de nuevo 
    // a la base de datos.
    for (Integer key:wrappedKeys) {
        setRowKey(key);
        visitor.process(context, key, argument);
    }
   } else {
    // Paso 2.2: Si el modelo no esta serializado hay que volver a
    // recuperarlo.
      wrappedKeys = new ArrayList<Integer>();
      // Para recuperar el modelo se actua sobre el proveedor
      // de datos, dataProvider.
      for (Factura f:dataProvider.getItemsByRange(firstRow,
           numberOfRows)) {
           wrappedKeys.add(f.getCodfactura());
           wrappedData.put(f.getCodfactura(), f);
           visitor.process(context, f.getCodfactura(),
                   argument);
      }
  }
}

El método getSerializableModel()

public SerializableDataModel getSerializableModel(Range range) {
  if (wrappedKeys!=null) {
    detached = true;
    return this;
  } else {
    return null;
  }
}

El método update()

Este método se ejecuta, dentro del ciclo de vida JSF en la fase de actualización del modelo, como resultado de una acción dentro de la tabla que, evidentemente, tiene que enviar la tabla como parte de la información a procesar en el servidor. Inicialmente este método puede estar vacío:

public void update() {

}

El método getRowData()

Devuelve el objeto asociado al identificador actual.

public Object getRowData() {

  if (currentPk==null) {
    return null;
  } else {
    Factura ret = wrappedData.get(currentPk);
    if (ret==null) {
      // Se utiliza el proveedor de datos para recuperar el objeto
      // asociado a una determinada clave o identificador.
      ret = getDataProvider().getItemByKey(currentPk);
      wrappedData.put(currentPk, ret);
      return ret;
    } else {
      return ret;
    }
  }
}

El método getRowCount()

Devuelve el número de filas total de la información con la que se trabaja.

private Integer rowCount;
@Override
public int getRowCount() {
  if (rowCount==null) {
    rowCount = new Integer(getDataProvider().getRowCount());
    return rowCount.intValue();
  } else {
    return rowCount.intValue();
  }
}

 

El modelo de datos se define en el alcance request.

<managed-bean>
  <managed-bean-name>modeloFacturas</managed-bean-name>
  <managed-bean-class>
    es.ematiz.paginacion.modelo.FacturaDataModel
  </managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
  <managed-property>
    <property-name>dataProvider</property-name>
    <value>#{proveedorFacturas}</value>
  </managed-property>
</managed-bean>

El proveedor de datos va a ser definido como MBean y, como se puede ver en el código, será inyectado en el modelo de datos como una propiedad manejada. Se describe a continuación el proceso de creación de dicho proveedor.

El proveedor de datos

Es el objeto responsable de la interacción con el sistema de almacenamiento sobre el que trabajamos para recuperar la información. Esta clase debe implementar la interface DataProvider que se encuentra en el paquete org.richfaces.model. Esta especificación define los siguientes métodos:

  • getItemByKey: devuelve un objeto a partir de su clave.
  • getItemsByRange: devuelve una lista de objetos en función de un rango de valores definido como parámetro.
  • getKey: devuelve la clave asociada a un objeto pasado como parámetro.
  • getRowCount: número de filas total.

Siguiendo nuestro ejemplo, el proveedor de datos para Facturas sería algo tal que así:

...
public class FacturaDataProvider
                            implements DataProvider<Factura>{
  // Atributos de la clase
  private static final long serialVersionUID = 1L;
  private Integer size = null;
  private List<Factura> rango;
  private Map<Integer,Factura> mapa =
                             new HashMap<Integer,Factura>();
  ...
}

- El atributo size define el número de elementos que se manejan.
- El atributo rango define el conjunto de objetos que realmente se van a cargar en memoria.
- El atributo mapa almacena pares Identificador de objeto, objeto.

El método getItemByKey()

Como su nombre indica, este método debe devolver el objeto asociado a una determinada clave o identificador.

@Override
public Factura getItemByKey(Object key) { 
    // Uno de los atributos de la clase es una mapa que almacena pares
    // claves/Objetos, por lo tanto, teniendo la clave recuperar el objeto
    // es algo evidente.      
    return mapa.get(key);
}

El método getKey()

Este método recupera la clave asociada al objeto pasado como parámetro.

@Override
public Object getKey(Factura f) { 
    // Para el caso de la clase Factura, su clave es codigo.
    return f.getCodigo();
}

El método getItemsByRange()

Es el método más importante, siendo responsable de la recuperación de SÓLO los objetos que va a mostrar la tabla en cada momento, es decir SÓLO recupera el rango definido mediante los parámetros firstRow y endRow.

public List<Factura> getItemsByRange(int firstRow, int endRow) {
       
  FacturaBD servicio = new FacturaBD();
  rango = servicio.findFacturas(firstRow, endRow);

  Iterator<Factura> it = rango.iterator();
  while (it.hasNext()) {
    Factura factura = (Factura) it.next();
    mapa.put(factura.getCodfactura(), factura);
  }
  return rango;
}

El método getRowCount()

Este método devuelve como valor de retorno el número total de datos con los que trabaja la tabla. Este valor determina el tamaño del scroller asociado a la tabla.

@Override
public int getRowCount() {

  if(size==null){
    FacturaBD servicio = new FacturaBD();
    size = servicio.getNumFacturas();
  }  
  return size.intValue();
}

Declaración del proveedor en faces-config.xml

El proveedor de datos debe ser un objeto definido en el alcance sesión, vamos a aprovechar el fichero de configuración de JavaServer Faces para definirlo y posteriormente inyectarlo en el modelo de datos:

<managed-bean>
  <managed-bean-name>proveedorFacturas</managed-bean-name>
  <managed-bean-class>
      es.ematiz.paginacion.modelo.FacturaDataProvider
  </managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

 

Como ya se ha comentado anteriormente, el proveedor debe ser inyectado en el modelo de datos.
Se muestra de nuevo la estructura que debemos incluir en el fichero de configuración de JavaServer Faces.

<managed-bean>
  <managed-bean-name>modeloFacturas</managed-bean-name>
  <managed-bean-class>
    es.ematiz.paginacion.modelo.FacturaDataModel
  </managed-bean-class>
  <managed-bean-scope>request</managed-bean-scope>
  <managed-property>
    <property-name>dataProvider</property-name>
    <value>#{proveedorFacturas}</value>
  </managed-property>
</managed-bean>

Incorporación en la vista

El modelo de datos definido y su proveedor asociado han sido definidos aprovechando la inyección de dependencias de JSF. Para poder utilizarlos en la vista sólo debemos utilizar lenguaje de expresión:

<h:form>
  <rich:dataTable id="tablaFacturas"
                value="#{paginacion2.facturas}"
                var="f" rows="5" reRender="scroll01"
                width="100%">
    <rich:column id="concepto">
      <f:facet name="header">
        <h:outputText value="Concepto"/>
      </f:facet>
      <h:outputText value="#{f.concepto}" />
    </rich:column>
    ...
  </rich:dataTable>
  <rich:datascroller id="scroll01" for="tablaFacturas"
                   maxPages="5" />
</h:form>

A continuación se muestra el resultado:

No se cargan en memoria todos los objetos, SÓLO los que se están visualizando en la tabla.

Análisis del ciclo de vida

La primera vez que se quiere visualizar la página, el ciclo de vida (como ya hemos comentado anteriormente es más corto):

  • Fase de restauración de la vista.
  • Fase de renderización: en esta fase se ejecutan los métodos del modelo de datos anteriormente descritos. En el caso del método walk(), como la variable detached está a falso, obligatoriamente accede a base de datos y recupera SÓLO los objetos que va a mostrar la tabla. El número de objetos cargados en memoria es mucho menor que en el primero caso. Posteriormente, se ejecuta el método getSerializableModel() que devuelve el modelo serializado poniendo la variable detached a verdadero (esto provoca que cuando se hagan peticiones sobre la página -Postbacks- no se recupere el modelo de la base de datos, ya que se encuentra serializado).

Finalmente, se obtiene el resultado de la renderización:


Para este caso, las únicas acciones que vamos a poder generar sobre la tabla se realizan mediante el scroller.
Éste va a lanzar una única petición a base de datos en la fase de renderización. En las demás fases no se hace nada relacionado con el acceso a base de datos, por lo tanto, todo correcto.

Actualización

Si quieres seguir aprendiendo sobre este tema puedes acceder al post "RichFaces, trabajando con tablas(3): ordenación y filtrado".

 

Comentarios
URL de Trackback:

comments powered by Disqus