Jak zbudować aplikację wielodostępną: samouczek dotyczący hibernacji

Opublikowany: 2022-03-11

Kiedy mówimy o aplikacjach chmurowych, w których każdy klient ma swoje oddzielne dane, musimy pomyśleć o tym, jak przechowywać i manipulować tymi danymi. Nawet przy wszystkich świetnych rozwiązaniach NoSQL, czasami nadal musimy używać starej dobrej relacyjnej bazy danych. Pierwszym rozwiązaniem, które może przychodzić na myśl, aby oddzielić dane, jest dodanie identyfikatora w każdej tabeli, dzięki czemu można je obsłużyć indywidualnie. To działa, ale co, jeśli klient poprosi o swoją bazę danych? Wydobycie wszystkich tych zapisów ukrytych między innymi byłoby bardzo kłopotliwe.

Wielodostępna aplikacja Java EE z Hibernate

Wielodostępność w Javie jest łatwiejsza niż kiedykolwiek dzięki Hibernate.
Ćwierkać

Zespół Hibernate już jakiś czas temu wymyślił rozwiązanie tego problemu. Zapewniają pewne punkty rozszerzeń, które umożliwiają kontrolę, skąd należy pobierać dane. To rozwiązanie ma możliwość kontrolowania danych za pomocą kolumny identyfikatora, wielu baz danych i wielu schematów. W tym artykule omówimy rozwiązanie wielu schematów.

Zabierzmy się więc do pracy!

Pierwsze kroki

Jeśli jesteś bardziej doświadczonym programistą Java i wiesz, jak wszystko skonfigurować, lub jeśli masz już własny projekt Java EE, możesz pominąć tę sekcję.

Najpierw musimy stworzyć nowy projekt Java. Używam Eclipse i Gradle, ale możesz użyć swojego preferowanego IDE i narzędzi do budowania, takich jak IntelliJ i Maven.

Jeśli chcesz używać tych samych narzędzi co ja, możesz wykonać następujące kroki, aby utworzyć swój projekt:

  • Zainstaluj wtyczkę Gradle w Eclipse
  • Kliknij Plik -> Nowy -> Inne…
  • Znajdź Gradle (STS) i kliknij Dalej
  • Podaj nazwę i wybierz Java Quickstart dla przykładowego projektu
  • Kliknij Zakończ

Świetnie! Powinna to być początkowa struktura pliku:

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

Możesz usunąć wszystkie pliki znajdujące się w folderach źródłowych, ponieważ są to tylko pliki przykładowe.

Do uruchomienia projektu używam Wildfly i pokażę, jak go skonfigurować (tu znowu możesz użyć swojego ulubionego narzędzia):

  • Pobierz Wildfly: http://wildfly.org/downloads/ (używam wersji 10)
  • Rozpakuj plik
  • Zainstaluj wtyczkę JBoss Tools w Eclipse
  • Na karcie Serwery kliknij prawym przyciskiem myszy dowolny pusty obszar i wybierz Nowy -> Serwer
  • Wybierz Wildfly 10.x (9.x działa również, jeśli 10 nie jest dostępne, w zależności od wersji Eclipse)
  • Kliknij Dalej, wybierz Utwórz nowe środowisko wykonawcze (następna strona) i ponownie kliknij Dalej
  • Wybierz folder, w którym rozpakowałeś Wildfly jako katalog domowy
  • Kliknij Zakończ

Teraz skonfigurujmy Wildfly, aby znał bazę danych:

  • Przejdź do folderu bin w folderze Wildfly
  • Uruchom add-user.bat lub add-user.sh (w zależności od systemu operacyjnego)
  • Postępuj zgodnie z instrukcjami, aby utworzyć użytkownika jako menedżera
  • W Eclipse ponownie przejdź do zakładki Serwery, kliknij prawym przyciskiem myszy utworzony serwer i wybierz Start
  • W przeglądarce wejdź na http://localhost:9990, czyli interfejs zarządzania
  • Wprowadź dane uwierzytelniające użytkownika, którego właśnie utworzyłeś
  • Wdróż plik jar sterownika swojej bazy danych:
    1. Przejdź do zakładki Wdrożenie i kliknij Dodaj
    2. Kliknij Dalej, wybierz plik jar sterownika
    3. Kliknij Dalej i Zakończ
  • Przejdź do zakładki Konfiguracja
  • Wybierz Podsystemy -> Źródła danych -> Inne niż XA
  • Kliknij Dodaj, wybierz swoją bazę danych i kliknij Dalej
  • Nadaj nazwę swojemu źródłu danych i kliknij Dalej
  • Wybierz kartę Wykryj sterownik i wybierz właśnie wdrożony sterownik
  • Wprowadź informacje o swojej bazie danych i kliknij Dalej
  • Kliknij Testuj połączenie, jeśli chcesz się upewnić, że informacje z poprzedniego kroku są poprawne
  • Kliknij Zakończ
  • Wróć do Eclipse i zatrzymaj działający serwer
  • Kliknij go prawym przyciskiem myszy, wybierz Dodaj i usuń
  • Dodaj swój projekt po prawej stronie
  • Kliknij Zakończ

W porządku, mamy skonfigurowane razem Eclipse i Wildfly!

To wszystkie konfiguracje wymagane poza projektem. Przejdźmy do konfiguracji projektu.

Projekt bootstrapowy

Teraz, gdy mamy skonfigurowane Eclipse i Wildfly oraz utworzony nasz projekt, musimy skonfigurować nasz projekt.

Pierwszą rzeczą, którą zrobimy, jest edycja build.gradle. Tak powinno wyglądać:

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

Wszystkie zależności są deklarowane jako „providedCompile”, ponieważ to polecenie nie dodaje zależności w końcowym pliku war. Wildfly ma już te zależności i w przeciwnym razie powodowałoby konflikty z aplikacjami.

W tym momencie możesz kliknąć prawym przyciskiem myszy swój projekt, wybrać Gradle (STS) -> Odśwież wszystko, aby zaimportować właśnie zadeklarowane zależności.

Czas stworzyć i skonfigurować plik „persistence.xml”, plik zawierający informacje, których potrzebuje Hibernate:

  • W folderze src/main/resource utwórz folder o nazwie META-INF
  • Wewnątrz tego folderu utwórz plik o nazwie persistence.xml

Zawartość pliku musi być podobna do poniższej, zmieniając jta-data-source, aby dopasować źródło danych utworzone w Wildfly i package com.toptal.andrehil.mt.hibernate na ten, który zamierzasz utworzyć w następnym sekcja (chyba że wybierzesz tę samą nazwę pakietu):

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

Klasy hibernacji

Konfiguracje dodane do persistence.xml wskazują dwie niestandardowe klasy MultiTenantProvider i SchemaResolver. Pierwsza klasa odpowiada za zapewnienie połączeń skonfigurowanych z odpowiednim schematem. Druga klasa odpowiada za rozwiązanie nazwy schematu, który ma zostać użyty.

Oto implementacja dwóch klas:

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

Składnia użyta w powyższych instrukcjach działa z PostgreSQL i niektórymi innymi bazami danych, należy to zmienić w przypadku, gdy baza danych ma inną składnię, aby zmienić bieżący schemat.

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

W tym momencie można już przetestować aplikację. Na razie nasz resolver wskazuje bezpośrednio na zakodowany na stałe schemat publiczny, ale już jest wywoływany. Aby to zrobić, zatrzymaj serwer, jeśli jest uruchomiony, i uruchom go ponownie. Możesz spróbować uruchomić go w trybie debugowania i umieścić punkt przerwania w dowolnym punkcie powyższych klas, aby sprawdzić, czy działa.

Praktyczne użycie resolwera

Jak więc przelicznik może faktycznie zawierać właściwą nazwę schematu?

Jednym ze sposobów, aby to osiągnąć, jest przechowywanie identyfikatora w nagłówku wszystkich żądań, a następnie utworzenie filtra w celu wstrzyknięcia nazwy schematu.

Zaimplementujmy klasę filtra, aby zilustrować użycie. Do przelicznika można uzyskać dostęp za pośrednictwem SessionFactory w Hibernate, więc wykorzystamy to, aby go pobrać i wstrzyknąć odpowiednią nazwę schematu.

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

Teraz, gdy jakakolwiek klasa otrzyma EntityManager, aby uzyskać dostęp do bazy danych, zostanie ona już skonfigurowana z odpowiednim schematem.

Dla uproszczenia pokazana tutaj implementacja pobiera identyfikator bezpośrednio z ciągu znaków w nagłówku, ale dobrym pomysłem jest użycie tokena uwierzytelniającego i przechowywanie identyfikatora w tokenie. Jeśli chcesz dowiedzieć się więcej na ten temat, proponuję zajrzeć do JSON Web Tokens (JWT). JWT to przyjemna i prosta biblioteka do manipulacji tokenami.

Jak z tego wszystkiego korzystać

Po skonfigurowaniu wszystkiego nie trzeba nic więcej robić w encjach i/lub klasach, które współdziałają z EntityManager . Wszystko, co uruchomisz z EntityManager, zostanie skierowane do schematu rozwiązanego przez utworzony filtr.

Teraz wszystko, co musisz zrobić, to przechwycić żądania po stronie klienta i wstrzyknąć identyfikator/token w nagłówku, który ma zostać wysłany po stronie serwera.

W prawdziwej aplikacji będziesz mieć lepszy sposób uwierzytelniania. Ogólna idea wielodostępności pozostanie jednak taka sama.

Link na końcu artykułu wskazuje na projekt użyty do napisania tego artykułu. Wykorzystuje Flyway do tworzenia 2 schematów i zawiera klasę encji o nazwie Car oraz klasę usług odpoczynku o nazwie CarService , których można użyć do przetestowania projektu. Możesz wykonać wszystkie poniższe kroki, ale zamiast tworzyć własny projekt, możesz go sklonować i użyć tego. Następnie, po uruchomieniu, możesz użyć prostego klienta HTTP (takiego jak rozszerzenie Postman dla Chrome) i wysłać żądanie GET do http://localhost:8080/javaee-mt/rest/cars z nagłówkami key:value:

  • nazwa użytkownika:joe; lub
  • nazwa użytkownika: fred.

W ten sposób żądania zwrócą różne wartości, które są w różnych schematach, jeden o nazwie joe, a drugi o nazwie „fred”.

Ostatnie słowa

Nie jest to jedyne rozwiązanie do tworzenia aplikacji wielodostępnych w świecie Java, ale jest to prosty sposób na osiągnięcie tego.

Należy pamiętać, że Hibernate nie generuje DDL podczas korzystania z konfiguracji wielodostępnej. Proponuję przyjrzeć się Flyway lub Liquibase, które są świetnymi bibliotekami do kontrolowania tworzenia baz danych. Jest to fajna rzecz do zrobienia, nawet jeśli nie zamierzasz korzystać z wielodostępności, ponieważ zespół Hibernate radzi, aby nie używać ich automatycznego generowania baz danych w produkcji.

Kod źródłowy użyty do stworzenia tego artykułu i konfiguracji środowiska można znaleźć na github.com/andrehil/JavaEEMT

Powiązane: Zaawansowany samouczek dotyczący klas Java: przewodnik po ponownym ładowaniu klas