
Multitenancy no Ruby On Rails
E ai programador(a), já conhece o conceito de Multitenancy?
Para entendermos esse conceito, vamos falar um pouco do modelo de uso de softwares pelas empresas. O mais comum até pouco tempo atrás (nos últimos anos) era as empresas comprarem uma cópia de um software (ou licença), instalarem em uma máquina local e utilizarem (mas isso veio progressivamente mudando).
Com a popularização do SaaS (Software as Service), os softwares em geral passaram a ficar na nuvem e as empresas passaram a pagar para utiliza-lo (ou seja, aluga-lo) se tornando inquilinos (ou em inglês tenants) da empresa que oferece o serviço.
As empresas que alugam esses softwares em geral necessitam que seus ambientes sejam separados e que a falha no software que um outro inquilino teve não afete o seu uso do produto. A arquitetura multitenancy (em tradução livre: múltiplos inquilinos) vem para nos ajudar a resolver essas necessidades e entregar software de qualidade.
Nesta arquitetura cada “tenant” (empresa que contratou o SaaS por exemplo) pode ter seus dados e configurações isolados dos outros inquilinos (apenas os membros daquele tenant vão poder ver e modificar os dados relacionados a ele), embora a aplicação que estará rodando seja apenas uma para todos os tenants.
Nesse Artigo você vai aprender a implementar o Multitenancy em um APP Ruby On Rails rapidamente, bora? 😁
Gem Apartment
Em aplicações Ruby on Rails, é possível utilizar uma gem chamada Apartment para facilitar a implementação dessa arquitetura. Ela é capaz de isolar dados baseados em uma conta ou empresa e permitir que outras informações estejam em um inquilino em comum.
O que vamos criar
Para que o conceito fique claro, vamos criar um projeto exemplo que exibirá informações sobre empresas. Os usuários autenticados poderão cadastrar sua empresa e adicionar uma lista de funcionários.
A gem Apartment nos ajudará a isolar os dados(funcionários) de cada empresa.
Ferramentas
- • Ruby 2.5
- • Rails 5.2
- • Twitter Bootstrap Rails
- • Jquery Rails
- • Devise
- • Devise Bootstrapped
- • Apartment
Caso não tenha o ruby, postgres ou rails instalados, prepare seu ambiente de desenvolvimento com este breve tutorial: Instalando o Rails e suas dependências no Linux
Criando nossa Aplicação de exemplo
Preparando o Projeto
Nesta primeira etapa você irá criar o projeto e adicionar as gems necessárias.
1- Crie o projeto rodando:
1 |
rails new about_company |
2- Adicione as seguintes gems ao Gemfile
1 2 3 4 |
gem "twitter-bootstrap-rails" gem "jquery-rails" gem 'devise' gem 'devise-bootstrapped' |
3- Instale as gems adicionadas executando
1 |
bundle install |
4- Para incluir os arquivos javascript das gems “Twitter Bootstrap Rails” e jQuery substitua o código de app/assets/javascripts/application.js por:
1 2 3 4 5 |
//= require jquery //= require twitter/bootstrap //= require rails-ujs //= require turbolinks //= require_tree . |
5- Rode o seguinte generate para incluir o bootstrap aos assets do projeto
1 |
rails generate bootstrap:install static |
Instalando o Devise
Agora você irá configurar a autenticação da aplicação com a gem Devise.
1- Rode o generator a seguir para criar o arquivo de configuração inicial do devise e o arquivo com os textos utilizados pelo i18n
1 |
rails g devise:install |
2- Crie o model e as views do devise
1 2 |
rails g devise User rails generate devise:views:bootstrapped |
3- Crie o banco de dados e execute a migration criada no passo anterior
1 2 |
rails db:create rails db:migrate |
4- Para exigir que o usuário se autentique antes de acessar alguma rota da aplicação, substitua o código de app/controllers/applications_controller.rb por:
1 2 3 |
class ApplicationController < ActionController::Base before_action :authenticate_user! end |
Criando a Company
Nessa etapa você disponibilizará as funcionalidades de criar uma nova empresa e listar todas as empresas.
1- Crie o model Company, ele possuirá os atributos nome, descrição e subdomínio.
1 |
rails g model Company name description:text subdomain |
O atributo subdomínio existe porque o usuário irá incluí-lo ao endereço para acessar a página de uma empresa.
2- Execute a migration criada
1 |
rails db:migrate |
3- Crie o controller Companies com as actions index e new
1 |
rails g controller Companies index new |
4- Substitua o conteúdo de app/controllers/companies_controller.rb por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class CompaniesController < ApplicationController def index @companies = Company.all end def new @company = Company.new end def create @company = Company.new(company_params) respond_to do |format| if @company.save format.html { redirect_to root_path, notice: 'Company criada com sucesso' } else format.html { render :new, error: 'Não foi possível criar a Company'} end end end private def company_params params.require(:company).permit(:name, :description, :subdomain) end end |
Aqui estamos criando um controller simples (parecido com um CRUD) que vai permitir a criação de uma nova company e a listagem de todas as companies.
5- Adicione as rotas do controller Companies substituindo o código do arquivo config/routes.rb por:
1 2 3 4 5 6 |
Rails.application.routes.draw do get 'companies/new', to: 'companies#new' post 'companies/create' devise_for :users root to: 'companies#index' end |
companies#index será a rota raiz da aplicação.
6- Substitua o conteúdo de app/views/companies/new.html.erb por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
<div class="page-header"> <h1>New Company</h1> </div> <div class="form row"> <div class="col-lg-10"> <%= form_with url: companies_create_path, local: true, :html => { :class => "form-horizontal thought" } do |form| %> <% if @company.errors.any? %> <div id="error_expl" class="panel panel-danger"> <div class="panel-heading"> <h3 class="panel-title"><%= pluralize(@thought.errors.count, "error") %> prohibited this thought from being saved:</h3> </div> <div class="panel-body"> <ul> <% @company.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> </div> <% end %> <div class="form-group"> <%= form.label :name, :class => 'control-label col-lg-2' %> <div class="col-lg-10"> <%= form.text_field :name, name: 'company[name]',:class => 'form-control' %> </div> <%= form.error_span(:name) %> </div> <div class="form-group"> <%= form.label :description, :class => 'control-label col-lg-2' %> <div class="col-lg-10"> <%= form.text_area :description, name: 'company[description]',:class => 'form-control' %> </div> <%= form.error_span(:description) %> </div> <div class="form-group"> <%= form.label :subdomain, :class => 'control-label col-lg-2' %> <div class="col-lg-10"> <%= form.text_field :subdomain, name: 'company[subdomain]',:class => 'form-control' %> </div> <%= form.error_span(:subdomain) %> </div> <div class="form-group"> <div class="col-lg-offset-2 col-lg-10"> <%= form.submit 'Save', :class => 'btn btn-primary' %> <%= link_to t('.cancel', :default => t("helpers.links.cancel")), root_path, :class => 'btn btn-default' %> </div> </div> <% end %> </div> </div> |
Você adicionou ao arquivo um formulário para a criação de uma nova company
7- Para listar todas as companies do projeto, substitua o código de app/views/companies/index.html.erb por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<div class="page-header"> <h1> Companies <%= link_to t('.new', :default => t("helpers.links.new")), companies_new_path, :class => 'btn btn-primary' %> </h1> </div> <div class="companies row"> <div class="col-lg-10 col-lg-offset-1"> <table class="table table-striped"> <thead> <tr> <th>Name</th> <th>Subdomain</th> </tr> </thead> <tbody> <% @companies.each do |company| %> <tr> <td><%= company.name %></td> <td><%= company.subdomain %></td> </tr> <% end %> </tbody> </table> </div> </div> |
Preparando a gem Apartment
Agora você irá instalar e configurar a gem Apartment
1- Adicione ao seu Gemfile
1 |
gem 'apartment' |
2- Instale a gem executando
1 |
bundle |
3- Crie o arquivo de configuração da gem apartment rodando
1 |
bundle exec rails generate apartment:install |
4. Realize configurações da gem apartment substituindo o código de config/inititializers/apartment.rb por:
1 2 3 4 5 6 7 8 |
require 'apartment/elevators/subdomain' Apartment.configure do |config| config.excluded_models = %w{ User Company } config.tenant_names = lambda { Company.pluck :subdomain } end Rails.application.config.middleware.use Apartment::Elevators::Subdomain |
Em excluded_models você informou quais models não são base de dados isoladas. Eles fazem parte de um ambiente compartilhado, guardando dados de todos usuários do sistema.
Em tenant_names você informou quais são os Inquilinos da aplicação. O apartment usa eles para rodar migrations para todos os inquilinos.
Também está configurado para que o middleware utilize configurações de subdomínio do Apartment. Desta forma, através da requisição a gem define qual inquilino será utilizado. Existe outras formas de fazer isso, para mais detalhes consulte a documentação da gem.
5- Continuando, adicione um callback ao model Company para que um Tenant seja criado toda vez que uma empresa for cadastrada. O nome do Tenant será o conteúdo do atributo subdomínio.
Substitua o código de app/models/company.rb por:
1 2 3 4 5 6 7 |
class Company < ApplicationRecord after_create :create_tenant def create_tenant Apartment::Tenant.create(subdomain) end end |
Suba a aplicação (rails server) e cadastre uma nova empresa
Ao salvar a empresa o callback criou um Tenant com o nome do subdomínio.
Isolando Dados
Agora você irá criar um modelo para salvar dados de forma isolada.
1- Crie o model de funcionários executando
1 |
rails g model Employee name job |
2- Rode a migração criada
1 |
rails db:migrate |
Perceba que a migração ocorreu no inquilino criado anteriormente. Se houvessem 10 inquilinos, a migração seria executada para todos.
3- Execute o generate para criar o controller Employees
1 |
rails g controller Employees |
4- Adicione ao controller app/controllers/employees_controller.rb as actions create e employee_params
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class EmployeesController < ApplicationController def create @employee = Employee.new(employee_params) respond_to do |format| if @employee.save format.html { redirect_to info_path, notice: 'Funcionário adicionado' } end end end private def employee_params params.require(:employee).permit(:name, :job) end end |
5- Substitua o código de config/routes.rb por:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class SubdomainConstraint def self.matches?(request) subdomains = %w{ www admin} request.subdomain.present? && subdomains.exclude?(request.subdomain) end end Rails.application.routes.draw do constraints SubdomainConstraint do get '/info', to: 'companies#show' post 'employee/create', to: 'employees#create' end get 'companies/new' post 'companies/create' devise_for :users root to: 'companies#index' end |
A constraint Subdomain verifica se o subdomínio existe e está da forma esperada. Somente quando o resultado é verdadeiro as rotas para companies#show e employees#create tornam-se disponíveis.
6- Adicione a action show ao controller Companies (app/controllers/companies_controller.rb).
1 2 3 4 5 |
def show @company = Company.find_by(subdomain: request.subdomain) @employees = Employee.all @empoyee = Employee.new end |
Como o model Employee faz parte da base de dados isolada de um Tenant, quando a consulta Employees.all é realizada, será retornado apenas employees do Tenant em questão.
7- Crie um arquivo chamado show.html.erb com o comando
1 |
touch app/views/companies/show.html.erb |
8- Adicione a ele o seguinte código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<div class="info row"> <div class="col-lg-10 col-lg-offset-1"> <div class="page-header"> <h1> <%= @company.name %></h1> </div> <p>This company is <%= @company.description %></p> <%= form_with url: employee_create_path, local: true, :html => { :class => "form-inline" } do |form| %> <div class="form-group"> <%= form.label :name, :class => 'control-label' %> <%= form.text_field :name, name: 'employee[name]',:class => 'form-control' %> </div> <div class="form-group"> <%= form.label :job, :class => 'control-label' %> <%= form.text_field :job, name: 'employee[job]',:class => 'form-control' %> </div> <div class="form-group"> <%= form.submit 'Add', :class => 'btn btn-primary' %> </div> <% end %> <% if @employees.size > 0 %> <h2> Employees</h2> <table class="table table-striped"> <thead> <tr> <th>Name</th> <th>Job</th> </tr> </thead> <tbody> <% @employees.each do |employee| %> <tr> <td><%= employee.name %></td> <td><%= employee.job %></td> </tr> <% end %> </tbody> </table> <% end %> </div> </div> |
Aqui é exibido o nome e descrição da empresa, um formulário para adicionar um novo funcionário e uma lista contendo todos funcionários da empresa.
9- Para compartilhar a sessão de um usuário entre subdomínios no devise, crie um arquivo de configuração rodando
1 |
touch config/initializers/session_store.rb |
10- Agora, adicione a ele o código:
1 2 3 4 |
Rails.application.config.session_store :cookie_store, key: '_project_management_session', domain: { development: '.lvh.me' }.fetch(Rails.env.to_sym, :all) |
Testando a implementação do Multitenancy
Vamos ver na prática como ficou o nosso projeto.
1- Para acessar subdomínios localmente utilize o endereço lvh.me ao em vez de localhost
2- Depois de realizar o login, crie uma nova empresa
3- Para acessar o Tenant da empresa, adicione o subdomínio a barra de endereço.
https://onebitcode.com/dating-advice-over-40/
4- Adicione alguns funcionários a esta empresa
chat dating site in usa for free without credit card required
5- Crie outra empresa, e acesse sua página de informações
https://onebitcode.com/tall-guys-dating-short-girl/
6- Mesmo fazendo uma consulta com Employee.all no controller show, não houve retorno de nenhum funcionário da primeira empresa.
Foi comprovado que os dados realmente estão isolados!!! 😁
Conclusão
Neste artigo você aprendeu em poucos minutos o que é e como utilizar a gem Apartment para criar aplicações SaaS com a arquitetura Multitenancy. A gem torna o processo muito simples para que você possa direcionar sua atenção às regras de negócio do projeto em questão.
Você conhecia o conceito de Multitenancy? Curtiu o artigo?
Deixe seu feedback pra gente 😀

Não perca nenhum conteúdo
Receba nosso resumo semanal com os novos posts, cursos, talks e vagas o/
Primeira vez no OneBitCode? Curtiu esse conteúdo?
O OneBitCode tem muito mais para você!
O OneBitCode traz conteúdos de qualidade, e em português, sobre programação com foco em Ruby on Rails e também JavaScript.
Além disso, aqui sempre levamos à você conteúdos valiosos sobre a carreira de programação, dicas sobre currículos, portfólios, perfil profissional, soft skills, enfim, tudo o que você precisa saber para continuar evoluindo como Programador(a)!
Fique por dentro de todos os conteúdos o/
Nossas redes sociais:
📹 • https://youtube.com/Onebitcode [Live todas as terças-feiras às 19h)
💻 • https://linkedin.com/company/onebitcode
🙂 • https://facebook.com/onebitcode
📱 • https://instagram.com/one_bit_code
🐦 • https://twitter.com/onebitcode
Nossos cursos:
🥇 • Programador Full Stack Javascript em 8 Semanas
💎 • Curso Completo de Ruby
⚙ • Minicurso: API Rails 5 Completo
🐞 • Minicurso de Testes para Ruby on Rails com RSpec
Espero que curta nossos conteúdos e sempre que precisar de ajuda, fala com a gente!
Estamos aqui para você 🙂
Bem-vindo à família OneBitCode o/
muito bom.
Se alguém procura uma alternativa usando o apartment sem subdomain, eu escrevi uma alternativa usando session:
https://medium.com/@tiagoandrgeraldi/rails-app-with-multi-tenancy-without-subdomains-25941fe876ec
Aí nao precisa tratar o subdomain do host
Se eu utilizar um Vue ou um React no front o conceito segue o mesmo ?
Segue sim Guilherme 🙂
Olá, tive um problema depois de criar um tenant,
o rails db:migrate passou a acusar erro:
migrate tenant_name tenant
rake aborted!
NoMethodError: undefined method `migrate’ for ActiveRecord::Migrator:Class
Tasks: TOP => apartment:migrate
(See full trace by running task with –trace)
(isso ocorreu especificamente no passo 2 do tópico “isolando Dados”)
O employee seria o user do devise? Não entendi como isolaria o login.
Nesse exemplo qualquer usuário cadastrado consegue acessar os subdominos de todas as companies?