Cómo construir una aplicación multiusuario: un tutorial de Hibernate

Publicado: 2022-03-11

Cuando hablamos de aplicaciones en la nube donde cada cliente tiene sus propios datos separados, debemos pensar en cómo almacenar y manipular estos datos. Incluso con todas las excelentes soluciones NoSQL que existen, a veces todavía necesitamos usar la buena base de datos relacional antigua. La primera solución que puede venir a la mente para separar los datos es agregar un identificador en cada tabla, para que pueda manejarse individualmente. Eso funciona, pero ¿qué sucede si un cliente solicita su base de datos? Sería muy engorroso recuperar todos esos registros escondidos entre los demás.

Aplicación Java EE multiusuario con Hibernate

La multitenencia en Java es más fácil que nunca con Hibernate.
Pío

El equipo de Hibernate ideó una solución a este problema hace un tiempo. Proporcionan algunos puntos de extensión que permiten controlar desde dónde se deben recuperar los datos. Esta solución tiene la opción de controlar los datos a través de una columna de identificador, múltiples bases de datos y múltiples esquemas. Este artículo cubrirá la solución de esquemas múltiples.

Entonces, ¡manos a la obra!

Empezando

Si eres un desarrollador de Java con más experiencia y sabes cómo configurarlo todo, o si ya tienes tu propio proyecto Java EE, puedes saltarte esta sección.

Primero, tenemos que crear un nuevo proyecto Java. Estoy usando Eclipse y Gradle, pero puede usar su IDE preferido y herramientas de construcción, como IntelliJ y Maven.

Si quieres usar las mismas herramientas que yo, puedes seguir estos pasos para crear tu proyecto:

  • Instalar el complemento Gradle en Eclipse
  • Haga clic en Archivo -> Nuevo -> Otro…
  • Busque Gradle (STS) y haga clic en Siguiente
  • Indique un nombre y elija Java Quickstart para el proyecto de muestra
  • Haga clic en Finalizar

¡Genial! Esta debería ser la estructura de archivo inicial:

 javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle

Puede eliminar todos los archivos que se encuentran dentro de las carpetas de origen, ya que son solo archivos de muestra.

Para ejecutar el proyecto, uso Wildfly y le mostraré cómo configurarlo (de nuevo, puede usar su herramienta favorita aquí):

  • Descarga Wildfly: http://wildfly.org/downloads/ (estoy usando la versión 10)
  • Descomprimir el archivo
  • Instale el complemento JBoss Tools en Eclipse
  • En la pestaña Servidores, haga clic con el botón derecho en cualquier área en blanco y elija Nuevo -> Servidor
  • Elija Wildfly 10.x (9.x también funciona si 10 no está disponible, según su versión de Eclipse)
  • Haga clic en Siguiente, elija Crear nuevo tiempo de ejecución (página siguiente) y haga clic en Siguiente nuevamente
  • Elija la carpeta donde descomprimió Wildfly como Directorio de inicio
  • Haga clic en Finalizar

Ahora, configuremos Wildfly para conocer la base de datos:

  • Vaya a la carpeta bin dentro de su carpeta Wildfly
  • Ejecute add-user.bat o add-user.sh (dependiendo de su sistema operativo)
  • Sigue los pasos para crear tu usuario como Administrador
  • En Eclipse, vaya a la pestaña Servidores nuevamente, haga clic derecho en el servidor que creó y seleccione Iniciar
  • En su navegador, acceda a http://localhost:9990, que es la interfaz de administración
  • Ingrese las credenciales del usuario que acaba de crear
  • Implemente el contenedor de controladores de su base de datos:
    1. Vaya a la pestaña Implementación y haga clic en Agregar
    2. Haga clic en Siguiente, elija el archivo jar de su controlador
    3. Haga clic en Siguiente y Finalizar
  • Ir a la pestaña Configuración
  • Elija Subsistemas -> Fuentes de datos -> No XA
  • Haga clic en Agregar, seleccione su base de datos y haga clic en Siguiente
  • Asigne un nombre a su fuente de datos y haga clic en Siguiente
  • Seleccione la pestaña Detectar controlador y elija el controlador que acaba de implementar
  • Ingrese la información de su base de datos y haga clic en Siguiente
  • Haga clic en Probar conexión si desea asegurarse de que la información del paso anterior sea correcta
  • Haga clic en Finalizar
  • Vuelva a Eclipse y detenga el servidor en ejecución
  • Haga clic derecho sobre él, seleccione Agregar y quitar
  • Agrega tu proyecto a la derecha
  • Haga clic en Finalizar

Muy bien, ¡tenemos Eclipse y Wildfly configurados juntos!

Estas son todas las configuraciones requeridas fuera del proyecto. Pasemos a la configuración del proyecto.

Proyecto de arranque

Ahora que tenemos Eclipse y Wildfly configurados y nuestro proyecto creado, necesitamos configurar nuestro proyecto.

Lo primero que vamos a hacer es editar build.gradle. Así es como debería verse:

 apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse' apply plugin: 'eclipse-wtp' sourceCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { jcenter() } eclipse { wtp { } } dependencies { providedCompile 'org.hibernate:hibernate-entitymanager:5.0.7.Final' providedCompile 'org.jboss.resteasy:resteasy-jaxrs:3.0.14.Final' providedCompile 'javax:javaee-api:7.0' }

Todas las dependencias se declaran como "providedCompile", porque este comando no agrega la dependencia en el archivo war final. Wildfly ya tiene estas dependencias y, de lo contrario, causaría conflictos con las de la aplicación.

En este punto, puede hacer clic con el botón derecho en su proyecto, seleccionar Gradle (STS) -> Actualizar todo para importar las dependencias que acabamos de declarar.

Es hora de crear y configurar el archivo “persistence.xml”, el archivo que contiene la información que necesita Hibernate:

  • En la carpeta de origen src/main/resource, cree una carpeta llamada META-INF
  • Dentro de esta carpeta, cree un archivo llamado persistence.xml

El contenido del archivo debe ser algo como lo siguiente, cambiando jta-data-source para que coincida con la fuente de datos que creó en Wildfly y el package com.toptal.andrehil.mt.hibernate al que va a crear en el próximo sección (a menos que elija el mismo nombre de paquete):

 <?xml version="1.0" encoding="UTF-8" ?> <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="pu"> <jta-data-source>java:/JavaEEMTDS</jta-data-source> <properties> <property name="hibernate.multiTenancy" value="SCHEMA"/> <property name="hibernate.tenant_identifier_resolver" value="com.toptal.andrehil.mt.hibernate.SchemaResolver"/> <property name="hibernate.multi_tenant_connection_provider" value="com.toptal.andrehil.mt.hibernate.MultiTenantProvider"/> </properties> </persistence-unit> </persistence>

Clases de hibernación

Las configuraciones agregadas a persistence.xml apuntan a dos clases personalizadas MultiTenantProvider y SchemaResolver. La primera clase es responsable de proporcionar conexiones configuradas con el esquema correcto. La segunda clase se encarga de resolver el nombre del esquema a utilizar.

Aquí está la implementación de las dos clases:

 public class MultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService { private static final long serialVersionUID = 1L; private DataSource dataSource; @Override public boolean supportsAggressiveRelease() { return false; } @Override public void injectServices(ServiceRegistryImplementor serviceRegistry) { try { final Context init = new InitialContext(); dataSource = (DataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name } catch (final NamingException e) { throw new RuntimeException(e); } } @SuppressWarnings("rawtypes") @Override public boolean isUnwrappableAs(Class clazz) { return false; } @Override public <T> T unwrap(Class<T> clazz) { return null; } @Override public Connection getAnyConnection() throws SQLException { final Connection connection = dataSource.getConnection(); return connection; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection(); try { connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e); } return connection; } @Override public void releaseAnyConnection(Connection connection) throws SQLException { try { connection.createStatement().execute("SET SCHEMA 'public'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [public]", e); } connection.close(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { releaseAnyConnection(connection); } }

La sintaxis que se usa en las declaraciones anteriores funciona con PostgreSQL y algunas otras bases de datos, esto debe cambiarse en caso de que su base de datos tenga una sintaxis diferente para cambiar el esquema actual.

 public class SchemaResolver implements CurrentTenantIdentifierResolver { private String tenantIdentifier = "public"; @Override public String resolveCurrentTenantIdentifier() { return tenantIdentifier; } @Override public boolean validateExistingCurrentSessions() { return false; } public void setTenantIdentifier(String tenantIdentifier) { this.tenantIdentifier = tenantIdentifier; } }

En este punto, ya es posible probar la aplicación. Por ahora, nuestro solucionador apunta directamente a un esquema público codificado, pero ya se está llamando. Para hacer esto, detenga su servidor si se está ejecutando y vuelva a iniciarlo. Puede intentar ejecutarlo en modo de depuración y colocar un punto de interrupción en cualquier punto de las clases anteriores para verificar si funciona.

Uso práctico del resolver

Entonces, ¿cómo podría el resolutor contener realmente el nombre correcto del esquema?

Una forma de lograr esto es mantener un identificador en el encabezado de todas las solicitudes y luego crear un filtro para inyectar el nombre del esquema.

Implementemos una clase de filtro para ejemplificar el uso. Se puede acceder al resolutor a través de SessionFactory de Hibernate, por lo que aprovecharemos eso para obtenerlo e inyectar el nombre de esquema correcto.

 @Provider public class AuthRequestFilter implements ContainerRequestFilter { @PersistenceUnit(unitName = "pu") private EntityManagerFactory entityManagerFactory; @Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory(); final SchemaResolver schemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver(); final String username = containerRequestContext.getHeaderString("username"); schemaResolver.setTenantIdentifier(username); } }

Ahora, cuando cualquier clase obtenga un EntityManager para acceder a la base de datos, ya estará configurado con el esquema correcto.

En aras de la simplicidad, la implementación que se muestra aquí obtiene el identificador directamente de una cadena en el encabezado, pero es una buena idea usar un token de autenticación y almacenar el identificador en el token. Si está interesado en saber más sobre este tema, le sugiero que eche un vistazo a JSON Web Tokens (JWT). JWT es una biblioteca agradable y simple para la manipulación de tokens.

Cómo usar todo esto

Con todo configurado, no hay nada más que hacer en sus entidades y/o clases que interactúan con EntityManager . Todo lo que ejecute desde un EntityManager se dirigirá al esquema resuelto por el filtro creado.

Ahora, todo lo que necesita hacer es interceptar solicitudes en el lado del cliente e inyectar el identificador/token en el encabezado para enviarlo al lado del servidor.

En una aplicación real, tendrá mejores medios de autenticación. Sin embargo, la idea general de la tenencia múltiple seguirá siendo la misma.

El enlace al final del artículo apunta al proyecto utilizado para escribir este artículo. Utiliza Flyway para crear 2 esquemas y contiene una clase de entidad llamada Car y una clase de servicio de descanso llamada CarService que se puede usar para probar el proyecto. Puede seguir todos los pasos a continuación, pero en lugar de crear su propio proyecto, puede clonarlo y usar este. Luego, cuando se ejecuta, puede usar un cliente HTTP simple (como la extensión Postman para Chrome) y realizar una solicitud GET a http://localhost:8080/javaee-mt/rest/cars con la clave de encabezado: valor:

  • nombre de usuario: joe; o
  • nombre de usuario: fred.

Al hacer esto, las solicitudes devolverán diferentes valores, que se encuentran en diferentes esquemas, uno llamado joe y el otro llamado "fred".

Ultimas palabras

Esta no es la única solución para crear aplicaciones multiusuario en el mundo Java, pero es una forma sencilla de lograrlo.

Una cosa a tener en cuenta es que Hibernate no genera DDL cuando se usa la configuración de múltiples inquilinos. Mi sugerencia es echar un vistazo a Flyway o Liquibase, que son excelentes bibliotecas para controlar la creación de bases de datos. Esto es algo agradable de hacer incluso si no va a utilizar multiusuario, ya que el equipo de Hibernate aconseja no utilizar la generación automática de bases de datos en producción.

El código fuente utilizado para crear este artículo y la configuración del entorno se pueden encontrar en github.com/andrehil/JavaEEMT

Relacionado: Tutorial avanzado de clases de Java: una guía para la recarga de clases