かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Hibernate4 + PostgreSQLでマルチテナントしてみよう

PostgreSQLスキーマをうまいこと使うとマルチテナントのアプリが作れそうだという雰囲気でした。アプリでガリガリSQL書くのは嫌いな人なのでJavaならJPA2.0使いたい。SQL書かないと嫌な感じになるところはSQLでやるので仕方ないとして簡単なものは簡単に済ませたいという心情です。

JPA2.0のマルチテナント

機能無し!

Hibernate4のマルチテナント

やれる…!ということなので、スキーマ単位でテナント分割する方向でいきましょう。

pom pom

pom.xmlに必要なライブラリの参照を追加します。hibernateと、postgresqlJDBCドライバを追加します。コネクションぷーリングはc3p0を使う感じで。

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.1.10.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-c3p0</artifactId>
    <version>4.1.10.Final</version>
</dependency>
<dependency>
    <groupId>org.hibernate.javax.persistence</groupId>
    <artifactId>hibernate-jpa-2.0-api</artifactId>
    <version>1.0.1.Final</version>
</dependency>
<dependency>
    <groupId>postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>9.1-901-1.jdbc4</version>
</dependency>

マルチテナント対応のコネクションプロバイダ作成

C3P0のコネクションプロバイダーにコネクションプールに委譲して、必要に応じてスキーマの設定を変えるSQLを発行してます。

package sample.mavenproject1;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import org.hibernate.service.jdbc.connections.internal.C3P0ConnectionProvider;
import org.hibernate.service.jdbc.connections.spi.MultiTenantConnectionProvider;
import org.hibernate.service.spi.Configurable;
import org.hibernate.service.spi.ServiceRegistryAwareService;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.service.spi.Stoppable;

public class PGMultitenantConnectionProvider implements 
        // マルチテナントに必要
        MultiTenantConnectionProvider, 
        // C3P0ConnectionProviderが実装してるインターフェース
        Configurable, 
        Stoppable,
        ServiceRegistryAwareService {

    private static final long serialVersionUID = 1L;
    // コネクションプールの実装は移譲する
    private C3P0ConnectionProvider provider = new C3P0ConnectionProvider();

    @Override
    public Connection getAnyConnection() throws SQLException {
        return this.provider.getConnection();
    }

    @Override
    public void releaseAnyConnection(Connection connection) throws SQLException {
        this.provider.closeConnection(connection);
    }

    @Override
    public Connection getConnection(String tenantIdentifier) throws SQLException {
        Connection conn = this.getAnyConnection();
        try (Statement stmt = conn.createStatement()) {
            // スキーマ切り替えを行う
            stmt.execute("set search_path to " + tenantIdentifier + ",public");
        }
        return conn;
    }

    @Override
    public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
        try (Statement stmt = connection.createStatement()) {
            // スキーマをpublicに戻しておく
            stmt.execute("set search_path to public");
        }
    }

    @Override
    public boolean supportsAggressiveRelease() {
        return this.provider.supportsAggressiveRelease();
    }

    @Override
    public boolean isUnwrappableAs(Class unwrapType) {
        return this.provider.isUnwrappableAs(unwrapType);
    }

    @Override
    public <T> T unwrap(Class<T> unwrapType) {
        return this.provider.unwrap(unwrapType);
    }

    @Override
    public void configure(Map configurationValues) {
        this.provider.configure(configurationValues);
    }

    @Override
    public void stop() {
        this.provider.stop();
    }

    @Override
    public void injectServices(ServiceRegistryImplementor serviceRegistry) {
        this.provider.injectServices(serviceRegistry);
    }
}

テナントIDを判別するクラス

テナントIDを識別するためのロジックを持ったクラスを定義します。CurrentTenantIdentifierResolverを実装すればOKです。

package sample.mavenproject1;

import org.hibernate.context.spi.CurrentTenantIdentifierResolver;

public class TenantIdResolver implements CurrentTenantIdentifierResolver {

    // 簡易実装でstaticフィールドからテナントIDを取得する
    public static String tenantIdentifier;

    @Override
    public String resolveCurrentTenantIdentifier() {
        return tenantIdentifier;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

本番では、ログイン情報からひっこぬいてくるとかになると思います。

persistence.xml

クラスの下準備はできたので、persistence.xmlの設定をします。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/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">
  <persistence-unit name="sample" transaction-type="RESOURCE_LOCAL">
    <!-- hibernate使うよ -->
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <!-- エンテティを登録 -->
    <class>sample.mavenproject1.jpa.Hogehoge</class>
    <class>sample.mavenproject1.jpa.Hugahuga</class>
    ...
    <class>sample.mavenproject1.jpa.FooBar</class>
    <properties>
      <!-- 接続情報 -->
      <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://hostname:5432/databasename"/>
      <property name="javax.persistence.jdbc.user" value="username"/>
      <property name="javax.persistence.jdbc.password" value="p@ssw0rd"/>
      <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      
      <!-- マルチテナント使うよという設定 -->
      <property name="hibernate.multiTenancy" value="SCHEMA" />
      <!-- マルチテナントの接続プロバイダのクラス名を設定 -->
      <property name="hibernate.multi_tenant_connection_provider" value="sample.mavenproject1.PGMultitenantConnectionProvider" />
      <!-- テナントIDを判別するためのクラス名を設定 -->
      <property name="hibernate.tenant_identifier_resolver" value="sample.mavenproject1.TenantIdResolver" />
      
      <!-- c3p0の接続プールの情報 -->
      <property name="hibernate.c3p0.min_size" value="5" />
      <property name="hibernate.c3p0.max_size" value="20" />
      <property name="hibernate.c3p0.timeout" value="1800" />
      <property name="hibernate.c3p0.max_statements" value="50" />
      <property name="hibernate.show_sql" value="true" />
    </properties>
  </persistence-unit>
</persistence>

アプリ実行

あとは、コード書けばOK。

EntityManagerFactory f = Persistence.createEntityManagerFactory("sample");
// テナントIDを設定してからEntityManagerを作成する
TenantIdResolver.tenantIdentifier = "tenant1";
{
    EntityManager em = f.createEntityManager();

    em.getTransaction().begin();
    // ここではtenant1とpublicのスキーマのテーブルが使われる
    em.getTransaction().commit();

    em.close();
}
// テナント名を変える
TenantIdResolver.tenantIdentifier = "tenant2";
{
    EntityManager em = f.createEntityManager();

    em.getTransaction().begin();
    // ここではtenant2とpublicのスキーマのテーブルが使われる
    em.getTransaction().commit();

    em.close();
}
f.close();