Como implementar multi-tenancy com isolamento por schema
1. Fundamentos do multi-tenancy por schema
Multi-tenancy é um padrão arquitetural onde uma única instância de software atende múltiplos clientes (tenants). O isolamento por schema é uma estratégia que oferece equilíbrio entre isolamento lógico e físico. Cada tenant possui seu próprio schema dentro do mesmo banco de dados, garantindo separação de dados sem os custos operacionais de bancos individuais.
Comparação com outras estratégias:
| Estratégia | Isolamento | Custo | Complexidade |
|---|---|---|---|
| Database por tenant | Total | Alto | Baixa |
| Schema por tenant | Alto | Médio | Médio |
| Shared schema (discriminador) | Baixo | Baixo | Alta |
Vantagens do schema por tenant:
- Isolamento completo de dados entre tenants
- Facilidade de backup/restore seletivo
- Possibilidade de customizações por schema
- Performance previsível (sem tabelas gigantescas)
- Migrações independentes por tenant
2. Arquitetura de banco de dados e design de schemas
A estrutura típica consiste em:
banco_multi_tenant/
├── public/ # Schema global (metadados, catálogo de tenants)
│ ├── tenants
│ ├── planos
│ └── logs_acesso
├── tenant_001/ # Schema do Tenant A
│ ├── usuarios
│ ├── pedidos
│ └── produtos
├── tenant_002/ # Schema do Tenant B
│ ├── usuarios
│ ├── pedidos
│ └── produtos
└── tenant_003/ # Schema do Tenant C
├── usuarios
├── pedidos
└── produtos
Geração automática de schemas via migrations:
-- Exemplo de migration para criar schema de tenant
CREATE SCHEMA IF NOT EXISTS tenant_{id} AUTHORIZATION app_user;
-- Criar tabelas dentro do schema do tenant
CREATE TABLE tenant_{id}.usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
criado_em TIMESTAMP DEFAULT NOW()
);
CREATE TABLE tenant_{id}.pedidos (
id SERIAL PRIMARY KEY,
usuario_id INTEGER REFERENCES tenant_{id}.usuarios(id),
valor_total DECIMAL(10,2),
status VARCHAR(20)
);
Gerenciamento de conexões:
-- Pool de conexões com schema dinâmico (PostgreSQL)
-- Configuração no application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/multi_tenant_db
spring.datasource.username=app_user
spring.datasource.password=secret
spring.jpa.properties.hibernate.multiTenancy=SCHEMA
spring.jpa.properties.hibernate.tenant_identifier_resolver=com.app.TenantResolver
3. Implementação da camada de roteamento e contexto do tenant
Identificação do tenant via header HTTP:
// Middleware para extrair tenant do header X-Tenant-ID
function tenantMiddleware(req, res, next) {
const tenantId = req.headers['x-tenant-id'];
if (!tenantId) {
return res.status(400).json({ error: 'Tenant ID é obrigatório' });
}
// Validar se tenant existe no schema público
const tenant = await db.query(
'SELECT id, schema_name FROM public.tenants WHERE id = $1 AND ativo = true',
[tenantId]
);
if (!tenant) {
return res.status(404).json({ error: 'Tenant não encontrado' });
}
// Armazenar no contexto da requisição
req.tenantId = tenant.id;
req.schemaName = tenant.schema_name;
next();
}
Propagação do contexto via thread-local (Java):
public class TenantContext {
private static final ThreadLocal<String> currentTenant = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
currentTenant.set(tenantId);
}
public static String getTenantId() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
4. Estratégias de consulta e manipulação de dados
Uso de SET search_path para queries dinâmicas:
-- Configurar o schema ativo para a sessão
SET search_path TO tenant_001, public;
-- Todas as queries subsequentes usarão o schema tenant_001
SELECT * FROM usuarios; -- Resolve para tenant_001.usuarios
SELECT * FROM tenants; -- Resolve para public.tenants
-- Exemplo em Node.js com pg-promise
async function queryWithTenant(tenantId, sql, params) {
const schemaName = `tenant_${tenantId}`;
await db.none(`SET search_path TO ${schemaName}, public`);
return db.any(sql, params);
}
Abstração via ORM com Hibernate:
// Configuração de multi-tenancy no Hibernate
@Configuration
public class HibernateConfig {
@Bean
public MultiTenantConnectionProvider multiTenantConnectionProvider() {
return new SchemaBasedMultiTenantConnectionProvider();
}
@Bean
public CurrentTenantIdentifierResolver tenantIdentifierResolver() {
return new TenantIdentifierResolver();
}
}
// Resolver que retorna o tenant do contexto
public class TenantIdentifierResolver
implements CurrentTenantIdentifierResolver {
@Override
public String resolveCurrentTenantIdentifier() {
return TenantContext.getTenantId();
}
}
Cache por tenant com Redis:
// Cache com chave composta: tenant_id + recurso
async function getCachedData(tenantId, key) {
const cacheKey = `tenant:${tenantId}:${key}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const data = await db.any(`SELECT * FROM tenant_${tenantId}.dados`);
await redis.setex(cacheKey, 3600, JSON.stringify(data));
return data;
}
5. Migrações e versionamento de esquemas
Estratégia centralizada com aplicação em massa:
-- Script para aplicar migration em todos os tenants
DO $$
DECLARE
tenant RECORD;
migration_sql TEXT;
BEGIN
-- Migration a ser aplicada
migration_sql := '
ALTER TABLE {schema}.usuarios
ADD COLUMN telefone VARCHAR(20);
';
FOR tenant IN
SELECT schema_name
FROM public.tenants
WHERE ativo = true
LOOP
-- Substituir placeholder pelo schema do tenant
EXECUTE REPLACE(migration_sql, '{schema}', tenant.schema_name);
-- Registrar migration aplicada
INSERT INTO public.migrations_log
(schema_name, migration_name, applied_at)
VALUES
(tenant.schema_name, 'v2_add_telefone', NOW());
END LOOP;
END $$;
Aplicação assíncrona com filas:
// Enfileirar migrações para execução paralela
async function applyMigrationAsync(migrationName, sqlTemplate) {
const tenants = await db.any('SELECT schema_name FROM public.tenants');
const jobs = tenants.map(tenant => ({
schema: tenant.schema_name,
sql: sqlTemplate.replace('{schema}', tenant.schema_name)
}));
// Enviar para fila de processamento
await queue.addBulk(jobs.map(job => ({
name: 'migration',
data: job
})));
}
// Worker que executa a migration
queue.process('migration', async (job) => {
const { schema, sql } = job.data;
try {
await db.none(`SET search_path TO ${schema}`);
await db.none(sql);
await db.none(`
INSERT INTO public.migrations_log
VALUES ($1, $2, NOW())
`, [schema, job.name]);
} catch (error) {
console.error(`Falha na migration para ${schema}:`, error);
throw error; // Re-tentar via fila
}
});
6. Segurança e isolamento entre tenants
Validação obrigatória de tenant em todas as queries:
// Interceptor que força o filtro por tenant
class TenantInterceptor {
beforeQuery(event) {
const tenantId = TenantContext.getTenantId();
// Adicionar WHERE tenant_id em toda query
if (event.query.toLowerCase().includes('from') &&
!event.query.toLowerCase().includes('tenant_id')) {
// Modificar query para incluir tenant_id
event.query = event.query.replace(
/FROM\s+(\w+)/gi,
`FROM $1 WHERE tenant_id = '${tenantId}'`
);
}
}
}
Roles e permissões por schema:
-- Criar role específica para cada tenant
CREATE ROLE tenant_001_user WITH LOGIN PASSWORD 'senha_segura';
GRANT USAGE ON SCHEMA tenant_001 TO tenant_001_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA tenant_001 TO tenant_001_user;
-- Revogar acesso ao schema público
REVOKE ALL ON SCHEMA public FROM tenant_001_user;
Monitoramento com identificação do tenant:
// Logger que inclui tenant ID
function logWithTenant(level, message, data = {}) {
const logEntry = {
timestamp: new Date().toISOString(),
level,
tenant_id: TenantContext.getTenantId(),
user_id: UserContext.getUserId(),
message,
...data
};
// Enviar para sistema de logging centralizado
logger.log(logEntry);
// Armazenar também no schema público para auditoria
db.none(`
INSERT INTO public.audit_logs
(tenant_id, user_id, action, details)
VALUES ($1, $2, $3, $4)
`, [logEntry.tenant_id, logEntry.user_id, message, JSON.stringify(data)]);
}
7. Operações avançadas e manutenção
Backup seletivo por schema:
#!/bin/bash
# Script para backup de schema específico
TENANT_ID=$1
SCHEMA_NAME="tenant_${TENANT_ID}"
BACKUP_FILE="backup_${SCHEMA_NAME}_$(date +%Y%m%d).sql"
pg_dump \
--host=localhost \
--port=5432 \
--username=app_user \
--dbname=multi_tenant_db \
--schema=${SCHEMA_NAME} \
--file=${BACKUP_FILE} \
--format=custom
echo "Backup do schema ${SCHEMA_NAME} concluído: ${BACKUP_FILE}"
Rebalanceamento de tenants entre databases:
-- Estratégia de sharding quando um tenant cresce demais
-- 1. Criar novo banco de dados
CREATE DATABASE shard_tenant_001;
-- 2. Migrar dados do schema para o novo banco
-- (usar pg_dump/pg_restore ou replicação lógica)
-- 3. Atualizar roteamento no schema público
UPDATE public.tenants
SET database_url = 'postgres://.../shard_tenant_001',
schema_name = 'public'
WHERE id = 'tenant_001';
Monitoramento de performance por tenant:
-- Query para identificar tenants com alto consumo
SELECT
t.id,
t.schema_name,
COUNT(*) as total_queries,
SUM(EXTRACT(EPOCH FROM (query_end - query_start))) as total_time
FROM public.tenants t
JOIN public.query_log q ON q.tenant_id = t.id
WHERE q.query_start > NOW() - INTERVAL '1 hour'
GROUP BY t.id, t.schema_name
ORDER BY total_time DESC
LIMIT 10;
Referências
-
PostgreSQL Documentation: Multi-Tenancy with Schemas — Documentação oficial do PostgreSQL sobre criação e gerenciamento de schemas para isolamento de dados.
-
Hibernate User Guide: Multi-Tenancy — Guia completo de implementação de multi-tenancy com Hibernate, incluindo estratégias por schema e banco.
-
Microsoft Docs: Multi-tenant SaaS Patterns — Padrões arquiteturais para aplicações SaaS multi-tenant, com exemplos práticos de isolamento.
-
AWS Multi-Tenancy Best Practices — Whitepaper da AWS sobre melhores práticas para arquiteturas multi-tenant em nuvem.
-
Ruby on Rails Guides: Multi-Tenancy with PostgreSQL Schemas — Tutorial prático de implementação de multi-tenancy por schema usando Rails e PostgreSQL.