Comment créer une application mutualisée : un didacticiel Hibernate

Publié: 2022-03-11

Lorsque nous parlons d'applications cloud où chaque client dispose de ses propres données distinctes, nous devons réfléchir à la manière de stocker et de manipuler ces données. Même avec toutes les excellentes solutions NoSQL existantes, nous devons parfois utiliser la bonne vieille base de données relationnelle. La première solution qui pourrait venir à l'esprit pour séparer les données est d'ajouter un identifiant dans chaque table, afin qu'elle puisse être manipulée individuellement. Cela fonctionne, mais que se passe-t-il si un client demande sa base de données ? Il serait très fastidieux de récupérer tous ces enregistrements cachés parmi les autres.

Application Java EE mutualisée avec Hibernate

La multilocation en Java est plus facile que jamais avec Hibernate.
Tweeter

L'équipe Hibernate a trouvé une solution à ce problème il y a quelque temps. Ils fournissent des points d'extension qui permettent de contrôler d'où les données doivent être récupérées. Cette solution a la possibilité de contrôler les données via une colonne d'identifiant, plusieurs bases de données et plusieurs schémas. Cet article couvrira la solution de schémas multiples.

Alors, mettons-nous au travail !

Commencer

Si vous êtes un développeur Java plus expérimenté et que vous savez tout configurer, ou si vous avez déjà votre propre projet Java EE, vous pouvez ignorer cette section.

Tout d'abord, nous devons créer un nouveau projet Java. J'utilise Eclipse et Gradle, mais vous pouvez utiliser votre IDE préféré et vos outils de création, tels qu'IntelliJ et Maven.

Si vous souhaitez utiliser les mêmes outils que moi, vous pouvez suivre ces étapes pour créer votre projet :

  • Installer le plugin Gradle sur Eclipse
  • Cliquez sur Fichier -> Nouveau -> Autre…
  • Trouvez Gradle (STS) et cliquez sur Suivant
  • Indiquez un nom et choisissez Java Quickstart pour l'exemple de projet
  • Cliquez sur Terminer

Génial! Cela devrait être la structure de fichier initiale :

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

Vous pouvez supprimer tous les fichiers qui se trouvent dans les dossiers source, car ce ne sont que des exemples de fichiers.

Pour exécuter le projet, j'utilise Wildfly, et je montrerai comment le configurer (encore une fois, vous pouvez utiliser votre outil préféré ici):

  • Télécharger Wildfly : http://wildfly.org/downloads/ (j'utilise la version 10)
  • Décompressez le fichier
  • Installez le plug-in JBoss Tools sur Eclipse
  • Dans l'onglet Serveurs, cliquez avec le bouton droit sur une zone vide et choisissez Nouveau -> Serveur
  • Choisissez Wildfly 10.x (9.x fonctionne également si 10 n'est pas disponible, selon votre version d'Eclipse)
  • Cliquez sur Suivant, choisissez Créer un nouveau runtime (page suivante) et cliquez à nouveau sur Suivant
  • Choisissez le dossier dans lequel vous avez décompressé Wildfly comme répertoire d'accueil
  • Cliquez sur Terminer

Maintenant, configurons Wildfly pour connaître la base de données :

  • Accédez au dossier bin dans votre dossier Wildfly
  • Exécutez add-user.bat ou add-user.sh (selon votre système d'exploitation)
  • Suivez les étapes pour créer votre utilisateur en tant que gestionnaire
  • Dans Eclipse, accédez à nouveau à l'onglet Serveurs, cliquez avec le bouton droit sur le serveur que vous avez créé et sélectionnez Démarrer
  • Sur votre navigateur, accédez à http://localhost:9990, qui est l'interface de gestion
  • Entrez les informations d'identification de l'utilisateur que vous venez de créer
  • Déployez le driver jar de votre base de données :
    1. Accédez à l'onglet Déploiement et cliquez sur Ajouter
    2. Cliquez sur Suivant, choisissez votre fichier jar de pilote
    3. Cliquez sur Suivant et Terminer
  • Allez dans l'onglet Configuration
  • Choisissez Sous-systèmes -> Sources de données -> Non-XA
  • Cliquez sur Ajouter, sélectionnez votre base de données et cliquez sur Suivant
  • Donnez un nom à votre source de données et cliquez sur Suivant
  • Sélectionnez l'onglet Détecter le pilote et choisissez le pilote que vous venez de déployer
  • Entrez les informations de votre base de données et cliquez sur Suivant
  • Cliquez sur Tester la connexion si vous voulez vous assurer que les informations de l'étape précédente sont correctes
  • Cliquez sur Terminer
  • Revenez à Eclipse et arrêtez le serveur en cours d'exécution
  • Faites un clic droit dessus, sélectionnez Ajouter et supprimer
  • Ajoutez votre projet à droite
  • Cliquez sur Terminer

Très bien, nous avons configuré Eclipse et Wildfly ensemble !

Il s'agit de toutes les configurations requises en dehors du projet. Passons à la configuration du projet.

Projet d'amorçage

Maintenant que nous avons configuré Eclipse et Wildfly et que notre projet a été créé, nous devons configurer notre projet.

La première chose que nous allons faire est de modifier build.gradle. Voici à quoi cela devrait ressembler :

 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' }

Les dépendances sont toutes déclarées comme "providedCompile", car cette commande n'ajoute pas la dépendance dans le fichier war final. Wildfly a déjà ces dépendances, et cela entraînerait des conflits avec celles de l'application sinon.

À ce stade, vous pouvez cliquer avec le bouton droit sur votre projet, sélectionner Gradle (STS) -> Actualiser tout pour importer les dépendances que nous venons de déclarer.

Il est temps de créer et de configurer le fichier "persistence.xml", le fichier qui contient les informations dont Hibernate a besoin :

  • Dans le dossier source src/main/resource, créez un dossier appelé META-INF
  • Dans ce dossier, créez un fichier nommé persistence.xml

Le contenu du fichier doit ressembler à ce qui suit, en changeant jta-data-source pour qu'il corresponde à la source de données que vous avez créée dans Wildfly et le package com.toptal.andrehil.mt.hibernate à celui que vous allez créer dans le prochain (sauf si vous choisissez le même nom de package) :

 <?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>

Classes d'hibernation

Les configurations ajoutées à persistence.xml pointent vers deux classes personnalisées MultiTenantProvider et SchemaResolver. La première classe est chargée de fournir des connexions configurées avec le bon schéma. La deuxième classe est chargée de résoudre le nom du schéma à utiliser.

Voici l'implémentation des deux classes :

 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 syntaxe utilisée dans les instructions ci-dessus fonctionne avec PostgreSQL et certaines autres bases de données, cela doit être changé au cas où votre base de données a une syntaxe différente pour changer le schéma actuel.

 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; } }

À ce stade, il est déjà possible de tester l'application. Pour l'instant, notre résolveur pointe directement vers un schéma public codé en dur, mais il est déjà appelé. Pour cela, arrêtez votre serveur s'il est en cours d'exécution et redémarrez-le. Vous pouvez essayer de l'exécuter en mode débogage et placer un point d'arrêt à n'importe quel point des classes ci-dessus pour vérifier s'il fonctionne.

Utilisation pratique du résolveur

Alors, comment le résolveur pourrait-il contenir le bon nom du schéma ?

Une façon d'y parvenir est de conserver un identifiant dans l'en-tête de toutes les requêtes, puis de créer un filtre pour injecter le nom du schéma.

Implémentons une classe de filtre pour illustrer l'utilisation. Le résolveur est accessible via la SessionFactory d'Hibernate, nous en profiterons donc pour l'obtenir et injecter le bon nom de schéma.

 @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); } }

Désormais, lorsqu'une classe obtient un EntityManager pour accéder à la base de données, elle sera déjà configurée avec le bon schéma.

Par souci de simplicité, l'implémentation présentée ici obtient l'identifiant directement à partir d'une chaîne dans l'en-tête, mais c'est une bonne idée d'utiliser un jeton d'authentification et de stocker l'identifiant dans le jeton. Si vous souhaitez en savoir plus sur ce sujet, je vous suggère de jeter un œil à JSON Web Tokens (JWT). JWT est une bibliothèque simple et agréable pour la manipulation de jetons.

Comment utiliser tout cela

Avec tout configuré, il n'y a rien d'autre à faire dans vos entités et/ou classes qui interagissent avec EntityManager . Tout ce que vous exécutez à partir d'un EntityManager sera dirigé vers le schéma résolu par le filtre créé.

Il ne vous reste plus qu'à intercepter les requêtes côté client et injecter l'identifiant/token dans l'en-tête à envoyer côté serveur.

Dans une application réelle, vous aurez un meilleur moyen d'authentification. L'idée générale de la multilocation, cependant, restera la même.

Le lien à la fin de l'article pointe vers le projet utilisé pour écrire cet article. Il utilise Flyway pour créer 2 schémas et contient une classe d'entité appelée Car et une classe de service de repos appelée CarService qui peuvent être utilisées pour tester le projet. Vous pouvez suivre toutes les étapes ci-dessous, mais au lieu de créer votre propre projet, vous pouvez le cloner et utiliser celui-ci. Ensuite, lors de l'exécution, vous pouvez utiliser un simple client HTTP (comme l'extension Postman pour Chrome) et envoyer une requête GET à http://localhost:8080/javaee-mt/rest/cars avec les en-têtes key:value :

  • nom d'utilisateur :joe ; ou
  • nom d'utilisateur : fred.

En faisant cela, les requêtes renverront différentes valeurs, qui se trouvent dans des schémas différents, l'un appelé joe et l'autre appelé "fred".

Derniers mots

Ce n'est pas la seule solution pour créer des applications mutualisées dans le monde Java, mais c'est un moyen simple d'y parvenir.

Une chose à garder à l'esprit est qu'Hibernate ne génère pas de DDL lors de l'utilisation d'une configuration multitenant. Ma suggestion est de jeter un œil à Flyway ou Liquibase, qui sont d'excellentes bibliothèques pour contrôler la création de bases de données. C'est une bonne chose à faire même si vous n'allez pas utiliser la multilocation, car l'équipe Hibernate conseille de ne pas utiliser leur génération automatique de base de données en production.

Le code source utilisé pour créer cet article et la configuration de l'environnement se trouvent sur github.com/andrehil/JavaEEMT

En relation : Tutoriel de classe Java avancé : un guide pour le rechargement de classe