W ogóle nie potrzebujesz wyzwalaczy ani PL / pgSQL.
Nie potrzebujesz nawet DEFERRABLE
ograniczeń.
I nie musisz przechowywać żadnych informacji nadmiarowo.
Dołącz identyfikator aktywnej wiadomości e-mail do users
tabeli, co spowoduje wzajemne odniesienia. Ktoś może pomyśleć, że potrzebujemy DEFERRABLE
ograniczenia, aby rozwiązać problem z kurczakiem i jajkami, polegający na wstawianiu użytkownika i jego aktywnego adresu e-mail, ale przy użyciu CTE modyfikujących dane nawet tego nie potrzebujemy.
Wymusza to zawsze dokładnie jeden aktywny e-mail na użytkownika :
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);
Usuń NOT NULL
ograniczenie z, users.email_id
aby „najwyżej jeden aktywny e-mail”. (Nadal możesz przechowywać wiele wiadomości e-mail na użytkownika, ale żaden z nich nie jest „aktywny”).
Państwo może zrobić active_email_fkey
DEFERRABLE
, aby umożliwić większą swobodę (insert użytkownika i adres e-mail w osobnych rozkazów samej transakcji), ale to nie jest konieczne .
Na user_id
pierwszym miejscu stawiam UNIQUE
ograniczenie email_fk_uni
optymalizacji indeksu. Detale:
Opcjonalny widok:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
Oto jak wstawiać nowych użytkowników za pomocą aktywnego adresu e-mail (zgodnie z wymaganiami):
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);
Szczególna trudność polega na tym, że nie mamy ani, user_id
ani email_id
na początek. Oba są numerami seryjnymi dostarczonymi z odpowiednich SEQUENCE
. Nie można tego rozwiązać za pomocą jednej RETURNING
klauzuli (kolejny problem z kurczakiem i jajkiem). Rozwiązanie to jest nextval()
jak opisano szczegółowo w połączonej odpowiedzi poniżej .
Jeśli nie znasz nazwy dołączonej sekwencji dla serial
kolumny, email.email_id
możesz ją zastąpić:
nextval('email_email_id_seq'::regclass)
z
nextval(pg_get_serial_sequence('email', 'email_id'))
Oto jak dodajesz nowy „aktywny” e-mail:
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.
Możesz zawrzeć polecenia SQL w funkcjach po stronie serwera, jeśli niektóre proste ORM nie są wystarczająco inteligentne, aby sobie z tym poradzić.
Ściśle powiązane, z dużym wyjaśnieniem:
Powiązane również:
O DEFERRABLE
ograniczeniach:
O nextval()
i pg_get_serial_sequence()
:
ON DELETE CASCADE
? Po prostu ciekawy (kaskadowanie na razie działa dobrze).Jeśli możesz dodać kolumnę do tabeli, poniższy schemat prawie działałby 1 :
Test SQLFiddle
Przetłumaczone z mojego macierzystego programu SQL Server, z pomocą a_horse_w_no_name
Jak wspomniano w komentarzu ypercube , możesz nawet pójść dalej:
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)
Efekt jest taki sam, ale jest prawdopodobnie prostszy i schludniejszy.
1 Problem polega na tym, że istniejące ograniczenia zapewniają tylko, że wiersz określany jako „aktywny” przez inny wiersz istnieje , a nie że jest on faktycznie aktywny. Nie znam Postgresa wystarczająco dobrze, aby samodzielnie wdrożyć dodatkowe ograniczenie (przynajmniej nie teraz), ale w SQL Server można to zrobić w następujący sposób:
Wysiłek ten poprawia się nieco w stosunku do oryginału poprzez użycie surogatu zamiast duplikowania pełnego adresu e-mail.
źródło
Jedynym sposobem na zrobienie jednego z nich bez zmian schematu jest użycie wyzwalacza PL / PgSQL.
W przypadku „dokładnie jednego” można uczynić odniesienia wzajemnymi, z jednym bytem
DEFERRABLE INITIALLY DEFERRED
. Tak więcA.b_id
(FK) odniesieniaB.b_id
(PK) iB.a_id
(FK) odniesieniaA.a_id
(PK). Wiele ORM itp. Nie jest jednak w stanie poradzić sobie z ograniczeniami, które można odłożyć. Więc w tym przypadku dodajesz odroczony FK od użytkownika do adresu w kolumnieactive_address_id
, zamiast używaćactive
flagi naaddress
.źródło
DEFERRABLE
.