PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Restrições de tabelas cruzadas no PostgreSQL

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 é era problemático (atualizado agora). 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 definir user_role.role_name = 'employee' , então deve haver um user.employee_nr correspondente primeiro.

  • Você ainda pode adicionar um employee_nr para qualquer user, e você ainda pode (então) marcar qualquer user_role com is_employee , independentemente do role_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 ser true ou false e é forçado a refletir a existência de um employee_nr pelo CHECK limitação. O gatilho mantém a coluna sincronizada automaticamente. Você pode permitir false adicionalmente para outros fins com apenas pequenas atualizações no design.

  • As regras para user_role.is_employee são ligeiramente diferentes:deve ser verdade se role_name = 'employee' . Aplicado por um CHECK restrição e definida automaticamente pelo gatilho novamente. Mas é permitido alterar role_name para outra coisa e ainda manter is_employee . Ninguém disse que um usuário com um employee_nr é obrigatório para ter uma entrada de acordo em user_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 com CHECK e restrições FK, que não permitem exceções.

  • A parte:coloquei a coluna is_employee primeiro na restrição UNIQUE (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