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/
