sql >> Base de Datos >  >> RDS >> PostgreSQL

La “O” en ORDBMS:herencia de PostgreSQL

En esta entrada de blog, repasaremos la herencia de PostgreSQL, tradicionalmente una de las principales características de PostgreSQL desde los primeros lanzamientos. Algunos usos típicos de la herencia en PostgreSQL son:

  • partición de tablas
  • multiusuario

PostgreSQL hasta la versión 10 implementó el particionamiento de tablas usando herencia. PostgreSQL 10 proporciona una nueva forma de partición declarativa. El particionamiento de PostgreSQL mediante herencia es una tecnología bastante madura, bien documentada y probada; sin embargo, la herencia en PostgreSQL desde la perspectiva del modelo de datos (en mi opinión) no está tan extendida, por lo que nos concentraremos en casos de uso más clásicos en este blog. Vimos en el blog anterior (opciones de tenencia múltiple para PostgreSQL) que uno de los métodos para lograr la tenencia múltiple es usar tablas separadas y luego consolidarlas a través de una vista. También vimos los inconvenientes de este diseño. En este blog mejoraremos este diseño usando la herencia.

Introducción a la herencia

Mirando hacia atrás en el método de múltiples usuarios implementado con tablas y vistas separadas, recordamos que su principal desventaja es la incapacidad de realizar inserciones/actualizaciones/eliminaciones. En el momento en que intentamos una actualización del alquiler ver obtendremos este ERROR:

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.

Entonces, necesitaríamos crear un disparador o una regla en el alquiler ver especificando una función para manejar la inserción/actualización/eliminación. La alternativa es usar la herencia. Cambiemos el esquema del blog anterior:

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

Ahora vamos a crear la tabla principal principal:

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

En términos OO, esta tabla corresponde a la superclase (en la terminología de Java). Ahora definamos las tablas secundarias heredando de public.rental y también agregar una columna para cada tabla que sea específica del dominio:p. el número de carnet de conducir (cliente) obligatorio en caso de turismos y el certificado opcional de navegación en barco.

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

Las dos tablas cars.rental y barcos.alquiler hereda todas las columnas de su padre 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

Notamos que omitimos la empresa columna en la definición de la tabla principal (y como consecuencia también en las tablas secundarias). ¡Esto ya no es necesario ya que la identificación del inquilino está en el nombre completo de la mesa! Más adelante veremos una manera fácil de averiguarlo en las consultas. Ahora insertemos algunas filas en las tres tablas (tomamos prestados clientes esquema y datos del blog anterior):

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

Ahora veamos qué hay en las tablas:

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)

¡Así que las mismas nociones de herencia que existen en los lenguajes orientados a objetos (como Java) existen también en PostgreSQL! Podemos pensar en esto de la siguiente manera:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instancia de public.rental
row cars.rental.id =2:instancia de cars.rental y public.rental
row boats.rental.id =3:instancia de boats.rental y public.rental

Dado que las filas de alquiler.de.barcos y alquiler.de.coches también son instancias de alquiler.público, es natural que aparezcan como filas de alquiler.público. Si solo queremos filas exclusivas de public.rental (en otras palabras, las filas insertadas directamente en public.rental), lo hacemos usando la palabra clave ÚNICA de la siguiente manera:

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

Una diferencia entre Java y PostgreSQL en lo que respecta a la herencia es la siguiente:Java no admite herencia múltiple mientras que PostgreSQL sí, es posible heredar de más de una tabla, por lo que en este sentido podemos pensar en tablas más como interfaces en Java.

Si queremos encontrar la tabla exacta en la jerarquía a la que pertenece una fila específica (el equivalente de obj.getClass().getName() en java) podemos hacerlo especificando la columna especial tableoid (oid de la tabla respectiva en pgclass ), convertido a regclass que da el nombre completo de la tabla:

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 lo anterior (tableoide diferente) podemos inferir que las tablas en la jerarquía son simplemente tablas antiguas de PostgreSQL, conectadas con una relación de herencia. Pero además de esto, actúan como mesas normales. Y esto se enfatizará más en la siguiente sección.

Hechos importantes y advertencias sobre la herencia de PostgreSQL

La tabla secundaria hereda:

  • Restricciones NO NULAS
  • COMPROBAR restricciones

La tabla secundaria NO hereda:

  • restricciones PRIMARY KEY
  • Restricciones ÚNICAS
  • Restricciones de FOREIGN KEY

Cuando aparecen columnas con el mismo nombre en la definición de más de una tabla en la jerarquía, esas columnas deben tener el mismo tipo y se fusionan en una sola columna. Si existe una restricción NOT NULL para un nombre de columna en cualquier parte de la jerarquía, se hereda a la tabla secundaria. Las restricciones CHECK con el mismo nombre también se fusionan y deben tener la misma condición.

Los cambios de esquema en la tabla principal (a través de ALTER TABLE) se propagan a través de la jerarquía que existe debajo de esta tabla principal. Y esta es una de las buenas características de la herencia en PostgreSQL.

Las políticas de seguridad y seguridad (RLS) se deciden en función de la tabla real que usamos. Si usamos una tabla principal, se usarán la seguridad y el RLS de esa tabla. Se da a entender que otorgar un privilegio en la tabla principal también da permiso a la(s) tabla(s) secundaria(s), pero solo cuando se accede a través de la tabla principal. Para acceder a la tabla secundaria directamente, debemos otorgar GRANT explícito directamente a la tabla secundaria, el privilegio en la tabla principal no será suficiente. Lo mismo es cierto para RLS.

Con respecto a la activación de activadores, los activadores a nivel de instrucción dependen de la tabla nombrada de la instrucción, mientras que los activadores a nivel de fila se activarán según la tabla a la que pertenece la fila real (por lo que podría ser una tabla secundaria).

Cosas a tener en cuenta:

  • La mayoría de los comandos funcionan en toda la jerarquía y admiten la notación ÚNICA. Sin embargo, algunos comandos de bajo nivel (REINDEX, VACUUM, etc.) solo funcionan en las tablas físicas nombradas por el comando. Asegúrese de leer la documentación cada vez en caso de duda.
  • Las restricciones FOREIGN KEY (la tabla principal está en el lado de referencia) no se heredan. Esto se resuelve fácilmente especificando la misma restricción FK en todas las tablas secundarias de la jerarquía.
  • A partir de este punto (PostgreSQL 10), no hay forma de tener un ÍNDICE ÚNICO global (CLAVE PRINCIPAL o restricciones ÚNICAS) en un grupo de tablas. Como resultado de esto:
    • Las restricciones PRIMARY KEY y UNIQUE no se heredan, y no hay una manera fácil de imponer la unicidad en una columna en todos los miembros de la jerarquía
    • Cuando la tabla principal está en el lado referenciado de una restricción FOREIGN KEY, la verificación se realiza solo para los valores de la columna en las filas que pertenecen genuinamente (físicamente) a la tabla principal, no en las tablas secundarias.
    • >

La última limitación es seria. De acuerdo con los documentos oficiales, no existe una buena solución para esto. Sin embargo, FK y la unicidad son fundamentales para cualquier diseño de base de datos serio. Buscaremos una manera de lidiar con esto.

Descargue el documento técnico hoy Gestión y automatización de PostgreSQL con ClusterControl Obtenga información sobre lo que necesita saber para implementar, monitorear, administrar y escalar PostgreSQLDescargar el documento técnico

La herencia en la práctica

En esta sección, convertiremos un diseño clásico con tablas simples, restricciones PRIMARY KEY/UNIQUE y FOREIGN KEY, a un diseño multiusuario basado en la herencia e intentaremos resolver los problemas (esperados según la sección anterior) que rostro. Consideremos el mismo negocio de alquiler que usamos como ejemplo en el blog anterior e imaginemos que al principio el negocio solo alquila autos (no botes u otro tipo de vehículos). Consideremos el siguiente esquema, con los vehículos de la empresa y el historial de servicio de dichos vehículos:

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

Ahora imaginemos que el sistema está en producción, y luego la empresa adquiere una segunda empresa que alquila barcos y tiene que integrarlos en el sistema, haciendo que las dos empresas operen de forma independiente en lo que respecta a la operación, pero de manera unificada para uso por el mgmt superior. Además, imaginemos que los datos de vehicle_service no deben dividirse ya que todas las filas deben ser visibles para ambas empresas. Entonces, lo que buscamos es brindar una solución de tenencia múltiple basada en la herencia en la tabla de vehículos. Primero, debemos crear un nuevo esquema para automóviles (el antiguo negocio) y uno para barcos y luego migrar los datos existentes a 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

Notamos que las nuevas tablas comparten el mismo valor predeterminado para la columna id (misma secuencia) que la tabla principal. Si bien esto está lejos de ser una solución al problema de unicidad global explicado en la sección anterior, es una solución alternativa, siempre que nunca se use un valor explícito para inserciones o actualizaciones. Si todas las tablas secundarias (automóviles.vehículo y botes.vehículo) se definen como se indicó anteriormente y nunca manipulamos explícitamente la identificación, entonces estaremos a salvo.

Dado que mantendremos solo la tabla public vehicle_service y esta hará referencia a las filas de las tablas secundarias, debemos eliminar la restricción FK:

rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Pero debido a que necesitamos mantener la consistencia equivalente en nuestra base de datos, debemos encontrar una solución para esto. Implementaremos esta restricción usando disparadores. Necesitamos agregar un disparador a vehicle_service que verifique que para cada INSERCIÓN o ACTUALIZACIÓN, el ID del vehículo apunte a una fila válida en algún lugar de la jerarquía public.vehicle*, y un disparador en cada una de las tablas de esta jerarquía que verifique eso para cada ELIMINACIÓN o ELIMINACIÓN. ACTUALIZAR en id, no existe ninguna fila en vehicle_service que apunte al valor anterior. (nota por la notación de vehículo * PostgreSQL implica esta y todas las tablas secundarias)

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 intentamos actualizar o insertar un valor para la columna ID del vehículo que no existe en el vehículo*, obtendremos un error:

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

Ahora bien, si insertamos una fila en cualquier tabla de la jerarquía, p. barcos.vehículo (que normalmente tomará id=2) y vuelva a intentarlo:

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

Entonces el INSERT anterior ahora tiene éxito. Ahora también debemos proteger esta relación FK en el otro lado, debemos asegurarnos de que no se permita ninguna actualización/eliminación en ninguna tabla de la jerarquía si la fila que se eliminará (o actualizará) está referenciada por 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();

Intentémoslo:

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

Ahora necesitamos mover los datos existentes en public.vehicle a 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 ;

Establecer session_replication_role TO replica evita la activación de desencadenadores normales. Tenga en cuenta que, después de mover los datos, es posible que queramos deshabilitar completamente la tabla principal (public.vehicle) para aceptar inserciones (muy probablemente a través de una regla). En este caso, en la analogía OO, trataríamos public.vehicle como una clase abstracta, es decir, sin filas (instancias). Usar este diseño para multiusuario se siente natural porque el problema a resolver es un caso de uso clásico para la herencia, sin embargo, los problemas que enfrentamos no son triviales. Esto ha sido discutido por la comunidad de hackers y esperamos futuras mejoras.