Cum să construiți o aplicație cu mai multe locatari: un tutorial Hibernate
Publicat: 2022-03-11Când vorbim despre aplicații cloud în care fiecare client are propriile date separate, trebuie să ne gândim la cum să stocăm și să manipulăm aceste date. Chiar și cu toate soluțiile excelente NoSQL disponibile, uneori mai trebuie să folosim vechea bază de date relațională bună. Prima soluție care ar putea veni în minte pentru a separa datele este să adăugați un identificator în fiecare tabel, astfel încât să poată fi gestionat individual. Funcționează, dar ce se întâmplă dacă un client îi cere baza de date? Ar fi foarte greoi să regăsești toate acele înregistrări ascunse printre altele.
Echipa Hibernate a venit cu o soluție la această problemă cu ceva timp în urmă. Ele oferă câteva puncte de extensie care permit controlului de unde ar trebui să fie preluate datele. Această soluție are opțiunea de a controla datele printr-o coloană de identificare, mai multe baze de date și mai multe scheme. Acest articol va acoperi soluția cu mai multe scheme.
Deci, să trecem la treabă!
Noțiuni de bază
Dacă sunteți un dezvoltator Java mai experimentat și știți să configurați totul sau dacă aveți deja propriul proiect Java EE, puteți sări peste această secțiune.
În primul rând, trebuie să creăm un nou proiect Java. Folosesc Eclipse și Gradle, dar puteți folosi IDE-ul și instrumentele de construcție preferate, cum ar fi IntelliJ și Maven.
Dacă doriți să utilizați aceleași instrumente ca și mine, puteți urma acești pași pentru a vă crea proiectul:
- Instalați pluginul Gradle pe Eclipse
- Faceți clic pe Fișier -> Nou -> Altele...
- Găsiți Gradle (STS) și faceți clic pe Următorul
- Informați un nume și alegeți Java Quickstart pentru exemplu de proiect
- Faceți clic pe Terminare
Grozav! Aceasta ar trebui să fie structura inițială a fișierului:
javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradlePuteți șterge toate fișierele care vin în folderele sursă, deoarece acestea sunt doar fișiere eșantion.
Pentru a rula proiectul, folosesc Wildfly și voi arăta cum să-l configurez (din nou puteți folosi instrumentul preferat aici):
- Descărcați Wildfly: http://wildfly.org/downloads/ (folosesc versiunea 10)
- Dezarhivați fișierul
- Instalați pluginul JBoss Tools pe Eclipse
- În fila Servere, faceți clic dreapta pe orice zonă goală și alegeți Nou -> Server
- Alegeți Wildfly 10.x (9.x funcționează și dacă 10 nu este disponibil, în funcție de versiunea dvs. Eclipse)
- Faceți clic pe Next, alegeți Create New Runtime (pagina următoare) și faceți clic din nou pe Next
- Alegeți folderul în care ați dezarhivat Wildfly ca director principal
- Faceți clic pe Terminare
Acum, să configuram Wildfly să cunoască baza de date:
- Accesați folderul bin din folderul Wildfly
- Executați add-user.bat sau add-user.sh (în funcție de sistemul de operare)
- Urmați pașii pentru a vă crea utilizatorul ca Manager
- În Eclipse, accesați din nou fila Servere, faceți clic dreapta pe serverul creat și selectați Start
- În browser, accesați http://localhost:9990, care este interfața de gestionare
- Introduceți acreditările utilizatorului pe care tocmai l-ați creat
- Implementați jarul de driver al bazei de date:
- Accesați fila Implementare și faceți clic pe Adăugați
- Faceți clic pe Următorul, alegeți fișierul jar de driver
- Faceți clic pe Următorul și Terminați
- Accesați fila Configurare
- Alegeți Subsisteme -> Surse de date -> Non-XA
- Faceți clic pe Adăugare, selectați baza de date și faceți clic pe Următorul
- Dați un nume sursei dvs. de date și faceți clic pe Următorul
- Selectați fila Detect Driver și alegeți driverul pe care tocmai l-ați implementat
- Introduceți informațiile bazei de date și faceți clic pe Următorul
- Faceți clic pe Testare conexiune dacă doriți să vă asigurați că informațiile de la pasul anterior sunt corecte
- Faceți clic pe Terminare
- Reveniți la Eclipse și opriți serverul care rulează
- Faceți clic dreapta pe el, selectați Adăugați și eliminați
- Adaugă proiectul tău în dreapta
- Faceți clic pe Terminare
Bine, avem Eclipse și Wildfly configurate împreună!
Acestea sunt toate configurațiile necesare în afara proiectului. Să trecem la configurarea proiectului.
Proiect de bootstrapping
Acum că avem Eclipse și Wildfly configurate și proiectul nostru creat, trebuie să ne configuram proiectul.
Primul lucru pe care îl vom face este să edităm build.gradle. Asa ar trebui sa arate:
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' }Dependențele sunt toate declarate ca „providedCompile”, deoarece această comandă nu adaugă dependența în fișierul război final. Wildfly are deja aceste dependențe și, în caz contrar, ar provoca conflicte cu cele ale aplicației.
În acest moment, puteți face clic dreapta pe proiect, selectați Gradle (STS) -> Refresh All pentru a importa dependențele pe care tocmai le-am declarat.
Este timpul să creați și să configurați fișierul „persistence.xml”, fișierul care conține informațiile de care are nevoie Hibernate:
- În folderul src/main/resource source, creați un folder numit META-INF
- În acest folder, creați un fișier numit persistence.xml
Conținutul fișierului trebuie să fie așa cum urmează, schimbând jta-data-source pentru a se potrivi cu sursa de date pe care ați creat-o în Wildfly și package com.toptal.andrehil.mt.hibernate cu cel pe care îl veți crea în următorul secțiune (cu excepția cazului în care alegeți același nume de pachet):
<?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>Clasele de hibernare
Configurațiile adăugate la persistence.xml indică două clase personalizate MultiTenantProvider și SchemaResolver. Prima clasă este responsabilă pentru furnizarea de conexiuni configurate cu schema potrivită. A doua clasă este responsabilă pentru rezolvarea numelui schemei care va fi utilizată.

Iată implementarea celor două clase:
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); } }Sintaxa utilizată în declarațiile de mai sus funcționează cu PostgreSQL și alte baze de date, aceasta trebuie schimbată în cazul în care baza de date are o sintaxă diferită pentru a schimba schema curentă.
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; } }În acest moment, este deja posibilă testarea aplicației. Deocamdată, soluția noastră indică direct o schemă publică codificată, dar este deja apelată. Pentru a face acest lucru, opriți serverul dacă rulează și porniți-l din nou. Puteți încerca să îl rulați în modul de depanare și să plasați un punct de întrerupere în orice punct al claselor de mai sus pentru a verifica dacă funcționează.
Utilizarea practică a rezolutorului
Deci, cum ar putea rezolvatorul să conțină de fapt numele corect al schemei?
O modalitate de a realiza acest lucru este să păstrați un identificator în antetul tuturor solicitărilor și apoi să creați un filtru pentru a injecta numele schemei.
Să implementăm o clasă de filtru pentru a exemplifica utilizarea. Soluția poate fi accesată prin SessionFactory a lui Hibernate, așa că vom profita de asta pentru a-l obține și a injecta numele corect al schemei.
@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); } }Acum, când orice clasă primește un EntityManager pentru a accesa baza de date, aceasta va fi deja configurată cu schema potrivită.
Din motive de simplitate, implementarea prezentată aici obține identificatorul direct dintr-un șir din antet, dar este o idee bună să utilizați un token de autentificare și să stocați identificatorul în token. Dacă sunteți interesat să aflați mai multe despre acest subiect, vă sugerez să aruncați o privire la JSON Web Tokens (JWT). JWT este o bibliotecă frumoasă și simplă pentru manipularea token-ului.
Cum să folosiți toate acestea
Cu totul configurat, nu este nevoie de altceva de făcut în entitățile și/sau clasele dvs. care interacționează cu EntityManager . Orice executați dintr-un EntityManager va fi direcționat către schema rezolvată de filtrul creat.
Acum, tot ce trebuie să faceți este să interceptați cererile din partea clientului și să injectați identificatorul/tokenul în antet pentru a fi trimis pe partea serverului.
Linkul de la sfârșitul articolului indică proiectul folosit pentru a scrie acest articol. Folosește Flyway pentru a crea 2 scheme și conține o clasă de entitate numită Car și o clasă de serviciu de odihnă numită CarService care poate fi folosită pentru a testa proiectul. Puteți urma toți pașii de mai jos, dar în loc să vă creați propriul proiect, îl puteți clona și utiliza pe acesta. Apoi, atunci când rulați, puteți utiliza un client HTTP simplu (cum ar fi extensia Postman pentru Chrome) și puteți face o solicitare GET către http://localhost:8080/javaee-mt/rest/cars cu antetele key:value:
- nume de utilizator: joe; sau
- nume de utilizator: fred.
Făcând acest lucru, cererile vor returna valori diferite, care sunt în scheme diferite, una numită joe și cealaltă numită „fred”.
Cuvinte finale
Aceasta nu este singura soluție pentru a crea aplicații multi-tenancy în lumea Java, dar este o modalitate simplă de a realiza acest lucru.
Un lucru de reținut este că Hibernate nu generează DDL atunci când se utilizează configurația multi-tenancy. Sugestia mea este să aruncați o privire la Flyway sau Liquibase, care sunt biblioteci grozave pentru a controla crearea bazelor de date. Acesta este un lucru drăguț de făcut, chiar dacă nu veți folosi multitenancy, deoarece echipa Hibernate sfătuiește să nu folosiți generarea automată a bazei de date în producție.
Codul sursă folosit pentru a crea acest articol și configurația mediului poate fi găsit la github.com/andrehil/JavaEEMT
