sql >> Databáze >  >> RDS >> PostgreSQL

„O“ v ORDBMS:PostgreSQL Inheritance

V tomto příspěvku na blogu projdeme dědictvím PostgreSQL, tradičně jednou z nejlepších funkcí PostgreSQL od prvních verzí. Některá typická použití dědičnosti v PostgreSQL jsou:

  • rozdělení tabulky
  • multipronájem

PostgreSQL až do verze 10 implementovalo dělení tabulek pomocí dědičnosti. PostgreSQL 10 poskytuje nový způsob deklarativního dělení. Dělení PostgreSQL pomocí dědičnosti je docela vyspělá technologie, dobře zdokumentovaná a otestovaná, nicméně dědičnost v PostgreSQL z pohledu datového modelu není (podle mého názoru) tak rozšířená, proto se v tomto blogu zaměříme na klasičtější případy použití. Z předchozího blogu (možnosti vícenásobného pronájmu pro PostgreSQL) jsme viděli, že jednou z metod, jak dosáhnout vícenásobného pronájmu, je použití samostatných tabulek a jejich konsolidace prostřednictvím pohledu. Také jsme viděli nevýhody tohoto designu. V tomto blogu vylepšíme tento design pomocí dědičnosti.

Úvod do dědičnosti

Když se podíváme zpět na metodu multi-tenancy implementovanou s oddělenými tabulkami a pohledy, připomeneme si, že její hlavní nevýhodou je nemožnost vkládat/aktualizovat/mazat. Ve chvíli, kdy zkoušíme aktualizaci půjčovny zobrazení, dostaneme tuto CHYBU:

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.

Potřebovali bychom tedy vytvořit spouštěč nebo pravidlo pro výpůjčku pohled určující funkci pro zpracování vložení/aktualizace/smazání. Alternativou je použití dědičnosti. Změňme schéma předchozího blogu:

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

Nyní vytvoříme hlavní nadřazenou tabulku:

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

Z hlediska OO tato tabulka odpovídá nadtřídě (v terminologii Java). Nyní pojďme definovat podřízené tabulky děděním z public.rental a také přidání sloupce pro každou tabulku, která je specifická pro danou doménu:např. povinné číslo řidičského (zákaznického) průkazu v případě automobilů a volitelné osvědčení o plavbě lodí.

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

Dvě tabulky cars.pronájem a půjčovna lodí zdědí všechny sloupce od jejich nadřazeného 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

Všimli jsme si, že jsme vynechali společnost v definici nadřazené tabulky (a v důsledku toho i v podřízených tabulkách). Toto již není potřeba, protože identifikace nájemce je v tabulce celým jménem! Později uvidíme snadný způsob, jak to zjistit v dotazech. Nyní vložíme několik řádků do tří tabulek (půjčujeme si zákazníky schéma a data z předchozího blogu):

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

Nyní se podívejme, co je v tabulkách:

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)

Takže stejné pojmy dědičnosti, které existují v objektově orientovaných jazycích (jako Java), existují také v PostgreSQL! Můžeme si to představit následovně:
public.rental:superclass
cars.rental:subclass
čluny.pronájem:podtřída
řádek public.rental.id =1:instance public.rental
řada cars.rental.id =2:instance cars.rental a public.rental
řada boats.rental.id =3:instance boats.rental a public.rental

Vzhledem k tomu, že řady boats.rental a cars.rental jsou také příklady public.rental, je přirozené, že se objevují jako řady public.rental. Pokud chceme pouze řádky bez public.rental (jinými slovy řádky vložené přímo do public.rental), uděláme to pomocí klíčového slova ONLY následovně:

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

Jeden rozdíl mezi Java a PostgreSQL, pokud jde o dědičnost, je tento:Java nepodporuje vícenásobnou dědičnost, zatímco PostgreSQL ano, je možné dědit z více než jedné tabulky, takže v tomto ohledu můžeme tabulky považovat spíše za rozhraní v Javě.

Pokud chceme zjistit přesnou tabulku v hierarchii, kam konkrétní řádek patří (ekvivalent obj.getClass().getName() v jazyce Java), můžeme to udělat zadáním speciálního sloupce tableoid (oid příslušné tabulky v pgclass ), přetypovaný do třídy regclass, která poskytuje úplný název tabulky:

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)

Z výše uvedeného (jiný tableoid) můžeme usuzovat, že tabulky v hierarchii jsou jen obyčejné staré PostgreSQL tabulky, spojené s dědičným vztahem. Ale kromě toho se chovají skoro jako normální stoly. A to bude dále zdůrazněno v následující části.

Důležitá fakta a upozornění týkající se dědičnosti PostgreSQL

Podřízená tabulka zdědí:

  • NEPLATNÁ omezení
  • ZKONTROLUJTE omezení

Podřízená tabulka NEdědí:

  • PRIMÁRNÍ KLÍČOVÁ omezení
  • JEDINEČNÁ omezení
  • ZAHRANIČNÍ KLÍČOVÁ omezení

Když se sloupce se stejným názvem objeví v definici více než jedné tabulky v hierarchii, musí mít tyto sloupce stejný typ a jsou sloučeny do jednoho sloupce. Pokud pro název sloupce kdekoli v hierarchii existuje omezení NOT NULL, pak se toto zdědí do podřízené tabulky. Omezení CHECK se stejným názvem jsou také sloučena a musí mít stejnou podmínku.

Změny schématu nadřazené tabulky (prostřednictvím ALTER TABLE) se šíří v hierarchii, která existuje pod touto nadřazenou tabulkou. A to je jedna z pěkných vlastností dědičnosti v PostgreSQL.

Bezpečnostní a bezpečnostní zásady (RLS) se rozhodují na základě skutečné tabulky, kterou používáme. Pokud použijeme nadřazenou tabulku, použije se zabezpečení a RLS této tabulky. Předpokládá se, že udělení oprávnění nadřazené tabulce dává oprávnění také podřízené tabulce (tabulkám), ale pouze v případě, že k nim přistupujete prostřednictvím nadřazené tabulky. Abychom přistupovali přímo k podřízené tabulce, pak musíme udělit explicitní GRANT přímo podřízené tabulce, privilegium nadřazené tabulky nebude stačit. Totéž platí pro RLS.

Pokud jde o spouštění spouštěčů, spouštěče na úrovni příkazu závisí na pojmenované tabulce příkazu, zatímco spouštěče na úrovni řádků budou spouštěny v závislosti na tabulce, do které skutečný řádek patří (může to být tedy podřízená tabulka).

Na co si dát pozor:

  • Většina příkazů funguje v celé hierarchii a podporuje ONLY notaci. Některé nízkoúrovňové příkazy (REINDEX, VACUUM atd.) však fungují pouze na fyzických tabulkách pojmenovaných příkazem. V případě pochybností si vždy přečtěte dokumentaci.
  • Omezení FOREIGN KEY (nadřazená tabulka je na straně odkazu) se nedědí. To lze snadno vyřešit zadáním stejného omezení FK ve všech podřízených tabulkách hierarchie.
  • Od tohoto okamžiku (PostgreSQL 10) neexistuje žádný způsob, jak mít globální UNIKÁTNÍ INDEX (PRIMÁRNÍ KLÍČE nebo UNIKÁTNÍ omezení) ve skupině tabulek. V důsledku toho:
    • Omezení PRIMARY KEY a UNIQUE se nedědí a neexistuje snadný způsob, jak vynutit jedinečnost sloupce napříč všemi členy hierarchie.
    • Pokud je nadřazená tabulka na odkazované straně omezení FOREIGN KEY, pak se kontrola provádí pouze pro hodnoty sloupce na řádcích skutečně (fyzicky) patřících do nadřazené tabulky, nikoli pro podřízené tabulky.

Poslední omezení je vážné. Podle oficiálních dokumentů pro to neexistuje žádné dobré řešení. Nicméně, FK a jedinečnost jsou zásadní pro jakýkoli seriózní návrh databáze. Podíváme se na způsob, jak se s tím vypořádat.

Stáhněte si Whitepaper Today Správa a automatizace PostgreSQL s ClusterControlZjistěte, co potřebujete vědět k nasazení, monitorování, správě a škálování PostgreSQLStáhněte si Whitepaper

Dědičnost v praxi

V této části převedeme klasický návrh s prostými tabulkami, omezeními PRIMARY KEY/UNIQUE a FOREIGN KEY na návrh pro více nájemců založený na dědičnosti a pokusíme se vyřešit (očekávané podle předchozí části) problémy, které jsme tvář. Podívejme se na stejnou půjčovnu, kterou jsme použili jako příklad v předchozím blogu, a představme si, že na začátku se firma zabývá pouze pronájmem aut (žádné lodě nebo jiné typy vozidel). Podívejme se na následující schéma s vozidly společnosti a servisní historií na těchto vozidlech:

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

Nyní si představme, že se systém vyrábí, a pak společnost získá druhou společnost, která pronajímá lodě a musí je integrovat do systému tak, že obě společnosti budou fungovat nezávisle, pokud jde o provoz, ale jednotným způsobem. použití horním mgmt. Představme si také, že data vehicle_service nesmí být rozdělena, protože všechny řádky musí být viditelné pro obě společnosti. Takže to, co hledáme, je poskytnout řešení pro více nájemců založené na dědění na tabulce vozidel. Nejprve bychom měli vytvořit nové schéma pro auta (staré podnikání) a jedno pro lodě a poté migrovat stávající data do 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

Upozorňujeme, že nové tabulky sdílejí stejnou výchozí hodnotu pro sloupec id (stejná sekvence) jako nadřazená tabulka. I když to zdaleka není řešení problému globální jedinečnosti vysvětleného v předchozí části, jde to obejít, za předpokladu, že pro vložení nebo aktualizace nebude nikdy použita žádná explicitní hodnota. Pokud jsou všechny dětské tabulky (cars.vehicle a boats.vehicle) definovány výše uvedeným způsobem a nikdy nebudeme explicitně manipulovat s id, pak budeme v bezpečí.

Protože ponecháme pouze tabulku public vehicle_service, která bude odkazovat na řádky dětských tabulek, musíme zrušit omezení FK:

rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Ale protože potřebujeme zachovat ekvivalentní konzistenci v naší databázi, musíme najít řešení, jak to obejít. Toto omezení implementujeme pomocí triggerů. Potřebujeme přidat spouštěč do vehicle_service, který kontroluje, že pro každé INSERT nebo UPDATE identifikátor vozidla ukazuje na platný řádek někde v hierarchii public.vehicle*, a jeden spouštěč v každé z tabulek této hierarchie, který kontroluje, že pro každé DELETE nebo UPDATE na id, ve službě vehicle_service neexistuje žádný řádek, který by ukazoval na starou hodnotu. (poznámka u označení vehicle* PostgreSQL implikuje tuto a všechny dětské tabulky)

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

Pokud se pokusíme aktualizovat nebo vložit hodnotu pro sloupec vehicleid, která ve vozidle neexistuje*, zobrazí se chyba:

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

Nyní, když vložíme řádek do libovolné tabulky v hierarchii, např. boats.vehicle (který normálně bude mít id=2) a zkuste to znovu:

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

Potom předchozí INSERT nyní uspěje. Nyní bychom také měli chránit tento vztah FK na druhé straně, musíme se ujistit, že v žádné tabulce v hierarchii není povolena žádná aktualizace/mazání, pokud na řádek, který má být odstraněn (nebo aktualizován), odkazuje 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();

Pojďme to zkusit:

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

Nyní potřebujeme přesunout stávající data v public.vehicle do 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 ;

Nastavení session_replication_role TO replika zabrání spouštění normálních spouštěčů. Všimněte si, že po přesunutí dat můžeme chtít úplně zakázat přijímání vložek v rodičovské tabulce (public.vehicle) (nejspíše prostřednictvím pravidla). V tomto případě bychom v OO analogii zacházeli s public.vehicle jako s abstraktní třídou, tedy bez řádků (instancí). Použití tohoto návrhu pro multi-nájem je přirozené, protože problém, který je třeba vyřešit, je klasický případ použití dědičnosti, nicméně problémy, kterým jsme čelili, nejsou triviální. Toto bylo diskutováno v komunitě hackerů a doufáme v budoucí zlepšení.


  1. provádění operací souvisejících s datem v PHP

  2. Liquibase/PostgreSQL:jak správně zachovat případ tabulky?

  3. MySQL Cross Server Select Query

  4. Počet výskytů určitého znaku v řetězci