Multi-Tenant - 01 - PK simples ou composta
Estou com uma dúvida técnica para a minha aplicação multi-tentant, alguns autores dizem que não é interessante usar chave composta para as tabelas, ou seja, tenant_id, id (serial), dizem que isso pode atrapalhar o bom funcionamento de alguns ORMs que não conseguem trabalhar bem com chaves compostas. Outros já dizem que é interessante sim usar sempre o tenant_id na chave primária, ou seja, manter o uso da chave composta para as primary keys porque melhora o desempenho e índices. Sabendo que estarei usando o posgresql nessa situação, o que você me recomenda?
Vou direto ao ponto: no seu cenário (PostgreSQL + multi-tenant + sqlx, sem ORM pesado) eu recomendo:
PK simples (
idbigint/uuid) + colunatenant_idobrigatória, indexada e usada em TODAS as consultas e constraints.
Não precisa usartenant_idna primary key para ter boa performance nem boa segurança.
Depois a gente “afina” isso, mas a ideia central é essa.
1. O que está em jogo aqui de verdade?
Tem 3 preocupações diferentes que a galera costuma misturar:
-
Modelo de dados / segurança
- Garantir que nada “vaze” entre tenants.
- Garantir que as uniqueness sejam sempre por tenant.
-
Performance / índices
- Filtros por
tenant_id. - Joins entre tabelas multi-tenant.
- Filtros por
-
Ferramentas / ORMs
- Vários ORMs ainda são chatos com chave composta.
- Ferramentas legadas (Delphi, etc.) geralmente assumem PK simples.
A escolha PK composta x PK simples impacta mais o item 3 do que 1 e 2.
2. Duas abordagens lado a lado
A) PK composta: PRIMARY KEY (tenant_id, id)
CREATE TABLE contas_pagar (
tenant_id BIGINT NOT NULL,
id BIGINT NOT NULL,
fornecedor_id BIGINT NOT NULL,
numero_doc VARCHAR(50) NOT NULL,
valor NUMERIC(15,2) NOT NULL,
vencimento DATE NOT NULL,
-- ...
CONSTRAINT contas_pagar_pk PRIMARY KEY (tenant_id, id)
);
Vantagens:
-
Todos os FKs vão carregar
tenant_idjunto, então:FOREIGN KEY (tenant_id, fornecedor_id) REFERENCES fornecedores (tenant_id, id)Isso obriga a sempre relacionar registros do mesmo tenant.
-
Se você for particionar por
tenant_id(partitioned tables),
o Postgres exige que a chave de partição esteja nos índices únicos/PK →
aqui isso já está resolvido.
Desvantagens:
-
FKs em todas as tabelas ficam mais verbosos: sempre
(tenant_id, id). -
Vários ORMs não lidam bem com chaves compostas:
- Mapeamento mais chato.
- Migrations automáticas quebrando.
-
Ferramentas/clientes legados (Delphi, etc.) também sofrem mais.
Performance:
-
O índice da PK é B-Tree em
(tenant_id, id). -
Consulta típica:
SELECT * FROM contas_pagar WHERE tenant_id = 10 AND id = 123;usa a PK perfeitamente, super rápido.
-
Consultas por tenant inteiro:
WHERE tenant_id = 10;também são bem atendidas (prefixo da key).
B) PK simples: PRIMARY KEY (id) + tenant_id fora da PK
CREATE TABLE contas_pagar (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
fornecedor_id BIGINT NOT NULL,
numero_doc VARCHAR(50) NOT NULL,
valor NUMERIC(15,2) NOT NULL,
vencimento DATE NOT NULL,
-- Unicidade por tenant (exemplo: não repetir documento dentro do tenant)
CONSTRAINT contas_pagar_uq_tenant_doc
UNIQUE (tenant_id, numero_doc)
);
-- Índice para acesso padrão por tenant + id
CREATE INDEX contas_pagar_tenant_id_idx
ON contas_pagar (tenant_id, id);
Vantagens:
-
Mais amigável para ORMs, ferramentas, libs geradoras de CRUD.
-
FKs mais simples:
FOREIGN KEY (fornecedor_id) REFERENCES fornecedores (id); -
No código Go/sqlx é trivial: sempre
idcomo PK,tenant_idcomo filtro.
Desempenho:
-
Para as consultas típicas:
SELECT * FROM contas_pagar WHERE tenant_id = 10 ORDER BY id DESC LIMIT 50;se você tiver o índice
(tenant_id, id), o desempenho é equivalente,
na prática, ao da PK composta. -
Para pegar um registro específico:
SELECT * FROM contas_pagar WHERE tenant_id = 10 AND id = 123;→ também usa o índice
(tenant_id, id).
Diferença real de performance entre as duas abordagens, no mundo real, é mínima se seus índices estiverem bem pensados.
Segurança / isolamento:
-
Aqui você NÃO depende da PK para garantir isolamento.
-
Você garante via:
-
RLS (Row Level Security) por
tenant_id:ALTER TABLE contas_pagar ENABLE ROW LEVEL SECURITY; CREATE POLICY contas_pagar_tenant_policy ON contas_pagar USING (tenant_id = current_settingBIGINT; -
Todo acesso à API sempre seta
app.tenant_idna sessão.
-
-
Toda constraint de unicidade que faz sentido ser “por tenant” inclui
tenant_id:UNIQUE (tenant_id, codigo_interno)UNIQUE (tenant_id, cnpj)etc.
3. E o que o PostgreSQL “prefere”?
PostgreSQL em si não “gosta mais” de uma ou outra do ponto de vista de engine.
O que pega é:
-
se você for particionar por tenant_id, aí sim:
- PK/índices únicos têm que incluir o
tenant_id. - Nesse cenário, PK composta
(tenant_id, id)é quase obrigatório.
- PK/índices únicos têm que incluir o
-
se você não pretende particionar por tenant agora, ou só por data (por exemplo),
a abordagem com PK simples é bem tranquila.
4. Minha recomendação prática pro SEU contexto
Considerando seu cenário:
- PostgreSQL
- Multi-tenant shared schema
- Você usa Go + sqlx, sem ORM pesado
- Vai ter front em React/Next, possível consumo por outros clientes (Delphi etc.)
- Tendência a criar APIs multi-projeto/linguagem
Eu seguiria este padrão como “default”:
-
PK simples em todas as tabelas
id BIGSERIAL PRIMARY KEY -- ou UUIDv7 se quiser distribuído -
tenant_id BIGINT NOT NULLem todas as tabelas -
Índice padrão para multi-tenant:
CREATE INDEX nome_tabela_tenant_id_idx ON nome_tabela (tenant_id, id); -
Toda unicidade de negócio, sempre incluindo
tenant_idUNIQUE (tenant_id, codigo), UNIQUE (tenant_id, cnpj), UNIQUE (tenant_id, email) -
RLS por tenant_id em tabelas sensíveis
ALTER TABLE nome_tabela ENABLE ROW LEVEL SECURITY; CREATE POLICY nome_tabela_tenant_policy ON nome_tabela USING (tenant_id = current_settingBIGINT; -
Sempre filtrar por tenant_id na camada de aplicação, mesmo com RLS.
5. Quando eu adotaria PK composta, então?
Eu partiria para PK composta (tenant_id, id) se:
-
Você decidir particionar todas as tabelas críticas por tenant agora ou em breve,
e quiser alinhar o design desde o começo. -
Você tiver certeza que:
- Não vai depender de ORM que sofre com chaves compostas, ou
- Está disposto a lidar com a dor extra nos mapeamentos.
-
Você quer a “garantia física” de que nenhum FK esquece o tenant
(porque todos os FKs vão ter(tenant_id, id)).
Resumindo em uma frase
Se você quer equilíbrio entre desempenho, ergonomia de código e compatibilidade com ferramentas, use PK simples (
id) +tenant_idobrigatório, bem indexado e com RLS.
PK composta (tenant_id, id) eu reservaria para cenários em que a partição por tenant e a garantia rígida via FK são mais importantes do que a compatibilidade com ORMs/ferramentas.
Perfeito, vamos cristalizar isso num template padrão que você pode reutilizar em qualquer contexto (ERP, CRM, SESMT, etc.).
Vou assumir o modelo que combinamos:
- PK simples:
id BIGSERIAL(ouUUIDse preferir) - Multi-tenant:
tenant_id BIGINT NOT NULL - Índice padrão:
(tenant_id, id) - Unicidade sempre por tenant
- RLS por
tenant_id
Vou usar um exemplo genérico de entidade principal clientes e uma tabela de junção clientes_grupos.
1. Convenções gerais (recomendo usar em todas as tabelas)
-- Convenções recomendadas:
-- - Todas as tabelas: tenant_id BIGINT NOT NULL
-- - PK simples: id BIGSERIAL (ou UUID se preferir)
-- - Auditoria: created_at / updated_at
-- - Índice padrão: (tenant_id, id)
-- - Unicidade por tenant: UNIQUE (tenant_id, ...)
-- Função genérica para updated_at (opcional, usar em todas)
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS trigger AS $
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
2. Template: Entidade principal (ex: clientes)
-- =========================================================
-- Tabela principal: clientes
-- =========================================================
CREATE TABLE public.clientes (
id BIGSERIAL PRIMARY KEY, -- PK simples
tenant_idBIGINT NOT NULL, -- identifica o tenant
nome TEXTNOT NULL,
emailTEXTNULL,
documentoTEXTNULL, -- CPF/CNPJ, etc.
ativoBOOLEAN NOT NULL DEFAULT TRUE,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice padrão multi-tenant (sempre crie isso)
CREATE INDEX clientes_tenant_id_idx
ON public.clientes (tenant_id, id);
-- Exemplo de unicidade por tenant (e-mail único dentro do tenant)
CREATE UNIQUE INDEX clientes_uq_tenant_email
ON public.clientes (tenant_id, lower(email))
WHERE email IS NOT NULL;
-- Exemplo de unicidade por tenant para documento (CPF/CNPJ)
CREATE UNIQUE INDEX clientes_uq_tenant_documento
ON public.clientes (tenant_id, documento)
WHERE documento IS NOT NULL;
-- Trigger de updated_at (padrão para todas as tabelas)
CREATE TRIGGER clientes_set_updated_at
BEFORE UPDATE ON public.clientes
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
RLS por tenant_id na tabela principal
-- Habilita RLS
ALTER TABLE public.clientes ENABLE ROW LEVEL SECURITY;
-- Política padrão: o tenant só enxerga o que tem o seu tenant_id
CREATE POLICY clientes_tenant_policy ON public.clientes
USING (tenant_id = current_settingBIGINT;
Na aplicação (Go), para cada request você seta
app.tenant_idna sessão do Postgres, e pronto.
3. Template: Tabela de junção (ex: clientes_grupos)
Vamos supor que você tenha uma tabela de grupos:
CREATE TABLE public.grupos (
id BIGSERIAL PRIMARY KEY,
tenant_idBIGINT NOT NULL,
nome TEXTNOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX grupos_tenant_id_idx
ON public.grupos (tenant_id, id);
CREATE UNIQUE INDEX grupos_uq_tenant_nome
ON public.grupos (tenant_id, lower(nome));
CREATE TRIGGER grupos_set_updated_at
BEFORE UPDATE ON public.grupos
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
ALTER TABLE public.grupos ENABLE ROW LEVEL SECURITY;
CREATE POLICY grupos_tenant_policy ON public.grupos
USING (tenant_id = current_settingBIGINT;
Agora a tabela de junção clientes_grupos (cliente pertence a vários grupos):
-- =========================================================
-- Tabela de junção: clientes_grupos
-- =========================================================
CREATE TABLE public.clientes_grupos (
id BIGSERIAL PRIMARY KEY, -- PK simples
tenant_idBIGINT NOT NULL,
cliente_id BIGINT NOT NULL,
grupo_id BIGINT NOT NULL,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice padrão multi-tenant para a junção
CREATE INDEX clientes_grupos_tenant_id_idx
ON public.clientes_grupos (tenant_id, id);
-- Índices para facilitar filtros típicos
CREATE INDEX clientes_grupos_tenant_cliente_idx
ON public.clientes_grupos (tenant_id, cliente_id);
CREATE INDEX clientes_grupos_tenant_grupo_idx
ON public.clientes_grupos (tenant_id, grupo_id);
-- Garante que não exista o mesmo cliente no mesmo grupo duas vezes
CREATE UNIQUE INDEX clientes_grupos_uq_tenant_cliente_grupo
ON public.clientes_grupos (tenant_id, cliente_id, grupo_id);
-- Foreign keys (note: FK só referenciam o id; isolamento vem pelo tenant_id + RLS)
ALTER TABLE public.clientes_grupos
ADD CONSTRAINT clientes_grupos_cliente_fk
FOREIGN KEY (cliente_id)
REFERENCES public.clientes (id)
ON DELETE CASCADE;
ALTER TABLE public.clientes_grupos
ADD CONSTRAINT clientes_grupos_grupo_fk
FOREIGN KEY (grupo_id)
REFERENCES public.grupos (id)
ON DELETE CASCADE;
-- RLS na tabela de junção
ALTER TABLE public.clientes_grupos ENABLE ROW LEVEL SECURITY;
CREATE POLICY clientes_grupos_tenant_policy ON public.clientes_grupos
USING (tenant_id = current_settingBIGINT;
Repare:
- A PK é sempre
idsimples.- Todas as consultas sempre filtram por
tenant_id.- As uniqueness importantes são por
(tenant_id, ...).- RLS garante que um tenant nunca veja dados de outro, mesmo se alguém esquecer o filtro na query.
4. Variante rápida com UUID (se quiser tirar o BIGSERIAL)
Se você quiser usar UUID (idealmente uuidv7 quando o Postgres suportar nativo / extensão), o padrão fica:
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE public.clientes (
id UUIDPRIMARY KEY DEFAULT uuid_generate_v4(),
tenant_idBIGINT NOT NULL,
nome TEXTNOT NULL,
-- ...
);
CREATE INDEX clientes_tenant_id_idx
ON public.clientes (tenant_id, id);
O resto do template (RLS, índices, junção) fica idêntico, só mudando o tipo de id.
Show time 😄
Segue o snippet base multi-tenant prontinho pra você jogar no repo e só substituir placeholders.
Vou usar PLACEHOLDERS EM MAIÚSCULO pra você localizar fácil.
1. Função genérica de updated_at (usar uma vez só no schema)
-- ==================================================================
-- Função padrão para manter updated_at sempre atualizado
-- ==================================================================
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS trigger AS $
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
2. Template base: TABELA PRINCIPAL (ENTIDADE)
Copie, cole e troque os PLACEHOLDERS:
NOME_TABELACAMPO_UNICO1,CAMPO_UNICO2CAMPO_NEGOCIO_...
-- ==================================================================
-- TEMPLATE: TABELA PRINCIPAL MULTI-TENANT
-- ==================================================================
CREATE TABLE public.NOME_TABELA (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
-- Campos de negócio (exemplos, ajuste conforme sua necessidade)
CAMPO_NEGOCIO_1 TEXTNOT NULL,
CAMPO_NEGOCIO_2 TEXTNULL,
CAMPO_NEGOCIO_3 NUMERIC(15,2) NULL,
CAMPO_NEGOCIO_4 BOOLEAN NOT NULL DEFAULT TRUE,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice padrão multi-tenant
CREATE INDEX NOME_TABELA_tenant_id_idx
ON public.NOME_TABELA (tenant_id, id);
-- Unicidade por tenant (se precisar)
-- Exemplo: CAMPO_UNICO1 deve ser único dentro do tenant
CREATE UNIQUE INDEX NOME_TABELA_uq_tenant_UNICO1
ON public.NOME_TABELA (tenant_id, lower(CAMPO_UNICO1))
WHERE CAMPO_UNICO1 IS NOT NULL;
-- Outro exemplo de unicidade por tenant
-- CREATE UNIQUE INDEX NOME_TABELA_uq_tenant_UNICO2
-- ON public.NOME_TABELA (tenant_id, CAMPO_UNICO2)
-- WHERE CAMPO_UNICO2 IS NOT NULL;
-- Trigger de updated_at
CREATE TRIGGER NOME_TABELA_set_updated_at
BEFORE UPDATE ON public.NOME_TABELA
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
-- ==================================================================
-- RLS: um tenant só enxerga o que tem o seu tenant_id
-- ==================================================================
ALTER TABLE public.NOME_TABELA ENABLE ROW LEVEL SECURITY;
CREATE POLICY NOME_TABELA_tenant_policy ON public.NOME_TABELA
USING (tenant_id = current_settingBIGINT;
3. Template base: TABELA DE JUNÇÃO (N:N)
Use quando tiver N:N: por exemplo
usuarios_grupos,produtos_categorias, etc.
Placeholders principais:
NOME_TABELA_JUNCAOTABELA_A,TABELA_BCAMPO_A_ID,CAMPO_B_ID
-- ==================================================================
-- TEMPLATE: TABELA DE JUNÇÃO MULTI-TENANT (N:N)
-- ==================================================================
CREATE TABLE public.NOME_TABELA_JUNCAO (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
CAMPO_A_ID BIGINT NOT NULL, -- FK para TABELA_A.id
CAMPO_B_ID BIGINT NOT NULL, -- FK para TABELA_B.id
-- Campos adicionais de negócio (opcional)
-- ex: papel, prioridade, etc.
-- CAMPO_EXTRATEXTNULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice padrão multi-tenant
CREATE INDEX NOME_TABELA_JUNCAO_tenant_id_idx
ON public.NOME_TABELA_JUNCAO (tenant_id, id);
-- Índices para acesso rápido pelos lados A e B
CREATE INDEX NOME_TABELA_JUNCAO_tenant_A_idx
ON public.NOME_TABELA_JUNCAO (tenant_id, CAMPO_A_ID);
CREATE INDEX NOME_TABELA_JUNCAO_tenant_B_idx
ON public.NOME_TABELA_JUNCAO (tenant_id, CAMPO_B_ID);
-- Garante que não exista duplicidade de relação dentro do tenant
CREATE UNIQUE INDEX NOME_TABELA_JUNCAO_uq_tenant_A_B
ON public.NOME_TABELA_JUNCAO (tenant_id, CAMPO_A_ID, CAMPO_B_ID);
-- Foreign keys (isolamento final vem de RLS + tenant_id na query)
ALTER TABLE public.NOME_TABELA_JUNCAO
ADD CONSTRAINT NOME_TABELA_JUNCAO_A_fk
FOREIGN KEY (CAMPO_A_ID)
REFERENCES public.TABELA_A (id)
ON DELETE CASCADE;
ALTER TABLE public.NOME_TABELA_JUNCAO
ADD CONSTRAINT NOME_TABELA_JUNCAO_B_fk
FOREIGN KEY (CAMPO_B_ID)
REFERENCES public.TABELA_B (id)
ON DELETE CASCADE;
-- RLS para a tabela de junção
ALTER TABLE public.NOME_TABELA_JUNCAO ENABLE ROW LEVEL SECURITY;
CREATE POLICY NOME_TABELA_JUNCAO_tenant_policy
ON public.NOME_TABELA_JUNCAO
USING (tenant_id = current_settingBIGINT;
4. Mini-exemplo de uso (só pra ver os placeholders “resolvidos”)
Se amanhã você quiser criar clientes:
CREATE TABLE public.clientes (
id BIGSERIAL PRIMARY KEY,
tenant_idBIGINT NOT NULL,
nome TEXTNOT NULL,
emailTEXTNULL,
documentoTEXTNULL,
ativoBOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX clientes_tenant_id_idx
ON public.clientes (tenant_id, id);
CREATE UNIQUE INDEX clientes_uq_tenant_email
ON public.clientes (tenant_id, lower(email))
WHERE email IS NOT NULL;
CREATE TRIGGER clientes_set_updated_at
BEFORE UPDATE ON public.clientes
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
ALTER TABLE public.clientes ENABLE ROW LEVEL SECURITY;
CREATE POLICY clientes_tenant_policy ON public.clientes
USING (tenant_id = current_settingBIGINT;
Se quiser, no próximo passo posso montar um arquivo de migration padrão (ex: 0001_template_multitenant.sql) já comentado, pra virar “boilerplate oficial” do teu stack.
Perfeito, vamos transformar tudo isso em um arquivo de migration padrão, já comentado, para você usar como boilerplate nos projetos.
A ideia é:
- Você copia esse conteúdo para algo como
0001_template_multitenant.sql; - Quando for criar uma tabela nova, duplica esse arquivo, ajusta os nomes e remove o que não precisar.
-- =====================================================================
-- MIGRATION: TEMPLATE PADRÃO MULTI-TENANT
-- Descrição:
-- - Cria função genérica de updated_at
-- - Cria tabela principal de exemplo (NOME_TABELA)
-- - Cria tabela de junção de exemplo (NOME_TABELA_JUNCAO)
--
-- Observações:
-- - Substitua todos os PLACEHOLDERS EM MAIÚSCULO pelos nomes reais:
-- NOME_TABELA, NOME_TABELA_JUNCAO,
-- CAMPO_NEGOCIO_1/2/3/4, CAMPO_UNICO1,
-- TABELA_A, TABELA_B, CAMPO_A_ID, CAMPO_B_ID
-- - Remova o que não fizer sentido para a migration específica.
-- =====================================================================
-- =====================================================================
-- 1) Função genérica de updated_at (criar uma vez por schema)
--- Se já existir em outra migration, você pode remover este bloco.
-- =====================================================================
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS trigger AS $
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
-- =====================================================================
-- 2) TABELA PRINCIPAL: NOME_TABELA
--- Exemplo de entidade multi-tenant com PK simples e RLS
-- =====================================================================
CREATE TABLE public.NOME_TABELA (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
-- Campos de negócio (exemplos, ajuste para a entidade real)
CAMPO_NEGOCIO_1 TEXT NOT NULL,
CAMPO_NEGOCIO_2 TEXT NULL,
CAMPO_NEGOCIO_3 NUMERIC(15,2) NULL,
CAMPO_NEGOCIO_4 BOOLEAN NOT NULL DEFAULT TRUE,
-- Auditoria
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice padrão multi-tenant: usado em praticamente todas as consultas
CREATE INDEX NOME_TABELA_tenant_id_idx
ON public.NOME_TABELA (tenant_id, id);
-- Exemplo de unicidade "por tenant"
-- Ajuste/remova conforme a regra de negócio:
-- - CAMPO_UNICO1 deve ser único dentro de cada tenant
CREATE UNIQUE INDEX NOME_TABELA_uq_tenant_UNICO1
ON public.NOME_TABELA (tenant_id, lower(CAMPO_UNICO1))
WHERE CAMPO_UNICO1 IS NOT NULL;
-- Trigger para manter updated_at atualizado
CREATE TRIGGER NOME_TABELA_set_updated_at
BEFORE UPDATE ON public.NOME_TABELA
FOR EACH ROW
EXECUTE FUNCTION set_updated_at();
-- RLS: cada tenant só enxerga suas próprias linhas
ALTER TABLE public.NOME_TABELA ENABLE ROW LEVEL SECURITY;
CREATE POLICY NOME_TABELA_tenant_policy
ON public.NOME_TABELA
USING (tenant_id = current_settingBIGINT;
-- =====================================================================
-- 3) TABELA DE JUNÇÃO: NOME_TABELA_JUNCAO
--- Ex.: relacionamento N:N (usuarios_grupos, produtos_categorias etc.)
-- =====================================================================
CREATE TABLE public.NOME_TABELA_JUNCAO (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
CAMPO_A_ID BIGINT NOT NULL, -- FK para TABELA_A.id
CAMPO_B_ID BIGINT NOT NULL, -- FK para TABELA_B.id
-- Campos adicionais de negócio (opcional)
-- EXEMPLO: papel, prioridade, data de vínculo etc.
-- CAMPO_EXTRATEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Índice padrão multi-tenant
CREATE INDEX NOME_TABELA_JUNCAO_tenant_id_idx
ON public.NOME_TABELA_JUNCAO (tenant_id, id);
-- Índices para facilitar buscas pelos dois lados da relação
CREATE INDEX NOME_TABELA_JUNCAO_tenant_A_idx
ON public.NOME_TABELA_JUNCAO (tenant_id, CAMPO_A_ID);
CREATE INDEX NOME_TABELA_JUNCAO_tenant_B_idx
ON public.NOME_TABELA_JUNCAO (tenant_id, CAMPO_B_ID);
-- Garante que não exista duplicidade de relação dentro do tenant
CREATE UNIQUE INDEX NOME_TABELA_JUNCAO_uq_tenant_A_B
ON public.NOME_TABELA_JUNCAO (tenant_id, CAMPO_A_ID, CAMPO_B_ID);
-- Foreign keys (apontam para as tabelas principais)
-- Ajuste TABELA_A, TABELA_B e ON DELETE conforme regra de negócio
ALTER TABLE public.NOME_TABELA_JUNCAO
ADD CONSTRAINT NOME_TABELA_JUNCAO_A_fk
FOREIGN KEY (CAMPO_A_ID)
REFERENCES public.TABELA_A (id)
ON DELETE CASCADE;
ALTER TABLE public.NOME_TABELA_JUNCAO
ADD CONSTRAINT NOME_TABELA_JUNCAO_B_fk
FOREIGN KEY (CAMPO_B_ID)
REFERENCES public.TABELA_B (id)
ON DELETE CASCADE;
-- RLS para a tabela de junção
ALTER TABLE public.NOME_TABELA_JUNCAO ENABLE ROW LEVEL SECURITY;
CREATE POLICY NOME_TABELA_JUNCAO_tenant_policy
ON public.NOME_TABELA_JUNCAO
USING (tenant_id = current_settingBIGINT;
-- =====================================================================
-- FIM DO TEMPLATE
-- =====================================================================