Vous n'avez pas du tout besoin de déclencheurs ou de PL / pgSQL.
Vous n'avez même pas besoin de DEFERRABLE
contraintes.
Et vous n'avez pas besoin de stocker des informations de manière redondante.
Incluez l'ID de l'e-mail actif dans le users
tableau, ce qui entraîne des références mutuelles. On pourrait penser que nous avons besoin d'une DEFERRABLE
contrainte pour résoudre le problème de l'insertion d'un utilisateur et de son e-mail actif, mais en utilisant des CTE modificateurs de données, nous n'en avons même pas besoin.
Cela applique à tout moment exactement un e-mail actif par utilisateur :
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);
Supprimez la NOT NULL
contrainte de users.email_id
pour en faire "au plus un e-mail actif". (Vous pouvez toujours stocker plusieurs e-mails par utilisateur, mais aucun n'est "actif".)
Vous pouvez faire active_email_fkey
DEFERRABLE
pour autoriser plus de latitude (insérer l'utilisateur et l'e-mail dans des commandes distinctes de la même transaction), mais ce n'est pas nécessaire .
Je mets d' user_id
abord dans la UNIQUE
contrainte email_fk_uni
d'optimiser la couverture de l'indice. Détails:
Vue optionnelle:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
Voici comment insérer de nouveaux utilisateurs avec un e-mail actif (si nécessaire):
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);
La difficulté spécifique est que nous n'avons ni user_id
ni email_id
pour commencer. Les deux sont des numéros de série fournis par le respectif SEQUENCE
. Il ne peut pas être résolu avec une seule RETURNING
clause (un autre problème de poulet et d'oeuf). La solution est nextval()
comme expliqué en détail dans la réponse liée ci-dessous .
Si vous ne connaissez pas le nom de la séquence jointe pour la serial
colonne, email.email_id
vous pouvez remplacer:
nextval('email_email_id_seq'::regclass)
avec
nextval(pg_get_serial_sequence('email', 'email_id'))
Voici comment ajouter un nouvel e-mail "actif":
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.
Vous pouvez encapsuler les commandes SQL dans des fonctions côté serveur si certains ORM simples ne sont pas assez intelligents pour y faire face.
Étroitement liés, avec de nombreuses explications:
Également lié:
A propos des DEFERRABLE
contraintes:
À propos nextval()
et pg_get_serial_sequence()
:
ON DELETE CASCADE
? Juste curieux (la cascade fonctionne bien pour l'instant).Si vous pouvez ajouter une colonne au tableau, le schéma suivant fonctionnerait presque 1 :
Test SQLFiddle
Traduit de mon serveur SQL natif, avec l'aide de a_horse_with_no_name
Comme ypercube l'a mentionné dans un commentaire, vous pouvez même aller plus loin:
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)
L'effet est le même, mais il est sans doute plus simple et plus net.
1 Le problème est que les contraintes existantes ne garantissent qu'une ligne appelée « active » par une autre ligne existe , non pas qu'il est réellement actif. Je ne connais pas assez bien Postgres pour implémenter la contrainte supplémentaire moi-même (du moins pas en ce moment), mais dans SQL Server, cela pourrait se faire de la manière suivante:
Cet effort améliore un peu l'original en utilisant un substitut plutôt que de dupliquer l'adresse e-mail complète.
la source
La seule façon de procéder sans modification de schéma est d'utiliser un déclencheur PL / PgSQL.
Pour le cas «exactement un», vous pouvez rendre les références mutuelles, l'une étant
DEFERRABLE INITIALLY DEFERRED
. DoncA.b_id
(FK) référenceB.b_id
(PK) etB.a_id
(FK) référenceA.a_id
(PK). De nombreux ORM, etc. ne peuvent cependant pas faire face à des contraintes reportables. Donc, dans ce cas, vous ajouteriez un FK différable de l'utilisateur à l'adresse sur une colonneactive_address_id
, au lieu d'utiliser unactive
indicateuraddress
.la source
DEFERRABLE
.