Como construir um aplicativo multilocatário: um tutorial de hibernação

Publicados: 2022-03-11

Quando falamos de aplicativos em nuvem onde cada cliente possui seus próprios dados separados, precisamos pensar em como armazenar e manipular esses dados. Mesmo com todas as ótimas soluções NoSQL existentes, às vezes ainda precisamos usar o bom e velho banco de dados relacional. A primeira solução que pode vir à mente para separar os dados é adicionar um identificador em cada tabela, para que possa ser tratado individualmente. Isso funciona, mas e se um cliente solicitar seu banco de dados? Seria muito complicado recuperar todos esses registros escondidos entre os outros.

Aplicativo Java EE de multilocação com Hibernate

A multilocação em Java é mais fácil do que nunca com o Hibernate.
Tweet

A equipe do Hibernate veio com uma solução para este problema há algum tempo. Eles fornecem alguns pontos de extensão que permitem controlar de onde os dados devem ser recuperados. Esta solução tem a opção de controlar os dados por meio de uma coluna identificadora, vários bancos de dados e vários esquemas. Este artigo abordará a solução de vários esquemas.

Então, vamos trabalhar!

Começando

Se você é um desenvolvedor Java mais experiente e sabe como configurar tudo, ou se já tem seu próprio projeto Java EE, pode pular esta seção.

Primeiro, temos que criar um novo projeto Java. Estou usando Eclipse e Gradle, mas você pode usar seu IDE preferido e ferramentas de construção, como IntelliJ e Maven.

Se você quiser usar as mesmas ferramentas que eu, você pode seguir estes passos para criar seu projeto:

  • Instale o plugin Gradle no Eclipse
  • Clique em Arquivo -> Novo -> Outro…
  • Encontre Gradle (STS) e clique em Avançar
  • Informe um nome e escolha Java Quickstart para projeto de amostra
  • Clique em Concluir

Excelente! Esta deve ser a estrutura inicial do arquivo:

 javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle

Você pode excluir todos os arquivos que estão dentro das pastas de origem, pois são apenas arquivos de amostra.

Para rodar o projeto, eu uso o Wildfly, e vou mostrar como configurá-lo (mais uma vez você pode usar sua ferramenta favorita aqui):

  • Baixe o Wildfly: http://wildfly.org/downloads/ (estou usando a versão 10)
  • Descompacte o arquivo
  • Instale o plugin JBoss Tools no Eclipse
  • Na guia Servidores, clique com o botão direito do mouse em qualquer área em branco e escolha Novo -> Servidor
  • Escolha Wildfly 10.x (9.x também funciona se 10 não estiver disponível, dependendo da sua versão do Eclipse)
  • Clique em Avançar, escolha Criar novo tempo de execução (próxima página) e clique em Avançar novamente
  • Escolha a pasta onde você descompactou o Wildfly como Home Directory
  • Clique em Concluir

Agora, vamos configurar o Wildfly para conhecer o banco de dados:

  • Vá para a pasta bin dentro da sua pasta Wildfly
  • Execute add-user.bat ou add-user.sh (dependendo do seu sistema operacional)
  • Siga as etapas para criar seu usuário como Gerente
  • No Eclipse, vá para a guia Servidores novamente, clique com o botão direito do mouse no servidor que você criou e selecione Iniciar
  • No seu navegador, acesse http://localhost:9990, que é a Interface de Gerenciamento
  • Insira as credenciais do usuário que você acabou de criar
  • Implante o jar do driver do seu banco de dados:
    1. Vá para a guia Implantação e clique em Adicionar
    2. Clique em Avançar, escolha o arquivo jar do driver
    3. Clique em Avançar e Concluir
  • Vá para a guia Configuração
  • Escolha Subsistemas -> Fontes de dados -> Não-XA
  • Clique em Adicionar, selecione seu banco de dados e clique em Avançar
  • Dê um nome à sua fonte de dados e clique em Avançar
  • Selecione a guia Detect Driver e escolha o driver que você acabou de implantar
  • Insira as informações do seu banco de dados e clique em Avançar
  • Clique em Test Connection se quiser ter certeza de que as informações da etapa anterior estão corretas
  • Clique em Concluir
  • Volte para o Eclipse e pare o servidor em execução
  • Clique com o botão direito nele, selecione Adicionar e Remover
  • Adicione seu projeto à direita
  • Clique em Concluir

Tudo bem, temos Eclipse e Wildfly configurados juntos!

Essas são todas as configurações necessárias fora do projeto. Vamos passar para a configuração do projeto.

Projeto de inicialização

Agora que temos o Eclipse e o Wildfly configurados e nosso projeto criado, precisamos configurar nosso projeto.

A primeira coisa que vamos fazer é editar build.gradle. É assim que deve ficar:

 apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse' apply plugin: 'eclipse-wtp' sourceCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { jcenter() } eclipse { wtp { } } dependencies { providedCompile 'org.hibernate:hibernate-entitymanager:5.0.7.Final' providedCompile 'org.jboss.resteasy:resteasy-jaxrs:3.0.14.Final' providedCompile 'javax:javaee-api:7.0' }

As dependências são todas declaradas como “providedCompile”, pois este comando não adiciona a dependência no arquivo war final. O Wildfly já possui essas dependências e, caso contrário, causaria conflitos com as do aplicativo.

Neste ponto, você pode clicar com o botão direito do mouse em seu projeto, selecionar Gradle (STS) -> Atualizar tudo para importar as dependências que acabamos de declarar.

Hora de criar e configurar o arquivo “persistence.xml”, o arquivo que contém as informações que o Hibernate precisa:

  • Na pasta de origem src/main/resource, crie uma pasta chamada META-INF
  • Dentro desta pasta, crie um arquivo chamado persistence.xml

O conteúdo do arquivo deve ser algo como o seguinte, alterando jta-data-source para corresponder à fonte de dados que você criou no Wildfly e o package com.toptal.andrehil.mt.hibernate para o que você criará no próximo seção (a menos que você escolha o mesmo nome de pacote):

 <?xml version="1.0" encoding="UTF-8" ?> <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" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="pu"> <jta-data-source>java:/JavaEEMTDS</jta-data-source> <properties> <property name="hibernate.multiTenancy" value="SCHEMA"/> <property name="hibernate.tenant_identifier_resolver" value="com.toptal.andrehil.mt.hibernate.SchemaResolver"/> <property name="hibernate.multi_tenant_connection_provider" value="com.toptal.andrehil.mt.hibernate.MultiTenantProvider"/> </properties> </persistence-unit> </persistence>

Classes de hibernação

As configurações adicionadas ao persistence.xml apontam para duas classes personalizadas MultiTenantProvider e SchemaResolver. A primeira classe é responsável por fornecer conexões configuradas com o esquema correto. A segunda classe é responsável por resolver o nome do esquema a ser utilizado.

Aqui está a implementação das duas classes:

 public class MultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService { private static final long serialVersionUID = 1L; private DataSource dataSource; @Override public boolean supportsAggressiveRelease() { return false; } @Override public void injectServices(ServiceRegistryImplementor serviceRegistry) { try { final Context init = new InitialContext(); dataSource = (DataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name } catch (final NamingException e) { throw new RuntimeException(e); } } @SuppressWarnings("rawtypes") @Override public boolean isUnwrappableAs(Class clazz) { return false; } @Override public <T> T unwrap(Class<T> clazz) { return null; } @Override public Connection getAnyConnection() throws SQLException { final Connection connection = dataSource.getConnection(); return connection; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection(); try { connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e); } return connection; } @Override public void releaseAnyConnection(Connection connection) throws SQLException { try { connection.createStatement().execute("SET SCHEMA 'public'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [public]", e); } connection.close(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { releaseAnyConnection(connection); } }

A sintaxe usada nas instruções acima funciona com PostgreSQL e alguns outros bancos de dados, isso deve ser alterado caso seu banco de dados tenha uma sintaxe diferente para alterar o esquema atual.

 public class SchemaResolver implements CurrentTenantIdentifierResolver { private String tenantIdentifier = "public"; @Override public String resolveCurrentTenantIdentifier() { return tenantIdentifier; } @Override public boolean validateExistingCurrentSessions() { return false; } public void setTenantIdentifier(String tenantIdentifier) { this.tenantIdentifier = tenantIdentifier; } }

Neste ponto, já é possível testar o aplicativo. Por enquanto, nosso resolvedor está apontando diretamente para um esquema público codificado, mas já está sendo chamado. Para fazer isso, pare o servidor se estiver em execução e inicie-o novamente. Você pode tentar executá-lo no modo de depuração e colocar ponto de interrupção em qualquer ponto das classes acima para verificar se está funcionando.

Uso prático do resolvedor

Então, como o resolvedor poderia realmente conter o nome correto do esquema?

Uma maneira de conseguir isso é manter um identificador no cabeçalho de todas as solicitações e, em seguida, criar um filtro para injetar o nome do esquema.

Vamos implementar uma classe de filtro para exemplificar o uso. O resolvedor pode ser acessado através da SessionFactory do Hibernate, então vamos aproveitar isso para obtê-lo e injetar o nome correto do esquema.

 @Provider public class AuthRequestFilter implements ContainerRequestFilter { @PersistenceUnit(unitName = "pu") private EntityManagerFactory entityManagerFactory; @Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory(); final SchemaResolver schemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver(); final String username = containerRequestContext.getHeaderString("username"); schemaResolver.setTenantIdentifier(username); } }

Agora, quando qualquer classe conseguir um EntityManager para acessar o banco de dados, ela já estará configurada com o esquema correto.

Para simplificar, a implementação mostrada aqui está obtendo o identificador diretamente de uma string no cabeçalho, mas é uma boa ideia usar um token de autenticação e armazenar o identificador no token. Se você estiver interessado em saber mais sobre esse assunto, sugiro dar uma olhada em JSON Web Tokens (JWT). JWT é uma biblioteca simples e agradável para manipulação de tokens.

Como usar tudo isso

Com tudo configurado, não há mais nada a fazer em suas entidades e/ou classes que interagem com EntityManager . Qualquer coisa que você executar de um EntityManager será direcionado para o esquema resolvido pelo filtro criado.

Agora, tudo o que você precisa fazer é interceptar solicitações no lado do cliente e injetar o identificador/token no cabeçalho para ser enviado ao lado do servidor.

Em uma aplicação real, você terá melhores meios de autenticação. A ideia geral de multilocação, no entanto, permanecerá a mesma.

O link no final do artigo aponta para o projeto usado para escrever este artigo. Ele usa Flyway para criar 2 esquemas e contém uma classe de entidade chamada Car e uma classe de serviço de descanso chamada CarService que pode ser usada para testar o projeto. Você pode seguir todos os passos abaixo, mas ao invés de criar seu próprio projeto, você pode cloná-lo e usar este. Então, ao executar, você pode usar um cliente HTTP simples (como a extensão Postman para Chrome) e fazer uma solicitação GET para http://localhost:8080/javaee-mt/rest/cars com os cabeçalhos key:value:

  • nome de usuário:joe; ou
  • nome de usuário: fred.

Ao fazer isso, as requisições retornarão valores diferentes, que estão em esquemas diferentes, um chamado joe e outro chamado “fred”.

Palavras finais

Essa não é a única solução para criar aplicativos de multilocação no mundo Java, mas é uma maneira simples de conseguir isso.

Uma coisa a ter em mente é que o Hibernate não gera DDL ao usar a configuração multitenancy. Minha sugestão é dar uma olhada no Flyway ou Liquibase, que são ótimas bibliotecas para controlar a criação de banco de dados. Isso é uma coisa legal de se fazer mesmo se você não for usar multitenancy, pois a equipe do Hibernate aconselha a não usar a geração automática de banco de dados em produção.

O código-fonte usado para criar este artigo e a configuração do ambiente podem ser encontrados em github.com/andrehil/JavaEEMT

Relacionado: Tutorial Avançado de Classe Java: Um Guia para Recarregamento de Classe