Klarifikasi
Perumusan persyaratan ini menyisakan ruang untuk interpretasi:
di mana UserRole.role_name
berisi nama peran karyawan.
Interpretasi saya:
dengan entri di UserRole
yang memiliki role_name = 'employee'
.
Konvensi penamaan . Anda adalah bermasalah (diperbarui sekarang). User
adalah kata yang dicadangkan dalam SQL standar dan Postgres. Itu ilegal sebagai pengidentifikasi kecuali jika dikutip ganda - yang akan keliru. Nama resmi pengguna sehingga Anda tidak perlu mengutip dua kali.
Saya menggunakan pengenal bebas masalah dalam implementasi saya.
Masalahnya
FOREIGN KEY
dan CHECK
kendala adalah alat kedap udara yang terbukti untuk menegakkan integritas relasional. Pemicu adalah fitur yang kuat, berguna, dan serbaguna tetapi lebih canggih, tidak terlalu ketat, dan dengan lebih banyak ruang untuk kesalahan desain dan kasus sudut.
Kasus Anda sulit karena kendala FK tampaknya tidak mungkin pada awalnya:memerlukan PRIMARY KEY
atau UNIQUE
batasan untuk referensi - tidak ada yang mengizinkan nilai NULL. Tidak ada batasan FK parsial, satu-satunya jalan keluar dari integritas referensial yang ketat adalah nilai NULL di referensi kolom karena default MATCH SIMPLE
perilaku kendala FK. Per dokumentasi:
MATCH SIMPLE
memungkinkan salah satu kolom kunci asing menjadi nol; jika salah satunya adalah nol, baris tidak perlu memiliki kecocokan dalam tabel yang dirujuk.
Jawaban terkait di dba.SE dengan lebih banyak:
- Konstrain kunci asing dua kolom hanya jika kolom ketiga BUKAN NULL
Solusinya adalah dengan memperkenalkan flag boolean is_employee
untuk menandai karyawan di kedua sisi, didefinisikan NOT NULL
di users
, tetapi diperbolehkan menjadi NULL
di user_role
:
Solusi
Ini memberlakukan persyaratan Anda dengan tepat , sambil meminimalkan kebisingan dan overhead:
CREATE TABLE users (
users_id serial PRIMARY KEY
, employee_nr int
, is_employee bool NOT NULL DEFAULT false
, CONSTRAINT role_employee CHECK (employee_nr IS NOT NULL = is_employee)
, UNIQUE (is_employee, users_id) -- required for FK (otherwise redundant)
);
CREATE TABLE user_role (
user_role_id serial PRIMARY KEY
, users_id int NOT NULL REFERENCES users
, role_name text NOT NULL
, is_employee bool CHECK(is_employee)
, CONSTRAINT role_employee
CHECK (role_name <> 'employee' OR is_employee IS TRUE)
, CONSTRAINT role_employee_requires_employee_nr_fk
FOREIGN KEY (is_employee, users_id) REFERENCES users(is_employee, users_id)
);
Itu saja.
Pemicu . ini bersifat opsional tetapi disarankan untuk kemudahan menyetel tag tambahan is_employee
secara otomatis dan Anda tidak perlu melakukan apa pun tambahan:
-- users
CREATE OR REPLACE FUNCTION trg_users_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = (NEW.employee_nr IS NOT NULL);
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF employee_nr ON users
FOR EACH ROW
EXECUTE PROCEDURE trg_users_insup_bef();
-- user_role
CREATE OR REPLACE FUNCTION trg_user_role_insup_bef()
RETURNS trigger AS
$func$
BEGIN
NEW.is_employee = true;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER insup_bef
BEFORE INSERT OR UPDATE OF role_name ON user_role
FOR EACH ROW
WHEN (NEW.role_name = 'employee')
EXECUTE PROCEDURE trg_user_role_insup_bef();
Sekali lagi, tanpa basa-basi, dioptimalkan dan hanya dipanggil saat dibutuhkan.
Fiddle SQL demo untuk Postgres 9.3. Harus bekerja dengan Postgres 9.1+.
Poin utama
-
Sekarang, jika kita ingin mengatur
user_role.role_name = 'employee'
, maka harus adauser.employee_nr
yang cocok pertama. -
Anda masih dapat menambahkan
employee_nr
ke apa saja pengguna, dan Anda masih dapat (kemudian) memberi tag padauser_role
denganis_employee
, terlepas darirole_name
yang sebenarnya . Mudah untuk dilarang jika Anda perlu, tetapi implementasi ini tidak memperkenalkan batasan lebih dari yang diperlukan. -
users.is_employee
hanya bisatrue
ataufalse
dan dipaksa untuk mencerminkan keberadaanemployee_nr
denganCHECK
paksaan. Pemicu membuat kolom tetap sinkron secara otomatis. Anda dapat mengizinkanfalse
tambahan untuk tujuan lain dengan hanya sedikit pembaruan pada desain. -
Aturan untuk
user_role.is_employee
sedikit berbeda:harus benar jikarole_name = 'employee'
. Ditegakkan olehCHECK
kendala dan diatur secara otomatis oleh pemicu lagi. Tapi itu diperbolehkan untuk mengubahrole_name
ke sesuatu yang lain dan tetap menyimpanis_employee
. Tidak ada yang mengatakan pengguna denganemployee_nr
wajib untuk memiliki entri yang sesuai diuser_role
, justru sebaliknya! Sekali lagi, mudah diterapkan tambahan jika diperlukan. -
Jika ada pemicu lain yang dapat mengganggu, pertimbangkan ini:
Cara Menghindari Looping Trigger Calls Di PostgreSQL 9.2.1
Namun kita tidak perlu khawatir akan aturan yang mungkin dilanggar karena pemicu di atas hanya untuk kenyamanan. Aturan itu sendiri ditegakkan denganCHECK
dan batasan FK, yang tidak mengizinkan pengecualian. -
Selain:Saya meletakkan kolom
is_employee
pertama dalam batasanUNIQUE (is_employee, users_id)
karena suatu alasan .users_id
sudah tercakup dalam PK, sehingga dapat menempati posisi kedua di sini:
Entitas asosiatif DB dan pengindeksan