Estrategias de diseño

5.5. Estrategias de diseño#

Cuando analizamos conectores, ya expusimos que uno de los patrones de diseño más socorridos es el patrón DAO. Al usar un ORM podríamos seguir esa misma estrategia y definir una interfaz CRUD para crear clases DAO para todas las entidades del modelo. En este caso, la única salvedad es que las implementaciones de los métodos de la interfaz no se basarían en JDBC sino en JPA. Ahora bien, muy comúnmente y a menos que nuestra intención sea reaprovechar una interfaz antigua implementada con JDBC, obrar de esta manera es un poco redundante, ya que el propio JPA tiene una orientación semejante a DAO.

Lo que sí es muy conveniente es intentar liberar al resto del código de las particularidades del acceso, así que conviene encerrarlas en una clase auxiliar. Podríamos proponer esta:

JpaBackend.java#
public class JpaBackend {

    /**
     * Lista de claves hash que representan las instancias creadas.
     * Se utiliza una lista para mantener el orden de creación y permitir la recuperación por índice.
     */
    private static ArrayList<Integer> keys = new ArrayList<>();
    /**
     * Mapa que asocia las claves hash con las instancias de EntityManagerFactory.
     */
    private static Map<Integer, EntityManagerFactory> instances = new HashMap<>();

    /**
     * Constructor privado para evitar instanciación.
     */
    private JpaBackend() { super(); }

    /**
     * Genera un {@link EntityManagerFactory} a partir del nombre de la unidad de persistencia
     * y un mapa que modifica sus propiedades.
     * @param persistenceUnit El nombre de la unidad de persistencia.
     * @param props El mapa que define las propiedades definidas en tiempo de ejecución.
     * @return Un índice que representa la instancia creada.
     * @throws IllegalArgumentException Si el nombre de la unidad de persistencia es nulo.
     * @throws IllegalStateException Si ya existe una instancia con esos parámetros.
     */
    public static int createEntityManagerFactory(String persistenceUnit, Map<String, String> props) {
        if(persistenceUnit == null) throw new IllegalArgumentException("El nombre de la unidad de persistencia no puede ser nulo"); 

        int hashCode = Objects.hash(persistenceUnit, props);

        EntityManagerFactory instance = instances.get(hashCode);
        if(instance != null && instance.isOpen()) {
            throw new IllegalStateException("Ya existe una EntityManagerFactory con esos parámetros");
        }
        instance = Persistence.createEntityManagerFactory(persistenceUnit, props);
        instances.put(hashCode, instance);
        keys.add(hashCode);
        return keys.size();
    }

    /**
     * Genera un {@link EntityManagerFactory} a partir del nombre de la unidad de persistencia
     * Se sobreentiende que no se modifica o añade ninguna propiedad.
     * @param persistenceUnit El nombre de la unidad de persistencia.
     * @return Un índice que representa la instancia creada.
     * @throws IllegalArgumentException Si el nombre de la unidad de persistencia es nulo.
     * @throws IllegalStateException Si ya existe una instancia con esos parámetros.
     */
    public static int createEntityManagerFactory(String persistenceUnit) {
        return createEntityManagerFactory(persistenceUnit, null);
    }

    /**
     * Devuelve un objeto EntityManagerFactory generado anteriormente.
     * @param index El índice de la instancia a recuperar.
     * @return La instancia de EntityManagerFactory correspondiente al índice.
     * @throws IllegalArgumentException Si el índice está fuera de rango.
     */
    public static EntityManagerFactory getEntityManagerFactory(int index) {
        if(index < 1 || index > keys.size()) throw new IllegalArgumentException("Índice fuera de rango");

        int hashCode = keys.get(index - 1);
        EntityManagerFactory instance = instances.get(hashCode);
        if(instance != null && instance.isOpen()) return instance;
        else {
            instances.remove(hashCode);
            keys.remove(index - 1);
            throw new IllegalStateException("La instancia solicitada no está disponible");
        }
    }

    /**
     * Devuelve un objeto EntityManagerFactory generado anteriormente. Sólo funciona si se generó uno.
     * @return El objeto resultante.
     * @throws IllegalStateException Si no hay ninguna instancia o si hay varias.
     */
    public static EntityManagerFactory getEntityManagerFactory() {
        EntityManagerFactory instance = null;

        switch(instances.size()) {
            case 1:
                instance = instances.values().iterator().next();
                if(instance.isOpen()) return instance;
                else {
                    reset();
                    throw new IllegalStateException("La instancia solicitada no está disponible");
                }
            case 0:
                throw new IllegalStateException("No hay disponible ninguna instancia");
            default:
                throw new IllegalStateException("Invocación ambigua: hay varios candidatos");
        }
    }

    /**
     * Elimina todos los objetos previamente creados.
     */
    public static void reset() {
        instances.clear();
        keys.clear();
    }

    // Transacciones.
    /**
     * Ejecuta una acción dentro de una transacción, devolviendo un resultado.
     * @param <T> El tipo de resultado de la acción.
     * @param index El índice de la instancia a utilizar.
     * @param action La acción a ejecutar.
     * @return El resultado de la acción.
     */
    public static <T> T transactionR(Integer index, Function<EntityManager, T> action) {
        EntityManagerFactory emf = index != null?getEntityManagerFactory(index):getEntityManagerFactory();
        try(EntityManager em = emf.createEntityManager()) {
            EntityTransaction tx = em.getTransaction();
            try {
                tx.begin();
                T result = action.apply(em);
                tx.commit();
                return result;
            }
            catch(Exception e) {
                if(tx != null && tx.isActive()) tx.rollback();
                throw new RuntimeException("Fallo en la transacción", e);
            }
        }
    }

    /**
     * Versión sin índice de {@link #transactionR(Integer, Function)} para cuando sólo hay una instancia.
     * @param <T> El tipo de resultado de la acción.
     * @param action La acción a ejecutar.
     * @return El resultado de la acción.
     */
    public static <T> T transactionR(Function<EntityManager, T> action) {
        return transactionR(null, action);
    }

    /**
     * Ejecuta una acción dentro de una transacción, sin devolver resultado.
     * @param index El índice de la instancia a utilizar.
     * @param action La acción a ejecutar.
     */
    public static void transaction(Integer index, Consumer<EntityManager> action) {
        transactionR(index, em -> {
            action.accept(em);
            return null;
        });
    }

    /**
     * Versión sin índice de {@link #transaction(Integer, Consumer)} para cuando sólo hay una instancia.
     * @param action La acción a ejecutar.
     */
    public static void transaction(Consumer<EntityManager> action) {
        transaction(null, action);
    }
    // Fin transacciones.
}

Esta clase implementa un patrón Singleton ampliado que posibilita la creación de una única fábrica por entidad de persistencia. Cada una de estas fábricas se asocia a un índice a partir del 1 para que podamos recuperarlas posteriormente cuando nos sean necesarias:

public static main(String[] args) throws Exception {}
   // Propiedades configuradas en tiempo de ejecución.
   Map<String, String> props = new HashMap<>();
   props.put("jakarta.persistence.jdbc.url", "jdbc:sqlite:centro.db");
   props.put("hibernate.show_sql", "true");

   int idx = JpaBackend.createEntityManagerFactory("MiUnidadP", props);

   // ...

   try(EntityManagerFactory emf = JpaBackend.getEntityManagerFactory(idx)) {
      operaciones(idx);  // Aquí se crean y usan objetos EntityManager
   }
}

private static void operaciones(int idx) {
   // ¡Ojo! No hay que cerrarlo.
   EntityManagerFactory emf = JpaEmFactory.getInstance(idx); // Devuelve el mismo objeto.

   try(em EntityFactory = emf.createEntityManager()) {
      EntityTransaction tr = em.getTransaction();
      try {
         tr.begin();
         Centro centro = new Centro(11004866, "IES Castillo de Luna", Centro.Titularidad.PUBLICA);
         em.persist(centro);
         tr.commit();
      }
      catch(Exception e) {
         if(tr != null && tr.isActive()) tr.rollback();
         throw new RuntimeException("Error al almacenar el centro", err);
      }
   }
}

Truco

Se ha permitido también que en caso de que sólo se haya creado una fábrica, ni siquiera sea necesario facilitar el índice:

try(EntityManagerFactory emf = JpaBackend.getEntityManagerFactory()) {
   operaciones();
}

Obtener la fábrica, sin embargo, nos obligaría a crear objetos EntityManager y a gestionar correctamente las transacciones con lo que no abstraería el código de las particularidades del almacenamiento. Por ese motivo la clase incluye algunos métodos para manejar automáticamente las transacciones:

// Usamos la fábrica 1 para hacer persistente un centro.
// También podríamos obviar el índice, si sólo hubiera una fábrica
JpaBackend.transaction(1, em -> {
   Centro centro = new Centro(11004866L, "IES Castillo de Luna", Centro.Titularidad.PUBLICA);
   em.persist(centro);
});


// Este método es capaz de devolver lo que devuelve la función del argumento.
// Como en el caso anterior, se puede obviar el índice 1.
Centro otro = JpaBackend.transactionR(1, em -> em.get(Centro.class, 11004866L));