Hibernate Search がマルチテナンシーに対応したので、Spring Boot と組み合わせて試してみた。
試した環境は下記の通り。
Spring Boot 1.2.5.RELEASE
Hibernate Search 5.3.0.Final
サンプルコードはここにも置いてあります。
https://github.com/tagbangers/spring-best-practices/tree/master/spring-best-practice-hibernate-search-multi-tenancy
マルチテナンシー (multi-tenancy) ってなに?
ひとつのアプリケーションを複数のお客さん(テナント)が使えるようにすること。SaaS型のサービスを構築する場合と考えればOK。
Hibernate ORM と Hibernate Search
結局のところ、Hibernate Search でマルチテナンシーを設定するには、親プロジェクトである Hibernate ORM のマルチテナンシーを設定すればOKということ。なので、今回の内容はほぼ Hibernate ORM に対するマルチテナンシーの設定になった。
DataSource の設定
Hibernate ORM におけるマルチテナンシーは、次の3つのやり方がある。
- データベースを分ける (今回はこれを使ってみる)
- スキーマを分ける
- ディスクリミネータを使う
ただし、今の時点での Spring Boot の設定はマルチテナンシーを想定していないので、そのままでは DataSource を複数定義できない。なので、@ConfigurationProperties を使って複数の DataSource が設定できるようにしてみる。
@ConfigurationProperties("spring.additional") public class AdditionalProperties { @NestedConfigurationProperty private DataSourceProperties datasource1; @NestedConfigurationProperty private DataSourceProperties datasource2; public DataSourceProperties getDatasource1() { return datasource1; } public void setDatasource1(DataSourceProperties datasource1) { this.datasource1 = datasource1; } public DataSourceProperties getDatasource2() { return datasource2; } public void setDatasource2(DataSourceProperties datasource2) { this.datasource2 = datasource2; } }
application.properties はこんな感じで。
spring.additional.datasource1.url=jdbc:h2:mem:test1 spring.additional.datasource2.url=jdbc:h2:mem:test2
2つの DataSource を Bean 登録する。
ポイントは Spring Boot のオートコンフィグレーションで dataSource が参照されるので、どちらかの DataSource の Bean名 には、dataSource という名前もつけること。
@Configuration public class DataSourceConfiguration { @Autowired private AdditionalProperties additionalProperties; @Bean(name = {"dataSource", "dataSource1"}) @ConfigurationProperties(prefix = "spring.additional.datasource1") public DataSource dataSource1() { DataSourceBuilder factory = DataSourceBuilder .create(this.additionalProperties.getDatasource1().getClassLoader()) .driverClassName(this.additionalProperties.getDatasource1().getDriverClassName()) .url(this.additionalProperties.getDatasource1().getUrl()); return factory.build(); } @Bean(name = "dataSource2") @ConfigurationProperties(prefix = "spring.additional.datasource2") public DataSource dataSource2() { DataSourceBuilder factory = DataSourceBuilder .create(this.additionalProperties.getDatasource2().getClassLoader()) .driverClassName(this.additionalProperties.getDatasource2().getDriverClassName()) .url(this.additionalProperties.getDatasource2().getUrl()); return factory.build(); } }
DataSourceAutoConfiguration は単一の DataSource しか考慮していないので、除外しておく。
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableConfigurationProperties(AdditionalProperties.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
CurrentTenantIdentifierResolver を実装する
マルチテナンシーを設定するにあたってまず大事なことは、何をもってお客さん (テナント) を判定するかを決めること。
今回は、URLの最初パスが tenant1 から始まるか、tenant2 から始まるかで判断することにする。URLのパスを判定するのは後で記載する MVC の Interceptor で行ない、RequestAttribute に入れておく。
@Component public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver { public static final String IDENTIFIER_ATTRIBUTE = CurrentTenantIdentifierResolverImpl.class.getName() + ".IDENTIFIER"; public static final String TENANT_1_IDENTIFIER = "tenant1"; public static final String TENANT_2_IDENTIFIER = "tenant2"; @Override public String resolveCurrentTenantIdentifier() { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes != null) { String identifier = (String) requestAttributes.getAttribute(IDENTIFIER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (identifier != null) { return identifier; } } return TENANT_1_IDENTIFIER; } @Override public boolean validateExistingCurrentSessions() { return true; } }
MultiTenantConnectionProvider を実装する
TenantIdentifier に対応する DataSource を定義する。
@Component public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { @Autowired private DataSource dataSource1; @Autowired private DataSource dataSource2; private Map<String, DataSource> dataSourceMap = new HashMap<>(); @PostConstruct public void init() { dataSourceMap.put(CurrentTenantIdentifierResolverImpl.TENANT_1_IDENTIFIER, dataSource1); dataSourceMap.put(CurrentTenantIdentifierResolverImpl.TENANT_2_IDENTIFIER, dataSource2); } @Override protected DataSource selectAnyDataSource() { return dataSource1; } @Override protected DataSource selectDataSource(String tenantIdentifier) { return dataSourceMap.get(tenantIdentifier); } }
EntityManager の設定
マルチテナンシー対応の EntityManager を定義する。
@Configuration @EnableConfigurationProperties(JpaProperties.class) public class MultiTenancyJpaConfiguration { @Autowired private DataSource dataSource; @Autowired private JpaProperties jpaProperties; @Autowired private MultiTenantConnectionProvider multiTenantConnectionProvider; @Autowired private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder factoryBuilder) { Map<String, Object> vendorProperties = new LinkedHashMap<>(); vendorProperties.putAll(jpaProperties.getHibernateProperties(dataSource)); vendorProperties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE); vendorProperties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider); vendorProperties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); return factoryBuilder.dataSource(dataSource) .packages(Item.class.getPackage().getName()) .properties(vendorProperties) .jta(false) .build(); } }
TenantIdentifier を決定するための Interceptor を定義する
Controller は /{tenant} でマッピングし、PathValiable から TenantIdentifier を取得することにする。
public class CurrentTenantIdentifierChangeInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Map<String, Object> pathVariables = (Map<String, Object>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); if (pathVariables.containsKey("tenant")) { request.setAttribute(CurrentTenantIdentifierResolverImpl.IDENTIFIER_ATTRIBUTE, pathVariables.get("tenant")); } return true; } }
WebMvcConfigurerAdapter を拡張して Interceptor を追加。
@Configuration public class WebMvcConfiguration extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new CurrentTenantIdentifierChangeInterceptor()); } }
以上で Hibernate Search のマルチテナンシー対応は完了。
Spring Boot に 2つの DataSource を認識させることがちょっとやっかいだが、Hibernate側のマルチテナンシーの対応はとてもシンプルに実現できる。
参考URL
https://docs.jboss.org/hibernate/orm/4.3/devguide/en-US/html/ch16.html
http://in.relation.to/2015/04/17/multi-tenancy-for-hibernate-search-users-520-beta-1-released/