PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

O “O” em ORDBMS:herança do PostgreSQL


Nesta entrada de blog, veremos a herança do PostgreSQL, tradicionalmente um dos principais recursos do PostgreSQL desde os primeiros lançamentos. Alguns usos típicos de herança no PostgreSQL são:
  • particionamento de tabela
  • multilocação

PostgreSQL até a versão 10 implementava particionamento de tabelas usando herança. O PostgreSQL 10 fornece uma nova forma de particionamento declarativo. O particionamento do PostgreSQL usando herança é uma tecnologia bastante madura, bem documentada e testada, porém a herança no PostgreSQL do ponto de vista do modelo de dados não é (na minha opinião) tão difundida, portanto, nos concentraremos em casos de uso mais clássicos neste blog. Vimos no blog anterior (opções de multilocação para PostgreSQL) que um dos métodos para obter multilocação é usar tabelas separadas e depois consolidá-las por meio de uma visualização. Também vimos as desvantagens desse design. Neste blog vamos aprimorar esse design usando herança.

Introdução à herança


Olhando para o método multi-tenancy implementado com tabelas e visualizações separadas, lembramos que sua principal desvantagem é a incapacidade de fazer inserções/atualizações/exclusões. No momento em que tentarmos atualizar o aluguel view, obteremos este ERRO:
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.

Então, precisaríamos criar um acionador ou uma regra no aluguel view especificando uma função para manipular a inserção/atualização/exclusão. A alternativa é usar herança. Vamos mudar o esquema do blog anterior:
template1=# create database rentaldb_hier;
template1=# \c rentaldb_hier
rentaldb_hier=# create schema boats;
rentaldb_hier=# create schema cars;

Agora vamos criar a tabela pai principal:
rentaldb_hier=# CREATE TABLE rental (
    id integer NOT NULL,
    customerid integer NOT NULL,
    vehicleno text,
    datestart date NOT NULL,
    dateend date
); 

Em termos OO esta tabela corresponde à superclasse (na terminologia java). Agora vamos definir as tabelas filhas herdando de public.rental e também adicionando uma coluna para cada tabela específica do domínio:por exemplo, o número da carteira de motorista (cliente) obrigatório no caso de carros e o certificado de navegação de barco opcional.
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);

As duas tabelas cars.rental e barcos.aluguel herdar todas as colunas de seu pai 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 a empresa coluna na definição da tabela pai (e consequentemente nas tabelas filhas também). Isso não é mais necessário, pois a identificação do inquilino está no nome completo da mesa! Veremos mais adiante uma maneira fácil de descobrir isso nas consultas. Agora vamos inserir algumas linhas nas três tabelas (pegamos emprestados clientes esquema e dados do 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);

Agora vamos ver o que está nas tabelas:
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)

Assim, as mesmas noções de herança que existem em linguagens Orientadas a Objetos (como Java) existem também no PostgreSQL! Podemos pensar assim:
public.rental:superclass
cars.rental:subclass
boats.rental:subclass
row public.rental.id =1:instância de public.rental
row cars.rental.id =2:instância de cars.rental e public.rental
row boats.rental.id =3:instância de boats.rental e public.rental

Uma vez que as filas de boats.rental e cars.rental também são instâncias de public.rental, é natural que apareçam como filas de public.rental. Se queremos apenas linhas exclusivas de public.rental (ou seja, as linhas inseridas diretamente em public.rental) fazemos isso usando a palavra-chave ONLY da seguinte forma:
rentaldb_hier=# select * from ONLY rental ;
 id | customerid |       vehicleno        | datestart  | dateend
----+------------+------------------------+------------+---------
  1 |          1 | SOME ABSTRACT PLATE NO | 2018-08-31 |
(1 row)

Uma diferença entre Java e PostgreSQL no que diz respeito à herança é esta:Java não suporta herança múltipla enquanto o PostgreSQL suporta, é possível herdar de mais de uma tabela, então nesse sentido podemos pensar em tabelas mais como interfaces em Java.

Se quisermos descobrir a tabela exata na hierarquia onde uma linha específica pertence (o equivalente a obj.getClass().getName() em java), podemos fazer especificando a coluna especial tableoid (oid da respectiva tabela em pgclass ), convertido para regclass que fornece o nome completo da tabela:
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)

A partir do acima (tableoid diferente) podemos inferir que as tabelas na hierarquia são apenas tabelas antigas do PostgreSQL, conectadas com um relacionamento de herança. Mas, além disso, eles agem como mesas normais. E isso será mais enfatizado na seção seguinte.

Fatos e advertências importantes sobre a herança do PostgreSQL


A tabela filha herda:
  • NOT NULL restrições
  • VERIFICAR restrições

A tabela filha NÃO herda:
  • Restrições de CHAVE PRIMÁRIA
  • Restrições ÚNICAS
  • Restrições FOREIGN KEY

Quando colunas com o mesmo nome aparecem na definição de mais de uma tabela na hierarquia, essas colunas devem ter o mesmo tipo e são mescladas em uma única coluna. Se existir uma restrição NOT NULL para um nome de coluna em qualquer lugar na hierarquia, isso será herdado para a tabela filha. As restrições CHECK com o mesmo nome também são mescladas e devem ter a mesma condição.

As alterações de esquema na tabela pai (via ALTER TABLE) são propagadas por toda a hierarquia que existe abaixo dessa tabela pai. E este é um dos recursos interessantes de herança no PostgreSQL.

Segurança e Políticas de Segurança (RLS) são decididas com base na tabela real que usamos. Se usarmos uma tabela pai, a segurança e o RLS dessa tabela serão usados. Está implícito que conceder um privilégio na tabela pai também dá permissão à(s) tabela(s) filha(s), mas somente quando acessada pela tabela pai. Para acessar a tabela filho diretamente, devemos dar GRANT explícito diretamente à tabela filho, o privilégio na tabela pai não será suficiente. O mesmo vale para RLS.

Com relação ao disparo de gatilhos, os gatilhos em nível de instrução dependem da tabela nomeada da instrução, enquanto os gatilhos em nível de linha serão acionados dependendo da tabela à qual a linha real pertence (portanto, pode ser uma tabela filha).

Coisas a observar:
  • A maioria dos comandos funciona em toda a hierarquia e suporta a notação ONLY. No entanto, alguns comandos de baixo nível (REINDEX, VACUUM, etc) funcionam apenas nas tabelas físicas nomeadas pelo comando. Certifique-se de ler a documentação sempre em caso de dúvida.
  • As restrições FOREIGN KEY (a tabela pai está no lado de referência) não são herdadas. Isso é facilmente resolvido especificando a mesma restrição FK em todas as tabelas filhas da hierarquia.
  • A partir deste ponto (PostgreSQL 10), não há como ter UNIQUE INDEX global (PRIMARY KEYs ou restrições UNIQUE) em um grupo de tabelas. Como resultado disso:
    • As restrições PRIMARY KEY e UNIQUE não são herdadas, e não há uma maneira fácil de impor exclusividade em uma coluna em todos os membros da hierarquia
    • Quando a tabela pai está no lado referenciado de uma restrição FOREIGN KEY, a verificação é feita apenas para os valores da coluna em linhas genuinamente (fisicamente) pertencentes à tabela pai, não em quaisquer tabelas filhas.
    • >

A última limitação é séria. De acordo com os documentos oficiais, não há uma boa solução para isso. No entanto, FK e exclusividade são fundamentais para qualquer projeto de banco de dados sério. Vamos procurar uma maneira de lidar com isso.
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper

Herança na prática


Nesta seção, vamos converter um design clássico com tabelas simples, restrições PRIMARY KEY/UNIQUE e FOREIGN KEY, para um design multi-tenant baseado em herança e tentaremos resolver os problemas (esperados de acordo com a seção anterior) que Rosto. Consideremos o mesmo negócio de aluguel que usamos como exemplo no blog anterior e imaginemos que no início o negócio faça apenas aluguel de carros (sem barcos ou outros tipos de veículos). Vamos considerar o seguinte esquema, com os veículos da empresa e o histórico de atendimento nesses veí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);

Agora vamos imaginar que o sistema está em produção, e então a empresa adquire uma segunda empresa que faz aluguel de barcos e tem que integrá-los ao sistema, fazendo com que as duas empresas operem de forma independente na operação, mas de forma unificada para uso pelo mgmt superior. Além disso, vamos imaginar que os dados do veículo_serviço não devem ser divididos, pois todas as linhas devem estar visíveis para ambas as empresas. Então, o que estamos procurando é fornecer uma solução multi-tenancy baseada em herança na tabela de veículos. Primeiro, devemos criar um novo esquema para carros (o antigo negócio) e outro para barcos e, em seguida, migrar os dados existentes para 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

Observamos que as novas tabelas compartilham o mesmo valor padrão para a coluna id (mesma sequência) que a tabela pai. Embora isso esteja longe de ser uma solução para o problema de exclusividade global explicado na seção anterior, é uma solução alternativa, desde que nenhum valor explícito seja usado para inserções ou atualizações. Se todas as tabelas filhas (cars.vehicle e boats.vehicle) forem definidas como acima, e nunca manipularmos explicitamente o id, estaremos seguros.

Como manteremos apenas a tabela public vehicle_service e isso fará referência a linhas de tabelas filhas, devemos eliminar a restrição FK:
rentaldb=# alter table vehicle_service drop CONSTRAINT vehicle_service_vehicleid_fkey ;

Mas porque precisamos manter a consistência equivalente em nosso banco de dados, devemos encontrar uma solução para isso. Implementaremos essa restrição usando gatilhos. Precisamos adicionar um gatilho ao Vehicle_service que verifica se para cada INSERT ou UPDATE o Vehicleid aponta para uma linha válida em algum lugar na hierarquia public.vehicle*, e um gatilho em cada uma das tabelas dessa hierarquia que verifica isso para cada DELETE ou UPDATE no id, não existe nenhuma linha em Vehicle_service que aponte para o valor antigo. (observe pela notação do veículo* PostgreSQL implica esta e todas as tabelas filhas)
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();

Se tentarmos atualizar ou inserir um valor para a coluna Vehicleid que não existe no Vehicle*, obteremos um erro:
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

Agora, se inserirmos uma linha em qualquer tabela na hierarquia, por exemplo. boats.vehicle (que normalmente terá id=2) e tente novamente:
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);

Então o INSERT anterior agora é bem-sucedido. Agora também devemos proteger esse relacionamento FK do outro lado, devemos garantir que nenhuma atualização/exclusão seja permitida em nenhuma tabela da hierarquia se a linha a ser excluída (ou atualizada) for 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();

Vamos tentar:
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

Agora precisamos mover os dados existentes em public.vehicle para 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 ;

Definir a réplica de session_replication_role TO impede o disparo de gatilhos normais. Observe que, depois de mover os dados, podemos querer desabilitar completamente a tabela pai (public.vehicle) de aceitar inserções (provavelmente por meio de uma regra). Neste caso, na analogia OO, trataríamos public.vehicle como uma classe abstrata, ou seja, sem linhas (instâncias). Usar esse design para multilocação parece natural porque o problema a ser resolvido é um caso de uso clássico para herança, no entanto, os problemas que enfrentamos não são triviais. Isso foi discutido pela comunidade de hackers e esperamos melhorias futuras.