Anda tidak perlu pemicu atau PL / pgSQL sama sekali.
Anda bahkan tidak perlu DEFERRABLE
kendala.
Dan Anda tidak perlu menyimpan informasi apa pun secara berlebihan.
Sertakan ID email aktif dalam users
tabel, menghasilkan referensi bersama. Orang mungkin berpikir kita perlu DEFERRABLE
kendala untuk menyelesaikan masalah ayam-dan-telur dari memasukkan pengguna dan emailnya yang aktif, tetapi menggunakan CTE pengubah data kita bahkan tidak memerlukan itu.
Ini memberlakukan tepat satu email aktif per pengguna setiap saat:
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);
Hapus NOT NULL
batasan dari users.email_id
untuk membuatnya "paling banyak satu email aktif". (Anda masih dapat menyimpan beberapa email per pengguna, tetapi tidak ada satu pun yang "aktif".)
Anda dapat membuat active_email_fkey
DEFERRABLE
lebih banyak kelonggaran (masukkan pengguna dan email dalam perintah terpisah dari transaksi yang sama ), tetapi itu tidak perlu .
Aku meletakkan user_id
pertama di UNIQUE
kendala email_fk_uni
untuk cakupan indeks mengoptimalkan. Detail:
Tampilan opsional:
CREATE VIEW user_with_active_email AS
SELECT * FROM users JOIN email USING (user_id, email_id);
Begini cara Anda memasukkan pengguna baru dengan email aktif (sesuai kebutuhan):
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);
Kesulitan khusus adalah bahwa kita tidak memiliki user_id
atau tidak email_id
memulai. Keduanya adalah nomor seri yang disediakan dari masing-masing SEQUENCE
. Itu tidak bisa diselesaikan dengan satu RETURNING
klausa (masalah ayam dan telur lainnya). Solusinya adalah nextval()
sebagaimana dijelaskan secara terperinci dalam jawaban terkait di bawah .
Jika Anda tidak tahu nama urutan terlampir untuk serial
kolom email.email_id
Anda dapat mengganti:
nextval('email_email_id_seq'::regclass)
dengan
nextval(pg_get_serial_sequence('email', 'email_id'))
Berikut cara Anda menambahkan email "aktif" baru:
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.
Anda mungkin merangkum perintah SQL dalam fungsi sisi server jika beberapa ORM yang berpikiran sederhana tidak cukup pintar untuk mengatasinya.
Terkait erat, dengan banyak penjelasan:
Juga terkait:
Tentang DEFERRABLE
kendala:
Tentang nextval()
dan pg_get_serial_sequence()
:
ON DELETE CASCADE
? Hanya ingin tahu (cascading berfungsi dengan baik untuk saat ini).Jika Anda bisa menambahkan kolom ke tabel, skema berikut akan hampir 1 bekerja:
Test SQLFiddle
Diterjemahkan dari SQL Server asli saya, dengan bantuan dari a_horse_with_no_name
Seperti ypercube disebutkan dalam komentar, Anda bahkan bisa melangkah lebih jauh:
UNIQUE INDEX ON emails (UserID) WHERE (EmailAddress = ActiveAddress)
Efeknya sama, tetapi bisa dibilang lebih sederhana dan lebih rapi.
1 Masalahnya adalah bahwa kendala yang ada hanya memastikan bahwa baris yang disebut 'aktif' dengan baris lain ada , bukan bahwa itu juga sebenarnya aktif. Saya tidak tahu Postgres cukup baik untuk mengimplementasikan kendala ekstra sendiri (setidaknya tidak sekarang), tetapi dalam SQL Server, itu bisa dilakukan dengan demikian:
Upaya ini sedikit meningkatkan pada aslinya dengan menggunakan pengganti daripada menduplikasi alamat email lengkap.
sumber
Satu-satunya cara untuk melakukan ini tanpa perubahan skema adalah dengan pemicu PL / PgSQL.
Untuk kasus "tepat satu", Anda dapat membuat referensi saling menguntungkan, dengan satu wujud
DEFERRABLE INITIALLY DEFERRED
. JadiA.b_id
(FK) referensiB.b_id
(PK) danB.a_id
(FK) referensiA.a_id
(PK). Banyak ORMs dll tidak dapat mengatasi kendala yang sulit ditangguhkan. Jadi, dalam hal ini Anda akan menambahkan FK yang ditangguhkan dari pengguna ke alamat pada kolomactive_address_id
, alih - alih menggunakanactive
benderaaddress
.sumber
DEFERRABLE
.