I det här blogginlägget går vi igenom PostgreSQL-arv, traditionellt en av PostgreSQL:s främsta funktioner sedan de tidiga släppen. Några typiska användningar av arv i PostgreSQL är:
- tabellpartitionering
- flerhyresrätt
PostgreSQL fram till version 10 implementerade tabellpartitionering med hjälp av arv. PostgreSQL 10 tillhandahåller ett nytt sätt för deklarativ partitionering. PostgreSQL-partitionering med hjälp av arv är en ganska mogen teknik, väldokumenterad och testad, men nedärvning i PostgreSQL ur ett datamodellperspektiv är (enligt min mening) inte så utbrett, därför kommer vi att koncentrera oss på mer klassiska användningsfall i den här bloggen. Vi såg från förra bloggen (multi-tenancy options för PostgreSQL) att en av metoderna för att uppnå multi-tenancy är att använda separata tabeller och sedan konsolidera dem via en vy. Vi såg också nackdelarna med denna design. I den här bloggen kommer vi att förbättra denna design med hjälp av arv.
Introduktion till arv
När vi ser tillbaka på metoden med flera hyresrätter implementerad med separata tabeller och vyer minns vi att dess stora nackdel är oförmågan att infoga/uppdateringar/ta bort. I samma ögonblick som vi provar en uppdatering av uthyrningen se att vi får detta FEL:
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 skulle behöva skapa en trigger eller en regel för uthyrningen vy som anger en funktion för att hantera infogning/uppdatering/radering. Alternativet är att använda arv. Låt oss ändra schemat för föregående blogg:
template1=# create database rentaldb_hier;
template1=# \c rentaldb_hier
rentaldb_hier=# create schema boats;
rentaldb_hier=# create schema cars;
Låt oss nu skapa den överordnade huvudtabellen:
rentaldb_hier=# CREATE TABLE rental (
id integer NOT NULL,
customerid integer NOT NULL,
vehicleno text,
datestart date NOT NULL,
dateend date
);
I OO-termer motsvarar denna tabell superklassen (i java-terminologi). Låt oss nu definiera barntabellerna genom att ärva från public.rental och även lägga till en kolumn för varje tabell som är specifik för domänen:t.ex. det obligatoriska körkortsnumret (kund) för bilar och det valfria båtseglingscertifikatet.
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 två borden cars.rental och boats.rental ärver alla kolumner från deras överordnade 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 märker att vi har utelämnat företaget kolumn i definitionen av den överordnade tabellen (och som en konsekvens även i de underordnade tabellerna). Detta behövs inte längre eftersom identifieringen av hyresgästen står i tabellens fullständiga namn! Vi kommer senare att se ett enkelt sätt att ta reda på detta i frågor. Låt oss nu infoga några rader i de tre tabellerna (vi lånar kunder schema och data från föregående blogg):
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);
Låt oss nu se vad som finns i tabellerna:
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å samma föreställningar om arv som finns i objektorienterade språk (som Java) finns också i PostgreSQL! Vi kan tänka på detta som följer:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instans av public.rental
row cars.rental.id =2:instans av cars.rental och public.rental
row boats.rental.id =3:instans av boats.rental och public.rental
Eftersom raderna av båtar.hyra och bilar.uthyrning också är instanser av offentlig.uthyrning är det naturligt att de visas som rader av offentlig.uthyrning. Om vi bara vill ha rader exklusive public.rental (med andra ord raderna infogade direkt i public.rental) gör vi det med hjälp av ENDAST nyckelord enligt följande:
rentaldb_hier=# select * from ONLY rental ;
id | customerid | vehicleno | datestart | dateend
----+------------+------------------------+------------+---------
1 | 1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
(1 row)
En skillnad mellan Java och PostgreSQL när det gäller arv är denna:Java stöder inte multipelt arv medan PostgreSQL gör det, det är möjligt att ärva från mer än en tabell, så i detta avseende kan vi tänka på tabeller mer som gränssnitt i Java.
Om vi vill ta reda på den exakta tabellen i hierarkin där en specifik rad hör hemma (motsvarigheten till obj.getClass().getName() i java) kan vi göra det genom att specificera tableoid specialkolumnen (oid av respektive tabell i pgclass ), castad till regclass som ger hela tabellens namn:
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)
Från ovanstående (olika tableoid) kan vi sluta oss till att tabellerna i hierarkin bara är vanliga gamla PostgreSQL-tabeller, kopplade till ett arvsförhållande. Men förutom detta fungerar de ungefär som vanliga bord. Och detta kommer att betonas ytterligare i följande avsnitt.
Viktiga fakta och varningar om PostgreSQL-arv
Den underordnade tabellen ärver:
- INTE NULL-begränsningar
- Kontrollera begränsningar
Den underordnade tabellen ärver INTE:
- PRIMÄRA NYCKELbegränsningar
- UNIKA begränsningar
- FREIGN KEY-begränsningar
När kolumner med samma namn visas i definitionen av mer än en tabell i hierarkin, måste dessa kolumner ha samma typ och slås samman till en enda kolumn. Om det finns en NOT NULL-begränsning för ett kolumnnamn någonstans i hierarkin så ärvs detta till den underordnade tabellen. CHECK-begränsningar med samma namn slås också samman och måste ha samma villkor.
Schemaändringar av den överordnade tabellen (via ALTER TABLE) sprids över hela hierarkin som finns under denna överordnade tabell. Och detta är en av de trevliga funktionerna med arv i PostgreSQL.
Säkerhets- och säkerhetspolicyer (RLS) bestäms utifrån den faktiska tabellen vi använder. Om vi använder en överordnad tabell kommer tabellens säkerhet och RLS att användas. Det antyds att beviljande av ett privilegium på den överordnade tabellen ger behörighet till den eller de underordnade tabellerna också, men endast när den nås via den överordnade tabellen. För att komma åt den underordnade tabellen direkt måste vi ge explicit GRANT direkt till den underordnade tabellen, privilegiet på den överordnade tabellen kommer inte att räcka. Detsamma gäller för RLS.
När det gäller aktivering av utlösare beror utlösare på uttalandenivå på den namngivna tabellen i påståendet, medan utlösare på radnivå kommer att aktiveras beroende på tabellen som den faktiska raden tillhör (så det kan vara en underordnad tabell).
Saker att se upp för:
- De flesta kommandon fungerar på hela hierarkin och stöder den ENDA notationen. Vissa lågnivåkommandon (REINDEX, VACUUM, etc) fungerar dock endast på de fysiska tabellerna som namnges av kommandot. Se till att läsa dokumentationen varje gång i tveksamma fall.
- FREIGN KEY-begränsningar (den överordnade tabellen är på referenssidan) ärvs inte. Detta löses enkelt genom att ange samma FK-begränsning i alla underordnade tabeller i hierarkin.
- Från och med denna punkt (PostgreSQL 10) finns det inget sätt att ha globalt UNIKT INDEX (PRIMÄRA NYCKLAR eller UNIKA begränsningar) på en grupp av tabeller. Som ett resultat av detta:
- PRIMÄRNYCKEL och UNIKA begränsningar ärvs inte, och det finns inget enkelt sätt att framtvinga unikhet på en kolumn över alla medlemmar i hierarkin
- När den överordnade tabellen är på den refererade sidan av en FOREIGN KEY-restriktion, kontrolleras endast värdena i kolumnen på rader som verkligen (fysiskt) tillhör den överordnade tabellen, inte några underordnade tabeller.
Den sista begränsningen är allvarlig. Enligt de officiella dokumenten finns det ingen bra lösning för detta. Men FK och unikhet är grundläggande för all seriös databasdesign. Vi kommer att undersöka ett sätt att hantera detta.
Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda WhitepaperArv i praktiken
I det här avsnittet kommer vi att konvertera en klassisk design med vanliga tabeller, PRIMÄRNYCKEL/UNIKA och FOREIGN KEY-begränsningar, till en design med flera hyresgäster baserad på arv och vi kommer att försöka lösa de (förväntade som i föregående avsnitt) problem som vi ansikte. Låt oss betrakta samma uthyrningsverksamhet som vi använde som exempel i förra bloggen och låt oss föreställa oss att verksamheten i början endast hyr bilar (inga båtar eller andra typer av fordon). Låt oss överväga följande schema, med företagets fordon och servicehistoriken på dessa fordon:
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);
Låt oss nu föreställa oss att systemet är i produktion, och sedan förvärvar företaget ett andra företag som gör båtuthyrning och måste integrera dessa i systemet, genom att de två företagen fungerar oberoende så långt som verksamheten sträcker sig, men på ett enhetligt sätt för Används av den översta administratören. Låt oss också föreställa oss att fordonsservicedata inte får delas eftersom alla rader måste vara synliga för båda företagen. Så det vi letar efter är att tillhandahålla en flerhyreslösning baserad på arv på fordonsbordet. Först bör vi skapa ett nytt schema för bilar (den gamla verksamheten) och ett för båtar och sedan migrera befintlig data till 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 noterar att de nya tabellerna delar samma standardvärde för kolumnen id (samma sekvens) som den överordnade tabellen. Även om detta är långt ifrån en lösning på det globala unikhetsproblemet som förklarades i föregående avsnitt, är det en omgång, förutsatt att inget explicit värde någonsin kommer att användas för infogningar eller uppdateringar. Om alla barntabeller (bilar.fordon och båtar.fordon) definieras enligt ovan, och vi aldrig explicit manipulerar ID, så är vi säkra.
Eftersom vi bara kommer att behålla tabellen public vehicle_service och detta kommer att referera till rader med underordnade tabeller, måste vi släppa FK-begränsningen:
rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;
Men eftersom vi behöver upprätthålla motsvarande konsistens i vår databas måste vi hitta en lösning för detta. Vi kommer att implementera denna begränsning med hjälp av triggers. Vi måste lägga till en trigger till vehicle_service som kontrollerar att fordons-ID för varje INSERT eller UPDATE pekar på en giltig rad någonstans i public.vehicle*-hierarkin, och en trigger i var och en av tabellerna i denna hierarki som kontrollerar att för varje DELETE eller UPPDATERING på id, ingen rad i fordonstjänst finns som pekar på det gamla värdet. (notera med fordonets* notation PostgreSQL antyder detta och alla underordnade tabeller)
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();
Om vi försöker uppdatera eller infoga med ett värde för kolumn vehicleid som inte finns i fordon* får vi ett felmeddelande:
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
Om vi nu infogar en rad i någon tabell i hierarkin, t.ex. boats.vehicle (som normalt tar id=2) och försök 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);
Då lyckas nu föregående INSERT. Nu bör vi också skydda denna FK-relation på andra sidan, vi måste se till att ingen uppdatering/radering är tillåten på någon tabell i hierarkin om raden som ska raderas (eller uppdateras) refereras av 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();
Låt oss prova 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 måste vi flytta befintlig data i public.vehicle till 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 ;
Att ställa in session_replication_role TO replica förhindrar att normala triggers utlöses. Observera att vi, efter att ha flyttat data, kanske vill helt inaktivera den överordnade tabellen (public.vehicle) för att acceptera inlägg (mest troligt via en regel). I det här fallet, i OO-analogin, skulle vi behandla public.vehicle som en abstrakt klass, dvs utan rader (instanser). Att använda den här designen för flerbostadsrätter känns naturligt eftersom problemet som ska lösas är ett klassiskt användningsfall för arv, men problemen vi stod inför är inte triviala. Detta har diskuterats av hackare, och vi hoppas på framtida förbättringar.