Multi-Tenant com VRaptor

No primeiro sistema multi-tenant que desenvolvi cada requisição enviava junto o nome do tenant - o nome estava salvo no cliente.
Mas, isso polui muito o código…

Por isso, já havia pensado na solução melhor para um próximo sistema: Ao logar-se no sistema seria criada uma sessão para o usuário. Assim, todas requisições saberiam quem é o cliente que está fazendo…

Durante a leitura do material RR-79 da Caelum que contém explicações sobre REST e Restfulie. Em uma parte fala-se sobre a importância do sistema ser Stateless:
“Stateless systems that provide cacheable resources imply in lesser costs as bandwidth is saved, latency is
minimized, fault tolerancy is increased and processing costs is divided between all origins.”

Puts… ai vi que minha idéia não era tão boa… devia pensar em algo diferente.

Ai pensei no seguinte: tenant.example.com.br
Pegar o nome do tenant do subdominio.

Em algumas horas cheguei no seguinte resultado, usando VRaptor.

[code]@Component
public class Tenant
{

public Tenant(HttpServletRequest request)
{
	this.tenantName = resolveTenantName(request);
}

private String tenantName;

public String getTenantName()
{
	return this.tenantName;
}

private String resolveTenantName(HttpServletRequest request)
{
	String url = request.getRequestURL().toString();
	String withoutProtocol;
	
	try 
	{
		withoutProtocol = url.split("//")[1];
	} catch (ArrayIndexOutOfBoundsException e) 
	{
		withoutProtocol = url.split("//")[0];
	}
	
	return withoutProtocol.split("\\.")[0];
}

}[/code]

Contexto 1: Aplicações que utilizam um banco de dados por cliente:

[code]@Component
public class CriadorDeSessionFactory implements ComponentFactory
{

private Map<String, SessionFactory> conexoes = new HashMap<String, SessionFactory>();

public CriadorDeSessionFactory(Tenant tenant)
{
	this.tenant = tenant;
}

private final Tenant tenant;

private SessionFactory abrirSessionFactory()
{
	SessionFactory conexao;
	Configuration configuracao = new Configuration();
	
	String arquivoConfiguracao = new StringBuilder().append(tenant.getTenantName()).append(".cfg.xml").toString();
	configuracao.configure(arquivoConfiguracao); 

	configuracao.addAnnotatedClass(Venda.class);
	
	conexao = configuracao.buildSessionFactory();
	
	return conexao;
}

@Override
public SessionFactory getInstance() 
{
	SessionFactory conexao = conexoes.get(tenant);
	
	if( conexao == null )
	{
		conexao = abrirSessionFactory();
	}
	
	return conexao;
}  

}[/code]

[code]@Component
public class TenantHibernateDao
{

public TenantHibernateDao(Session session)
{
	this.session = session;
}

private final Session session;

public T save(T object)
{
	this.session.save(object);
	return object;
}

...other methods to interact with database...

}
[/code]
Contexto 2: O mesmo banco de dados para todos os clientes.

Não seria necessário sobrescrever o criador de session factory padrão do VRaptor. Uma vez que, só haveria um banco de dados.
A diferença está no TenantHibernateDao, no qual teria que injetar o Tenant e colocar um filtro para acessar somente os dados corretos.

[code]@Component
public class TenantHibernateDao
{

public TenantHibernateDao(Session session, Tenant tenant)
{
	this.session = session;
	this.tenant = tenant;
}

private final Session session;
private final Tenant tenant;	

public T save(T object)
{
	//usar o tenant.getTenantName() para incluir um filtro...

	this.session.save(object);
	return object;
}

...other methods to interact with database...

}[/code]

Exemplo de uso em Dao (fica o mesmo em ambos os contextos).

[code]@Component
public class VendaDao
{

public VendaDao(TenantHibernateDao<Venda> tenant)
{
	this.tenant = tenant;
}
	
private final TenantHibernateDao<Venda> tenant;

public Venda salvar(Venda venda)
{
	return tenant.save(venda);
}

}[/code]

É só injetar o DAO no Controller e seriam acessados apenas os dados corretos do Tenant.

Isso é só um protótipo. Vocês acham válida essa abordagem?

acho interessante a abordagem…

só alguns detalhes: CriadorDeSessionFactory deveria ser @ApplicationScoped e o Map é de <Tenant, SessionFactory>

depender do subdomínio para decidir qual é o tenant pode dificultar um pouco o desenvolvimento, pois vc não vai mais poder acessar o sistema a partir do localhost:8080 usual

essa discussão no tectura é bem legal para saber as vantagens e desvantagens dessas abordagens:
http://www.tectura.com.br/topics/abordagens_de_multitenant

Por que deve ser @ApplicationScoped?
@ApplicationScoped não quer dizer que será chamado apenas uma vez na aplicação?

Está correto. O Map deve ser de <Tenant, SessionFactory> e preciso implementar o equals e o hashcode no Tenant.
Não cheguei a testar essa parte em execução justamente por causa da dificuldade que é trabalhar com os subdominios (ao menos no inicio).
Essa prática de usar o subdominio é bem comum em aplicações Ruby on Rails. Pelo que eu vi é necessário configurar o servidor para redirecionar todos os requests de subdominio para a aplicação. - preciso ler mais sobre o assunto…

Essa discussão realmente é bem interessante mas não tem o mesmo enfoque. Pois, lá a discussão gira em torno da teoria:
uma instância por cliente x uma instância para todos os clientes. E aqui o enfoque é na implementação. Durante a discussão percebe-se que o maior receio é sobre a segurança dos dados (gerenciamento dos tenants). Inclusive é citado um plugin do Grails que faz esse gerenciamento.

Acredito que é possível obter um resultado 100% confiável com o uso do VRaptor. Continuarei meus testes depois… primeiro preciso aprender a usar o Restfulie rsrs

Acho legal a forma que o Google AppEngine, faz o multi-tenant.

Um objeto Key como chave primaria, onde nele tem o id(sequencial), o namespace(que seria o nome do tenant), e mais algumas informações.

Nessa aplicação, tenho um interceptor onde seto o Namespace atual, nesse caso o id do usuário, dessa forma não preciso adicionar uma condição nos filtros para filtrar somente o daquele usuário, isso fica de forma automática, porque no objeto Key, já tem o namespace.

http://code.google.com/intl/pt-BR/appengine/docs/java/multitenancy/multitenancy.html

Será que não daria para fazer algo com o hibernate?

Acredito que sim. Não sei se de forma genérica e automática… mas tem…

É necessária uma tabela que armazena todas as tenants. E toda tabela deve ter um ligação com essa.

O hibernate permite colocar um filter na session.
http://www.java2s.com/Code/Java/Hibernate/HibernateFilterDemo.htm

Então, é possível utilizar o ComponentFactory do Vraptor para que toda sessão criada automaticamente use esse filtro. Nesse componente é possível ter injetado o Tenant como mencionei acima para pegar da URL o tenant.

Deveria haver duas classes separadas uma que implementa o tenant com banco de dados compartilhado e outra que não. Pois, no caso que usa banco de dados compartilhado (como GAE). Você precisa saber o id desse tenant no banco e não o nome.

Então, deveria ter um map que armazena <Tenant, Long> (tenant, id).

No caso do GAE onde fica esse ID do usuário? na sessão?

[quote=RafaelViana]Por que deve ser @ApplicationScoped?
@ApplicationScoped não quer dizer que será chamado apenas uma vez na aplicação?
[/quote]

@ApplicationScoped é o mesmo que singleton, ou seja, só vai ter uma instância dessa classe em toda a aplicação,
mas o método getInstance() será chamado toda vez que alguém precisar de uma SessionFactory.
Precisa ser assim senão o mapa será criado toda requisição, então não adianta nada usá-lo.

Ok. Obrigado, entendido.

Isso mesmo @RafaelViana, o id vem da Sessão do Usuario.

Hmm, muito boa a sugestão. Isso poderia talvez virar um cookbook?

Há algum tempo eu fiz um sistema multi-tenant que usava o dominio para verificar qual o cliente, e era algo parecido com a tua implementação. A base de dados era a mesma para cada cliente, mas em catalog diferentes. Assim eu criei um query interceptor no JPA que transformava cada consulta a alguma entidade em tenant.entidade. Algo como

para

Isso. A ideia é fornecer um ponto de referencia. Do mesmo modo que há o vraptor-blank-project para iniciar novos projetos. Poderia existir um projeto base com o multi-tenant já implementado.

Estou criando um projeto aqui. Assim que tiver uma base coloco github.

Mudando de assunto…

garcia-jj ou Lucas Cavalcanti vocês que são os ‘caras’ do Vraptor rsrs achei um ‘probleminha’ em uma das implementações:

Ao utilizar a abordagem de bancos de dados separados. O CriadorDeSessionFactory precisa saber qual banco de dados criar, porem é @ApplicationScoped. Não tenho como injetar o Tenant que é @RequestScope (ele não pode ser @ApplicationScoped, pois precisa ter injetado o HttpSession se buscar o tenant da sessão ou HttpServletRequest se buscar o tenant da url).

E agora? Há como utilizar uma assinatura diferente para o método getInstance? (unica forma possivel que pensei…)

Meio off-topic… eu acompanho o Steve Ebersole no github e no blog dele, e tenho notado uma movimentação para suporte nativo no hibernate para multi-tenant. Se não me engano havia uma issue para suporte a partir da familia 4.x. Vai demorar um pouco, mas creio que vai ficar bem legal.

Haverá também um webminar da JBoss dia 16/fev se não me engano. Ele será apresentado pelo Steve, que vai apresentar uns exemplos com multi-tenant usando um banco por usuário, schema/catalog e com particionamento.

Rafael, creio que vocẽ deve é trabalhar com Session, e não com a SessionFactory (do hibernate). A tua Session é request-scoped, mas a SessionFactory (do hibernate) é application. Eu faria uma fabrica para a SessionFactory (do Hibernate) e uma para a Session que seria reques-scope.

[quote=garcia-jj]Meio off-topic… eu acompanho o Steve Ebersole no github e no blog dele, e tenho notado uma movimentação para suporte nativo no hibernate para multi-tenant. Se não me engano havia uma issue para suporte a partir da familia 4.x. Vai demorar um pouco, mas creio que vai ficar bem legal.

Haverá também um webminar da JBoss dia 16/fev se não me engano. Ele será apresentado pelo Steve, que vai apresentar uns exemplos com multi-tenant usando um banco por usuário, schema/catalog e com particionamento.

Rafael, creio que vocẽ deve é trabalhar com Session, e não com a SessionFactory (do hibernate). A tua Session é request-scoped, mas a SessionFactory (do hibernate) é application. Eu faria uma fabrica para a SessionFactory (do Hibernate) e uma para a Session que seria reques-scope.[/quote]

Bom saber… esse demorar um pouco seria meio ano? um ano?

A palestra dele dia 16 é 9:00 de San Francisco seria 3h da manhã no Brasil :frowning: ?

Achei o post que você deve ter visto: http://relation.to/Bloggers/MultitenancyInHibernate
Eu atualmente trabalho com bancos de dados separados e session factories separados, mas já percebi que são objetos caros (como ele comenta no post). Vi uma vez uma implementação do ConnetionProvider mas não achei muito local de pesquisa. Agora vi no blog dele… vou correr atrás desses termos para ver se consigo implementar. Ficaria legal… :slight_smile:

O e-mail que eu recebi dizia Wednesday, February 16, 2011 | 17:00 UTC, ou seja, 15h aqui no Brasil, tendo em relação o GMT-2 de Porto Alegre.

Você não pode criar dblink entre as bases? No meu atual projeto precisei fazer isso, então foram criados dblink para os bancos de cada tenant (filiais da empresa), e então eu criei um interceptor no JPa que intercepta cada query e altera para algo como meu_tenant.minha_tabela.

Jeito feio de fazer:

crie um atributo estático na classe CriadorDeSessionFactory que é um Map<Tenant, SessionFactory>, transforme-a em @RequestScoped
e selecione o tenant pelo request.

jeito mais elegante:

deixe o CriadorDeSessionFactory @RequestScoped, e crie um outro component @ApplicationScoped - SessionFactoryRepository por exemplo -
e o use para selecionar a SessionFactory correta

Perfeito! Ficou bem elegante essa implementação.

Agora surgiu outra dúvida. Tentei resolver aqui, mas não consegui. Qualquer controller precisa ter injetado um Dao. Ou TenantHibernateDao (que seria como um dao genérico para eliminar a necessidade de ter um dao para cada entidade) ou injetar o dao da sua entidade.

No entanto, qualquer um desses dao tem como dependencia a Session do Hibernate. Que tem como dependencia a SessionFactory. (a factory do request é criada antes de chamar algum método do resource). Desse modo, não será possível definir a tenant dentro de qualquer Controller que tenha como dependencia um DAO.

A solução seria criar um @Resource sem dependencia do Session responsável apenas por setar a Tenant. Isso deveria ser chamado antes do login (pois para logar necessito saber a tenant correta para procurar as informações no banco de dados).

Alguma solução mais elegante e eficaz?

garcia-jj

Realmente é às 15h diminui 6h em vez somar 6h nos cálculos rsrs Estou confirmado no webminar. O conteudo vai ser interessante.

dblink pelo que vi é do Oracle? Nunca trabalhei com Oracle nem havia ouvido falar sobre dblink. Irei dar uma olhada.

Dblink é do Oracle, e também tem no pgsql. O SQL Server se não me engano usa o nome Linked Servers. Isso tudo nada mais é do que você cria um link local para uma base remota, e essa base remota fica como se fosse um catalog.

[quote=RafaelViana]Perfeito! Ficou bem elegante essa implementação.

Agora surgiu outra dúvida. Tentei resolver aqui, mas não consegui. Qualquer controller precisa ter injetado um Dao. Ou TenantHibernateDao (que seria como um dao genérico para eliminar a necessidade de ter um dao para cada entidade) ou injetar o dao da sua entidade.

No entanto, qualquer um desses dao tem como dependencia a Session do Hibernate. Que tem como dependencia a SessionFactory. (a factory do request é criada antes de chamar algum método do resource). Desse modo, não será possível definir a tenant dentro de qualquer Controller que tenha como dependencia um DAO.

A solução seria criar um @Resource sem dependencia do Session responsável apenas por setar a Tenant. Isso deveria ser chamado antes do login (pois para logar necessito saber a tenant correta para procurar as informações no banco de dados).

Alguma solução mais elegante e eficaz?
[/quote]

Deixa eu ver se eu entendi:

Controller depende do Dao que depende da Session que depende da SessionFactory que depende do Tenant que depende do Request, certo?

vc precisa de um controller pra definir qual é o tenant? não dá pra fazer isso só com a url?

Lucas,

Vi no Groups do VRaptor que vocês implementaram um projeto usando Multi-Tenant + Vraptor ano passado:
http://groups.google.com/group/caelum-vraptor/browse_thread/thread/2feed90dc6d8c186/140396c3ca93f3f2?lnk=gst&q=tenant#140396c3ca93f3f2

http://groups.google.com/group/caelum-vraptor/browse_thread/thread/a559075924c94cd6/5a590fc78b4abae5?lnk=gst&q=tenant#5a590fc78b4abae5

Estou retomando esse projeto de Multi-Tenant + VRaptor (do inicio desse topico)… E continua a dúvida em como armazenar o tenan name. Vocês “armazenaram” o Tenant Name na URL da requisição?. Ex:
tenant1.projeto.com.br/clientes
tenant2.projeto.com.br/clientes
…?

Se sim, você poderia me passar mais informações como vocês faziam para testar esse ambiente na máquina local. Já que no ínicio desse tópico você falou que essa abordagem dificultava a programação?

Pelo que vi preciso fazer mudanças no DNS para redirecionar todos esses subdominios “fakes” para a aplicação principal… Você consegue me passar alguns links de referencia que vocês usaram? Ou algumas palavras-chaves para nortear minha pesquisa rsrs

Valeu!

Outra dúvida…

Quando estou usando multiplos subdomínios como fica o certificado SSL tenho que ter um SSL para cada subdominio… Como vocês contornaram isso?

bom, eu usei um proxy reverso do apache, mudando de:

http://.meudominio.com para http://127.0.0.1/minhaApp/

e a aplicação nem precisava saber que o domínio é variável.

assim fica fácil de testar local…

qto ao certificado ssl, dá pra comprar um certificado pra *.meudominio.com