Esclarecimentos
A formulação deste requisito deixa espaço para interpretação:
onde
UserRole.role_name
contém um nome de função de funcionário. Minha interpretação:
com uma entrada em
UserRole
que tem role_name = 'employee'
. Sua convenção de nomenclatura
User
é uma palavra reservada no SQL padrão e no Postgres. É ilegal como identificador, a menos que entre aspas duplas - o que seria desaconselhável. Nomes legais de usuário para que você não precise usar aspas duplas. Estou usando identificadores sem problemas em minha implementação.
O problema
FOREIGN KEY
e CHECK
restrição são as ferramentas comprovadas e herméticas para impor a integridade relacional. Os gatilhos são recursos poderosos, úteis e versáteis, mas mais sofisticados, menos rígidos e com mais espaço para erros de projeto e casos de canto. Seu caso é difícil porque uma restrição FK parece impossível no início:ela requer uma
PRIMARY KEY
ou UNIQUE
restrição de referência - nenhuma permite valores NULL. Não há restrições FK parciais, a única saída da integridade referencial estrita são valores NULL no referenciamento colunas devido ao padrão MATCH SIMPLE
comportamento das restrições FK. Por documentação:
MATCH SIMPLE
permite que qualquer uma das colunas de chave estrangeira seja nula; se algum deles for nulo, a linha não precisará ter uma correspondência na tabela referenciada.
Resposta relacionada no dba.SE com mais:
- Restrição de chave estrangeira de duas colunas somente quando a terceira coluna NÃO é NULL
A solução é introduzir um sinalizador booleano
is_employee
para marcar funcionários em ambos os lados, definido NOT NULL
em users
, mas pode ser NULL
em user_role
:Solução
Isso aplica seus requisitos exatamente , mantendo o ruído e a sobrecarga ao mínimo:
CREATE TABLE users (
users_id serial PRIMARY KEY
, employee_nr int
, is_employee bool NOT NULL DEFAULT false
, CONSTRAINT role_employee CHECK (employee_nr IS NOT NULL = is_employee)
, UNIQUE (is_employee, users_id) -- required for FK (otherwise redundant)
);
CREATE TABLE user_role (
user_role_id serial PRIMARY KEY
, users_id int NOT NULL REFERENCES users
, role_name text NOT NULL
, is_employee bool CHECK(is_employee)
, CONSTRAINT role_employee
CHECK (role_name <> 'employee' OR is_employee IS TRUE)
, CONSTRAINT role_employee_requires_employee_nr_fk
FOREIGN KEY (is_employee, users_id) REFERENCES users(is_employee, users_id)
);
Isso é tudo.
Esses gatilhos são opcionais, mas recomendados por conveniência para definir as tags adicionadas
is_employee
automaticamente e você não precisa fazer nada extra:-- users
CREATE OR REPLACE FUNCTION trg_users_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE PROCEDURE trg_users_insup_bef();
-- user_role
CREATE OR REPLACE FUNCTION trg_user_role_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_role
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE PROCEDURE trg_user_role_insup_bef();
Novamente, sem sentido, otimizado e chamado apenas quando necessário.
Fiddle SQL demonstração do Postgres 9.3. Deve funcionar com o Postgres 9.1+.
Pontos principais
-
Agora, se quisermos definiruser_role.role_name = 'employee'
, então deve haver umuser.employee_nr
correspondente primeiro.
-
Você ainda pode adicionar umemployee_nr
para qualquer user, e você ainda pode (então) marcar qualqueruser_role
comis_employee
, independentemente dorole_name
real . Fácil de desabilitar, se necessário, mas essa implementação não apresenta mais restrições do que o necessário.
-
users.is_employee
só pode sertrue
oufalse
e é forçado a refletir a existência de umemployee_nr
peloCHECK
limitação. O gatilho mantém a coluna sincronizada automaticamente. Você pode permitirfalse
adicionalmente para outros fins com apenas pequenas atualizações no design.
-
As regras parauser_role.is_employee
são ligeiramente diferentes:deve ser verdade serole_name = 'employee'
. Aplicado por umCHECK
restrição e definida automaticamente pelo gatilho novamente. Mas é permitido alterarrole_name
para outra coisa e ainda manteris_employee
. Ninguém disse que um usuário com umemployee_nr
é obrigatório para ter uma entrada de acordo emuser_role
, apenas o contrário! Novamente, fácil de aplicar adicionalmente, se necessário.
-
Se houver outros gatilhos que possam interferir, considere isto:
Como evitar chamadas de gatilho em loop no PostgreSQL 9.2.1
Mas não precisamos nos preocupar que as regras possam ser violadas porque os gatilhos acima são apenas por conveniência. As regras em si são aplicadas comCHECK
e restrições FK, que não permitem exceções.
-
A parte:coloquei a colunais_employee
primeiro na restriçãoUNIQUE (is_employee, users_id)
por um motivo .users_id
já está coberto no PK, então pode ficar em segundo lugar aqui:
Entidades associativas de banco de dados e indexação