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

// Java
@Override
public Class<Celular> getEntityType() {
    return Celular.class;
}

@Override
public Celular createExample() {
    return new Celular();
}

// Xtend
override def getEntityType() {
    typeof(Celular)
}
 
override def createExample() {
    new Celular
}

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).

override def Predicate<Cliente> getCriterio(Cliente example) {
    var result = this.criterioTodas
    if (example.numero != null) {
        result = new AndPredicate(result, this.getCriterioPorNumero(example.numero))
    }
    if (example.nombre != null) {
        result = new AndPredicate(result, this.getCriterioPorNombre(example.nombre))
    }
    if (example.modeloCelular != null) {
        result = new AndPredicate(result, this.getCriterioPorModelo(example.modeloCelular))
    }
    result
}
    
override getCriterioTodas() {
    [ Cliente cliente | true ] as Predicate<Cliente>
}
    
def getCriterioPorNumero(Integer numero) {
    [ Cliente cliente | cliente.numero.equals(numero) ] as Predicate<Cliente>
}
 
def getCriterioPorNombre(String nombre) {
    [ Cliente cliente | cliente.nombre.toLowerCase.contains(nombre.toLowerCase) ] as Predicate<Cliente>
}
    
def getCriterioPorModelo(Modelo modelo) {
    [ Cliente cliente | cliente.modeloCelular.equals(modelo) ] as Predicate<Cliente>
}

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

def search(Integer numero, String nombre) {
    allInstances.filter [cliente | this.match(numero, cliente.numero) && 
        this.match(nombre, cliente.nombre)].toList
}

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
  • si tenemos un esquema persistente, tenemos que veriificar 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.

// Xtend
class CelularesBootstrap extends CollectionBasedBootstrap {

    /**
     * Inicialización del juego de datos del repositorio
     * 
     * Nota: en ejemplos anteriores estaba en el método init
     * del repo, esto acoplaba innecesariamente el juego de datos
     * con su repositorio
     * 
     */
    override run() {
        val repoModelos = RepoModelos.instance
        ...
        val nokiaAsha = repoModelos.create("NOKIA ASHA 501", 700f, true)
        val lgOptimusL5 = repoModelos.create("LG OPTIMUS L5 II", 920f, false)

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

class CelularApplication extends Application {

    new(CelularesBootstrap bootstrap) {
        super(bootstrap)
    }

    static def void main(String[] args) {
        new CelularApplication(new CelularesBootstrap).start()
    }

Cómo usar los repositorios

La búsqueda se dispara mediante un searchByExample()

// Java
Celular celularABuscar = new Celular(unNumero, unNombre);
List<Celular> resultados = repoCelulares.searchByExample(celularABuscar);

// Xtend
val celularABuscar = new Celular => [
    numero = unNumero
    nombre = unNombre
]
resultados = repoCelulares.searchByExample(celularABuscar)

o bien con un search():

// Java
List<Celular> resultados = repoCelulares.search(numero, nombre);

// Xtend
resultados = repoCelulares.search(numero, nombre)

Una forma de actualizar / crear un objeto podría ser
// Java
if (getModelObject().isNew()) {
    getRepoCelulares().create(getModelObject());
} else {
    getRepoCelulares().update(getModelObject());
}

// Xtend
if (modelObject.isNew) {
    repoCelulares.create(modelObject)
} else {
    repoCelulares.update(modelObject)
}

Subpáginas (1): Arena - ApplicationContext
Comments