So erstellen Sie eine mandantenfähige Anwendung: Ein Hibernate-Tutorial
Veröffentlicht: 2022-03-11Wenn wir über Cloud-Anwendungen sprechen, bei denen jeder Client über seine eigenen separaten Daten verfügt, müssen wir darüber nachdenken, wie diese Daten gespeichert und bearbeitet werden. Trotz all der großartigen NoSQL-Lösungen da draußen müssen wir manchmal immer noch die gute alte relationale Datenbank verwenden. Die erste Lösung, die Ihnen zum Trennen von Daten in den Sinn kommt, besteht darin, jeder Tabelle einen Bezeichner hinzuzufügen, damit sie einzeln behandelt werden können. Das funktioniert, aber was ist, wenn ein Kunde nach seiner Datenbank fragt? Es wäre sehr mühsam, all diese Aufzeichnungen wiederzufinden, die zwischen den anderen versteckt sind.
Das Hibernate-Team hat vor einiger Zeit eine Lösung für dieses Problem gefunden. Sie bieten einige Erweiterungspunkte, die es einem ermöglichen, zu steuern, woher Daten abgerufen werden sollen. Diese Lösung bietet die Möglichkeit, die Daten über eine Identifikatorspalte, mehrere Datenbanken und mehrere Schemas zu steuern. In diesem Artikel wird die Lösung mit mehreren Schemas behandelt.
Also ran an die Arbeit!
Einstieg
Wenn Sie ein erfahrenerer Java-Entwickler sind und wissen, wie man alles konfiguriert, oder wenn Sie bereits ein eigenes Java-EE-Projekt haben, können Sie diesen Abschnitt überspringen.
Zuerst müssen wir ein neues Java-Projekt erstellen. Ich verwende Eclipse und Gradle, aber Sie können Ihre bevorzugte IDE und Erstellungstools wie IntelliJ und Maven verwenden.
Wenn Sie dieselben Tools wie ich verwenden möchten, können Sie diesen Schritten folgen, um Ihr Projekt zu erstellen:
- Installieren Sie das Gradle-Plugin auf Eclipse
- Klicken Sie auf Datei -> Neu -> Andere…
- Suchen Sie Gradle (STS) und klicken Sie auf Weiter
- Geben Sie einen Namen an und wählen Sie Java Quickstart für Beispielprojekt
- Klicken Sie auf Fertig stellen
Toll! Dies sollte die anfängliche Dateistruktur sein:
javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle
Sie können alle Dateien löschen, die sich in den Quellordnern befinden, da es sich nur um Beispieldateien handelt.
Um das Projekt auszuführen, verwende ich Wildfly, und ich werde zeigen, wie es konfiguriert wird (auch hier können Sie Ihr Lieblingstool verwenden):
- Wildfly herunterladen: http://wildfly.org/downloads/ (ich verwende Version 10)
- Entpacken Sie die Datei
- Installieren Sie das JBoss Tools-Plugin auf Eclipse
- Klicken Sie auf der Registerkarte Server mit der rechten Maustaste auf einen leeren Bereich und wählen Sie Neu -> Server
- Wählen Sie Wildfly 10.x (9.x funktioniert auch, wenn 10 nicht verfügbar ist, abhängig von Ihrer Eclipse-Version)
- Klicken Sie auf Weiter, wählen Sie Neue Laufzeit erstellen (nächste Seite) und klicken Sie erneut auf Weiter
- Wählen Sie den Ordner, in den Sie Wildfly entpackt haben, als Home-Verzeichnis aus
- Klicken Sie auf Fertig stellen
Lassen Sie uns nun Wildfly so konfigurieren, dass es die Datenbank kennt:
- Gehen Sie zum bin-Ordner in Ihrem Wildfly-Ordner
- Führen Sie add-user.bat oder add-user.sh aus (abhängig von Ihrem Betriebssystem)
- Befolgen Sie die Schritte, um Ihren Benutzer als Manager zu erstellen
- Wechseln Sie in Eclipse erneut zur Registerkarte Server, klicken Sie mit der rechten Maustaste auf den von Ihnen erstellten Server, und wählen Sie Start aus
- Greifen Sie in Ihrem Browser auf http://localhost:9990 zu, die Verwaltungsschnittstelle
- Geben Sie die Anmeldeinformationen des gerade erstellten Benutzers ein
- Stellen Sie das Treiber-JAR Ihrer Datenbank bereit:
- Wechseln Sie zur Registerkarte Bereitstellung und klicken Sie auf Hinzufügen
- Klicken Sie auf Weiter und wählen Sie Ihre Treiber-JAR-Datei aus
- Klicken Sie auf Weiter und Fertig stellen
- Wechseln Sie zur Registerkarte Konfiguration
- Wählen Sie Subsysteme -> Datenquellen -> Non-XA
- Klicken Sie auf Hinzufügen, wählen Sie Ihre Datenbank aus und klicken Sie auf Weiter
- Geben Sie Ihrer Datenquelle einen Namen und klicken Sie auf Weiter
- Wählen Sie die Registerkarte Treiber erkennen und den soeben bereitgestellten Treiber aus
- Geben Sie Ihre Datenbankinformationen ein und klicken Sie auf Weiter
- Klicken Sie auf Verbindung testen, wenn Sie sicherstellen möchten, dass die Informationen des vorherigen Schritts korrekt sind
- Klicken Sie auf Fertig stellen
- Gehen Sie zurück zu Eclipse und stoppen Sie den laufenden Server
- Klicken Sie mit der rechten Maustaste darauf, wählen Sie Hinzufügen und Entfernen
- Fügen Sie Ihr Projekt rechts hinzu
- Klicken Sie auf Fertig stellen
In Ordnung, wir haben Eclipse und Wildfly zusammen konfiguriert!
Dies sind alle Konfigurationen, die außerhalb des Projekts erforderlich sind. Kommen wir zur Projektkonfiguration.
Bootstrapping-Projekt
Nachdem wir Eclipse und Wildfly konfiguriert und unser Projekt erstellt haben, müssen wir unser Projekt konfigurieren.
Als erstes bearbeiten wir build.gradle. So sollte es aussehen:
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' }
Die Abhängigkeiten werden alle als „providedCompile“ deklariert, da dieser Befehl die Abhängigkeit nicht in der endgültigen WAR-Datei hinzufügt. Wildfly hat diese Abhängigkeiten bereits und würde sonst zu Konflikten mit denen der App führen.
An dieser Stelle können Sie mit der rechten Maustaste auf Ihr Projekt klicken und Gradle (STS) -> Refresh All auswählen, um die soeben deklarierten Abhängigkeiten zu importieren.
Es ist an der Zeit, die Datei „persistence.xml“ zu erstellen und zu konfigurieren, die Datei, die die Informationen enthält, die Hibernate benötigt:
- Erstellen Sie im Ordner src/main/resource source einen Ordner namens META-INF
- Erstellen Sie in diesem Ordner eine Datei namens persistence.xml
Der Inhalt der Datei muss etwa wie folgt sein, wobei die jta-data-source so geändert werden muss, dass sie mit der Datenquelle übereinstimmt, die Sie in Wildfly erstellt haben, und das package com.toptal.andrehil.mt.hibernate
zu dem Paket, das Sie im nächsten erstellen werden Abschnitt (es sei denn, Sie wählen denselben Paketnamen):
<?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>
Klassen überwintern
Die zu persistence.xml hinzugefügten Konfigurationen verweisen auf zwei benutzerdefinierte Klassen MultiTenantProvider und SchemaResolver. Die erste Klasse ist dafür verantwortlich, Verbindungen bereitzustellen, die mit dem richtigen Schema konfiguriert sind. Die zweite Klasse ist für die Auflösung des Namens des zu verwendenden Schemas verantwortlich.

Hier ist die Implementierung der beiden Klassen:
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); } }
Die in den obigen Anweisungen verwendete Syntax funktioniert mit PostgreSQL und einigen anderen Datenbanken, dies muss geändert werden, falls Ihre Datenbank eine andere Syntax hat, um das aktuelle Schema zu ändern.
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; } }
An dieser Stelle ist es bereits möglich, die Anwendung zu testen. Im Moment zeigt unser Resolver direkt auf ein hartcodiertes öffentliches Schema, aber es wird bereits aufgerufen. Stoppen Sie dazu Ihren Server, falls er ausgeführt wird, und starten Sie ihn erneut. Sie können versuchen, es im Debug-Modus auszuführen und einen Haltepunkt an einem beliebigen Punkt der obigen Klassen zu platzieren, um zu überprüfen, ob es funktioniert.
Praktische Verwendung des Resolvers
Wie könnte der Resolver also tatsächlich den richtigen Namen des Schemas enthalten?
Eine Möglichkeit, dies zu erreichen, besteht darin, einen Bezeichner im Header aller Anforderungen beizubehalten und dann einen Filter zu erstellen, um den Namen des Schemas einzufügen.
Lassen Sie uns eine Filterklasse implementieren, um die Verwendung zu veranschaulichen. Auf den Resolver kann über die SessionFactory von Hibernate zugegriffen werden, also werden wir dies nutzen, um ihn zu erhalten und den richtigen Schemanamen einzufügen.
@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); } }
Wenn nun eine Klasse einen EntityManager erhält, um auf die Datenbank zuzugreifen, ist sie bereits mit dem richtigen Schema konfiguriert.
Der Einfachheit halber erhält die hier gezeigte Implementierung den Bezeichner direkt aus einer Zeichenfolge im Header, aber es ist eine gute Idee, ein Authentifizierungstoken zu verwenden und den Bezeichner im Token zu speichern. Wenn Sie mehr über dieses Thema erfahren möchten, empfehle ich einen Blick auf JSON Web Tokens (JWT). JWT ist eine schöne und einfache Bibliothek zur Token-Manipulation.
Wie man all dies verwendet
Wenn alles konfiguriert ist, müssen Sie in Ihren Entitäten und/oder Klassen, die mit EntityManager
interagieren, nichts weiter tun. Alles, was Sie von einem EntityManager ausführen, wird an das vom erstellten Filter aufgelöste Schema geleitet.
Jetzt müssen Sie nur noch Anfragen auf der Clientseite abfangen und den Bezeichner/das Token in den Header einfügen, der an die Serverseite gesendet werden soll.
Der Link am Ende des Artikels verweist auf das Projekt, mit dem dieser Artikel geschrieben wurde. Es verwendet Flyway zum Erstellen von zwei Schemas und enthält eine Entitätsklasse namens Car und eine Rest-Service-Klasse namens CarService
, die zum Testen des Projekts verwendet werden können. Sie können alle folgenden Schritte ausführen, aber anstatt Ihr eigenes Projekt zu erstellen, können Sie es klonen und dieses verwenden. Dann können Sie beim Ausführen einen einfachen HTTP-Client (wie die Postman-Erweiterung für Chrome) verwenden und eine GET-Anfrage an http://localhost:8080/javaee-mt/rest/cars mit den Headern key:value stellen:
- Benutzername:joe; oder
- Benutzername:fred.
Dadurch geben die Anfragen verschiedene Werte zurück, die sich in verschiedenen Schemata befinden, eines namens joe und das andere namens „fred“.
Letzte Worte
Dies ist nicht die einzige Lösung zum Erstellen mandantenfähiger Anwendungen in der Java-Welt, aber es ist ein einfacher Weg, dies zu erreichen.
Beachten Sie, dass Hibernate bei Verwendung der mandantenfähigen Konfiguration keine DDL generiert. Mein Vorschlag ist, einen Blick auf Flyway oder Liquibase zu werfen, die großartige Bibliotheken zur Steuerung der Datenbankerstellung sind. Dies ist eine gute Sache, auch wenn Sie keine Mandantenfähigkeit verwenden, da das Hibernate-Team empfiehlt, die automatische Datenbankgenerierung nicht in der Produktion zu verwenden.
Den zum Erstellen dieses Artikels verwendeten Quellcode und die Umgebungskonfiguration finden Sie unter github.com/andrehil/JavaEEMT