sql >> Database teknologi >  >> RDS >> PostgreSQL

"O" i ORDBMS:PostgreSQL Inheritance

I dette blogindlæg gennemgår vi PostgreSQL-arv, traditionelt en af ​​PostgreSQLs topfunktioner siden de tidlige udgivelser. Nogle typiske anvendelser af arv i PostgreSQL er:

  • tabelopdeling
  • multi-lejemål

PostgreSQL indtil version 10 implementerede tabelpartitionering ved hjælp af arv. PostgreSQL 10 giver en ny måde til deklarativ partitionering. PostgreSQL-partitionering ved hjælp af arv er en ret moden teknologi, veldokumenteret og testet, men nedarvning i PostgreSQL fra et datamodelperspektiv er (efter min mening) ikke så udbredt, derfor vil vi koncentrere os om mere klassiske use cases i denne blog. Vi så fra den tidligere blog (multi-tenancy muligheder for PostgreSQL), at en af ​​metoderne til at opnå multi-tenancy er at bruge separate tabeller og derefter konsolidere dem via en visning. Vi så også ulemperne ved dette design. I denne blog vil vi forbedre dette design ved hjælp af arv.

Introduktion til arv

Når vi ser tilbage på metoden med flere lejemål implementeret med separate tabeller og visninger, husker vi, at dens største ulempe er manglende evne til at foretage indsættelser/opdateringer/sletninger. I det øjeblik, vi prøver en opdatering på udlejningen se, vi får denne FEJL:

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.

Så vi bliver nødt til at oprette en trigger eller en regel på udlejningen visning, der angiver en funktion til at håndtere indsættelse/opdatering/sletning. Alternativet er at bruge arv. Lad os ændre skemaet for den forrige blog:

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

Lad os nu oprette den overordnede hovedtabel:

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

I OO-termer svarer denne tabel til superklassen (i java-terminologi). Lad os nu definere børnetabellerne ved at arve fra public.rental og også tilføje en kolonne for hver tabel, der er specifik for domænet:f.eks. det obligatoriske kørekortnummer (kunde) i tilfælde af biler og det valgfri bådsejladscertifikat.

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);

De to borde cars.rental og boats.rental arve alle kolonnerne fra deres overordnede 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

Vi bemærker, at vi har udeladt virksomheden kolonne i definitionen af ​​den overordnede tabel (og som en konsekvens også i de underordnede tabeller). Dette er ikke længere nødvendigt, da lejerens identifikation er i tabellens fulde navn! Vi vil senere se en nem måde at finde ud af dette på i forespørgsler. Lad os nu indsætte nogle rækker i de tre tabeller (vi låner kunder skema og data fra den forrige 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);

Lad os nu se, hvad der står i tabellerne:

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)

Så de samme forestillinger om arv, som findes i objektorienterede sprog (som Java), findes også i PostgreSQL! Vi kan tænke på dette som følger:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instans af public.rental
row cars.rental.id =2:forekomst af cars.rental og public.rental
row boats.rental.id =3:forekomst af boats.rental og public.rental

Da rækkerne af både.udlejning og biler.udlejning også er eksempler på offentlig.udlejning, er det naturligt, at de optræder som rækker af offentlig.udlejning. Hvis vi kun ønsker rækker eksklusiv public.rental (med andre ord rækkerne indsat direkte til public.rental), gør vi det ved at bruge ONLY søgeordet som følger:

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

En forskel mellem Java og PostgreSQL, hvad angår arv, er denne:Java understøtter ikke multipel nedarvning, mens PostgreSQL gør det, det er muligt at arve fra mere end én tabel, så i denne henseende kan vi tænke på tabeller mere som grænseflader i Java.

Hvis vi ønsker at finde ud af den nøjagtige tabel i hierarkiet, hvor en specifik række hører hjemme (svarende til obj.getClass().getName() i java), kan vi gøre det ved at specificere tableoid specialkolonnen (oid af den respektive tabel i pgclass ), castet til regclass, som giver det fulde tabelnavn:

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)

Fra ovenstående (anden tableoid) kan vi udlede, at tabellerne i hierarkiet blot er almindelige gamle PostgreSQL-tabeller, forbundet med et arveforhold. Men udover dette fungerer de stort set som normale borde. Og dette vil blive yderligere understreget i det følgende afsnit.

Vigtige fakta og forbehold om PostgreSQL-arv

Den underordnede tabel arver:

  • IKKE NULL-begrænsninger
  • TJEK begrænsninger

Den underordnede tabel arver IKKE:

  • PRIMÆRE NØGLE-begrænsninger
  • UNIKKE begrænsninger
  • UDENLANDSKE NØGLE-begrænsninger

Når kolonner med samme navn vises i definitionen af ​​mere end én tabel i hierarkiet, skal disse kolonner have samme type og flettes til én enkelt kolonne. Hvis der eksisterer en NOT NULL-begrænsning for et kolonnenavn hvor som helst i hierarkiet, nedarves dette til den underordnede tabel. CHECK-begrænsninger med samme navn flettes også og skal have samme betingelse.

Skemaændringer til den overordnede tabel (via ALTER TABLE) udbredes i hele hierarkiet, der findes under denne overordnede tabel. Og dette er en af ​​de gode egenskaber ved arv i PostgreSQL.

Sikkerheds- og sikkerhedspolitikker (RLS) bestemmes ud fra den faktiske tabel, vi bruger. Hvis vi bruger en overordnet tabel, vil tabellens sikkerhed og RLS blive brugt. Det er underforstået, at tildeling af et privilegium på den overordnede tabel også giver tilladelse til de underordnede tabeller, men kun når de tilgås via den overordnede tabel. For at få direkte adgang til den underordnede tabel, så skal vi give eksplicit GRANT direkte til den underordnede tabel, privilegiet på den overordnede tabel vil ikke være tilstrækkeligt. Det samme gælder for RLS.

Med hensyn til udløsning af udløsere afhænger udløsere på sætningsniveau af den navngivne tabel i sætningen, mens udløsere på rækkeniveau vil blive udløst afhængigt af den tabel, den faktiske række tilhører (så det kan være en undertabel).

Ting at være opmærksom på:

  • De fleste kommandoer virker på hele hierarkiet og understøtter den ENESTE notation. Nogle kommandoer på lavt niveau (REINDEX, VACUUM osv.) virker dog kun på de fysiske tabeller, der er navngivet af kommandoen. Sørg for at læse dokumentationen hver gang i tvivlstilfælde.
  • FOREIGN KEY-begrænsninger (den overordnede tabel er på referencesiden) nedarves ikke. Dette løses nemt ved at angive den samme FK-begrænsning i alle underordnede tabeller i hierarkiet.
  • På dette tidspunkt (PostgreSQL 10) er der ingen måde at have global UNIQUE INDEX (PRIMÆRE NØGLER eller UNIQUE begrænsninger) på en gruppe af tabeller. Som et resultat af dette:
    • PRIMÆR NØGLE og UNIKKE begrænsninger nedarves ikke, og der er ingen nem måde at gennemtvinge entydighed på en kolonne på tværs af alle medlemmer af hierarkiet
    • Når den overordnede tabel er på den refererede side af en FOREIGN KEY-begrænsning, kontrolleres der kun for værdierne af kolonnen på rækker, der reelt (fysisk) tilhører den overordnede tabel, ikke nogen underordnede tabeller.

Den sidste begrænsning er alvorlig. Ifølge de officielle dokumenter er der ingen god løsning på dette. Men FK og unikhed er grundlæggende for ethvert seriøst databasedesign. Vi vil undersøge en måde at håndtere dette på.

Download Whitepaper Today PostgreSQL Management &Automation med ClusterControlFå flere oplysninger om, hvad du skal vide for at implementere, overvåge, administrere og skalere PostgreSQLDownload Whitepaper

Arv i praksis

I dette afsnit vil vi konvertere et klassisk design med almindelige tabeller, PRIMÆR NØGLE/UNIKKE og UDENLANDSKE NØGLE begrænsninger, til et multi-lejer design baseret på arv, og vi vil forsøge at løse de (forventede som i forrige afsnit) problemer, som vi ansigt. Lad os overveje den samme udlejningsvirksomhed, som vi brugte som eksempel i den forrige blog, og lad os forestille os, at virksomheden i begyndelsen kun udfører biludlejning (ingen både eller andre typer køretøjer). Lad os overveje følgende skema med virksomhedens køretøjer og servicehistorikken på disse køretøjer:

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);

Lad os nu forestille os, at systemet er i produktion, og så køber virksomheden et andet firma, som laver bådudlejning og skal integrere dem i systemet, ved at de to virksomheder opererer uafhængigt, så vidt som driften rækker, men på en samlet måde for brug af den øverste mgmt. Lad os også forestille os, at vehicle_service-dataene ikke må opdeles, da alle rækker skal være synlige for begge virksomheder. Så det, vi leder efter, er at levere en multi-lejemålsløsning baseret på arv på køretøjsbordet. Først bør vi oprette et nyt skema for biler (den gamle virksomhed) og et for både og derefter migrere eksisterende data til cars.vehicle:

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

Vi bemærker, at de nye tabeller deler den samme standardværdi for kolonne id (samme rækkefølge) som den overordnede tabel. Selvom dette langt fra er en løsning på det globale unikkeproblem, der blev forklaret i det foregående afsnit, er det en omvej, forudsat at der aldrig vil blive brugt nogen eksplicit værdi til indsættelser eller opdateringer. Hvis alle børnetabeller (cars.vehicle og boats.vehicle) er defineret som ovenfor, og vi aldrig eksplicit manipulerer id, så er vi sikre.

Da vi kun beholder tabellen med offentlige køretøjer, og denne vil referere til rækker af børnetabeller, skal vi droppe FK-begrænsningen:

rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Men fordi vi skal bevare den tilsvarende konsistens i vores database, må vi finde en løsning på dette. Vi vil implementere denne begrænsning ved hjælp af triggere. Vi skal tilføje en trigger til vehicle_service, som kontrollerer, at køretøjs-id'et for hver INSERT eller UPDATE peger på en gyldig række et eller andet sted i public.vehicle*-hierarkiet, og en trigger på hver af tabellerne i dette hierarki, som kontrollerer, at for hver DELETE eller OPDATERING på id, der findes ingen række i vehicle_service, der peger på den gamle værdi. (bemærk ved køretøjets* notation PostgreSQL antyder dette og alle børnetabeller)

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();

Hvis vi forsøger at opdatere eller indsætte med en værdi for kolonne vehicleid, der ikke findes i køretøj*, får vi en fejlmeddelelse:

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

Hvis vi nu indsætter en række i en tabel i hierarkiet, f.eks. boats.vehicle (som normalt tager id=2) og prøv igen:

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);

Så lykkes den forrige INSERT nu. Nu bør vi også beskytte dette FK-forhold på den anden side, vi skal sørge for, at ingen opdatering/sletning er tilladt på nogen tabel i hierarkiet, hvis rækken, der skal slettes (eller opdateres), refereres af vehicle_service:

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();

Lad os prøve det:

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

Nu skal vi flytte de eksisterende data i public.vehicle til cars.vehicle.

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 ;

Indstilling af session_replication_role TIL replika forhindrer udløsning af normale triggere. Bemærk, at efter at have flyttet dataene, vil vi måske helt deaktivere den overordnede tabel (public.vehicle) for at acceptere inserts (sandsynligvis via en regel). I dette tilfælde vil vi i OO-analogien behandle public.vehicle som en abstrakt klasse, dvs. uden rækker (instanser). At bruge dette design til multi-lejemål føles naturligt, fordi problemet, der skal løses, er en klassisk use case for arv, men de problemer, vi stod over for, er ikke trivielle. Dette er blevet diskuteret af hackerfællesskabet, og vi håber på fremtidige forbedringer.


  1. Sådan får du *alt* tilbage fra en lagret procedure ved hjælp af JDBC

  2. Sådan beregnes kumulativ total i MySQL

  3. MySQL-serveren er forsvundet ved import af stor sql-fil

  4. Vil et SQL Server-job springe en planlagt kørsel over, hvis det allerede kører?