PostgreSQLのスキーマをうまいこと使うとマルチテナントのアプリが作れそうだという雰囲気でした。アプリでガリガリSQL書くのは嫌いな人なのでJavaならJPA2.0使いたい。SQL書かないと嫌な感じになるところはSQLでやるので仕方ないとして簡単なものは簡単に済ませたいという心情です。
JPA2.0のマルチテナント
機能無し!
pom pom
pom.xmlに必要なライブラリの参照を追加します。hibernateと、postgresqlのJDBCドライバを追加します。コネクションぷーリングは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();