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 SIMPLEmemungkinkan 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_nryang cocok pertama. -
Anda masih dapat menambahkan
employee_nrke apa saja pengguna, dan Anda masih dapat (kemudian) memberi tag padauser_roledenganis_employee, terlepas darirole_nameyang sebenarnya . Mudah untuk dilarang jika Anda perlu, tetapi implementasi ini tidak memperkenalkan batasan lebih dari yang diperlukan. -
users.is_employeehanya bisatrueataufalsedan dipaksa untuk mencerminkan keberadaanemployee_nrdenganCHECKpaksaan. Pemicu membuat kolom tetap sinkron secara otomatis. Anda dapat mengizinkanfalsetambahan untuk tujuan lain dengan hanya sedikit pembaruan pada desain. -
Aturan untuk
user_role.is_employeesedikit berbeda:harus benar jikarole_name = 'employee'. Ditegakkan olehCHECKkendala dan diatur secara otomatis oleh pemicu lagi. Tapi itu diperbolehkan untuk mengubahrole_nameke sesuatu yang lain dan tetap menyimpanis_employee. Tidak ada yang mengatakan pengguna denganemployee_nrwajib 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 denganCHECKdan batasan FK, yang tidak mengizinkan pengecualian. -
Selain:Saya meletakkan kolom
is_employeepertama dalam batasanUNIQUE (is_employee, users_id)karena suatu alasan .users_idsudah tercakup dalam PK, sehingga dapat menempati posisi kedua di sini:
Entitas asosiatif DB dan pengindeksan