PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

„O“ в ORDBMS:Наследяване от PostgreSQL

В този запис в блога ще преминем през наследяването на PostgreSQL, традиционно една от най-добрите функции на PostgreSQL от ранните версии. Някои типични употреби на наследяване в PostgreSQL са:

  • разделяне на таблица
  • мулти-наемане

PostgreSQL до версия 10 имплементира разделяне на таблици с помощта на наследяване. PostgreSQL 10 предоставя нов начин за декларативно разделяне. Разделянето на PostgreSQL с помощта на наследяване е доста зряла технология, добре документирана и тествана, но наследяването в PostgreSQL от гледна точка на модела на данни (според мен) не е толкова широко разпространено, затова ще се концентрираме върху по-класически случаи на употреба в този блог. Видяхме от предишния блог (опции за множество наемания за PostgreSQL), че един от методите за постигане на мулти-наемане е да се използват отделни таблици и след това да се консолидират чрез изглед. Видяхме и недостатъците на този дизайн. В този блог ще подобрим този дизайн с помощта на наследяване.

Въведение в наследяването

Поглеждайки назад към метода с много наемане, реализиран с разделящи таблици и изгледи, си припомняме, че основният му недостатък е невъзможността да се правят вмъквания/актуализации/изтривания. В момента, в който опитаме актуализация за наема преглед, ще получим тази ГРЕШКА:

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.

Така че ще трябва да създадем задействане или правило за наемането изглед, указващ функция за обработка на вмъкване/актуализация/изтриване. Алтернативата е да се използва наследяване. Нека променим схемата на предишния блог:

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

Сега нека създадем основната родителска таблица:

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

В термините на OO тази таблица съответства на суперкласа (в терминологията на Java). Сега нека дефинираме дъщерните таблици чрез наследяване от public.rental и също така добавяне на колона за всяка таблица, която е специфична за домейна:напр. задължителния номер на шофьорска книжка (клиент) в случай на автомобили и незадължителния сертификат за плаване на лодка.

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

Двете маси cars.rental и лодки.под наем наследяват всички колони от своя родител 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

Забелязваме, че сме пропуснали компанията колона в дефиницията на родителската таблица (и в резултат на това и в дъщерните таблици). Това вече не е необходимо, тъй като идентификацията на наемателя е в пълното име на таблицата! По-късно ще видим лесен начин да разберете това чрез запитвания. Сега нека вмъкнем няколко реда в трите таблици (заемаме клиенти схема и данни от предишния блог):

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

Сега нека видим какво има в таблиците:

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)

Така че същите понятия за наследяване, които съществуват в обектно-ориентирани езици (като Java), съществуват и в PostgreSQL! Можем да мислим за това по следния начин:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:екземпляр на public.rental
row cars.rental.id =2:екземпляр на cars.rental и public.rental
row boats.rental.id =3:екземпляр на boats.rental и public.rental

Тъй като редовете от boats.rental и cars.rental също са случаи на public.rental, естествено е те да се появяват като редове public.rental. Ако искаме само редове без public.rental (с други думи редовете, вмъкнати директно в public.rental), ние го правим с помощта на ключовата дума ONLY, както следва:

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

Една разлика между Java и PostgreSQL по отношение на наследяването е следната:Java не поддържа множествено наследяване, докато PostgreSQL го прави, възможно е да се наследява от повече от една таблици, така че в това отношение можем да мислим за таблици по-скоро като интерфейси в Java.

Ако искаме да разберем точната таблица в йерархията, където принадлежи конкретен ред (еквивалент на obj.getClass().getName() в java), можем да направим, като посочим специалната колона tableoid (oid на съответната таблица в pgclass ), прехвърлен към regclass, който дава пълното име на таблицата:

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)

От горното (различен таблоид) можем да заключим, че таблиците в йерархията са просто обикновени стари PostgreSQL таблици, свързани с връзка на наследяване. Но освен това те действат почти като нормални маси. И това ще бъде допълнително подчертано в следващия раздел.

Важни факти и предупреждения относно наследяването на PostgreSQL

Дъщерната таблица наследява:

  • Ограничения НЕ NULL
  • ПРОВЕРАЙТЕ ограниченията

Дъщерната таблица НЕ наследява:

  • Ограничения на PRIMARY KEY
  • УНИКАЛНИ ограничения
  • Ограничения FOREIGN KEY

Когато колони с едно и също име се появят в дефиницията на повече от една таблици в йерархията, тогава тези колони трябва да имат същия тип и да се обединят в една единствена колона. Ако съществува ограничение NOT NULL за име на колона някъде в йерархията, тогава това се наследява от дъщерната таблица. CHECK ограниченията със същото име също са обединени и трябва да имат същото условие.

Промените в схемата в родителската таблица (чрез ALTER TABLE) се разпространяват в цялата йерархия, която съществува под тази родителска таблица. И това е една от хубавите характеристики на наследяването в PostgreSQL.

Политиките за сигурност и сигурност (RLS) се определят въз основа на действителната таблица, която използваме. Ако използваме родителска таблица, тогава ще се използват сигурността и RLS на тази таблица. Подразбира се, че предоставянето на привилегия на родителската таблица дава разрешение и на дъщерната(ите) таблица(и), но само когато е достъпна през родителската таблица. За да получим директен достъп до дъщерната таблица, тогава трябва да дадем изрично GRANT директно на дъщерната таблица, привилегията на родителската таблица няма да е достатъчна. Същото важи и за RLS.

По отношение на задействането на задействания, тригерите на ниво оператор зависят от наименуваната таблица на оператора, докато тригерите на ниво ред ще се задействат в зависимост от таблицата, към която принадлежи действителният ред (така че може да е дъщерна таблица).

Неща, за които трябва да внимавате:

  • Повечето команди работят върху цялата йерархия и поддържат ЕДИНСТВЕНА нотация. Въпреки това, някои команди от ниско ниво (REINDEX, VACUUM и т.н.) работят само върху физическите таблици, посочени от командата. Не забравяйте да прочетете документацията всеки път в случай на съмнение.
  • Ограниченията FOREIGN KEY (родителската таблица е от референтната страна) не се наследяват. Това лесно се решава чрез посочване на едно и също FK ограничение във всички дъщерни таблици от йерархията.
  • От този момент (PostgreSQL 10) няма начин да имате глобален УНИКАЛЕН ИНДЕКС (ПЪРВИЧНИ КЛЮЧЕВЕ или УНИКАЛНИ ограничения) върху група таблици. В резултат на това:
    • Ограниченията PRIMARY KEY и UNIQUE не се наследяват и няма лесен начин да се наложи уникалност на колона във всички членове на йерархията.
    • Когато родителската таблица е от реферираната страна на ограничение FOREIGN KEY, тогава проверката се прави само за стойностите на колоната на редове, които действително (физически) принадлежат на родителската таблица, а не за всички дъщерни таблици.

Последното ограничение е сериозно. Според официалните документи няма добро решение за това. Въпреки това, FK и уникалността са основни за всеки сериозен дизайн на база данни. Ще търсим начин да се справим с това.

Изтеглете Бялата книга днес Управление и автоматизация на PostgreSQL с ClusterControl Научете какво трябва да знаете, за да внедрите, наблюдавате, управлявате и мащабирате PostgreSQLD Изтеглете Бялата книга

Наследяване на практика

В този раздел ще преобразуваме класически дизайн с обикновени таблици, ПЪРВИЧЕН КЛЮЧ/УНИКАЛЕН и ВЪНШЕН КЛЮЧ, в дизайн с множество наематели, базиран на наследяване и ще се опитаме да решим (очакваните според предишния раздел) проблеми, които ние лице. Нека разгледаме същия бизнес за отдаване под наем, който използвахме като пример в предишния блог, и нека си представим, че в началото бизнесът прави само коли под наем (без лодки или други видове превозни средства). Нека разгледаме следната схема с превозните средства на компанията и сервизната история на тези превозни средства:

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

Сега нека си представим, че системата е в производство, а след това компанията придобива втора компания, която отдава лодки под наем и трябва да ги интегрира в системата, като двете компании работят независимо, доколкото става въпрос, но по единен начин за използване от най-горния МГМТ. Също така, нека си представим, че данните за vehicle_service не трябва да се разделят, тъй като всички редове трябва да са видими и за двете компании. Така че това, което търсим, е да предоставим решение за няколко наемания, базирано на наследяване на масата на превозното средство. Първо, трябва да създадем нова схема за автомобили (старият бизнес) и една за лодки и след това да мигрираме съществуващите данни към 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

Отбелязваме, че новите таблици споделят същата стойност по подразбиране за колона id (същата последователност) като родителската таблица. Въпреки че това далеч не е решение на проблема с глобалната уникалност, обяснен в предишния раздел, това е заобикаляне, при условие че никога няма да се използва изрична стойност за вмъквания или актуализации. Ако всички дъщерни таблици (cars.vehicle и boats.vehicle) са дефинирани както по-горе и ние никога не манипулираме изрично идентификатор, тогава ще бъдем в безопасност.

Тъй като ще запазим само публичната таблица vehicle_service и това ще препраща към редове от дъщерни таблици, трябва да премахнем FK ограничението:

rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Но тъй като трябва да поддържаме еквивалентната последователност в нашата база данни, трябва да намерим решение за това. Ще приложим това ограничение, използвайки тригери. Трябва да добавим тригер към vehicle_service, който проверява, че за всяко INSERT или UPDATE идентификаторът на превозното средство сочи към валиден ред някъде в йерархията public.vehicle* и едно задействане във всяка от таблиците на тази йерархия, което проверява това за всяко DELETE или АКТУАЛИЗИРАНЕ на id, няма ред в vehicle_service, който да сочи към старата стойност. (забележка от превозното средство* нотация PostgreSQL предполага това и всички дъщерни таблици)

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

Ако се опитаме да актуализираме или вмъкнем със стойност за колона vehicleid, която не съществува в превозното средство*, тогава ще получим грешка:

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

Сега, ако вмъкнем ред в която и да е таблица в йерархията, напр. boats.vehicle (което обикновено ще приеме id=2) и опитайте отново:

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

Тогава предишният INSERT вече е успешен. Сега трябва да защитим и тази FK връзка от другата страна, трябва да се уверим, че не е позволено обновяване/изтриване на която и да е таблица в йерархията, ако редът, който трябва да бъде изтрит (или актуализиран), е посочен от 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();

Нека опитаме:

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

Сега трябва да преместим съществуващите данни в public.vehicle в 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 ;

Задаването на session_replication_role ЗА реплика предотвратява задействането на нормални тригери. Имайте предвид, че след преместване на данните може да искаме напълно да деактивираме родителската таблица (public.vehicle) за приемане на вмъквания (най-вероятно чрез правило). В този случай, в аналогията с OO, ще третираме public.vehicle като абстрактен клас, т.е. без редове (екземпляри). Използването на този дизайн за многократно наемане се чувства естествено, защото проблемът, който трябва да бъде решен, е класически случай на употреба за наследяване, но проблемите, с които се сблъскахме, не са тривиални. Това беше обсъдено от общността на хакерите и се надяваме на бъдещи подобрения.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Изявление на PostgreSQL IF

  2. PostgreSQL 8.4 предоставя DML привилегии за всички таблици на роля

  3. Как да създадете редни числа в PostgreSQL

  4. Няколко области на подобрения в PostgreSQL 9.4

  5. Как да използвам динамични имена на колони в оператор UPDATE или SELECT във функция?