マルチテナントアプリケーションを構築する方法:Hibernateチュートリアル

公開: 2022-03-11

各クライアントが独自のデータを持っているクラウドアプリケーションについて話すときは、このデータを保存および操作する方法を考える必要があります。 優れたNoSQLソリューションがすべて揃っていても、古き良きリレーショナルデータベースを使用する必要がある場合があります。 データを分離するために思い浮かぶ最初の解決策は、すべてのテーブルに識別子を追加して、個別に処理できるようにすることです。 それは機能しますが、クライアントがデータベースを要求した場合はどうなりますか? 他のレコードの中に隠されているすべてのレコードを取得するのは非常に面倒です。

Hibernateを使用したマルチテナンシーJavaEEアプリケーション

Javaでのマルチテナンシーは、Hibernateを使用するとこれまでになく簡単になります。
つぶやき

Hibernateチームは、少し前にこの問題の解決策を考え出しました。 これらは、データを取得する場所を制御できるようにするいくつかの拡張ポイントを提供します。 このソリューションには、識別子列、複数のデータベース、および複数のスキーマを介してデータを制御するオプションがあります。 この記事では、複数のスキーマソリューションについて説明します。

さあ、仕事に取り掛かりましょう!

入門

経験豊富なJava開発者であり、すべてを構成する方法を知っている場合、または独自のJava EEプロジェクトを既に持っている場合は、このセクションをスキップできます。

まず、新しいJavaプロジェクトを作成する必要があります。 私はEclipseとGradleを使用していますが、IntelliJやMavenなどの好みのIDEと構築ツールを使用できます。

私と同じツールを使用したい場合は、次の手順に従ってプロジェクトを作成できます。

  • EclipseにGradleプラグインをインストールします
  • [ファイル]->[新規]->[その他]をクリックします…
  • Gradle(STS)を見つけて、[次へ]をクリックします
  • 名前を通知し、サンプルプロジェクトにJavaクイックスタートを選択します
  • [完了]をクリックします

すごい! これは初期のファイル構造である必要があります。

 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を使用しています)
  • ファイルを解凍します
  • EclipseにJBossToolsプラグインをインストールします
  • [サーバー]タブで、空白の領域を右クリックし、[新規]->[サーバー]を選択します。
  • Wildfly 10.xを選択します(Eclipseのバージョンによっては、10が使用できない場合でも9.xが機能します)
  • [次へ]をクリックし、[新しいランタイムの作成(次のページ)]を選択して、もう一度[次へ]をクリックします
  • Wildflyを解凍したフォルダをホームディレクトリとして選択します
  • [完了]をクリックします

それでは、データベースを認識するようにWildflyを設定しましょう。

  • Wildflyフォルダー内のbinフォルダーに移動します
  • add-user.batまたはadd-user.shを実行します(OSによって異なります)
  • 手順に従って、ユーザーをマネージャーとして作成します
  • Eclipseで、[サーバー]タブに再度移動し、作成したサーバーを右クリックして、[開始]を選択します。
  • ブラウザで、管理インターフェイスであるhttp:// localhost:9990にアクセスします。
  • 作成したユーザーの資格情報を入力します
  • データベースのドライバーjarをデプロイします。
    1. [展開]タブに移動し、[追加]をクリックします
    2. [次へ]をクリックして、ドライバーjarファイルを選択します
    3. [次へ]をクリックして終了します
  • [構成]タブに移動します
  • [サブシステム]->[データソース]->[非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' }

このコマンドは最終的なwarファイルに依存関係を追加しないため、依存関係はすべて「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>

Hibernateクラス

persistence.xmlに追加された構成は、2つのカスタムクラスMultiTenantProviderとSchemaResolverを指します。 最初のクラスは、適切なスキーマで構成された接続を提供する役割を果たします。 2番目のクラスは、使用するスキーマの名前を解決する役割を果たします。

2つのクラスの実装は次のとおりです。

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

この時点で、アプリケーションをテストすることはすでに可能です。 今のところ、リゾルバーはハードコードされたパブリックスキーマを直接指していますが、すでに呼び出されています。 これを行うには、サーバーが実行されている場合はサーバーを停止し、サーバーを再起動します。 デバッグモードで実行し、上記のクラスの任意のポイントにブレークポイントを設定して、機能しているかどうかを確認できます。

リゾルバの実用化

では、リゾルバーに実際にスキーマの正しい名前を含めるにはどうすればよいでしょうか。

これを実現する1つの方法は、すべてのリクエストのヘッダーに識別子を保持してから、スキーマの名前を挿入するためのフィルターを作成することです。

使用法を例示するためにフィルタークラスを実装しましょう。 リゾルバーはHibernateのSessionFactoryを介してアクセスできるため、それを利用してリゾルバーを取得し、適切なスキーマ名を挿入します。

 @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 Token(JWT)を確認することをお勧めします。 JWTは、トークンを操作するための優れたシンプルなライブラリです。

このすべてを使用する方法

すべてが構成されたら、 EntityManagerと対話するエンティティやクラスで他に何もする必要はありません。 EntityManagerから実行するものはすべて、作成されたフィルターによって解決されたスキーマに転送されます。

これで、クライアント側でリクエストをインターセプトし、サーバー側に送信するヘッダーに識別子/トークンを挿入するだけで済みます。

実際のアプリケーションでは、より優れた認証手段があります。 ただし、マルチテナンシーの一般的な考え方は変わりません。

記事の最後にあるリンクは、この記事の執筆に使用されたプロジェクトを指しています。 Flywayを使用して2つのスキーマを作成し、Carというエンティティクラスと、プロジェクトのテストに使用できるCarServiceというRESTサービスクラスが含まれています。 以下のすべての手順に従うことができますが、独自のプロジェクトを作成する代わりに、プロジェクトを複製してこれを使用することができます。 次に、実行時に、単純なHTTPクライアント(ChromeのPostman拡張機能など)を使用して、ヘッダーkey:value:を使用してhttp:// localhost:8080 / javaee-mt / rest/carsにGETリクエストを送信できます。

  • username:joe; また
  • ユーザー名:フレッド。

これを行うことにより、リクエストは異なるスキーマにある異なる値を返します。1つはjoeと呼ばれ、もう1つは「fred」と呼ばれます。

最後の言葉

これは、Javaの世界でマルチテナンシーアプリケーションを作成するための唯一のソリューションではありませんが、これを実現するための簡単な方法です。

覚えておくべきことの1つは、マルチテナンシー構成を使用する場合、HibernateはDDLを生成しないということです。 私の提案は、データベースの作成を制御するための優れたライブラリであるFlywayまたはLiquibaseを調べることです。 これは、マルチテナンシーを使用しない場合でも、Hibernateチームが本番環境で自動データベース生成を使用しないようにアドバイスしているため、良いことです。

この記事と環境構成の作成に使用されたソースコードは、github.com / andrehil/JavaEEMTにあります。

関連:高度なJavaクラスチュートリアル:クラスのリロードのガイド