Dans cette entrée de blog, nous passerons en revue l'héritage PostgreSQL, traditionnellement l'une des principales fonctionnalités de PostgreSQL depuis les premières versions. Certaines utilisations typiques de l'héritage dans PostgreSQL sont :
- partitionnement de table
- multilocation
PostgreSQL jusqu'à la version 10 implémentait le partitionnement de table par héritage. PostgreSQL 10 fournit une nouvelle méthode de partitionnement déclaratif. Le partitionnement PostgreSQL utilisant l'héritage est une technologie assez mature, bien documentée et testée, mais l'héritage dans PostgreSQL du point de vue du modèle de données n'est (à mon avis) pas si répandu, nous nous concentrerons donc sur des cas d'utilisation plus classiques dans ce blog. Nous avons vu dans le blog précédent (options de multi-location pour PostgreSQL) que l'une des méthodes pour réaliser la multi-location est d'utiliser des tables séparées puis de les consolider via une vue. Nous avons également vu les inconvénients de cette conception. Dans ce blog, nous allons améliorer cette conception en utilisant l'héritage.
Introduction à l'héritage
En repensant à la méthode multi-tenant implémentée avec des tables et des vues séparées, nous rappelons que son inconvénient majeur est l'impossibilité de faire des insertions/mises à jour/suppressions. Au moment où nous essayons une mise à jour sur la location voir, nous aurons cette ERREUR :
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.
Donc, nous aurions besoin de créer un déclencheur ou une règle sur la location view spécifiant une fonction pour gérer l'insertion/mise à jour/suppression. L'alternative est d'utiliser l'héritage. Changeons le schéma du blog précédent :
template1=# create database rentaldb_hier;
template1=# \c rentaldb_hier
rentaldb_hier=# create schema boats;
rentaldb_hier=# create schema cars;
Créons maintenant la table parent principale :
rentaldb_hier=# CREATE TABLE rental (
id integer NOT NULL,
customerid integer NOT NULL,
vehicleno text,
datestart date NOT NULL,
dateend date
);
En termes OO cette table correspond à la superclasse (dans la terminologie java). Définissons maintenant les tables enfants en héritant de public.rental et en ajoutant également une colonne pour chaque table spécifique au domaine :par ex. le numéro de permis de conduire (client) obligatoire dans le cas des voitures et le certificat de navigation en bateau facultatif.
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);
Les deux tableaux cars.rental et bateaux.location hériter de toutes les colonnes de leur parent 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
Nous remarquons que nous avons omis la entreprise colonne dans la définition de la table parent (et par conséquent dans les tables enfants également). Ce n'est plus nécessaire puisque l'identification du locataire est dans le nom complet du tableau ! Nous verrons plus loin un moyen simple de le savoir dans les requêtes. Insérons maintenant quelques lignes dans les trois tableaux (nous empruntons des clients schéma et données du blog précédent) :
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);
Voyons maintenant ce qu'il y a dans les tableaux :
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)
Ainsi, les mêmes notions d'héritage qui existent dans les langages orientés objet (comme Java) existent aussi dans PostgreSQL ! Nous pouvons penser à cela comme suit :
public.rental :superclasse
cars.rental :sous-classe
boats.rental :sous-classe
row public.rental.id =1 :instance de public.rental
ligne cars.rental.id =2 :instance de cars.rental et public.rental
row boats.rental.id =3 :instance de boats.rental et public.rental
Puisque les rangées de boats.rental et cars.rental sont aussi des instances de public.rental, il est naturel qu'elles apparaissent comme des rangées de public.rental. Si nous ne voulons que des lignes exclusives de public.rental (c'est-à-dire les lignes insérées directement dans public.rental), nous le faisons en utilisant le mot-clé ONLY comme suit :
rentaldb_hier=# select * from ONLY rental ;
id | customerid | vehicleno | datestart | dateend
----+------------+------------------------+------------+---------
1 | 1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
(1 row)
Une différence entre Java et PostgreSQL en ce qui concerne l'héritage est la suivante :Java ne prend pas en charge l'héritage multiple alors que PostgreSQL le fait, il est possible d'hériter de plusieurs tables, donc à cet égard, nous pouvons penser à des tables plus comme des interfaces en Java.
Si nous voulons connaître la table exacte dans la hiérarchie à laquelle appartient une ligne spécifique (l'équivalent de obj.getClass().getName() en java), nous pouvons le faire en spécifiant la colonne spéciale tableoid (oid de la table respective dans pgclass ), converti en regclass qui donne le nom complet de la table :
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)
De ce qui précède (tableoid différent), nous pouvons déduire que les tables de la hiérarchie sont simplement de vieilles tables PostgreSQL, liées à une relation d'héritage. Mais à part cela, ils agissent à peu près comme des tables normales. Et cela sera davantage souligné dans la section suivante.
Faits importants et mises en garde concernant l'héritage PostgreSQL
La table enfant hérite :
- Contraintes NOT NULL
- Contraintes CHECK
La table enfant n'hérite PAS :
- Contraintes PRIMARY KEY
- Contraintes UNIQUES
- Contraintes FOREIGN KEY
Lorsque des colonnes portant le même nom apparaissent dans la définition de plusieurs tables de la hiérarchie, ces colonnes doivent avoir le même type et sont fusionnées en une seule colonne. Si une contrainte NOT NULL existe pour un nom de colonne n'importe où dans la hiérarchie, elle est héritée de la table enfant. Les contraintes CHECK portant le même nom sont également fusionnées et doivent avoir la même condition.
Les modifications de schéma apportées à la table parent (via ALTER TABLE) sont propagées dans toute la hiérarchie qui existe sous cette table parent. Et c'est l'une des fonctionnalités intéressantes de l'héritage dans PostgreSQL.
Les politiques de sécurité et de sécurité (RLS) sont décidées en fonction du tableau réel que nous utilisons. Si nous utilisons une table parent, la sécurité et le RLS de cette table seront utilisés. Il est sous-entendu que l'octroi d'un privilège sur la table parent donne également l'autorisation à la ou aux tables enfants, mais uniquement lors d'un accès via la table parent. Pour accéder directement à la table enfant, nous devons alors donner un GRANT explicite directement à la table enfant, le privilège sur la table parent ne suffira pas. Il en va de même pour RLS.
En ce qui concerne le déclenchement des déclencheurs, les déclencheurs au niveau de l'instruction dépendent de la table nommée de l'instruction, tandis que les déclencheurs au niveau de la ligne seront déclenchés en fonction de la table à laquelle appartient la ligne réelle (il peut donc s'agir d'une table enfant).
Points à surveiller :
- La plupart des commandes fonctionnent sur toute la hiérarchie et prennent en charge la notation ONLY. Cependant, certaines commandes de bas niveau (REINDEX, VACUUM, etc.) ne fonctionnent que sur les tables physiques nommées par la commande. Assurez-vous de lire la documentation à chaque fois en cas de doute.
- Les contraintes FOREIGN KEY (la table parent étant côté référence) ne sont pas héritées. Ceci est facilement résolu en spécifiant la même contrainte FK dans toutes les tables enfants de la hiérarchie.
- À ce stade (PostgreSQL 10), il n'y a aucun moyen d'avoir un INDEX UNIQUE global (CLÉ PRIMAIRE ou contraintes UNIQUE) sur un groupe de tables. En conséquence :
- Les contraintes PRIMARY KEY et UNIQUE ne sont pas héritées, et il n'existe aucun moyen simple d'appliquer l'unicité à une colonne parmi tous les membres de la hiérarchie
- Lorsque la table parent est du côté référencé d'une contrainte FOREIGN KEY, la vérification est effectuée uniquement pour les valeurs de la colonne sur les lignes appartenant véritablement (physiquement) à la table parent, et non sur les tables enfants.
La dernière limitation est sérieuse. Selon les documents officiels, il n'y a pas de bonne solution de contournement pour cela. Cependant, FK et l'unicité sont fondamentales pour toute conception de base de données sérieuse. Nous allons chercher un moyen de gérer cela.
Téléchargez le livre blanc aujourd'hui PostgreSQL Management &Automation with ClusterControlDécouvrez ce que vous devez savoir pour déployer, surveiller, gérer et faire évoluer PostgreSQLTélécharger le livre blancL'héritage en pratique
Dans cette section, nous allons convertir une conception classique avec des tables simples, des contraintes PRIMARY KEY/UNIQUE et FOREIGN KEY, en une conception multi-tenant basée sur l'héritage et nous essaierons de résoudre les problèmes (attendus selon la section précédente) que nous Visage. Considérons la même entreprise de location que nous avons utilisée comme exemple dans le blog précédent et imaginons qu'au début l'entreprise ne fait que de la location de voitures (pas de bateaux ou d'autres types de véhicules). Considérons le schéma suivant, avec les véhicules de l'entreprise et l'historique d'entretien de ces véhicules :
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);
Imaginons maintenant que le système soit en production, puis l'entreprise acquiert une deuxième société qui s'occupe de la location de bateaux et doit les intégrer dans le système, en faisant fonctionner les deux sociétés de manière indépendante en ce qui concerne l'exploitation, mais de manière unifiée pour utilisation par le top mgmt. Imaginons également que les données vehicle_service ne doivent pas être fractionnées puisque toutes les lignes doivent être visibles pour les deux sociétés. Donc, ce que nous recherchons, c'est fournir une solution multi-tenant basée sur l'héritage sur la table des véhicules. Tout d'abord, nous devons créer un nouveau schéma pour les voitures (l'ancienne entreprise) et un autre pour les bateaux, puis migrer les données existantes vers 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
Nous notons que les nouvelles tables partagent la même valeur par défaut pour la colonne id (même séquence) que la table parent. Bien que ce soit loin d'être une solution au problème d'unicité globale expliqué dans la section précédente, il s'agit d'un contournement, à condition qu'aucune valeur explicite ne soit jamais utilisée pour les insertions ou les mises à jour. Si toutes les tables enfants (cars.vehicle et boats.vehicle) sont définies comme ci-dessus, et que nous ne manipulons jamais explicitement l'identifiant, alors nous serons en sécurité.
Comme nous ne conserverons que la table publique vehicle_service et que celle-ci référencera les lignes des tables enfants, nous devons supprimer la contrainte FK :
rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;
Mais parce que nous devons maintenir la cohérence équivalente dans notre base de données, nous devons trouver une solution pour cela. Nous allons implémenter cette contrainte à l'aide de déclencheurs. Nous devons ajouter un déclencheur à vehicle_service qui vérifie que pour chaque INSERT ou UPDATE, le vehicleid pointe vers une ligne valide quelque part dans la hiérarchie public.vehicle*, et un déclencheur sur chacune des tables de cette hiérarchie qui vérifie que pour chaque DELETE ou UPDATE sur id, aucune ligne dans vehicle_service n'existe qui pointe vers l'ancienne valeur. (notez par la notation véhicule* que PostgreSQL implique ceci et toutes les tables enfants)
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();
Si nous essayons de mettre à jour ou d'insérer une valeur pour la colonne vehicleid qui n'existe pas dans vehicle*, nous obtiendrons une erreur :
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
Maintenant, si nous insérons une ligne dans n'importe quelle table de la hiérarchie, par ex. boats.vehicle (qui prendra normalement id=2) et réessayez :
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);
Ensuite, l'INSERT précédent réussit maintenant. Maintenant, nous devons également protéger cette relation FK de l'autre côté, nous devons nous assurer qu'aucune mise à jour/suppression n'est autorisée sur aucune table de la hiérarchie si la ligne à supprimer (ou mettre à jour) est référencée par 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();
Essayons :
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
Nous devons maintenant déplacer les données existantes dans public.vehicle vers 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 ;
La définition de session_replication_role TO réplique empêche le déclenchement des déclencheurs normaux. Notez qu'après avoir déplacé les données, nous pouvons vouloir désactiver complètement la table parent (public.vehicle) d'accepter les insertions (très probablement via une règle). Dans ce cas, dans l'analogie OO, nous traiterions public.vehicle comme une classe abstraite, c'est-à-dire sans lignes (instances). L'utilisation de cette conception pour la multilocation semble naturelle car le problème à résoudre est un cas d'utilisation classique pour l'héritage, cependant, les problèmes auxquels nous avons été confrontés ne sont pas anodins. Cela a été discuté par la communauté des hackers, et nous espérons de futures améliorations.