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

Das „O“ in ORDBMS:PostgreSQL-Vererbung

In diesem Blogeintrag gehen wir auf die PostgreSQL-Vererbung ein, traditionell eines der Top-Features von PostgreSQL seit den frühen Versionen. Einige typische Anwendungen der Vererbung in PostgreSQL sind:

  • Tabellenpartitionierung
  • Mandantenfähigkeit

PostgreSQL hat bis Version 10 die Tabellenpartitionierung mit Vererbung implementiert. PostgreSQL 10 bietet eine neue Art der deklarativen Partitionierung. Die PostgreSQL-Partitionierung mittels Vererbung ist eine ziemlich ausgereifte Technologie, gut dokumentiert und getestet, jedoch ist die Vererbung in PostgreSQL aus Sicht des Datenmodells (meiner Meinung nach) nicht so weit verbreitet, daher konzentrieren wir uns in diesem Blog auf klassischere Anwendungsfälle. Wir haben im vorherigen Blog (Mandantenfähigkeitsoptionen für PostgreSQL) gesehen, dass eine der Methoden zum Erreichen von Mandantenfähigkeit darin besteht, separate Tabellen zu verwenden und sie dann über eine Ansicht zu konsolidieren. Wir haben auch die Nachteile dieses Designs gesehen. In diesem Blog werden wir dieses Design durch Vererbung verbessern.

Einführung in die Vererbung

Wenn wir auf die Multi-Tenancy-Methode zurückblicken, die mit separaten Tabellen und Ansichten implementiert wurde, erinnern wir uns, dass ihr Hauptnachteil darin besteht, dass Einfügungen/Aktualisierungen/Löschungen nicht möglich sind. In dem Moment, in dem wir versuchen, die Vermietung zu aktualisieren Ansicht erhalten wir diesen FEHLER:

ERROR:  cannot insert into view "rental"
DETAIL:  Views containing UNION, INTERSECT, or EXCEPT are not automatically updatable.
HINT:  To enable inserting into the view, provide an INSTEAD OF INSERT trigger or an unconditional ON INSERT DO INSTEAD rule.

Wir müssten also einen Auslöser oder eine Regel für die Vermietung erstellen Ansicht, die eine Funktion zum Behandeln des Einfügens/Aktualisierens/Löschens angibt. Die Alternative ist die Verwendung der Vererbung. Lassen Sie uns das Schema des vorherigen Blogs ändern:

template1=# create database rentaldb_hier;
template1=# \c rentaldb_hier
rentaldb_hier=# create schema boats;
rentaldb_hier=# create schema cars;

Lassen Sie uns nun die übergeordnete Haupttabelle erstellen:

rentaldb_hier=# CREATE TABLE rental (
    id integer NOT NULL,
    customerid integer NOT NULL,
    vehicleno text,
    datestart date NOT NULL,
    dateend date
); 

In OO-Begriffen entspricht diese Tabelle der Oberklasse (in der Java-Terminologie). Lassen Sie uns nun die untergeordneten Tabellen durch Erben definieren aus public.rental und das Hinzufügen einer Spalte für jede Tabelle, die für die Domäne spezifisch ist:z. die obligatorische (Kunden-)Führerscheinnummer bei Autos und das optionale Bootssegelzertifikat.

rentaldb_hier=# create table cars.rental(driv_lic_no text NOT NULL) INHERITS (public.rental);
rentaldb_hier=# create table boats.rental(sail_cert_no text) INHERITS (public.rental);

Die beiden Tabellen cars.rental und boote.vermietung erben alle Spalten von ihrer übergeordneten public.rental :
 

rentaldb_hier=# \d cars.rental
                           Table "cars.rental"
     Column     |         Type          | Collation | Nullable | Default
----------------+-----------------------+-----------+----------+---------
 id             | integer               |           | not null |
 customerid     | integer               |           | not null |
 vehicleno      | text                  |           |          |
 datestart      | date                  |           | not null |
 dateend        | date                  |           |          |
 driv_lic_no | text                  |           | not null |
Inherits: rental
rentaldb_hier=# \d boats.rental
                         Table "boats.rental"
    Column    |         Type          | Collation | Nullable | Default
--------------+-----------------------+-----------+----------+---------
 id           | integer               |           | not null |
 customerid   | integer               |           | not null |
 vehicleno    | text                  |           |          |
 datestart    | date                  |           | not null |
 dateend      | date                  |           |          |
 sail_cert_no | text                  |           |          |
Inherits: rental

Wir bemerken, dass wir die Firma weggelassen haben Spalte in der Definition der übergeordneten Tabelle (und folglich auch in den untergeordneten Tabellen). Dies wird nicht mehr benötigt, da die Identifikation des Mieters im vollständigen Namen der Tabelle steht! Wir werden später einen einfachen Weg sehen, dies in Abfragen herauszufinden. Lassen Sie uns nun einige Zeilen in die drei Tabellen einfügen (wir leihen uns Kunden Schema und Daten aus dem vorherigen Blog):

rentaldb_hier=# insert into rental (id, customerid, vehicleno, datestart) VALUES(1,1,'SOME ABSTRACT PLATE NO',current_date);
rentaldb_hier=# insert into cars.rental (id, customerid, vehicleno, datestart,driv_lic_no) VALUES(2,1,'INI 8888',current_date,'gr690131');
rentaldb_hier=# insert into boats.rental (id, customerid, vehicleno, datestart) VALUES(3,2,'INI 9999',current_date);

Sehen wir uns nun an, was in den Tabellen steht:

rentaldb_hier=# select * from rental ;
 id | customerid |       vehicleno        | datestart  | dateend
----+------------+------------------------+------------+---------
  1 |          1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
  2 |          1 | INI 8888               | 2018-08-31 |
  3 |          2 | INI 9999               | 2018-08-31 |
(3 rows)
rentaldb_hier=# select * from boats.rental ;
 id | customerid | vehicleno | datestart  | dateend | sail_cert_no
----+------------+-----------+------------+---------+--------------
  3 |          2 | INI 9999  | 2018-08-31 |         |
(1 row)
rentaldb_hier=# select * from cars.rental ;
 id | customerid | vehicleno | datestart  | dateend | driv_lic_no
----+------------+-----------+------------+---------+-------------
  2 |          1 | INI 8888  | 2018-08-31 |         | gr690131
(1 row)

Die gleichen Vererbungskonzepte, die in objektorientierten Sprachen (wie Java) existieren, gibt es also auch in PostgreSQL! Wir können uns das wie folgt vorstellen:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instance of public.rental
row cars.rental.id =2:Instanz von cars.rental und public.rental
row boots.rental.id =3:Instanz von boots.rental und public.rental

Da die Reihen boots.vermietung und autos.vermietung ebenfalls Instanzen öffentlicher.vermietung sind, erscheinen sie natürlich als Reihen öffentlicher.vermietung. Wenn wir nur Zeilen ohne public.rental wollen (mit anderen Worten, die direkt in public.rental eingefügten Zeilen), verwenden wir das Schlüsselwort ONLY wie folgt:

rentaldb_hier=# select * from ONLY rental ;
 id | customerid |       vehicleno        | datestart  | dateend
----+------------+------------------------+------------+---------
  1 |          1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
(1 row)

Ein Unterschied zwischen Java und PostgreSQL in Bezug auf die Vererbung ist folgender:Java unterstützt keine Mehrfachvererbung, während PostgreSQL dies tut, es ist möglich, von mehr als einer Tabelle zu erben, also können wir uns in dieser Hinsicht Tabellen eher wie Schnittstellen vorstellen in Java.

Wenn wir die genaue Tabelle in der Hierarchie herausfinden möchten, zu der eine bestimmte Zeile gehört (das Äquivalent von obj.getClass().getName() in Java), können wir dies tun, indem wir die Tableoid-Sonderspalte (oid der jeweiligen Tabelle in pgclass ), in Regclass gecastet, was den vollständigen Tabellennamen ergibt:

rentaldb_hier=# select tableoid::regclass,* from rental ;
   tableoid   | id | customerid |       vehicleno        | datestart  | dateend
--------------+----+------------+------------------------+------------+---------
 rental       |  1 |          1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
 cars.rental  |  2 |          1 | INI 8888               | 2018-08-31 |
 boats.rental |  3 |          2 | INI 9999               | 2018-08-31 |
(3 rows)

Aus dem obigen (anderen Tableoid) können wir schließen, dass die Tabellen in der Hierarchie nur einfache alte PostgreSQL-Tabellen sind, die mit einer Vererbungsbeziehung verbunden sind. Aber abgesehen davon verhalten sie sich ziemlich genau wie normale Tische. Und dies wird im folgenden Abschnitt weiter betont.

Wichtige Fakten und Vorbehalte zur PostgreSQL-Vererbung

Die untergeordnete Tabelle erbt:

  • NOT NULL-Einschränkungen
  • CHECK-Einschränkungen

Die untergeordnete Tabelle erbt NICHT:

  • PRIMARY KEY-Einschränkungen
  • EINZIGARTIGE Beschränkungen
  • FREMDSCHLÜSSEL-Einschränkungen

Wenn Spalten mit demselben Namen in der Definition von mehr als einer Tabelle in der Hierarchie erscheinen, müssen diese Spalten denselben Typ haben und werden zu einer einzigen Spalte zusammengeführt. Wenn für einen Spaltennamen irgendwo in der Hierarchie eine NOT NULL-Einschränkung vorhanden ist, wird diese an die untergeordnete Tabelle vererbt. CHECK-Einschränkungen mit demselben Namen werden ebenfalls zusammengeführt und müssen dieselbe Bedingung haben.

Schemaänderungen an der übergeordneten Tabelle (über ALTER TABLE) werden in der gesamten Hierarchie weitergegeben, die unterhalb dieser übergeordneten Tabelle vorhanden ist. Und das ist eines der netten Merkmale der Vererbung in PostgreSQL.

Sicherheit und Sicherheitsrichtlinien (RLS) werden basierend auf der von uns verwendeten Tabelle festgelegt. Wenn wir eine übergeordnete Tabelle verwenden, werden die Sicherheit und das RLS dieser Tabelle verwendet. Es wird impliziert, dass das Gewähren eines Privilegs für die übergeordnete Tabelle auch die Berechtigung für die untergeordneten Tabellen erteilt, jedoch nur, wenn auf sie über die übergeordnete Tabelle zugegriffen wird. Um direkt auf die untergeordnete Tabelle zuzugreifen, müssen wir explizit GRANT direkt an die untergeordnete Tabelle geben, das Privileg für die übergeordnete Tabelle reicht nicht aus. Dasselbe gilt für RLS.

In Bezug auf das Auslösen von Triggern hängen Trigger auf Anweisungsebene von der benannten Tabelle der Anweisung ab, während Trigger auf Zeilenebene abhängig von der Tabelle ausgelöst werden, zu der die tatsächliche Zeile gehört (es könnte also eine untergeordnete Tabelle sein).

Dinge, auf die Sie achten sollten:

  • Die meisten Befehle funktionieren in der gesamten Hierarchie und unterstützen die NUR-Notation. Einige Low-Level-Befehle (REINDEX, VACUUM usw.) funktionieren jedoch nur mit den durch den Befehl benannten physischen Tabellen. Lesen Sie im Zweifelsfall immer die Dokumentation.
  • FREMDSCHLÜSSEL-Einschränkungen (die übergeordnete Tabelle befindet sich auf der referenzierenden Seite) werden nicht geerbt. Dies lässt sich leicht lösen, indem in allen untergeordneten Tabellen der Hierarchie dieselbe FK-Einschränkung angegeben wird.
  • Zum jetzigen Zeitpunkt (PostgreSQL 10) gibt es keine Möglichkeit, einen globalen UNIQUE INDEX (PRIMARY KEYs oder UNIQUE Constraints) für eine Gruppe von Tabellen zu haben. Als Ergebnis:
    • PRIMARY KEY- und UNIQUE-Einschränkungen werden nicht vererbt, und es gibt keine einfache Möglichkeit, die Eindeutigkeit einer Spalte über alle Mitglieder der Hierarchie hinweg zu erzwingen
    • Wenn sich die übergeordnete Tabelle auf der referenzierten Seite einer FOREIGN KEY-Einschränkung befindet, wird die Überprüfung nur für die Werte der Spalte in Zeilen durchgeführt, die wirklich (physisch) zur übergeordneten Tabelle gehören, nicht zu untergeordneten Tabellen.

Die letzte Einschränkung ist schwerwiegend. Laut den offiziellen Dokumenten gibt es dafür keine gute Problemumgehung. FK und Eindeutigkeit sind jedoch grundlegend für jeden ernsthaften Datenbankentwurf. Wir werden nach einer Möglichkeit suchen, damit umzugehen.

Laden Sie noch heute das Whitepaper PostgreSQL-Verwaltung und -Automatisierung mit ClusterControl herunterErfahren Sie, was Sie wissen müssen, um PostgreSQL bereitzustellen, zu überwachen, zu verwalten und zu skalierenLaden Sie das Whitepaper herunter

Vererbung in der Praxis

In diesem Abschnitt werden wir ein klassisches Design mit einfachen Tabellen, PRIMARY KEY/UNIQUE- und FOREIGN KEY-Einschränkungen in ein auf Vererbung basierendes mandantenfähiges Design umwandeln, und wir werden versuchen, die (wie im vorherigen Abschnitt erwarteten) Probleme zu lösen, die wir haben Gesicht. Betrachten wir dasselbe Vermietungsgeschäft, das wir im vorherigen Blog als Beispiel verwendet haben, und stellen wir uns vor, dass das Geschäft zu Beginn nur Autovermietungen anbietet (keine Boote oder andere Fahrzeugtypen). Betrachten wir das folgende Schema mit den Fahrzeugen des Unternehmens und der Wartungshistorie dieser Fahrzeuge:

create table vehicle (id SERIAL PRIMARY KEY, plate_no text NOT NULL, maker TEXT NOT NULL, model TEXT NOT NULL,vin text not null);
create table vehicle_service(id SERIAL PRIMARY KEY, vehicleid INT NOT NULL REFERENCES vehicle(id), service TEXT NOT NULL, date_performed DATE NOT NULL DEFAULT now(), cost real not null);
rentaldb=# insert into vehicle (plate_no,maker,model,vin) VALUES ('INI888','Hyundai','i20','HH999');
rentaldb=# insert into vehicle_service (vehicleid,service,cost) VALUES(1,'engine oil change/filters',50);

Stellen wir uns nun vor, das System befindet sich in der Produktion, und dann erwirbt das Unternehmen ein zweites Unternehmen, das Bootsvermietungen anbietet, und muss diese in das System integrieren, indem die beiden Unternehmen unabhängig voneinander arbeiten, soweit es den Betrieb betrifft, aber auf einheitliche Weise für Verwendung durch das oberste mgmt. Stellen wir uns außerdem vor, dass die Daten von vehicle_service nicht aufgeteilt werden dürfen, da alle Zeilen für beide Unternehmen sichtbar sein müssen. Wir suchen also nach einer Multi-Tenancy-Lösung, die auf Vererbung in der Fahrzeugtabelle basiert. Zuerst sollten wir ein neues Schema für Autos (das alte Geschäft) und eines für Boote erstellen und dann vorhandene Daten nach cars.vehicle migrieren:

rentaldb=# create schema cars;
rentaldb=# create table cars.vehicle (CONSTRAINT vehicle_pkey PRIMARY KEY(id) ) INHERITS (public.vehicle);
rentaldb=# \d cars.vehicle
                              Table "cars.vehicle"
  Column  |  Type   | Collation | Nullable |               Default               
----------+---------+-----------+----------+-------------------------------------
 id       | integer |           | not null | nextval('vehicle_id_seq'::regclass)
 plate_no | text    |           | not null |
 maker    | text    |           | not null |
 model    | text    |           | not null |
 vin      | text    |           | not null |
Indexes:
    "vehicle_pkey" PRIMARY KEY, btree (id)
Inherits: vehicle
rentaldb=# create schema boats;
rentaldb=# create table boats.vehicle (CONSTRAINT vehicle_pkey PRIMARY KEY(id) ) INHERITS (public.vehicle);
rentaldb=# \d boats.vehicle
                              Table "boats.vehicle"
  Column  |  Type   | Collation | Nullable |               Default               
----------+---------+-----------+----------+-------------------------------------
 id       | integer |           | not null | nextval('vehicle_id_seq'::regclass)
 plate_no | text    |           | not null |
 maker    | text    |           | not null |
 model    | text    |           | not null |
 vin      | text    |           | not null |
Indexes:
    "vehicle_pkey" PRIMARY KEY, btree (id)
Inherits: vehicle

Wir stellen fest, dass die neuen Tabellen denselben Standardwert für die Spalte id verwenden (gleiche Reihenfolge) wie die übergeordnete Tabelle. Dies ist zwar weit entfernt von einer Lösung für das im vorherigen Abschnitt erläuterte globale Eindeutigkeitsproblem, es ist jedoch eine Umgehung, vorausgesetzt, es wird niemals ein expliziter Wert für Einfügungen oder Aktualisierungen verwendet. Wenn alle untergeordneten Tabellen (cars.vehicle und boots.vehicle) wie oben definiert sind und wir die ID niemals explizit manipulieren, sind wir auf der sicheren Seite.

Da wir nur die Tabelle public vehicle_service behalten und diese Zeilen von untergeordneten Tabellen referenzieren wird, müssen wir die FK-Einschränkung löschen:

rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Aber da wir die äquivalente Konsistenz in unserer Datenbank aufrechterhalten müssen, müssen wir eine Lösung dafür finden. Wir werden diese Einschränkung mithilfe von Triggern implementieren. Wir müssen einen Trigger zu vehicle_service hinzufügen, der prüft, dass bei jedem INSERT oder UPDATE die vehicleid auf eine gültige Zeile irgendwo in der public.vehicle*-Hierarchie zeigt, und einen Trigger in jeder der Tabellen dieser Hierarchie, der dies bei jedem DELETE or prüft UPDATE auf id, keine Zeile in vehicle_service existiert, die auf den alten Wert zeigt. (Beachten Sie, dass PostgreSQL dies und alle untergeordneten Tabellen durch die Fahrzeug*-Notation impliziert)

CREATE OR REPLACE FUNCTION public.vehicle_service_fk_to_vehicle() RETURNS TRIGGER
        LANGUAGE plpgsql
AS $$
DECLARE
tmp INTEGER;
BEGIN
        IF (TG_OP = 'DELETE') THEN
          RAISE EXCEPTION 'TRIGGER : % called on unsuported op : %',TG_NAME, TG_OP;
        END IF;
        SELECT vh.id INTO tmp FROM public.vehicle vh WHERE vh.id=NEW.vehicleid;
        IF NOT FOUND THEN
          RAISE EXCEPTION '%''d % (id=%) with NEW.vehicleid (%) does not match any vehicle ',TG_OP, TG_TABLE_NAME, NEW.id, NEW.vehicleid USING ERRCODE = 'foreign_key_violation';
        END IF;
        RETURN NEW;
END
$$
;
CREATE CONSTRAINT TRIGGER vehicle_service_fk_to_vehicle_tg AFTER INSERT OR UPDATE ON public.vehicle_service FROM public.vehicle DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE public.vehicle_service_fk_to_vehicle();

Wenn wir versuchen, einen Wert für die Spalte „VehicleID“ zu aktualisieren oder einzufügen, der in „Vehicle*“ nicht vorhanden ist, erhalten wir einen Fehler:

rentaldb=# insert into vehicle_service (vehicleid,service,cost) VALUES(2,'engine oil change/filters',50);
ERROR:  INSERT'd vehicle_service (id=2) with NEW.vehicleid (2) does not match any vehicle
CONTEXT:  PL/pgSQL function vehicle_service_fk_to_vehicle() line 10 at RAISE

Wenn wir nun eine Zeile in eine beliebige Tabelle in der Hierarchie einfügen, z. boots.vehicle (was normalerweise id=2 nimmt) und versuchen Sie es erneut:

rentaldb=# insert into boats.vehicle (maker, model,plate_no,vin) VALUES('Zodiac','xx','INI000','ZZ20011');
rentaldb=# select * from vehicle;
 id | plate_no |  maker  | model |   vin   
----+----------+---------+-------+---------
  1 | INI888   | Hyundai | i20   | HH999
  2 | INI000   | Zodiac  | xx    | ZZ20011
(2 rows)
rentaldb=# insert into vehicle_service (vehicleid,service,cost) VALUES(2,'engine oil change/filters',50);

Dann ist das vorherige INSERT nun erfolgreich. Jetzt sollten wir diese FK-Beziehung auch auf der anderen Seite schützen, wir müssen sicherstellen, dass keine Aktualisierung/Löschung auf irgendeiner Tabelle in der Hierarchie erlaubt ist, wenn die zu löschende (oder zu aktualisierende) Zeile von vehicle_service referenziert wird:

CREATE OR REPLACE FUNCTION public.vehicle_fk_from_vehicle_service() RETURNS TRIGGER
        LANGUAGE plpgsql
AS $$
DECLARE
tmp INTEGER;
BEGIN
        IF (TG_OP = 'INSERT') THEN
          RAISE EXCEPTION 'TRIGGER : % called on unsuported op : %',TG_NAME, TG_OP;
        END IF;
        IF (TG_OP = 'DELETE' OR OLD.id <> NEW.id) THEN
          SELECT vhs.id INTO tmp FROM vehicle_service vhs WHERE vhs.vehicleid=OLD.id;
          IF FOUND THEN
            RAISE EXCEPTION '%''d % (OLD id=%) matches existing vehicle_service with id=%',TG_OP, TG_TABLE_NAME, OLD.id,tmp USING ERRCODE = 'foreign_key_violation';
          END IF;
        END IF;
        IF (TG_OP = 'UPDATE') THEN
                RETURN NEW;
        ELSE
                RETURN OLD;
        END IF;
END
$$
;
CREATE CONSTRAINT TRIGGER vehicle_fk_from_vehicle_service AFTER DELETE OR UPDATE
ON public.vehicle FROM vehicle_service DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE vehicle_fk_from_vehicle_service();
CREATE CONSTRAINT TRIGGER vehicle_fk_from_vehicle_service AFTER DELETE OR UPDATE
ON cars.vehicle FROM vehicle_service DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE vehicle_fk_from_vehicle_service();
CREATE CONSTRAINT TRIGGER vehicle_fk_from_vehicle_service AFTER DELETE OR UPDATE
ON boats.vehicle FROM vehicle_service DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE vehicle_fk_from_vehicle_service();

Probieren wir es aus:

rentaldb=# delete from vehicle where id=2;
ERROR:  DELETE'd vehicle (OLD id=2) matches existing vehicle_service with id=3
CONTEXT:  PL/pgSQL function vehicle_fk_from_vehicle_service() line 11 at RAISE

Jetzt müssen wir die vorhandenen Daten in public.vehicle nach cars.vehicle verschieben.

rentaldb=# begin ;
rentaldb=# set constraints ALL deferred ;
rentaldb=# set session_replication_role TO replica;
rentaldb=# insert into cars.vehicle select * from only public.vehicle;
rentaldb=# delete from only public.vehicle;
rentaldb=# commit ;

Das Festlegen von session_replication_role TO replica verhindert das Auslösen normaler Trigger. Beachten Sie, dass wir nach dem Verschieben der Daten möglicherweise die übergeordnete Tabelle (public.vehicle) vollständig deaktivieren möchten, um Einfügungen zu akzeptieren (höchstwahrscheinlich über eine Regel). In diesem Fall würden wir in der OO-Analogie public.vehicle als abstrakte Klasse behandeln, d. h. ohne Zeilen (Instanzen). Die Verwendung dieses Designs für Mandantenfähigkeit fühlt sich natürlich an, da das zu lösende Problem ein klassischer Anwendungsfall für die Vererbung ist, aber die Probleme, mit denen wir konfrontiert waren, sind nicht trivial. Dies wurde von der Hacker-Community diskutiert und wir hoffen auf zukünftige Verbesserungen.