Você não precisa de gatilhos ou PL / pgSQL.
Você nem precisa de DEFERRABLE
restrições.
E você não precisa armazenar nenhuma informação de forma redundante.
Inclua o ID do email ativo na users
tabela, resultando em referências mútuas. Pode-se pensar que precisamos de uma DEFERRABLE
restrição para resolver o problema do ovo e da galinha de inserir um usuário e seu email ativo, mas usando CTEs modificadores de dados, nem precisamos disso.
Isso aplica exatamente um e-mail ativo por usuário em todos os momentos:
CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
, email_id int NOT NULL -- FK to active email, constraint added below
);
CREATE TABLE email (
email_id serial PRIMARY KEY
, user_id int NOT NULL REFERENCES users ON DELETE CASCADE ON UPDATE CASCADE
, email text NOT NULL
, CONSTRAINT email_fk_uni UNIQUE(user_id, email_id) -- for FK constraint below
);
ALTER TABLE users ADD CONSTRAINT active_email_fkey
FOREIGN KEY (user_id, email_id) REFERENCES email(user_id, email_id);
Remova a NOT NULL
restrição de users.email_id
para torná-la "no máximo um email ativo". (Você ainda pode armazenar vários e-mails por usuário, mas nenhum deles está "ativo".)
Você pode fazer active_email_fkey
DEFERRABLE
para permitir mais liberdade (inserção de usuário e e-mail em comandos separados da mesma transação), mas isso é não é necessário .
Coloquei o user_id
primeiro na UNIQUE
restrição email_fk_uni
para otimizar a cobertura do índice. Detalhes:
Visualização opcional:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
Veja como você insere novos usuários com um email ativo (conforme necessário):
WITH new_data(username, email) AS (
VALUES
('usr1', '[email protected]') -- new users with *1* active email
, ('usr2', '[email protected]')
, ('usr3', '[email protected]')
)
, u AS (
INSERT INTO users(username, email_id)
SELECT n.username, nextval('email_email_id_seq'::regclass)
FROM new_data n
RETURNING *
)
INSERT INTO email(email_id, user_id, email)
SELECT u.email_id, u.user_id, n.email
FROM u
JOIN new_data n USING (username);
A dificuldade específica é que não temos nem user_id
nem email_id
para começar. Ambos são números de série fornecidos pelo respectivo SEQUENCE
. Não pode ser resolvido com uma única RETURNING
cláusula (outro problema de galinha e ovo). A solução é nextval()
como explicado em pormenor na resposta ligada abaixo .
Se você não souber o nome da sequência anexada para a serial
coluna, email.email_id
poderá substituir:
nextval('email_email_id_seq'::regclass)
com
nextval(pg_get_serial_sequence('email', 'email_id'))
Veja como você adiciona um novo email "ativo":
WITH e AS (
INSERT INTO email (user_id, email)
VALUES (3, '[email protected]')
RETURNING *
)
UPDATE users u
SET email_id = e.email_id
FROM e
WHERE u.user_id = e.user_id;
SQL Fiddle.
Você pode encapsular os comandos SQL nas funções do servidor, se algum ORM simplificado não for inteligente o suficiente para lidar com isso.
Intimamente relacionado, com ampla explicação:
Também relacionado:
Sobre DEFERRABLE
restrições:
Sobre nextval()
e pg_get_serial_sequence()
:
ON DELETE CASCADE
? Apenas curioso (em cascata está funcionando bem por enquanto).Se você pode adicionar uma coluna à tabela, o seguinte esquema funcionaria quase 1 :
Test SQLFiddle
Traduzido do meu SQL Server nativo, com a ajuda de a_horse_with_no_name
Como o ypercube mencionado em um comentário, você pode ir além:
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)
O efeito é o mesmo, mas é sem dúvida mais simples e mais puro.
1 O problema é que as restrições existentes apenas garantir que uma linha referida como 'ativo' por outra linha existe , não que isso também é realmente ativo. Eu não conheço o Postgres suficientemente bem para implementar a restrição extra (pelo menos não agora), mas no SQL Server, isso poderia ser feito da seguinte maneira:
Esse esforço melhora um pouco o original, usando um substituto em vez de duplicar o endereço de email completo.
fonte
A única maneira de fazer um desses sem alterações de esquema é com um gatilho PL / PgSQL.
Para o caso "exatamente um", você pode tornar as referências mútuas, com um ser
DEFERRABLE INITIALLY DEFERRED
. Portanto,A.b_id
referências (FK)B.b_id
(PK) eB.a_id
referênciasA.a_id
( FK) (PK). Muitos ORMs etc não podem lidar com restrições adiadas. Portanto, nesse caso, você adicionaria um FK adiado do usuário para endereçar em uma colunaactive_address_id
, em vez de usar umactive
sinalizadoraddress
.fonte
DEFERRABLE
.