Klarstellungen
Die Formulierung dieser Anforderung lässt Interpretationsspielraum:
wobei UserRole.role_name
enthält einen Mitarbeiterrollennamen.
Meine Interpretation:
mit einem Eintrag in UserRole
das hat role_name = 'employee'
.
Ihre Namenskonvention ist war problematisch (jetzt aktualisiert). User
ist ein reserviertes Wort in Standard-SQL und Postgres. Es ist als Bezeichner illegal, es sei denn, es wird in doppelte Anführungszeichen gesetzt - was nicht ratsam wäre. Rechtliche Benutzernamen, damit Sie keine doppelten Anführungszeichen setzen müssen.
Ich verwende in meiner Implementierung problemlose Identifikatoren.
Das Problem
FOREIGN KEY
und CHECK
Constraint sind die bewährten, luftdichten Werkzeuge, um relationale Integrität zu erzwingen. Auslöser sind leistungsstarke, nützliche und vielseitige Funktionen, aber raffinierter, weniger streng und mit mehr Spielraum für Designfehler und Sonderfälle.
Ihr Fall ist schwierig, weil eine FK-Einschränkung zunächst unmöglich erscheint:Sie erfordert einen PRIMARY KEY
oder UNIQUE
Einschränkung auf Referenz - keine erlaubt NULL-Werte. Es gibt keine partiellen FK-Einschränkungen, der einzige Ausweg aus der strikten referenziellen Integrität sind NULL-Werte in der Referenzierung Spalten aufgrund der Voreinstellung MATCH SIMPLE
Verhalten von FK-Einschränkungen. Per Dokumentation:
MATCH SIMPLE
lässt zu, dass jede der Fremdschlüsselspalten null ist; Wenn einer von ihnen null ist, muss die Zeile keine Übereinstimmung in der referenzierten Tabelle haben.
Verwandte Antwort auf dba.SE mit mehr:
- Zweispaltige Fremdschlüsselbeschränkung nur, wenn dritte Spalte NICHT NULL ist
Die Problemumgehung besteht darin, ein boolesches Flag is_employee
einzuführen um Mitarbeiter auf beiden Seiten zu markieren, definiert NOT NULL
in User
, darf aber NULL
sein in user_role
:
Lösung
Dadurch werden Ihre Anforderungen genau durchgesetzt , während Rauschen und Overhead auf ein Minimum reduziert werden:
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)
);
Das ist alles.
Diese Auslöser sind optional, aber der Einfachheit halber wird empfohlen, die hinzugefügten Tags is_employee
zu setzen automatisch und Sie müssen nichts tun zusätzlich:
-- 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();
Auch hier wieder sachlich, optimiert und nur bei Bedarf aufgerufen.
SQL-Geige Demo für Postgres 9.3. Sollte mit Postgres 9.1+ funktionieren.
Wichtige Punkte
-
Wenn wir nun
user_role.role_name = 'employee'
setzen wollen , dann muss es eine passendeuser.employee_nr
geben zuerst. -
Sie können immer noch eine
employee_nr
hinzufügen zu beliebigen user, und Sie können (dann) immer noch jedeuser_role
taggen mitis_employee
, unabhängig vom tatsächlichenrole_name
. Bei Bedarf einfach zu verbieten, aber diese Implementierung führt nicht mehr Einschränkungen als erforderlich ein. -
users.is_employee
kann nurtrue
sein oderfalse
und ist gezwungen, die Existenz eineremployee_nr
widerzuspiegeln durch denCHECK
Zwang. Der Trigger hält die Spalte automatisch synchron. Sie könntenfalse
zulassen zusätzlich für andere Zwecke mit nur geringfügigen Änderungen am Design. -
Die Regeln für
user_role.is_employee
unterscheiden sich geringfügig:Es muss wahr sein, wennrole_name = 'employee'
. Durch einenCHECK
erzwungen Constraint und automatisch durch den Trigger wieder gesetzt. Aber es ist erlaubt,role_name
zu ändern zu etwas anderem und behalte trotzdemis_employee
. Niemand sagte, ein Benutzer mit eineremployee_nr
ist erforderlich einen entsprechenden Eintrag inuser_role
haben , genau umgekehrt! Auch hier ist es einfach, bei Bedarf zusätzlich durchzusetzen. -
Wenn es andere Trigger gibt, die stören könnten, bedenken Sie Folgendes:
So vermeiden Sie Looping-Trigger-Aufrufe in PostgreSQL 9.2.1
Wir müssen uns jedoch keine Sorgen machen, dass Regeln verletzt werden könnten, da die obigen Trigger nur der Bequemlichkeit dienen. Die Regeln an sich werden mitCHECK
erzwungen und FK-Einschränkungen, die keine Ausnahmen zulassen. -
Beiseite:Ich habe die Spalte
is_employee
eingefügt zuerst in der EinschränkungUNIQUE (is_employee, users_id)
aus einem bestimmten Grund .users_id
ist bereits in der PK enthalten, kann also hier an zweiter Stelle stehen:
DB assoziative Entitäten und Indizierung