Arena - Repositorios

Intro a Repositorios o Homes

La forma que le vamos a dar nosotros a la interfaz con nuestra persistencia es utilizando objetos "Repositorio". Tal vez los hayan escuchado nombrar como DAOs o Homes. Un repositorio es el lugar donde "viven" todos los objetos de un mismo tipo.

Interfaz de un repositorio

Definición

Un repositorio o home almacena objetos de diferente tipo, lo parametrizamos en la definición mediante el uso de generics. Entonces hablamos de la interfaz Repo<T> en Arena como un objeto que sabe donde viven los objetos del tipo T (pueden ser de clases diferentes pero comparten el mismo tipo T en común).

T debe ser subclase de Entity (definido en el package org.uqbar.commons.model), que le agrega

  • un identificador unívoco a cada objeto (Integer id)
  • un mensaje isNew() que determina si el objeto tiene identificador (presumiblemente porque fue agregado a algún repositorio)
  • redefiniciones de los métodos hashCode() y equals() para que trabajen en base al identificador unívoco
  • template methods validateCreate() y validateDelete() para redefinir validaciones en el alta y en la eliminación

Servicios

¿Qué servicios ofrece un repositorio?

  • create: agregar un objeto
  • delete: eliminar un objeto
  • update: actualizar un objeto, lo que en una colección equivaldría a
    • modificar el objeto que está en la posición x
    • o eliminar el objeto viejo y luego crear uno nuevo
  • diferentes tipos de búsquedas, los repos que provee Arena incluyen
    • searchById: búsqueda por un identificador unívoco, ya que los repos trabajan con subclases de Entity
    • searchByExample: le pasamos un objeto prototípico y la búsqueda considera los criterios en donde hay información de ese objeto prototípico (ver en el panel de búsqueda)
    • allInstances: traer todos los objetos que forman parte del repositorio.


Implementaciones de repositorios en Arena

El paquete org.uqbar.commons.model (de uqbar-domain) define las siguientes implementaciones:

  • AbstractAutogeneratedIdRepo: clase abstracta que sabe generar identificadores unívocos a los objetos y ofrece dos implementaciones default para los métodos
    • create
    • y delete, delegando en hook methods validateCreate() y validateDelete() respectivamente.
  • CollectionBasedRepo: implementación que trabaja con una colección en memoria, no persiste (cuando la aplicación se baja, todos los datos se pierden). Además del create y delete que hereda de AbstractAutogeneratedIdRepo le agrega las siguientes definiciones:
    • update
    • searchById: búsqueda por identificador unívoco (por eso almacena objetos que extienden Entity)
    • allInstances: permite conocer todos los objetos que viven en esa colección en memoria

Otras estrategias

El paquete arena-pers ofrece una implementación que persiste a un repositorio de grafos neo4j:

  • PersistentRepo: es una clase abstracta que implementa la interfaz Repo<T> y que hay que redefinir en nuestra aplicación. Para más información recomendamos acceder a esta página

Uso de un repositorio

Los repositorios por lo general se definen como singletons, ya que

  • necesitamos tener una única forma de acceder al repositorio
  • y porque funciona como centralizador de todos los pedidos de actualización y de consulta de los objetos T del sistema

Para definir un repositorio podemos

  1. armar nuestra propia implementación de Repo<T>
  2. o bien subclasificar alguna de las implementaciones existentes, como CollectionBasedRepo.

Qué necesitamos redefinir para implementar un CollectionBasedRepo

  • createExample(): debe devolver un objeto T
  • getEntityType(): debe devolver la clase T. Estos dos métodos no aportan demasiado, pero son necesarios porque en tiempo de ejecución Arena necesita conocer esta información que se pierde luego de compilar
  • Predicate<T> getCriterio(T example): el criterio que podemos establecer para hacer la búsqueda by example. Aquí es donde definimos qué criterio utilizamos al buscar con un objeto prototipo. Algunos ejemplos:
    • matchea si el nombre del socio es igual al nombre del ejemplo
    • matchea si el nombre del socio contiene el nombre del ejemplo
    • matchea si el nombre del socio comienza con el nombre del ejemplo o el número coincide
    • matchea si el nombre del socio comienza con el nombre del ejemplo y el número coincide

Ejemplos: veamos el createExample y el getEntityType para un Repo de objetos clientes de una compañía de celulares

Y definimos un criterio de búsqueda by example para la búsqueda de clientes combina una búsqueda por número exacto, por nombre contiene y por modelo de celular exacto. Se busca que cumpla todos los criterios ingresados (AndPredicate).

Implementar una búsqueda específica

Si no queremos trabajar la búsqueda "by example", debemos implementar nuestro propio método search. Ejemplo: implementamos una búsqueda ad-hoc de clientes por número o nombre:

El mismo ejemplo en Java puede verse en el ejemplo de los Celulares.

Cómo y dónde instanciar los repositorios

Si el repo es un Singleton, basta con referenciarlo mediante el mensaje RepoCelulares.instance. Otra técnica consiste en utilizar un objeto ApplicationContext.

  • al correr la aplicación, los singletons se instancian la primera vez en la pantalla inicial o bien en el Application
  • si estamos ejecutando los tests, el método setup() o init() -uno que tenga la annotation @Before-

Bootstrap: crear un juego de datos

Es importante generar un juego de datos de prueba: podríamos definir un método init() que se llame en el constructor del repo. El inconveniente que presenta este approach es que acopla un determinado repositorio al fixture o juego de datos de la aplicación, sin poder diferenciar distintos entornos (desarrollo, pruebas del TP, entrega final productiva, etc.). La alternativa recomendada entonces es diseñar un objeto que implemente la interfaz Bootstrap:

El método isPending() nos dice cuándo debe ejecutarse el método run():

  • si estamos utilizando un repo en memoria, cada vez que inicie la aplicación debemos cargar el juego de datos. La clase CollectionBasedBootstrap asume que estás trabajando en un esquema no persistente, así que provee la implementación por defecto (para la materia Interfaces de Usuario tu clase Bootstrap debería heredar de CollectionBasedBootstrap)
  • si tenemos un esquema persistente, tenemos que verificar que los datos no existan en la base para cargarlos una sola vez

En el método run() estará el script de inicialización del juego de datos.

Luego al instanciar la aplicación le pasamos un bootstrap:

Cómo usar los repositorios

La búsqueda se dispara mediante un searchByExample()

o bien con un search():

Una forma de actualizar / crear un objeto podría ser

El método getRepoCelulares() obtiene el repositorio de celulares (mediante el acceso al singleton global o bien utilizando la técnica del Application Context)