Sie brauchen überhaupt keine Trigger oder PL / pgSQL.
Sie haben nicht einmal müssen DEFERRABLE
Einschränkungen.
Und Sie müssen keine Informationen redundant speichern.
Fügen Sie die ID der aktiven E-Mail in die users
Tabelle ein, wodurch sich gegenseitige Verweise ergeben. Man könnte meinen, wir brauchen eine DEFERRABLE
Einschränkung, um das Henne-Ei-Problem des Einfügens eines Benutzers und seiner aktiven E-Mail zu lösen, aber mit datenmodifizierenden CTEs brauchen wir das nicht einmal.
Dies erzwingt zu jeder Zeit genau eine aktive E-Mail pro Benutzer :
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);
Entfernen Sie die NOT NULL
Einschränkung von users.email_id
, um es zu "höchstens einer aktiven E-Mail" zu machen. (Sie können immer noch mehrere E-Mails pro Benutzer speichern, aber keine davon ist "aktiv".)
Sie können machen active_email_fkey
DEFERRABLE
mehr Spielraum ermöglichen (insert Benutzer und E - Mail in separaten Befehlen der gleichen Transaktion), aber das ist nicht notwendig .
Ich habe user_id
die UNIQUE
Einschränkung email_fk_uni
zur Optimierung der Indexabdeckung an erster Stelle gestellt . Einzelheiten:
Optionale Ansicht:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
So fügen Sie neue Benutzer mit einer aktiven E-Mail ein (nach Bedarf):
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);
Die besondere Schwierigkeit besteht darin, dass wir weder haben user_id
noch email_id
anfangen. Beides sind die Seriennummern der jeweiligen SEQUENCE
. Es kann nicht mit einer einzigen RETURNING
Klausel gelöst werden (ein weiteres Henne-Ei-Problem). Die Lösung ist nextval()
wie in der verknüpften Antwort unten ausführlich erläutert .
Wenn Sie den Namen der angehängten Sequenz für die Spalte nicht kennen , können Sie Folgendes ersetzen:serial
email.email_id
nextval('email_email_id_seq'::regclass)
mit
nextval(pg_get_serial_sequence('email', 'email_id'))
So fügen Sie eine neue "aktive" E-Mail hinzu:
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-Geige.
Sie können die SQL-Befehle in serverseitigen Funktionen einkapseln, wenn ein einfacher ORM nicht klug genug ist, um damit umzugehen.
Eng verwandt mit ausführlichen Erklärungen:
Auch verwandt:
Über DEFERRABLE
Einschränkungen:
Über nextval()
und pg_get_serial_sequence()
:
ON DELETE CASCADE
? Einfach nur neugierig (Cascading funktioniert im Moment einwandfrei).Wenn Sie der Tabelle eine Spalte hinzufügen können, würde das folgende Schema fast 1 funktionieren:
Test SQLFiddle
Übersetzt von meinem nativen SQL Server mit Hilfe von a_horse_with_no_name
Wie ypercube in einem Kommentar erwähnt hat, können Sie sogar noch weiter gehen:
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)
Der Effekt ist der gleiche, aber wohl einfacher und ordentlicher.
1 Das Problem ist, dass die vorhandenen Einschränkungen nur sicherstellen, dass eine Zeile, die von einer anderen Zeile als "aktiv" bezeichnet wird, vorhanden ist , nicht, dass sie auch tatsächlich aktiv ist. Ich kenne Postgres nicht gut genug, um die zusätzliche Einschränkung selbst zu implementieren (zumindest momentan nicht), aber in SQL Server könnte dies folgendermaßen erfolgen:
Dieser Aufwand verbessert das Original ein wenig, indem ein Ersatz verwendet wird, anstatt die vollständige E-Mail-Adresse zu duplizieren.
quelle
Die einzige Möglichkeit, beide ohne Schemaänderungen auszuführen, ist ein PL / PgSQL-Trigger.
Für den "genau einen" Fall können Sie die Referenzen gegenseitig machen, mit einem Wesen
DEFERRABLE INITIALLY DEFERRED
. AlsoA.b_id
(FK) ReferenzenB.b_id
(PK) undB.a_id
(FK) ReferenzenA.a_id
(PK). Viele ORMs usw. können jedoch nicht mit aufschiebbaren Einschränkungen umgehen. In diesem Fall würden Sie also eine verschiebbare FK vom Benutzer zur Adresse in einer Spalte hinzufügenactive_address_id
, anstatt einactive
Flag an zu verwendenaddress
.quelle
DEFERRABLE
.