PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

„O” w ORDBMS:Dziedziczenie PostgreSQL

W tym wpisie na blogu omówimy dziedziczenie PostgreSQL, tradycyjnie jedną z najważniejszych funkcji PostgreSQL od wczesnych wydań. Oto kilka typowych zastosowań dziedziczenia w PostgreSQL:

  • partycjonowanie tabeli
  • wielu najemców

PostgreSQL do wersji 10 implementował partycjonowanie tabel przy użyciu dziedziczenia. PostgreSQL 10 zapewnia nowy sposób partycjonowania deklaratywnego. Partycjonowanie PostgreSQL z wykorzystaniem dziedziczenia to dość dojrzała technologia, dobrze udokumentowana i przetestowana, jednak dziedziczenie w PostgreSQL z perspektywy modelu danych nie jest (moim zdaniem) tak rozpowszechnione, dlatego w tym blogu skupimy się na bardziej klasycznych przypadkach użycia. Z poprzedniego bloga (opcje wielodostępności dla PostgreSQL) widzieliśmy, że jedną z metod uzyskania wielodostępności jest użycie oddzielnych tabel, a następnie konsolidacja ich za pomocą widoku. Widzieliśmy również wady tego projektu. Na tym blogu ulepszymy ten projekt za pomocą dziedziczenia.

Wprowadzenie do dziedziczenia

Patrząc wstecz na metodę wielodostępną zaimplementowaną z oddzielnymi tabelami i widokami, przypominamy, że jej główną wadą jest brak możliwości wstawiania/aktualizacji/usuwania. W chwili, gdy wypróbujemy aktualizację w wypożyczeniu widok otrzymamy ten BŁĄD:

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.

Tak więc musielibyśmy utworzyć wyzwalacz lub regułę w wypożyczeniu widok określający funkcję do obsługi wstawiania/aktualizowania/usuwania. Alternatywą jest użycie dziedziczenia. Zmieńmy schemat poprzedniego bloga:

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

Teraz stwórzmy główną tabelę nadrzędną:

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

W terminologii OO ta tabela odpowiada superklasie (w terminologii java). Teraz zdefiniujmy tabele potomne przez dziedziczenie z public.rental, a także dodanie kolumny dla każdej tabeli, która jest specyficzna dla domeny:np. obowiązkowy numer prawa jazdy (klienta) w przypadku samochodów oraz opcjonalny certyfikat żeglarski łodzi.

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

Dwa stoły cars.rental i boats.rental dziedziczy wszystkie kolumny od swojego rodzica 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

Zauważyliśmy, że pominęliśmy firmę w definicji tabeli nadrzędnej (a w konsekwencji również w tabelach podrzędnych). Nie jest to już potrzebne, ponieważ identyfikacja najemcy znajduje się w pełnej nazwie tabeli! Później zobaczymy prosty sposób na znalezienie tego w zapytaniach. Teraz wstawmy kilka wierszy w trzech tabelach (pożyczamy klientów schemat i dane z poprzedniego bloga):

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

Zobaczmy teraz, co jest w tabelach:

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 więc te same pojęcia dziedziczenia, które istnieją w językach zorientowanych obiektowo (takich jak Java), istnieją również w PostgreSQL! Możemy o tym pomyśleć w następujący sposób:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instancja public.rental
row cars.rental.id =2:instancja cars.rental i public.rental
row boats.rental.id =3:instancja boats.rental i public.rental

Ponieważ rzędy boats.rental i cars.rental są również przykładami public.rental, naturalne jest, że pojawiają się one jako rzędy public.rental. Jeśli chcemy tylko wierszy z wyłączeniem public.rental (innymi słowy wierszy wstawionych bezpośrednio do public.rental), robimy to za pomocą słowa kluczowego ONLY w następujący sposób:

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

Jedyna różnica między Javą i PostgreSQL, jeśli chodzi o dziedziczenie, jest taka:Java nie obsługuje dziedziczenia wielokrotnego, podczas gdy PostgreSQL tak, możliwe jest dziedziczenie z więcej niż jednej tabeli, więc w tym kontekście możemy myśleć o tabelach bardziej jak interfejsy w Javie.

Jeśli chcemy znaleźć dokładną tabelę w hierarchii, do której należy określony wiersz (odpowiednik obj.getClass().getName() w java), możemy to zrobić, określając specjalną kolumnę tableoid (oid odpowiedniej tabeli w pgclass ), rzutowane na regclass, co daje pełną nazwę tabeli:

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 powyższego (inny tableoid) możemy wywnioskować, że tabele w hierarchii są zwykłymi starymi tabelami PostgreSQL, połączonymi relacją dziedziczenia. Ale poza tym zachowują się prawie jak normalne stoły. Zostanie to dokładniej podkreślone w następnej sekcji.

Ważne fakty i zastrzeżenia dotyczące dziedziczenia PostgreSQL

Tabela podrzędna dziedziczy:

  • Ograniczenia NOT NULL
  • SPRAWDŹ ograniczenia

Tabela podrzędna NIE dziedziczy:

  • Ograniczenia KLUCZU PODSTAWOWEGO
  • UNIKALNE ograniczenia
  • Ograniczenia KLUCZA OBCEGO

Gdy kolumny o tej samej nazwie pojawiają się w definicji więcej niż jednej tabeli w hierarchii, wówczas te kolumny muszą mieć ten sam typ i są scalane w jedną kolumnę. Jeśli istnieje ograniczenie NOT NULL dla nazwy kolumny w dowolnym miejscu w hierarchii, jest ono dziedziczone do tabeli podrzędnej. Ograniczenia CHECK o tej samej nazwie są również scalane i muszą mieć ten sam warunek.

Zmiany schematu w tabeli nadrzędnej (poprzez ALTER TABLE) są propagowane w całej hierarchii istniejącej poniżej tej tabeli nadrzędnej. I to jest jedna z fajnych funkcji dziedziczenia w PostgreSQL.

Zasady bezpieczeństwa i polityki bezpieczeństwa (RLS) są ustalane na podstawie rzeczywistej tabeli, której używamy. Jeśli użyjemy tabeli nadrzędnej, zostaną użyte zabezpieczenia i RLS tej tabeli. Zakłada się, że nadanie uprawnień do tabeli nadrzędnej daje uprawnienia również tabelom podrzędnym, ale tylko wtedy, gdy dostęp do nich uzyskuje się za pośrednictwem tabeli nadrzędnej. Aby uzyskać bezpośredni dostęp do tabeli podrzędnej, musimy bezpośrednio nadać GRANT tabeli podrzędnej, uprawnienia do tabeli nadrzędnej nie wystarczą. To samo dotyczy RLS.

Jeśli chodzi o uruchamianie wyzwalaczy, wyzwalacze na poziomie instrukcji zależą od nazwanej tabeli instrukcji, podczas gdy wyzwalacze na poziomie wiersza będą uruchamiane w zależności od tabeli, do której należy rzeczywisty wiersz (więc może to być tabela podrzędna).

Na co należy uważać:

  • Większość poleceń działa na całej hierarchii i obsługuje TYLKO notację. Jednak niektóre polecenia niskiego poziomu (REINDEX, VACUUM itp.) działają tylko na tabelach fizycznych nazwanych przez polecenie. W razie wątpliwości zapoznaj się z dokumentacją za każdym razem.
  • Ograniczenia KLUCZA OBCEGO (tabela nadrzędna będąca po stronie odniesienia) nie są dziedziczone. Można to łatwo rozwiązać, określając to samo ograniczenie FK we wszystkich tabelach podrzędnych hierarchii.
  • Od tego momentu (PostgreSQL 10) nie ma możliwości posiadania globalnego UNIQUE INDEX (klucze PRIMARY lub ograniczenia UNIQUE) w grupie tabel. W wyniku tego:
    • Ograniczenia KLUCZ GŁÓWNY i UNIKALNE nie są dziedziczone i nie ma łatwego sposobu na wymuszenie unikalności kolumny we wszystkich członkach hierarchii
    • Gdy tabela nadrzędna znajduje się po stronie, do której odnosi się ograniczenie FOREIGN KEY, sprawdzane jest tylko wartości kolumny w wierszach rzeczywiście (fizycznie) należących do tabeli nadrzędnej, a nie w tabelach podrzędnych.

Ostatnie ograniczenie jest poważne. Według oficjalnych dokumentów nie ma dobrego obejścia tego problemu. Jednak FK i unikatowość są podstawą każdego poważnego projektu bazy danych. Zastanowimy się, jak sobie z tym poradzić.

Pobierz oficjalny dokument już dziś Zarządzanie i automatyzacja PostgreSQL za pomocą ClusterControlDowiedz się, co musisz wiedzieć, aby wdrażać, monitorować, zarządzać i skalować PostgreSQLPobierz oficjalny dokument

Dziedziczenie w praktyce

W tej sekcji przekonwertujemy klasyczny projekt ze zwykłymi tabelami, ograniczeniami PRIMARY KEY/UNIKALNY i OBCEJ, na projekt wielodostępny oparty na dziedziczeniu i spróbujemy rozwiązać (oczekiwane zgodnie z poprzednim rozdziałem) problemy, które Twarz. Rozważmy tę samą działalność związaną z wynajmem, której użyliśmy jako przykład w poprzednim blogu i wyobraźmy sobie, że na początku firma zajmuje się tylko wynajmem samochodów (bez łodzi i innych typów pojazdów). Rozważmy następujący schemat z pojazdami firmy i historią serwisową tych pojazdów:

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

Teraz wyobraźmy sobie, że system jest w produkcji, a następnie firma przejmuje drugą firmę, która wynajmuje łodzie i musi je zintegrować z systemem, dzięki temu, że obie firmy działają niezależnie w zakresie operacji, ale w zunifikowany sposób dla wykorzystanie przez najwyższe kierownictwo. Wyobraźmy sobie również, że dane vehicle_service nie mogą być dzielone, ponieważ wszystkie wiersze muszą być widoczne dla obu firm. Poszukujemy więc rozwiązania dla wielu najemców, opartego na dziedziczeniu po tabeli pojazdów. Najpierw powinniśmy utworzyć nowy schemat dla samochodów (stary biznes) i jeden dla łodzi, a następnie przenieść istniejące dane 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

Zauważamy, że nowe tabele mają tę samą domyślną wartość dla kolumny id (taka sama sekwencja) jak tabela nadrzędna. Chociaż jest to dalekie od rozwiązania problemu globalnego unikatowości wyjaśnionego w poprzedniej sekcji, jest to obejście, pod warunkiem, że żadna wyraźna wartość nie będzie nigdy używana do wstawiania lub aktualizacji. Jeśli wszystkie tabele podrzędne (cars.vehicle i boats.vehicle) są zdefiniowane jak powyżej i nigdy nie manipulujemy jawnie identyfikatorem, wtedy będziemy bezpieczni.

Ponieważ zachowamy tylko publiczną tabelę vehicle_service, która będzie odwoływać się do wierszy tabel podrzędnych, musimy usunąć ograniczenie FK:

rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Ale ponieważ musimy zachować równoważną spójność w naszej bazie danych, musimy znaleźć rozwiązanie tego problemu. Zaimplementujemy to ograniczenie za pomocą wyzwalaczy. Musimy dodać wyzwalacz do vehicle_service, który sprawdza, czy dla każdego INSERT lub UPDATE identyfikator pojazdu wskazuje prawidłowy wiersz gdzieś w hierarchii public.vehicle*, oraz jeden wyzwalacz w każdej z tabel tej hierarchii, który sprawdza to dla każdego DELETE lub UPDATE na id, nie istnieje wiersz w vehicle_service, który wskazuje na starą wartość. (uwaga po oznaczeniu pojazdu* PostgreSQL oznacza tę i wszystkie tabele potomne)

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

Jeśli spróbujemy zaktualizować lub wstawić wartość kolumny id pojazdu, która nie istnieje w pojeździe*, otrzymamy błąd:

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

Teraz, jeśli wstawimy wiersz w dowolnej tabeli w hierarchii, np. boats.vehicle (który normalnie przyjmuje id=2) i spróbuj ponownie:

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

Wtedy poprzednia INSERT teraz się powiedzie. Teraz powinniśmy również chronić tę relację FK po drugiej stronie, musimy upewnić się, że żadna aktualizacja/usunięcie nie jest dozwolona w żadnej tabeli w hierarchii, jeśli do wiersza, który ma zostać usunięty (lub zaktualizowany) odwołuje się 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();

Spróbujmy:

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

Teraz musimy przenieść istniejące dane z 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 ;

Ustawienie session_replication_role TO replika zapobiega uruchamianiu normalnych wyzwalaczy. Zauważ, że po przeniesieniu danych możemy chcieć całkowicie wyłączyć tabelę nadrzędną (public.vehicle) przyjmowania wstawek (najprawdopodobniej poprzez regułę). W tym przypadku w analogii OO potraktowalibyśmy public.vehicle jako klasę abstrakcyjną, czyli bez wierszy (instancji). Korzystanie z tego projektu w przypadku wielu najemców wydaje się naturalne, ponieważ problem, który należy rozwiązać, jest klasycznym przypadkiem użycia dziedziczenia, jednak problemy, z którymi się zmierzyliśmy, nie są trywialne. Zostało to omówione przez społeczność hakerów i mamy nadzieję na przyszłe ulepszenia.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Używanie Barmana do odzyskiwania po awarii PostgreSQL

  2. Zadanie cron do usuwania starych danych z postgres na debianie

  3. połącz się z serwerem postgres na silniku obliczeniowym Google

  4. Wstawiaj dane w 3 tabelach jednocześnie za pomocą Postgres

  5. Błąd składni przy lub w pobliżu użytkownika podczas dodawania ograniczenia Postgres