Как создать многопользовательское приложение: руководство по Hibernate
Опубликовано: 2022-03-11Когда мы говорим об облачных приложениях, где у каждого клиента есть свои отдельные данные, нам нужно подумать о том, как хранить эти данные и управлять ими. Даже со всеми замечательными решениями NoSQL иногда нам все еще нужно использовать старую добрую реляционную базу данных. Первое решение, которое может прийти в голову для разделения данных, — добавить идентификатор в каждую таблицу, чтобы с ними можно было работать индивидуально. Это работает, но что, если клиент запросит свою базу данных? Было бы очень обременительно получить все эти записи, скрытые среди других.
Команда Hibernate некоторое время назад придумала решение этой проблемы. Они предоставляют некоторые точки расширения, которые позволяют контролировать, откуда следует извлекать данные. Это решение позволяет управлять данными через столбец идентификаторов, несколько баз данных и несколько схем. В этой статье будет рассмотрено решение с несколькими схемами.
Итак, приступим к работе!
Начиная
Если вы более опытный Java-разработчик и знаете, как все настроить, или если у вас уже есть собственный проект Java EE, вы можете пропустить этот раздел.
Во-первых, мы должны создать новый проект Java. Я использую Eclipse и Gradle, но вы можете использовать предпочитаемую вами IDE и инструменты для сборки, такие как IntelliJ и Maven.
Если вы хотите использовать те же инструменты, что и я, вы можете выполнить следующие шаги для создания своего проекта:
- Установите плагин Gradle на Eclipse
- Нажмите «Файл» -> «Создать» -> «Другое…».
- Найдите Gradle (STS) и нажмите «Далее».
- Сообщите имя и выберите Java Quickstart для примера проекта.
- Нажмите Готово
Здорово! Это должна быть начальная структура файла:
javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle
Вы можете удалить все файлы, которые находятся в исходных папках, так как это всего лишь примеры файлов.
Для запуска проекта я использую Wildfly, и я покажу, как его настроить (опять же здесь вы можете использовать свой любимый инструмент):
- Загрузите Wildfly: http://wildfly.org/downloads/ (я использую версию 10)
- Разархивируйте файл
- Установите подключаемый модуль JBoss Tools в Eclipse.
- На вкладке «Серверы» щелкните правой кнопкой мыши любую пустую область и выберите «Создать» -> «Сервер».
- Выберите Wildfly 10.x (9.x также работает, если 10 недоступна, в зависимости от вашей версии Eclipse)
- Нажмите «Далее», выберите «Создать новую среду выполнения» (следующая страница) и снова нажмите «Далее».
- Выберите папку, в которую вы распаковали Wildfly, в качестве домашнего каталога.
- Нажмите Готово
Теперь давайте настроим Wildfly, чтобы он знал базу данных:
- Перейдите в папку bin внутри папки Wildfly.
- Выполните add-user.bat или add-user.sh (в зависимости от вашей ОС)
- Следуйте инструкциям, чтобы создать пользователя в качестве менеджера.
- В Eclipse снова перейдите на вкладку «Серверы», щелкните правой кнопкой мыши созданный вами сервер и выберите «Пуск».
- В браузере откройте http://localhost:9990, это интерфейс управления.
- Введите учетные данные пользователя, которого вы только что создали
- Разверните jar драйвера вашей базы данных:
- Перейдите на вкладку «Развертывание» и нажмите «Добавить».
- Нажмите «Далее», выберите jar-файл вашего драйвера.
- Нажмите «Далее» и «Готово»
- Перейдите на вкладку Конфигурация
- Выберите «Подсистемы» -> «Источники данных» -> «Не-XA».
- Нажмите «Добавить», выберите свою базу данных и нажмите «Далее».
- Дайте имя вашему источнику данных и нажмите «Далее».
- Перейдите на вкладку «Определить драйвер» и выберите драйвер, который вы только что развернули.
- Введите информацию о своей базе данных и нажмите «Далее».
- Нажмите «Проверить соединение», если вы хотите убедиться, что информация предыдущего шага верна.
- Нажмите Готово
- Вернитесь в Eclipse и остановите работающий сервер.
- Щелкните его правой кнопкой мыши, выберите «Добавить и удалить».
- Добавьте свой проект справа
- Нажмите Готово
Хорошо, мы настроили Eclipse и Wildfly вместе!
Это все конфигурации, необходимые вне проекта. Перейдем к настройке проекта.
Начальный проект
Теперь, когда у нас настроены Eclipse и Wildfly и создан наш проект, нам нужно настроить наш проект.
Первое, что мы собираемся сделать, это отредактировать build.gradle. Вот как это должно выглядеть:
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' }
Все зависимости объявлены как «providedCompile», потому что эта команда не добавляет зависимость в окончательный файл войны. У Wildfly уже есть эти зависимости, иначе это вызовет конфликты с зависимостями приложения.
На этом этапе вы можете щелкнуть правой кнопкой мыши свой проект, выбрать Gradle (STS) -> Обновить все, чтобы импортировать зависимости, которые мы только что объявили.
Пришло время создать и настроить файл «persistence.xml», содержащий информацию, необходимую Hibernate:
- В исходной папке src/main/resource создайте папку с именем META-INF.
- Внутри этой папки создайте файл с именем persistence.xml.
Содержимое файла должно быть примерно таким: измените jta-data-source, чтобы он соответствовал источнику данных, который вы создали в Wildfly, и package com.toptal.andrehil.mt.hibernate
на тот, который вы собираетесь создать в следующем раздел (если вы не выберете то же имя пакета):
<?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>
Спящие классы
Конфигурации, добавленные в файл persistence.xml, указывают на два пользовательских класса MultiTenantProvider и SchemaResolver. Первый класс отвечает за предоставление подключений, сконфигурированных с правильной схемой. Второй класс отвечает за разрешение имени используемой схемы.

Вот реализация двух классов:
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); } }
Синтаксис, используемый в приведенных выше утверждениях, работает с PostgreSQL и некоторыми другими базами данных, его необходимо изменить, если ваша база данных имеет другой синтаксис для изменения текущей схемы.
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; } }
На этом этапе уже можно протестировать приложение. На данный момент наш преобразователь указывает прямо на жестко закодированную общедоступную схему, но она уже вызывается. Для этого остановите свой сервер, если он работает, и запустите его снова. Вы можете попробовать запустить его в режиме отладки и поставить точку останова в любой точке классов выше, чтобы проверить, работает ли он.
Практическое использование резольвера
Итак, как распознаватель может содержать правильное имя схемы?
Один из способов добиться этого — сохранить идентификатор в заголовке всех запросов, а затем создать фильтр для внедрения имени схемы.
Давайте реализуем класс фильтра, чтобы проиллюстрировать использование. Доступ к распознавателю можно получить через SessionFactory Hibernate, поэтому мы воспользуемся этим, чтобы получить его и внедрить правильное имя схемы.
@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); } }
Теперь, когда любой класс получает EntityManager для доступа к базе данных, он уже будет настроен с правильной схемой.
Для простоты показанная здесь реализация получает идентификатор непосредственно из строки в заголовке, но рекомендуется использовать маркер аутентификации и хранить идентификатор в маркере. Если вам интересно узнать больше об этой теме, я предлагаю взглянуть на JSON Web Tokens (JWT). JWT — хорошая и простая библиотека для манипулирования токенами.
Как использовать все это
Когда все настроено, в ваших сущностях и/или классах, которые взаимодействуют с EntityManager
, больше ничего делать не нужно. Все, что вы запускаете из EntityManager, будет направлено в схему, разрешенную созданным фильтром.
Теперь все, что вам нужно сделать, это перехватить запросы на стороне клиента и внедрить идентификатор/токен в заголовок для отправки на сторону сервера.
Ссылка в конце статьи указывает на проект, использованный для написания этой статьи. Он использует Flyway для создания двух схем и содержит класс сущностей с именем Car и класс обслуживания для отдыха с именем CarService
, который можно использовать для тестирования проекта. Вы можете выполнить все шаги, описанные ниже, но вместо того, чтобы создавать свой собственный проект, вы можете клонировать его и использовать этот. Затем при запуске вы можете использовать простой HTTP-клиент (например, расширение Postman для Chrome) и сделать запрос GET на http://localhost:8080/javaee-mt/rest/cars с ключом заголовков: значение:
- имя пользователя:Джо; или
- имя пользователя: фред.
При этом запросы будут возвращать разные значения, находящиеся в разных схемах, одно из которых называется joe, а другое — «fred».
Заключительные слова
Это не единственное решение для создания мультиарендных приложений в мире Java, но это простой способ добиться этого.
Следует иметь в виду, что Hibernate не генерирует DDL при использовании многопользовательской конфигурации. Я предлагаю взглянуть на Flyway или Liquibase, которые являются отличными библиотеками для управления созданием базы данных. Это хорошо, даже если вы не собираетесь использовать мультиарендность, поскольку команда Hibernate советует не использовать автоматическое создание базы данных в рабочей среде.
Исходный код, использованный для создания этой статьи, и конфигурацию среды можно найти по адресу github.com/andrehil/JavaEEMT.