Um guia para multilocação no Hibernate 5
1. Introdução
Multitenancy permite que vários clientes ou locatários usem um único recurso ou, no contexto deste artigo, uma única instância de banco de dados. O objetivo éto isolate the information each tenant needs from the shared database.
Neste tutorial, apresentaremos várias abordagensto configuring multitenancy in Hibernate 5.
2. Dependências do Maven
Precisaremos incluirthe hibernate-core dependency no arquivopom.xml:
org.hibernate
hibernate-core
5.2.12.Final
Para o teste, usaremos um banco de dados H2 na memória, então vamos adicionarthis dependency ao arquivopom.xml:
com.h2database
h2
1.4.196
3. Compreendendo a multilocação no Hibernate
Conforme mencionado no oficialHibernate User Guide, existem três abordagens para multilocação no Hibernate:
-
Separate Schema – um esquema por locatário na mesma instância de banco de dados físico
-
Separate Database – uma instância de banco de dados física separada por locatário
-
Partitioned (Discriminator) Data – os dados de cada inquilino são particionados por um valor discriminador
O acompanhamento dePartitioned (Discriminator) Data approach isn’t yet supported by Hibernate. emthis JIRA issue para progresso futuro.
Como de costume, o Hibernate abstrai a complexidade em torno da implementação de cada abordagem.
Tudo o que precisamos é deprovide an implementation of these two interfaces:
-
MultiTenantConnectionProvider - fornece conexões por locatário
-
CurrentTenantIdentifierResolver - resolve o identificador de inquilino a usar
Vamos ver mais detalhadamente cada conceito antes de examinar os exemplos de abordagens de banco de dados e esquema
3.1. MultiTenantConnectionProvider
Basicamente, essa interface fornece uma conexão com o banco de dados para um identificador de inquilino concreto.
Vejamos seus dois métodos principais:
interface MultiTenantConnectionProvider extends Service, Wrapped {
Connection getAnyConnection() throws SQLException;
Connection getConnection(String tenantIdentifier) throws SQLException;
// ...
}
Se o Hibernate não puder resolver o identificador de locatário a ser usado, ele usará o métodogetAnyConnection para obter uma conexão. Caso contrário, ele usará o métodogetConnection.
O Hibernate fornece duas implementações desta interface dependendo de como definimos as conexões do banco de dados:
-
Usando a interfaceDataSource do Java - usaríamos a implementaçãoDataSourceBasedMultiTenantConnectionProviderImpl
-
Usando a interfaceConnectionProvider do Hibernate - usaríamos a implementaçãoAbstractMultiTenantConnectionProvider
3.2. CurrentTenantIdentifierResolver
Existemmany possible ways to resolve a tenant identifier. Por exemplo, nossa implementação pode usar um identificador de inquilino definido em um arquivo de configuração.
Outra maneira poderia ser usar o identificador de inquilino de um parâmetro de caminho.
Vamos ver esta interface:
public interface CurrentTenantIdentifierResolver {
String resolveCurrentTenantIdentifier();
boolean validateExistingCurrentSessions();
}
O Hibernate chama o métodoresolveCurrentTenantIdentifier para obter o identificador do inquilino. Se quisermos que o Hibernate valide que todas as sessões existentes pertencem ao mesmo identificador de inquilino, o métodovalidateExistingCurrentSessions deve retornar verdadeiro.
4. Abordagem de Esquema
Nesta estratégia, usaremos diferentes esquemas ou usuários na mesma instância física do banco de dados. This approach should be used when we need the best performance for our application and can sacrifice special database features such as backup per tenant.
Além disso, vamos simular a interfaceCurrentTenantIdentifierResolver para fornecer um identificador de locatário como nossa escolha durante o teste:
public abstract class MultitenancyIntegrationTest {
@Mock
private CurrentTenantIdentifierResolver currentTenantIdentifierResolver;
private SessionFactory sessionFactory;
@Before
public void setup() throws IOException {
MockitoAnnotations.initMocks(this);
when(currentTenantIdentifierResolver.validateExistingCurrentSessions())
.thenReturn(false);
Properties properties = getHibernateProperties();
properties.put(
AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER,
currentTenantIdentifierResolver);
sessionFactory = buildSessionFactory(properties);
initTenant(TenantIdNames.MYDB1);
initTenant(TenantIdNames.MYDB2);
}
protected void initTenant(String tenantId) {
when(currentTenantIdentifierResolver
.resolveCurrentTenantIdentifier())
.thenReturn(tenantId);
createCarTable();
}
}
Nossa implementação da interfaceMultiTenantConnectionProvider seráset the schema to use every time a connection is requested:
class SchemaMultiTenantConnectionProvider
extends AbstractMultiTenantConnectionProvider {
private ConnectionProvider connectionProvider;
public SchemaMultiTenantConnectionProvider() throws IOException {
this.connectionProvider = initConnectionProvider();
}
@Override
protected ConnectionProvider getAnyConnectionProvider() {
return connectionProvider;
}
@Override
protected ConnectionProvider selectConnectionProvider(
String tenantIdentifier) {
return connectionProvider;
}
@Override
public Connection getConnection(String tenantIdentifier)
throws SQLException {
Connection connection = super.getConnection(tenantIdentifier);
connection.createStatement()
.execute(String.format("SET SCHEMA %s;", tenantIdentifier));
return connection;
}
private ConnectionProvider initConnectionProvider() throws IOException {
Properties properties = new Properties();
properties.load(getClass()
.getResourceAsStream("/hibernate.properties"));
DriverManagerConnectionProviderImpl connectionProvider
= new DriverManagerConnectionProviderImpl();
connectionProvider.configure(properties);
return connectionProvider;
}
}
Portanto, usaremos um banco de dados H2 em memória com dois esquemas - um para cada locatário.
Vamos configurarhibernate.properties para usar o modo de multilocação de esquema e nossa implementação da interfaceMultiTenantConnectionProvider:
hibernate.connection.url=jdbc:h2:mem:mydb1;DB_CLOSE_DELAY=-1;\
INIT=CREATE SCHEMA IF NOT EXISTS MYDB1\\;CREATE SCHEMA IF NOT EXISTS MYDB2\\;
hibernate.multiTenancy=SCHEMA
hibernate.multi_tenant_connection_provider=\
com.example.hibernate.multitenancy.schema.SchemaMultiTenantConnectionProvider
Para fins de nosso teste, configuramos a propriedadehibernate.connection.url para criar dois esquemas. Isso não deve ser necessário para uma aplicação real, uma vez que os esquemas já devem estar em vigor.
Para nosso teste, adicionaremos uma entradaCar no inquilinomyDb1. Verificaremos se essa entrada foi armazenada em nosso banco de dados e se não está no inquilinomyDb2:
@Test
void whenAddingEntries_thenOnlyAddedToConcreteDatabase() {
whenCurrentTenantIs(TenantIdNames.MYDB1);
whenAddCar("myCar");
thenCarFound("myCar");
whenCurrentTenantIs(TenantIdNames.MYDB2);
thenCarNotFound("myCar");
}
Como podemos ver no teste, mudamos o inquilino ao chamar o métodowhenCurrentTenantIs.
5. Abordagem de banco de dados
A abordagem de multilocação do banco de dados usadifferent physical database instances per tenant. Como cada inquilino está totalmente isolado,we should choose this strategy when we need special database features like backup per tenant more than we need the best performance.
Para a abordagem de banco de dados, usaremos a mesma classeMultitenancyIntegrationTest e a interfaceCurrentTenantIdentifierResolver acima.
Para a interfaceMultiTenantConnectionProvider, usaremos uma coleçãoMap para obter umConnectionProvider por identificador de locatário:
class MapMultiTenantConnectionProvider
extends AbstractMultiTenantConnectionProvider {
private Map connectionProviderMap
= new HashMap<>();
public MapMultiTenantConnectionProvider() throws IOException {
initConnectionProviderForTenant(TenantIdNames.MYDB1);
initConnectionProviderForTenant(TenantIdNames.MYDB2);
}
@Override
protected ConnectionProvider getAnyConnectionProvider() {
return connectionProviderMap.values()
.iterator()
.next();
}
@Override
protected ConnectionProvider selectConnectionProvider(
String tenantIdentifier) {
return connectionProviderMap.get(tenantIdentifier);
}
private void initConnectionProviderForTenant(String tenantId)
throws IOException {
Properties properties = new Properties();
properties.load(getClass().getResourceAsStream(
String.format("/hibernate-database-%s.properties", tenantId)));
DriverManagerConnectionProviderImpl connectionProvider
= new DriverManagerConnectionProviderImpl();
connectionProvider.configure(properties);
this.connectionProviderMap.put(tenantId, connectionProvider);
}
}
CadaConnectionProvider é preenchido por meio do arquivo de configuraçãohibernate-database-<tenant identifier>.properties, que contém todos os detalhes de conexão:
hibernate.connection.driver_class=org.h2.Driver
hibernate.connection.url=jdbc:h2:mem:;DB_CLOSE_DELAY=-1
hibernate.connection.username=sa
hibernate.dialect=org.hibernate.dialect.H2Dialect
Finalmente, vamos atualizar ohibernate.properties novamente para usar o modo de multilocação do banco de dados e nossa implementação da interfaceMultiTenantConnectionProvider:
hibernate.multiTenancy=DATABASE
hibernate.multi_tenant_connection_provider=\
com.example.hibernate.multitenancy.database.MapMultiTenantConnectionProvider
Se executarmos exatamente o mesmo teste da abordagem de esquema, o teste passará novamente.
6. Conclusão
Este artigo aborda o suporte do Hibernate 5 para multilocação usando o banco de dados separado e abordagens de esquema separadas. Fornecemos implementações e exemplos muito simplistas para investigar as diferenças entre essas duas estratégias.
Os exemplos de código completos usados neste artigo estão disponíveis em nossoGithub project.