Transforme aplicações web em serviços Multi Tenant
Este artigo apresenta passo a passo a conversão de uma aplicação web em um Software como Serviço com suporte a Multi Tenancy, utilizando as versões mais recentes de tecnologias como Spring e JPA.
Usando uma aplicação com poucas funcionalidades, mas com frameworks e APIs empregadas em projetos do mundo real, este artigo visa fornecer a base teórica e uma implementação de referência para que os leitores possam criar seus próprios SaaS Multi Tenant em Java.
Guia do artigo:
- O que é Multi Tenancy?
- Como implementar Multi Tenancy
- A aplicação de exemplo
- A aplicação iCardapio
- SaaS Multi Tenant iCardapio
- Obtendo e executando o iCardapio
- Tornando o iCardapio um SaaS Multi Tenant
- Particionando os dados
- Mapeamento da entidade JPA Restaurant
- Marcando a entidade Product como Multi Tenant
- Criando o Tenant Resolver
- Particionando os Dados dos Tenants
- dequando a classe RestaurantFacade
- Adequando a classe RestaurantController
- Criando a página inicial do serviço
- Executando o serviço
- Conclusão
Software como Serviço (SaaS – Software as a Service) é um modelo de distribuição de software que vem ganhando força com a expansão da Internet e das tecnologias de Cloud Computing. Existem SaaS com diferentes características, mas a principal delas, presente em todos os SaaS, é a mudança no direito de propriedade do software em si. Em um software tradicional, o cliente compra uma cópia ou uma licença do software, enquanto que em um SaaS o cliente paga para poder usar o software. Essa é uma relação comercial mais parecida com um aluguel, e é por esse motivo que clientes de um SaaS são muitas vezes chamados de Tenants, que significa locatário em inglês.
É importante esclarecer que, dependendo do modelo de negócios utilizado pelo provedor do SaaS, nem sempre os clientes pagam diretamente para utilizar os serviços. Os modelos de negócio mais conhecidos são os seguintes: Subscrição, onde existe um pagamento recorrente de valores; o Freemium/Premium, onde uma porcentagem dos usuários paga para ter acesso a funcionalidades ou recursos extras do serviço; ou mesmo Ad Supported, que são SaaS aonde o lucro para o provedor do serviço não vem diretamente dos usuários que o utilizam, e sim de um terceiro que paga para ter seu anúncio publicado, entre outros.
Existe muita confusão em relação à definição de Software como Serviço, sendo que muitas pessoas confundem SaaS com SOA (Service Oriented Architecture), que é uma arquitetura para construção de softwares e não um modelo de distribuição. Um SaaS pode ser construído usando a arquitetura SOA, mas isso não é um requisito. Outro ponto a ser esclarecido é que alguns autores limitam o termo SaaS a apenas softwares empresariais, o que deixaria de fora dessa classificação serviços bastante conhecidos como Twitter, Facebook e Google Search, os quais são classificados por esses autores como Serviços de Internet. Este artigo, no entanto, usa a definição de SaaS mais geral, de modo que são exemplos de SaaS serviços como Shopify, Basecamp, Granatum, Salesforce.com, Facebook, Google Search, Google Maps, entre tantos outros.
Além da questão da propriedade do software, algumas outras características são muito recorrentes em SaaS, como o uso através de um navegador Web, o acesso através da Internet, a pouca ou inexistente necessidade de configuração, a não necessidade de infraestrutura própria (como servidores, backup, redes, no-breaks), as constantes melhorias e atualizações oferecidas sem custo adicional, a possibilidade de parar de usar o serviço a qualquer momento, entre outras. No entanto, uma das principais características dos SaaS é seu usual baixo custo quando comparado a sistemas tradicionais, onde é necessário comprar uma licença ou manter instalações on-premises.
O preço pelo qual esses serviços são oferecidos é alcançado graças a um conjunto de fatores. O primeiro deles é o uso da Internet e navegadores Web para o uso do sistema, permitindo que pessoas e empresas no mundo inteiro possam ser consideradas clientes em potencial, podendo, portanto atingir uma escala muito maior que softwares tradicionais. O segundo fator é a expansão das tecnologias de Cloud Computing, que permite aos provedores de serviço alugar infraestrutura pronta e escalável para executar suas aplicações com um custo cada vez mais baixo. E o terceiro fator, não menos importante, é o uso de arquiteturas de software escaláveis e padrões de desenvolvimento que permitem minimizar o custo de desenvolvimento, manutenção e execução do serviço.
Esse conjunto de fatores permite aos provedores de serviço atender a um grande número de clientes simultaneamente, de modo que eles possam conseguir lucro com a escala de usuários que possuem, mesmo com custos ao cliente final muito menores que os de softwares que precisam de licenças. Vale ressaltar que ter uma grande base de usuários possibilita que o provedor crie outras fontes de renda, como a venda de dados resultantes da análise do perfil dos usuários ou a criação de portais que agregam o conteúdo de todos os clientes. Por exemplo, um SaaS para imobiliárias poderia criar um portal com os imóveis agregados de todas as imobiliárias que são suas clientes, já que possui acesso direto a todas as informações necessárias. Esse portal poderia ser de grande utilidade para possíveis locatários ou compradores, e permitiria ao provedor do SaaS vender espaços publicitários no portal, ou melhorar o posicionamento dos imóveis de determinadas imobiliárias no resultado das buscas, entre outras possibilidades.
Entretanto, o desenvolvimento de SaaS que possibilitem cobrar preços tão baixos e que possam utilizar esses modelos de negócio requer que os softwares que os implementam sejam escaláveis, seguros e tenham alta disponibilidade. O desenvolvimento de SaaS com todas essas características é um assunto bastante extenso e com diferentes abordagens. Dentro deste contexto, este artigo visa fornecer as bases para a construção de serviços escaláveis a partir de aplicações web existentes, fazendo com que essas aplicações sejam convertidas em serviços Multi Tenant.
O que é Multi Tenancy?
Tenant é uma palavra em inglês que é traduzida para locatário, assim como locatário de um imóvel, por exemplo. Esse termo é utilizado em SaaS uma vez que ele reflete o modelo de cobrança geralmente adotado por esses serviços, onde os clientes pagam de alguma forma para utilizá-los. Em um SaaS geralmente o cliente é dono de seus dados, enquanto que o provedor é o dono do software em si.
Um serviço é dito Multi Tenant quando uma única instância ou instalação é capaz de atender vários clientes (Tenants) simultaneamente, ao invés de ter uma instância/instalação para cada cliente. No entanto, um Tenant em um SaaS pode ser representado por diferentes entidades. Por exemplo, um Tenant em um SaaS que ofereça controle financeiro pessoal corresponde a um único Usuário. Já em um SaaS que suporte o controle financeiro de toda uma empresa, com inúmeros usuários e diferentes perfis, um Tenant corresponde a uma empresa, e assim por diante.
O que define uma aplicação Multi Tenant, portanto, é a capacidade que uma única instância de uma aplicação tem de servir a diferentes Tenants e seus usuários, segregando os dados desses Tenants de maneira que um usuário de um Tenant não possa ver ou alterar os dados de outros Tenants. A Figura 1 mostra em alto nível uma aplicação Multi Tenant.
Como contraponto, um exemplo de SaaS não Multi Tenant seria algum provedor que faz uma instalação específica para cada um de seus clientes. Por exemplo, existem provedores de serviço que usam uma solução intermediária para atender grupos de clientes com necessidades parecidas, com uma instância A servindo a um determinado grupo de clientes e outra instância B servindo a outro grupo de clientes.
Alguns autores criaram classificações de maturidade para serviços SaaS, partindo dos menos maduros, onde cada cliente tem sua própria instalação e customizações, até o nível onde existe apenas uma instância Multi Tenant do serviço, que é capaz de atender aos requisitos de todos os clientes através de configurações ou com mecanismos de extensão e customização de suas funcionalidades, como processos de negócio ou mesmo do design de suas interfaces visuais. No entanto, não existe até o presente momento uma classificação de maturidade padrão e totalmente aceita.
Como implementar Multi Tenancy
Basicamente a implementação de uma aplicação web Multi Tenant consiste em determinar o Tenant ao qual uma requisição se refere, e a partir disso, segregar os dados desse Tenant para compor a resposta.
O processo que determina o Tenant a partir de uma requisição é chamado de Tenant Resolution, e o componente responsável por essa funcionalidade é chamado de Tenant Resolver, sendo que existem diferentes formas de implementação. Por exemplo, pode-se determinar o Tenant a partir de atributos da URL de uma requisição web, como utilizar para cada Tenant um domínio ou subdomínio diferente (ex: http://<tenant_id>.saas.com), ou até mesmo uma parte da URL específica para cada Tenant (ex: http://www.saas.com/<tenant_id>/). Outra forma é salvar o Tenant ao qual um usuário pertence na tabela de usuários, e então só particionar os dados após o login, conforme exemplificado na Figura 2.
A segregação dos dados dos Tenant independe do mecanismo de resolução escolhido, e também pode ser implementada de diferentes maneiras. Uma solução é criar um banco de dados ou um schema diferente para cada Tenant. Outra forma é utilizar uma coluna que identifique o Tenant em cada linha das tabelas.
Cada forma de particionamento tem seus prós e contras. Por exemplo, ao utilizar bancos de dados ou schemas diferentes, não é necessário repensar em índices e o risco de que os dados de um Tenant “vazem” para os de outros Tenants é menor. No entanto, é preciso ter todo um controle de conexão para os diferentes schemas, a manutenção e evolução desse banco de dados se tornam mais complexas, mais recursos de infraestrutura como memória podem ser necessários e é mais complexo extrair dados gerais da aplicação, como padrão de uso, perfis de usuário e de utilização, entre outros.
Compartilhando um único schema e segregando os dados através de uma coluna que identifica o Tenant trás a vantagem da menor complexidade de manutenção e evolução, mas trás como desvantagem a necessidade de repensar nos índices para incluírem a coluna que especifica o Tenant, o maior cuidado em não expor dados de um cliente para os demais, entre outros.
A aplicação de exemplo
A forma como o particionamento dos dados será implementado em um SaaS Multi Tenant precisa ser escolhida cedo no processo de desenvolvimento ou migração de uma aplicação, pois as tecnologias e os mecanismos utilizados na implementação dependem fortemente dessa decisão. Além disso, por afetar diretamente a camada de persistência da aplicação, uma mudança de mecanismo em um sistema em produção acarretaria não apenas mudanças na implementação em si, mas também possivelmente na migração dos dados existentes.
O mesmo nível de atenção não precisa ser empregado no mecanismo de resolução dos Tenants, pois além de ser independente do mecanismo de particionamento, ele é utilizado bem no início do processamento das requisições, e seu funcionamento pode ser encapsulado através da definição de uma API que retorne o Tenant Id baseado na requisição web corrente.
Este artigo irá utilizar a resolução de Tenants através do subdomínio nas URLs de requisição e o particionamento dos dados será obtido através da utilização de uma coluna tenant_id nas tabelas com dados compartilhados entre os Tenants, de modo que a implementação de exemplo, apresentada nas seções a seguir, será baseada nessas características.
A aplicação iCardapio
O foco principal desse artigo é criar um SaaS a partir de uma aplicação web existente, e não criar uma aplicação web do zero, de modo que foi criada uma aplicação de exemplo para facilitar esse processo. Essa aplicação, chamada de iCardapio, tem um conjunto bem restrito de funcionalidades, mas utiliza vários frameworks e APIs de mercado, de modo que possa servir de base para a construção de sistemas que serão usados no mundo real, e não apenas fornecer um exemplo simples para apresentar o conceito.
A aplicação iCardapio permite que um restaurante possa disponibilizar seu cardápio e informações básicas na Internet. As tecnologias utilizadas na sua implementação foram as versões mais recentes (no momento da escrita deste artigo) dos frameworks Spring, Spring MVC, Spring Data, Spring Security e JPA com EclipseLink.
Alguns desses frameworks/APIs foram escolhidos apenas pela preferência do autor, mas outros foram escolhidos pelo nível de suporte que oferecem ao desenvolvimento de aplicações Multi Tenant. Apenas para exemplificar, foi escolhido JPA com EclipseLink ao invés de JPA com Hibernate por esse último não oferecer, no momento da escrita deste artigo, suporte nativo à criação de aplicações Multi Tenant quando particionando os dados usando colunas. O suporte existente é apenas para particionamento através do uso de diferentes databases ou schemas.
SaaS Multi Tenant iCardapio
Durante o restante deste artigo transformaremos o iCardapio em um SaaS Multi Tenant utilizando as tecnologias e os mecanismos propostos. Além desses requisitos não funcionais, vamos destacar alguns requisitos funcionais que são interessantes em sistemas desse tipo. O serviço proposto irá suportar que o próprio dono do restaurante cadastre e mantenha o seu Tenant, e o serviço será acessível a usuários não autenticados (clientes do restaurante) e usuários autenticados (administrador do Tenant).
Será possível ao dono do restaurante cadastrar um nome, slogan, telefone, endereço e escolher seu subdomínio, além de poder cadastrar seus produtos, especificando preço, descrição e categoria. As categorias, no entanto, serão pré-definidas para que o provedor do serviço possa, em um momento futuro, montar mais facilmente um portal com os produtos de todos os restaurantes, embora essa funcionalidade não seja implementada neste artigo.
Decisões de implementação
Conhecendo as características da aplicação existente e a forma como queremos que a versão Multi Tenant funcione, é possível tomar algumas decisões quanto a implementação.
Como é necessário saber o Tenant ao qual uma requisição se refere desde o primeiro acesso de um usuário, usaremos o subdomínio da URL como identificador do Tenant. Apenas para explicar melhor, outra estratégia seria obter o Tenant Id durante o login do usuário, consultando seu cadastro, mas essa estratégia não é útil para casos como o da aplicação em questão, pois ela terá usuários sem login acessando o sistema e não seria possível ter landing pages diferentes para cada restaurante.
Quanto ao particionamento dos dados, a aplicação original utiliza a versão 2.1 do JPA e usa o EclipseLink como provider, sendo que ambos suportam Multi Tenancy. EclipseLink é uma implementação de referência da especificação JPA, sendo que ele suporta diferentes estratégias de particionamento dos dados, incluindo o particionamento utilizando colunas para diferenciar os dados de cada Tenant.
Além disso, é preciso ter uma forma de fornecer ao mecanismo de particionamento dos dados o Tenant que foi obtido a partir da requisição web. Basicamente, é necessário utilizar o Tenant obtido para modificar todos os acessos ao banco de dados para filtrar as queries por Tenant Id e incluir o Tenant Id nos novos dados que serão cadastrados ou modificados. Portanto, como estamos utilizando uma versão do JPA que suporta Multi Tenancy, precisamos apenas entender como esse mecanismo funciona, e então encontrar uma forma de fazer essa ligação.
O suporte a Multi Tenancy no JPA 2.1 é bastante simples, bastando especificar uma variável de contexto com o Tenant Id depois que um Entity Manager for criado (mais precisamente, quando uma transação for criada). Existem alguns pontos específicos do provider (EclipseLink) que precisam ser levados em conta, como anotar as entidades que terão suporte a Multi Tenancy com uma annotation @MultiTenant, qual a coluna que será usada para conter o Tenant Id, entre outros, mas esses serão destacados durante a apresentação da implementação.
Portanto, o ponto de ligação entre o Tenant Resolver e o Data Partitioner na aplicação será o momento logo após a criação de uma transação pelos Entity Managers. No caso da aplicação de exemplo, essa ligação será feita utilizando recursos da biblioteca Spring Data, que facilita o acesso aos dados com seus Repositories. O funcionamento dessa biblioteca foge ao escopo deste artigo, mas pode-se pensar nos Repositories como DAOs, e a vantagem para o problema em questão é que esses Repositories são criados através de Factories, as quais é possível fornecer implementações customizadas. Em outras palavras, é possível estender a Factory padrão, para que sempre que uma transação for criada no Entity Manager Repository, ou seja, logo antes de cada operação que acessa o banco de dados, utilizar o Tenant Resolver para especificar a variável de contexto com o Tenant Id, automatizando e abstraindo o processo.
Apenas para enfatizar, o Tenant Id deve ser setado depois que uma transação é criada, e não quando o Entity Manager é criado. Isso ficará mais claro durante a apresentação da implementação.
Obtendo e executando o iCardapio
Tanto a versão convencional quanto a Multi Tenant do iCardapio estão disponíveis no GitHub em https://github.com/michetti/icardapio. A branch principal corresponde à versão convencional, enquanto que a versão Multi Tenant está na branch de nome multitenant. Esses códigos também estão disponíveis na página da revista.
Para poder executar essa aplicação, são necessários os seguintes softwares:
- Java JDK >= 6;
- MySQL >= 5.5;
- Maven;
- Git.
Para obter a aplicação base, será necessário fazer o clone diretamente do projeto no GitHub ou fazer um fork do repositório e então fazer o clone desse novo fork. O uso do Git e do GitHub também foge ao escopo deste artigo, mas o mais simples é fazer um clone do repositório, usando o seguinte comando:
git clone https://github.com/michetti/icardapio.git
Para criar uma branch local da versão Multi Tenant disponibilizada no repositório, pode-se utilizar o seguinte comando:
git checkout -b multitenant origin/multitenant
Esse comando irá criar a branch e fazer seu checkout, de modo que será necessário voltar a branch master para continuar seguindo o artigo. No entanto, para que possamos a todo o momento comparar a versão local com as de referência, o mais interessante é criar uma nova branch local a partir da branch master.
Para isso será necessário voltar à branch master para então criar uma nova branch a partir dela. Inicie então com o seguinte comando:
git checkout master
Em seguida, a nova branch pode ser criada com os seguintes comandos:
git branch mymultitenant
git checkout mymultitenant
Para verificar a branch corrente e ver os arquivos modificados a qualquer momento, pode-se utilizar o comando git status. Para ver a mudança no conteúdo desses arquivos desde o último commit, pode-se utilizar o git diff. Para comparar uma branch com outra, pode-se utilizar o comando git diff <branch a ser comparada>. Por exemplo, para comparar a branch mymultitenant com a branch master, o comando seria git diff master.
O projeto está configurado para usar o MySQL, e por facilidade, um banco de dados chamado icardapio, com usuário e senha icardapio. Esses dados podem ser modificados no arquivo application.properties no projeto. Para criar esse banco de dados com essas configurações padrão, é necessário abrir o shell do MySQL com algum usuário com permissão para criar bancos de dados, usuários e dar grants. Se o usuário root do MySQL for utilizado, pode-se acessar o shell com o seguinte comando:
mysql -u root –p
Dentro do shell do MySQL, é necessário executar os seguintes comandos:
create database icardapio;
grant all privileges on icardapio.* to icardapio@localhost identified by ‘icardapio’;
flush privileges;
exit;
Para verificar se tudo foi criado corretamente, pode-se usar o comando:
mysql -u icardapio icardapio -picardapio
Se a conexão for aceita, então o banco de dados, usuário e senha foram criados corretamente.
Para executar o projeto em um Tomcat embedded, pode-se empregar os seguintes comandos, de dentro do diretório raiz do projeto:
mvn clean package
mvn tomcat:run
Esses comandos irão executar o aplicativo, que ficará acessível na seguinte URL: http://localhost:8080/icardapio. Para facilitar o usa da aplicação, existe uma action que cria alguns dados iniciais, como por exemplo, algumas categorias de produtos. Para criar essas categorias, basta acessar a URL: http://localhost:8080/icardapio/createMasterData.
Para importar a aplicação em uma IDE como a Spring STS, que é baseada no Eclipse e que tem ferramentas para trabalhar com Spring, pode-se utilizar o seguinte comando de dentro do diretório raiz da aplicação:
mvn eclipse:eclipse -wtpversion=”2.0”.
Feito isso, é necessário abrir a IDE e importar o projeto. Para facilitar o desenvolvimento dentro da IDE e ter acesso aos comandos de start e stop do servidor, além de poder empregar os recursos de debug mais facilmente, é recomendado utilizar um servidor de aplicações separado ao invés de usar o servidor embedded. Por exemplo, pode-se utilizar uma instalação de servidores de aplicação como o GlassFish, JBoss, ou mesmo do próprio Tomcat, e então cadastrar esse servidor na IDE e fazer o deploy do projeto nesse servidor.
Tornando o iCardapio um SaaS Multi Tenant
Conforme explicado anteriormente, para tornar nossa aplicação um SaaS Multi Tenant com as características mencionadas, será necessário particionar os dados no banco de dados, definir um mecanismo para obter o Tenant corrente em cada requisição, e por fim, utilizar o Tenant obtido para recuperar os dados apenas desse Tenant e então compor a resposta. As seções a seguir visam implementar cada um desses mecanismos.
Particionando os dados
Uma das primeiras tarefas no desenvolvimento de sistemas Multi Tenant é definir quais tabelas do banco de dados irão conter dados de diversos Tenants. Este projeto contém apenas duas entidades JPA mapeadas para o banco de dados, sendo elas Category e Product. Como a aplicação inicialmente contém dados de apenas um restaurante, a classe Restaurant não é persistente, e é populada com dados fixos no projeto.
Para tornar o sistema Multi Tenant, será necessário suportar N restaurantes, de modo que essa entidade passará a ser mapeada usando JPA. Além disso, no contexto do iCardapio, o restaurante irá representar o Tenant, e irá conter informações como o subdomínio que ele irá utilizar, e também o Tenant Id, que será usado em outras tabelas para particionar os dados. Por simplicidade, o próprio Id da tabela Restaurant será usado como Tenant Id.
Já a entidade Product será Multi Tenant, de modo que precisará conter um novo campo para armazenar o Id do Tenant ao qual pertence. Já a entidade Category, por decisão de projeto, não será Multi Tenant, de modo que seus dados possam ser compartilhados e acessados por todos os Tenants.
Mapeamento da entidade JPA Restaurant
A primeira modificação a ser implementada é o mapeamento da classe Restaurant como uma entidade JPA. A Listagem 1 mostra a inclusão da annotation @Entity, o mapeamento do campo id para que seja automaticamente gerado pelo banco de dados, e o encapsulamento da obtenção do Tenant Id em um método, para que esse mecanismo possa ser modificado no futuro com menos impactos ao restante do projeto.
@Entity
public class Restaurant implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@Transient
public Long getTenantId() {
return this.id;
}
A classe Restaurant deve então ser listada no arquivo persistence.xml para que seja reconhecida como uma entidade JPA, como realizado na Listagem 2.
<persistence-unit name="icardapio" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>br.com.icardapio.entity.Restaurant</class>
<class>br.com.icardapio.entity.Category</class>
<class>br.com.icardapio.entity.Product</class>
</persistence-unit>
Por fim, deve-se criar um Spring Data Repository para essa nova entidade, com a definição de um método para obter um Restaurant a partir de um subdomínio. É importante notar que basta definir a interface do Repository conforme a Listagem 3, pois a implementação concreta será criada automaticamente pela biblioteca Spring Data neste caso.
package br.com.icardapio.repositories;
import br.com.icardapio.entity.Restaurant;
public interface RestaurantsRepository extends
BaseRepository<Restaurant, Long> {
Restaurant getBySubdomain(String subdomain);
}
Marcando a entidade Product como Multi Tenant
Com a classe Restaurant mapeada, o próximo passo é identificar quais entidades JPA devem ser consideradas Multi Tenant. Para isso, usaremos algumas annotations do framework de persistência EclipseLink para marcar a entidade Product como Multi Tenant, como expõe a Listagem 4. Também é possível utilizar arquivos de mapeamento em XML ao invés de usar essas annotations na classe.
@Multitenant
@TenantDiscriminatorColumn(name="TENANT_ID",
discriminatorType=DiscriminatorType.INTEGER,
contextProperty=PersistenceUnitProperties.
MULTITENANT_PROPERTY_DEFAULT)
public class Product implements Serializable {
A annotation @MultiTenant identifica ao Eclipse Link que essa classe deve ser tratada como Multi Tenant, enquanto que a annotation @TenantDiscriminatorColumn identifica qual é a coluna na tabela do banco de dados que irá conter o id do Tenant, e suas propriedades. Nesse exemplo, o nome da coluna será TENANT_ID e seu tipo será INTEGER. Já a propriedade contextProperty identifica qual o nome da propriedade de contexto do Entity Manager que irá conter o Tenant Id, ou seja, ela identifica a propriedade que faz a ligação entre o mecanismo de resolução do Tenant com o mecanismo de particionamento dos dados.
A documentação do Eclipse Link indica que o campo Tenant Id não precisa ser mapeado nas entidades JPA que serão Multi Tenant. No entanto, se ele for mapeado, deve-se adicionar insertable=false e updatable=false na anotação @Column, conforme pode ser visto na Listagem 5, pois ele não pode ser manipulado diretamente no sistema. Outro ponto importante referente às entidades que suportarão Multi Tenancy são as implementações dos métodos hashCode() e equals(). Esses métodos podem ser modificados para levarem em conta a propriedade tenantId, embora não seja necessário, já que os dados virão particionados do banco de dados e não teremos comparações entre objetos de Tenants diferentes; pelo menos não com o conjunto de funcionalidades que estão planejadas inicialmente para o serviço.
@Column(name="TENANT_ID", insertable=false, updatable=false)
public Long tenantId;
public Long getTenantId() {
return tenantId;
}
@Override
public int hashCode() {
return new HashCodeBuilder(5, 7)
.append(tenantId)
.append(category)
.append(name)
.toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) { return false; }
if (obj == this) { return true; }
if (obj.getClass() != getClass()) {
return false;
}
Product rhs = (Product) obj;
return new EqualsBuilder()
.appendSuper(super.equals(obj))
.append(tenantId, rhs.getTenantId())
.append(category, rhs.getCategory())
.append(name, rhs.getName())
.isEquals();
}
Criando o Tenant Resolver
Com o particionamento dos dados implementado, podemos passar para a criação do Tenant Resolver, que irá obter o Tenant referente à requisição web corrente. Para isso, é uma boa prática definir inicialmente a interface desse mecanismo, como pode ser visto na Listagem 6.
package br.com.icardapio.multitenancy;
import java.io.Serializable;
public interface CurrentTenantResolver<T extends Serializable> {
T getCurrentTenantId();
T getMasterTenantId();
boolean isMasterTenant();
}
Em seguida, deve-se definir um mecanismo de resolução específico para as necessidades do serviço, que neste caso, usará o subdomínio da URL das requisições Web. A implementação da obtenção do tenant a partir do subdomínio pode ser vista no método getCurrentTenantFromSubdomain() na Listagem 7. Este método irá procurar na tabela Restaurant o id da tupla que contém o subdomínio da URL requisitada, que conforme mencionado anteriormente, será usado como Tenant Id nas tabelas Multi Tenant do sistema.
public class ICardapioTenantResolver implements
CurrentTenantResolver<Long> {
private static String MASTER_TENANT_SUBDOMAIN = "www";
@Autowired
private RestaurantsRepository restaurantsRepository;
protected Long getCurrentTenantFromSubdomain() {
ServletRequestAttributes attr =
(ServletRequestAttributes)RequestContextHolder.
currentRequestAttributes();
String subdomain = attr.getRequest().getServerName().
split("\\.")[0];
Restaurant restaurant = restaurantsRepository.getBySubdomain
(subdomain);
return restaurant != null ?
restaurant.getTenantId() : 0;
}
@Override
public Long getMasterTenantId() {
return
restaurantsRepository.getBySubdomain
(MASTER_TENANT_SUBDOMAIN).getTenantId();
}
@Override
public boolean isMasterTenant() {
Long masterTenant = getMasterTenantId();
Long tenantFromSubdomain = getCurrentTenantFromSubdomain();
return masterTenant.equals(tenantFromSubdomain);
}
@Override
public Long getCurrentTenantId() {
return getCurrentTenantFromSubdomain();
}
}
Essa implementação também define um Tenant Master, que irá representar a página do serviço em si. Esse tentant será identificado pelo subdomínio www, que nessa implementação simplificada está hardcoded no código do Tenant Resolver. Em seguida, como pode ser visto na Listagem 8, deve-se mapear o Tenant Resolver criado como um bean no Spring.
<!-- Tenant Resolver -->
<bean id="tenantResolver" class="br.com.icardapio.multitenancy.
ICardapioTenantResolver" />
Particionando os Dados dos Tenants
Conforme mencionado anteriormente, o serviço utilizará uma Factory de Repositories customizada do Spring Data para criar Entity Managers. Essa etapa pode ser feita de diferentes maneiras, mas este artigo usa uma abordagem bastante pragmática, que simplesmente estende a Factory usada normalmente e adiciona chamadas a um método que define a variável de contexto com o Tenant Id.
O primeiro passo é estender a classe SimpleJpaRepository, de maneira que seja possível passar como parâmetro o Tenant Resolver criado anteriormente, para que ela seja capaz de definir a variável de contexto utilizada no particionamento dos dados. Essa implementação é simples, mas muito longa para ser colocada na íntegra neste artigo, de modo que apenas alguns dos métodos sobrescritos serão apresentados aqui, e o restante pode ser verificado no código disponibilizado no GitHub. Basicamente, os construtores da classe passam a receber o Tenant Resolve como parâmetro e um método é criado para setar o Tenant Id no Entity Manager. Em seguida, todos os métodos que acessam o banco de dados devem ser sobrescritos de modo a sempre chamarem o método criado para setar o Tenant Id antes de realmente executar a operação no banco de dados. Parte da implementação final da classe MultiTenantSimpleJpaRepository pode ser vista na Listagem 9.
//packages e imports omitidos...
public class MultiTenantSimpleJpaRepository<T, ID extends Serializable>
extends SimpleJpaRepository<T, ID> {
private final CurrentTenantResolver<Long> tenantResolver;
private final EntityManager em;
public MultiTenantSimpleJpaRepository(JpaEntityInformation<T, ?>
entityInformation, EntityManager em,
CurrentTenantResolver<Long> tenantResolver) {
super(entityInformation, em);
this.tenantResolver = tenantResolver;
this.em = em;
}
public MultiTenantSimpleJpaRepository(Class<T> domainClass,
EntityManager em, CurrentTenantResolver<Long> tenantResolver) {
super(domainClass, em);
this.tenantResolver = tenantResolver;
this.em = em;
}
protected void setCurrentTenant() {
em.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT,
tenantResolver.getCurrentTenantId());
}
@Override
public <S extends T> S save(S entity) {
setCurrentTenant();
return super.save(entity);
}
@Override
public void setLockMetadataProvider(LockMetadataProvider
lockMetadataProvider) {
super.setLockMetadataProvider(lockMetadataProvider);
}
//restante dos métodos omitidos, veja código no GitHub
}
Observe que o método setCurrentTenant() não deve ser chamado ao sobrescrever o método setLockMetadataProvider(), pois esse método será chamado antes de termos uma requisição web, e portanto, a obtenção do Tenant Id falharia.
Com um Repository customizado para usar o Tenant Resolver, é necessário estender a classe JpaRepositoryFactory para que ela crie repositórios que utilizem a classe definida anteriormente como implementação, ao invés da implementação padrão, conforme a Listagem 10.
//declaração de pacote e imports omitidos...
public class MultiTenantJpaRepositoryFactory extends JpaRepositoryFactory {
private final CurrentTenantResolver<Long> currentTenantResolver;
public MultiTenantJpaRepositoryFactory(
EntityManager entityManager,
CurrentTenantResolver<Long> currentTenantResolver) {
super(entityManager);
this.currentTenantResolver = currentTenantResolver;
}
@Override
@SuppressWarnings("unchecked")
protected JpaRepository<?, ?> getTargetRepository(
RepositoryMetadata metadata, EntityManager entityManager) {
final JpaEntityInformation<?, Serializable> entityInformation =
getEntityInformation(metadata.getDomainType());
final SimpleJpaRepository<?, ?> repo =
new MultiTenantSimpleJpaRepository(
entityInformation,
entityManager,
currentTenantResolver);
repo.setLockMetadataProvider(
LockModeRepositoryPostProcessor.
INSTANCE.getLockMetadataProvider());
return repo;
}
@Override
protected Class<?> getRepositoryBaseClass
(RepositoryMetadata metadata) {
return MultiTenantSimpleJpaRepository.class;
}
}
A Factory estendida recebe o Tenant Resolver como um dos parâmetros em seu construtor e sempre retorna o Repository do tipo MultiTenantSimpleJpaRepository, criado anteriormente, quando o método getTargetRepository() é chamado. A Listagem 11 mostra a classe do bean Spring responsável pela criação dessa Factory.
//declaração de pacote e imports omitidos...
public class MultiTenantJpaRepositoryFactoryBean
<T extends Repository<S, ID>, S, ID extends Serializable>
extends JpaRepositoryFactoryBean<T, S, ID> {
private CurrentTenantResolver<Long> currentTenantResolver;
@Override
protected RepositoryFactorySupport createRepositoryFactory
(EntityManager entityManager) {
return new MultiTenantJpaRepositoryFactory
(entityManager,currentTenantResolver);
}
@Override
public void afterPropertiesSet() {
Assert.notNull(currentTenantResolver,
"CurrentTenantResolver must not be null!");
super.afterPropertiesSet();
}
@Autowired
public void setCurrentTenantResolver(CurrentTenantResolver
<Long> currentTenantResolver) {
this.currentTenantResolver = currentTenantResolver;
}
}
Em seguida, é preciso especificar nos arquivos do Spring que esse bean deve ser utilizado como Factory dos Repositories. Para isso, é necessário modificar o elemento jpa:repositories no arquivo root-context.xml, para que fique de acordo com a Listagem 12.
<!-- JPA Repositories -->
<jpa:repositories base-package="br.com.icardapio.repositories"
factory-class=
"br.com.icardapio.multitenancy.MultiTenantJpaRepositoryFactoryBean" />
Além disso, uma modificação da configuração de cache do bean EntityManagerFactory precisa ser feita, para que fique como na Listagem 13. Essa é uma configuração específica do EclipseLink para que o Tenant Id seja usado para compor a chave do cache das entidades, para que não ocorram colisões entre entidades de Tenants diferentes.
<!-- configuracao do bean entity manager factory -->
<bean id="entityManagerFactory" class=
"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter" ref="jpaVendorAdapter" />
<property name="persistenceUnitName" value="icardapio" />
<property name="jpaDialect">
<bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect" />
</property>
<property name="jpaPropertyMap">
<props>
<prop key="eclipselink.weaving">false</prop>
<prop key="eclipselink.multitenant.tenants-share-cache">true</prop>
</props>
</property>
</bean>
Adequando a classe RestaurantFacade
Com a camada de persistência do sistema adequada a Multi Tenancy, são necessárias algumas modificações em classes de outras camadas para que o sistema possa funcionar corretamente. Uma dessas modificados será necessária no método createMasterData(), na classe RestaurantFacade, para criar o nosso Tenant Master quando for executado. Observe a Listagem 14. O Tenant Master é diferente dos demais e será usado para mostrar as páginas do serviço em si. No caso desta aplicação, teremos formulários para o cadastro de novos restaurantes, mas em outros casos, esse Tenant poderia ser usado para destacar as funcionalidades e características do serviço, preços de pacote, telefones e forma para contato, entre outras funções.
public void createMasterData() {
// cria os dados do tenant principal do sistema
if (restaurantsRepository.getBySubdomain("www") == null) {
Restaurant restaurant = new Restaurant();
restaurant.setName("iCardapio");
restaurant.setSubdomain("www");
restaurant.setSlogan("Seu Cardapio na Internet");
restaurant.setPhone("11 3114-2334");
restaurant.setAddress("Av Dr Gentil de Moura, 850");
restaurant.setCity("Sao Paulo");
restaurantsRepository.save(restaurant);
}
// cria algumas categorias de exemplo
if (categoriesRepository.count() <= 0) {
for(String categoryName: new String[]
{ "Pizza", "Massas", "Bebidas", "Sobremesas" }) {
categoriesRepository.save(new Category
(categoryName));
}
}
}
Também vamos modificar o método getRestaurant() para que ele sempre retorne o Restaurant que se refere ao Tenant da requisição corrente. Para isso, é necessário adicionar a referência ao bean tenantResolver e restaurantRespositories, e então modificar o método em questão para que utilize esses recursos, de acordo com a Listagem 15.
@Autowired
private CurrentTenantResolver<Long> tenantResolver;
@Autowired
private RestaurantsRepository restaurantsRepository;
public Restaurant getRestaurant() {
return restaurantsRepository.findOne
(tenantResolver.getCurrentTenantId());
}
Como nosso serviço também permitirá que novos restaurantes/tenants sejam cadastrados, precisamos adicionar essa funcionalidade ao nosso Facade, como indica a Listagem 16.
public Restaurant addRestaurant(Restaurant restaurant) {
return restaurantsRepository.save(restaurant);
}
É importante notar que apesar da necessidade de usar o tenantResolver no método que retorna o restaurante corrente, nenhuma outra referência é feita ao Tenant em si, já que esse controle é feito de forma automática pelo suporte do JPA a Multi Tenancy. Desse modo, quando o método getAllCategories() no Facade é chamado, ele retorna todas as categorias, já que essa entidade não é Multi Tenant, mas retorna apenas os produtos pertencentes ao Tenant corrente. Isso ficará mais claro durante a execução do serviço posteriormente.
Adequando a classe RestaurantController
As seções anteriores adequaram a camada de persistência e o Facade da aplicação a Multi Tenancy. Serão feitas agora algumas modificações no único Controller do sistema. Deste modo, o método home() na classe RestaurantController irá mostrar uma página inicial diferente caso o serviço seja acessado com o subdomínio do Tenant Master (www). Para isso, deve-se adicionar a referência ao bean tenantResolver nessa classe e alterar o método home() para que fique igual ao exibido na Listagem 17.
@Autowired
private CurrentTenantResolver<Long> tenantResolver;
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(@ModelAttribute("tenant") Restaurant tenant, Model model) {
// adiciona informações sobre o tenant corrente
Restaurant restaurant = facade.getRestaurant();
model.addAttribute("restaurant", restaurant);
if (tenantResolver.isMasterTenant()) {
model.addAttribute("tenant", tenant);
return "landing";
} else {
List<Category> categories = facade.getAllCategories();
model.addAttribute("categories", categories);
return "home";
}
}
A página inicial do serviço será adicionada posteriormente, mas ela irá possibilitar que novos restaurantes sejam cadastrados através de um form. Para que essa funcionalidade funcione corretamente, os métodos especificados na Listagem 18 precisam ser adicionados ao controller.
@RequestMapping(value = "/addRestaurant", method = RequestMethod.POST)
public String addRestaurant(@ModelAttribute("tenant")
Restaurant restaurant, Model model,
final RedirectAttributes redirectAttributes) {
restaurant = facade.addRestaurant(restaurant);
return "redirect:" + getRestaurantFullUrl(restaurant);
}
@ModelAttribute("tenant")
public Restaurant getRestaurantObject() {
return new Restaurant();
}
protected String getRestaurantFullUrl
(Restaurant restaurant) {return "http://" +
restaurant.getSubdomain() + ".lvh.me:8080/icardapio";
}
Agora, quando um novo restaurante for cadastrado, o usuário será redirecionado para a página inicial desse novo restaurante. O domínio lvh.me é bastante útil no desenvolvimento de aplicações web que usam subdomínios, já que ele sempre aponta para localhost, mesmo quando acessado com um subdomínio. Ou seja, se um Tenant com subdomínio restaurante1 for criado, será possível acessá-lo localmente usando a URL http://restaurante1.lvh.me:8080/icardapio, ao invés de ter que mapear algum nome em um arquivo como /etc/hosts, por exemplo.
Criando a página inicial do serviço
Com a adequação das camadas de persistência, Facade e Controller do sistema concluídas, a última modificação necessária para tornar o serviço compatível com os requisitos propostos é a criação da página landing.jsp. Essa será a página inicial do serviço, que será exibida quando o sistema for acessado através do subdomínio www, ou seja, através da URL http://www.lvh.me:8080/icardapio. Essa página será usada para o cadastro de novos restaurantes, mas por ser muito extensa não será apresentada na íntegra. A Listagem 19 mostra apenas o form para adicionar o Tenant.
<f:form modelAttribute="tenant" method="post" action="addRestaurant">
<fieldset>
<legend>Cadastre seu restaurante e seu cardápio agora mesmo!</legend>
<f:label path="name">Nome</f:label>
<f:input path="name" type="text" class="input-block-level"
placeholder="Ex: Dona Maria Pizzaria" autofocus="autofocus" />
<f:label path="slogan">Slogan</f:label>
<f:input path="slogan" type="text" class="input-block-level"
placeholder="Ex: A melhor pizza do Ipiranga" />
<f:label path="subdomain">Subdominio</f:label>
<f:input path="subdomain" type="text" class="input-block-level"
placeholder="Ex: donamaria" />
<f:label path="phone">Telefone</f:label>
<f:input path="phone" type="text" class="input-block-level"
placeholder="Ex: (11) 3115-2345" />
<f:label path="address">Address</f:label>
<f:input path="address" type="text" class="input-block-level"
placeholder="Ex: Av Dr Gentil de Moura, 850" />
<f:label path="city">Cidade</f:label>
<f:input path="city" type="text" class="input-block-level"
placeholder="Ex: São Paulo/SP" />
</fieldset>
<f:button class="btn btn-primary pull-right">Criar</f:button>
</f:form>
Executando o serviço
Após todos esses passos, o serviço é agora funcional. Para testá-lo, deve-se utilizar o comando mvn tomcat:run. Também é importante executar novamente a criação dos dados master, para que os dados do Tenant Master sejam cadastrados no banco de dados. Isso pode ser feito acessando a URL http://www.lvh.me:8080/icardapio/createMasterData.
Com os dados criados, será feito um redirecionamento para a página landing do serviço, onde será possível criar outros restaurantes usando qualquer subdomínio válido, e então cadastrar produtos diferentes para cada um dos restaurantes criados para ver o mecanismo de resolução de Tenants e particionamento dos dados em funcionamento.
Conclusão
Este artigo abordou a construção de um SaaS Multi Tenant a partir de uma aplicação web convencional, utilizando os frameworks e APIs mais recentes do Java. Também foi mostrado que a construção de um SaaS é um assunto extenso, e sua implementação dependente do escopo e características do serviço a ser construído.
Muitas pesquisas estão sendo feitas nessa área nos mais diferentes temas tanto no âmbito acadêmico quanto empresarial, como a customização e configuração de interfaces e de processos de negócio por Tenant, o controle dos recursos computacionais utilizados por cada Tenant (para impedir que um único Tenant utilize todos os recursos computacionais e prejudique o desempenho dos demais), na segurança das informações, na integração com aplicações instaladas on-premises, nas melhores opções para deploy dos serviços, entre tantos outros.
Também foi apresentado que os principais frameworks e APIs Java vêm adotando cada vez mais recursos nativos para suportar a construção de aplicações Multi Tenant, principalmente nas camadas de persistência. Desse modo, embora frameworks importantes como o Hibernate não tenham ainda um suporte completo, esses recursos estão em seus roadmaps, mostrando que Java é uma opção viável para construção desse tipo de sistema.
É importante ressaltar que a implementação aqui apresentada, embora longa, foi bastante simplificada para que pudesse atender aos requisitos de espaço de um artigo, e deve, portanto ser trabalhada para serviços que pretendam entrar em produção. No entanto, ela pode servir como base ou referência para essas implementações.
Por fim, vale ressaltar que este artigo utilizou como base documentações e trabalhos de outros autores na Internet. As principais referências utilizadas podem ser encontradas na seção Links.
LinksArtigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo