PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Tabellenübergreifende Einschränkungen in PostgreSQL

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 passende user.employee_nr geben zuerst.

  • Sie können immer noch eine employee_nr hinzufügen zu beliebigen user, und Sie können (dann) immer noch jede user_role taggen mit is_employee , unabhängig vom tatsächlichen role_name . Bei Bedarf einfach zu verbieten, aber diese Implementierung führt nicht mehr Einschränkungen als erforderlich ein.

  • users.is_employee kann nur true sein oder false und ist gezwungen, die Existenz einer employee_nr widerzuspiegeln durch den CHECK Zwang. Der Trigger hält die Spalte automatisch synchron. Sie könnten false 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, wenn role_name = 'employee' . Durch einen CHECK erzwungen Constraint und automatisch durch den Trigger wieder gesetzt. Aber es ist erlaubt, role_name zu ändern zu etwas anderem und behalte trotzdem is_employee . Niemand sagte, ein Benutzer mit einer employee_nr ist erforderlich einen entsprechenden Eintrag in user_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 mit CHECK erzwungen und FK-Einschränkungen, die keine Ausnahmen zulassen.

  • Beiseite:Ich habe die Spalte is_employee eingefügt zuerst in der Einschränkung UNIQUE (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