In dit blogbericht gaan we in op PostgreSQL-overerving, traditioneel een van de belangrijkste functies van PostgreSQL sinds de vroege releases. Enkele typische toepassingen van overerving in PostgreSQL zijn:
- tabelpartitionering
- multi-tenancy
PostgreSQL tot versie 10 implementeerde tabelpartitionering met behulp van overerving. PostgreSQL 10 biedt een nieuwe manier van declaratieve partitionering. PostgreSQL-partitionering met behulp van overerving is een behoorlijk volwassen technologie, goed gedocumenteerd en getest, maar overerving in PostgreSQL vanuit een datamodelperspectief is (naar mijn mening) niet zo wijdverbreid, daarom zullen we ons in deze blog concentreren op meer klassieke gebruiksscenario's. We zagen uit de vorige blog (multi-tenancy opties voor PostgreSQL) dat een van de methoden om multi-tenancy te bereiken is om aparte tabellen te gebruiken en deze vervolgens te consolideren via een view. We zagen ook de nadelen van dit ontwerp. In deze blog zullen we dit ontwerp verbeteren met behulp van overerving.
Inleiding tot overerving
Terugkijkend op de multi-tenancy-methode die is geïmplementeerd met gescheiden tabellen en views, herinneren we ons dat het grootste nadeel ervan het onvermogen is om invoegingen/updates/verwijderingen uit te voeren. Op het moment dat we een update proberen over de verhuur kijk, we krijgen deze FOUT:
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.
We zouden dus een trigger of een regel moeten maken voor de verhuur view die een functie specificeert om het invoegen/bijwerken/verwijderen af te handelen. Het alternatief is om overerving te gebruiken. Laten we het schema van de vorige blog veranderen:
template1=# create database rentaldb_hier;
template1=# \c rentaldb_hier
rentaldb_hier=# create schema boats;
rentaldb_hier=# create schema cars;
Laten we nu de hoofdoudertabel maken:
rentaldb_hier=# CREATE TABLE rental (
id integer NOT NULL,
customerid integer NOT NULL,
vehicleno text,
datestart date NOT NULL,
dateend date
);
In OO-termen komt deze tabel overeen met de superklasse (in Java-terminologie). Laten we nu de onderliggende tabellen definiëren door overerven van public.rental en ook het toevoegen van een kolom voor elke tabel die specifiek is voor het domein:b.v. het verplichte rijbewijsnummer (klant) bij auto's en het optionele vaarbewijs.
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 twee tabellen cars.rental en boats.rental erven alle kolommen van hun bovenliggende 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
We merken dat we het bedrijf hebben weggelaten kolom in de definitie van de bovenliggende tabel (en dus ook in de onderliggende tabellen). Dit is niet meer nodig aangezien de identificatie van de huurder op de volledige naam van de tabel staat! We zullen later een gemakkelijke manier zien om dit in query's te achterhalen. Laten we nu enkele rijen invoegen in de drie tabellen (we lenen klanten schema en gegevens uit de vorige 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);
Laten we nu eens kijken wat er in de tabellen staat:
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)
Dus dezelfde noties van overerving die bestaan in objectgeoriënteerde talen (zoals Java) bestaan ook in PostgreSQL! We kunnen dit als volgt zien:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instantie van public.rental
row cars.rental.id =2:instantie van cars.rental en public.rental
row boats.rental.id =3:instantie van boats.rental en public.rental
Aangezien de rijen boten.verhuur en auto's.verhuur ook voorbeelden van public.rental zijn, is het logisch dat ze verschijnen als rijen public.rental. Als we alleen rijen willen exclusief public.rental (met andere woorden de rijen die rechtstreeks in public.rental worden ingevoegd), doen we het als volgt met het ENIGE trefwoord:
rentaldb_hier=# select * from ONLY rental ;
id | customerid | vehicleno | datestart | dateend
----+------------+------------------------+------------+---------
1 | 1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
(1 row)
Een verschil tussen Java en PostgreSQL wat betreft overerving is dit:Java ondersteunt geen meervoudige overerving, terwijl PostgreSQL dat wel doet, het is mogelijk om van meer dan één tabellen te erven, dus in dit opzicht kunnen we tabellen meer als interfaces beschouwen in Java.
Als we de exacte tabel in de hiërarchie willen weten waar een specifieke rij thuishoort (het equivalent van obj.getClass().getName() in java), kunnen we dat doen door de speciale tableoid-kolom (oid van de respectieve tabel in <) op te geven. em>pgclass ), gecast naar regclass die de volledige tabelnaam geeft:
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)
Uit het bovenstaande (andere tableoid) kunnen we afleiden dat de tabellen in de hiërarchie gewoon oude PostgreSQL-tabellen zijn, verbonden met een overervingsrelatie. Maar daarnaast gedragen ze zich vrijwel als normale tafels. En dit zal verder worden benadrukt in de volgende sectie.
Belangrijke feiten en kanttekeningen over PostgreSQL-overerving
De onderliggende tabel erft:
- NIET NULL-beperkingen
- CONTROLEER beperkingen
De kindtabel erft NIET:
- PRIMAIRE SLEUTEL-beperkingen
- UNIEKE beperkingen
- BUITENLANDSE KEY-beperkingen
Als kolommen met dezelfde naam voorkomen in de definitie van meer dan één tabel in de hiërarchie, dan moeten die kolommen van hetzelfde type zijn en worden samengevoegd tot één enkele kolom. Als er ergens in de hiërarchie een NOT NULL-beperking bestaat voor een kolomnaam, wordt deze overgenomen naar de onderliggende tabel. CHECK-beperkingen met dezelfde naam worden ook samengevoegd en moeten dezelfde voorwaarde hebben.
Schemawijzigingen in de bovenliggende tabel (via ALTER TABLE) worden doorgevoerd in de hiërarchie die onder deze bovenliggende tabel bestaat. En dit is een van de leuke eigenschappen van overerving in PostgreSQL.
Beveiligings- en beveiligingsbeleid (RLS) wordt bepaald op basis van de daadwerkelijke tabel die we gebruiken. Als we een bovenliggende tabel gebruiken, worden de beveiliging en RLS van die tabel gebruikt. Er wordt gesuggereerd dat het verlenen van een privilege op de bovenliggende tabel ook toestemming geeft voor de onderliggende tabel(len), maar alleen wanneer toegang via de bovenliggende tabel. Om rechtstreeks toegang te krijgen tot de onderliggende tabel, moeten we expliciete GRANT rechtstreeks aan de onderliggende tabel geven, het privilege op de bovenliggende tabel is niet voldoende. Hetzelfde geldt voor RLS.
Wat betreft het activeren van triggers, zijn triggers op instructieniveau afhankelijk van de benoemde tabel van de instructie, terwijl triggers op rijniveau worden geactiveerd afhankelijk van de tabel waartoe de daadwerkelijke rij behoort (het kan dus een onderliggende tabel zijn).
Dingen om op te letten:
- De meeste commando's werken op de hele hiërarchie en ondersteunen de ENIGE notatie. Sommige opdrachten op laag niveau (REINDEX, VACUUM, enz.) werken echter alleen op de fysieke tabellen die door de opdracht worden genoemd. Zorg ervoor dat u de documentatie elke keer leest in geval van twijfel.
- FOREIGN KEY-beperkingen (de bovenliggende tabel bevindt zich aan de verwijzende kant) worden niet overgenomen. Dit is eenvoudig op te lossen door dezelfde FK-beperking op te geven in alle onderliggende tabellen van de hiërarchie.
- Vanaf dit punt (PostgreSQL 10), is er geen manier om een globale UNIEKE INDEX (PRIMAIRE SLEUTELS of UNIEKE beperkingen) op een groep tabellen te hebben. Als gevolg hiervan:
- PRIMARY KEY en UNIQUE-beperkingen worden niet geërfd, en er is geen gemakkelijke manier om uniciteit af te dwingen voor een kolom voor alle leden van de hiërarchie
- Als de bovenliggende tabel zich aan de kant van een FOREIGN KEY-beperking bevindt, wordt alleen gecontroleerd op de waarden van de kolom op rijen die echt (fysiek) bij de bovenliggende tabel horen, niet op onderliggende tabellen. >
De laatste beperking is een serieuze. Volgens de officiële documenten is hier geen goede oplossing voor. FK en uniciteit zijn echter fundamenteel voor elk serieus databaseontwerp. We zullen een manier zoeken om hiermee om te gaan.
Download de whitepaper vandaag PostgreSQL-beheer en -automatisering met ClusterControlLees wat u moet weten om PostgreSQL te implementeren, bewaken, beheren en schalenDownload de whitepaperOvererving in de praktijk
In deze sectie zullen we een klassiek ontwerp met duidelijke tabellen, PRIMARY KEY/UNIQUE en FOREIGN KEY-beperkingen, converteren naar een multi-tenant ontwerp op basis van overerving en we zullen proberen de (verwachte volgens de vorige sectie) problemen op te lossen die we hebben gezicht. Laten we hetzelfde verhuurbedrijf beschouwen dat we in de vorige blog als voorbeeld hebben gebruikt en laten we ons voorstellen dat het bedrijf in het begin alleen autoverhuur doet (geen boten of andere soorten voertuigen). Laten we het volgende schema eens bekijken, met de voertuigen van het bedrijf en de onderhoudsgeschiedenis van die voertuigen:
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);
Laten we ons nu voorstellen dat het systeem in productie is, en dan verwerft het bedrijf een tweede bedrijf dat boten verhuurt en deze in het systeem moet integreren, door de twee bedrijven onafhankelijk te laten opereren voor zover de operatie gaat, maar op een uniforme manier voor gebruik door de hoogste mgmt. Laten we ons ook voorstellen dat de voertuigservicegegevens niet moeten worden opgesplitst, omdat alle rijen voor beide bedrijven zichtbaar moeten zijn. Dus waar we naar op zoek zijn, is een multi-tenancy-oplossing op basis van overerving op de voertuigtabel. Eerst moeten we een nieuw schema maken voor auto's (het oude bedrijf) en een voor boten en vervolgens de bestaande gegevens migreren naar 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
We merken op dat de nieuwe tabellen dezelfde standaardwaarde delen voor kolom id (dezelfde volgorde) als de bovenliggende tabel. Hoewel dit verre van een oplossing is voor het wereldwijde uniciteitsprobleem dat in de vorige sectie is uitgelegd, is het een tijdelijke oplossing, op voorwaarde dat er nooit een expliciete waarde wordt gebruikt voor invoegingen of updates. Als alle kindertafels (auto's.voertuig en boten.voertuig) zijn gedefinieerd zoals hierboven, en we id nooit expliciet manipuleren, dan zijn we veilig.
Aangezien we alleen de tabel public vehicle_service behouden en dit verwijst naar rijen met onderliggende tabellen, moeten we de FK-beperking laten vallen:
rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;
Maar omdat we de equivalente consistentie in onze database moeten behouden, moeten we hier een oplossing voor vinden. We zullen deze beperking implementeren met behulp van triggers. We moeten een trigger toevoegen aan vehicle_service die controleert dat voor elke INSERT of UPDATE de vehicleid verwijst naar een geldige rij ergens in de public.vehicle*-hiërarchie, en één trigger op elk van de tabellen van deze hiërarchie die dat controleert voor elke DELETE of UPDATE op id, er bestaat geen rij in vehicle_service die naar de oude waarde verwijst. (let op de voertuignotatie* PostgreSQL impliceert dit en alle kindertafels)
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();
Als we proberen een waarde voor de kolom voertuig-id bij te werken of in te voegen die niet bestaat in voertuig*, krijgen we een foutmelding:
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
Als we nu een rij invoegen in een tabel in de hiërarchie, b.v. boat.vehicle (waar normaal id=2) voor nodig is en probeer het opnieuw:
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);
Dan lukt de vorige INSERT nu wel. Nu moeten we ook deze FK-relatie aan de andere kant beschermen, we moeten ervoor zorgen dat geen enkele tabel in de hiërarchie wordt bijgewerkt/verwijderd als de rij die moet worden verwijderd (of bijgewerkt) wordt verwezen door voertuig_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();
Laten we het proberen:
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 moeten we de bestaande gegevens in public.vehicle verplaatsen naar 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 ;
Door session_replication_role TO replica in te stellen, wordt voorkomen dat normale triggers worden geactiveerd. Merk op dat we, na het verplaatsen van de gegevens, mogelijk de bovenliggende tabel (public.vehicle) van het accepteren van invoegingen volledig willen uitschakelen (waarschijnlijk via een regel). In dit geval zouden we in de OO-analogie public.vehicle als een abstracte klasse behandelen, d.w.z. zonder rijen (instanties). Het gebruik van dit ontwerp voor multi-tenancy voelt natuurlijk aan, omdat het op te lossen probleem een klassiek geval is voor overerving, maar de problemen waarmee we werden geconfronteerd, zijn niet triviaal. Dit is besproken door de hackersgemeenschap en we hopen op toekomstige verbeteringen.