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

Acionadores do PostgreSQL e noções básicas de funções armazenadas


Nota de Variousnines:Este blog está sendo publicado postumamente quando Berend Tober faleceu em 16 de julho de 2018. Honramos suas contribuições para a comunidade PostgreSQL e desejamos paz para nosso amigo e escritor convidado.

Em um artigo anterior discutimos o pseudo-tipo serial PostgreSQL, que é útil para preencher valores de chaves sintéticas com números inteiros incrementados. Vimos que empregar a palavra-chave de tipo de dados serial em uma instrução de linguagem de definição de dados de tabela (DDL) é implementada como uma declaração de coluna do tipo inteiro que é preenchida, em uma inserção de banco de dados, com um valor padrão derivado de uma simples chamada de função. Esse comportamento automatizado de invocar código funcional como parte da resposta integral à atividade da linguagem de manipulação de dados (DML) é um recurso poderoso de sofisticados sistemas de gerenciamento de banco de dados relacional (RDBMS) como o PostgreSQL. Neste artigo, nos aprofundamos em outro aspecto mais capaz de invocar automaticamente o código personalizado, ou seja, o uso de gatilhos e funções armazenadas.Introdução

Casos de uso para gatilhos e funções armazenadas


Vamos falar sobre por que você pode querer investir na compreensão de gatilhos e funções armazenadas. Ao construir o código DML no próprio banco de dados, você pode evitar a implementação duplicada de código relacionado a dados em vários aplicativos separados que podem ser criados para interagir com o banco de dados. Isso garante a execução consistente do código DML para validação de dados, limpeza de dados ou outras funcionalidades, como auditoria de dados (ou seja, registro de alterações) ou manutenção de uma tabela de resumo independentemente de qualquer aplicativo de chamada. Outro uso comum de gatilhos e funções armazenadas é tornar as visualizações graváveis, ou seja, habilitar inserções e/ou atualizações em visualizações complexas ou proteger determinados dados de coluna de modificações não autorizadas. Além disso, os dados processados ​​no servidor, e não no código do aplicativo, não cruzam a rede, portanto, há um risco menor de os dados serem expostos a espionagem, bem como uma redução no congestionamento da rede. Além disso, no PostgreSQL, as funções armazenadas podem ser configuradas para executar código em um nível de privilégio mais alto do que o usuário da sessão, o que admite alguns recursos poderosos. Faremos alguns exemplos depois.

O caso contra gatilhos e funções armazenadas


Uma revisão de comentários na lista de discussão PostgreSQL General revelou algumas opiniões desfavoráveis ​​ao uso de triggers e funções armazenadas que mencionei aqui para completar e para encorajar você e sua equipe a pesar os prós e contras de sua implementação.

Entre as objeções estavam, por exemplo, a percepção de que as funções armazenadas não são fáceis de manter, exigindo, portanto, uma pessoa experiente com habilidades sofisticadas e conhecimento em administração de banco de dados para gerenciá-las. Alguns profissionais de software relataram que os controles de alterações corporativas em sistemas de banco de dados são geralmente mais vigorosos do que no código do aplicativo, de modo que, se as regras de negócios ou outra lógica forem implementadas no banco de dados, fazer alterações à medida que os requisitos evoluem é proibitivamente complicado. Outro ponto de vista considera os gatilhos como um efeito colateral inesperado de alguma outra ação e, como tal, podem ser obscuros, facilmente perdidos, difíceis de depurar e frustrantes de manter e, portanto, geralmente devem ser a última escolha, não a primeira.

Essas objeções podem ter algum mérito, mas se você pensar sobre isso, os dados são um ativo valioso e, portanto, você provavelmente deseja uma pessoa ou equipe qualificada e experiente responsável pelo RDBMS em uma organização corporativa ou governamental e, da mesma forma, Change As placas de controle são um componente comprovado da manutenção sustentável de um sistema de informações de registro, e o efeito colateral de uma pessoa é também a poderosa conveniência de outra, que é o ponto de vista adotado para o balanço deste artigo.

Declarando um gatilho


Vamos começar a aprender as porcas e parafusos. Existem muitas opções disponíveis na sintaxe geral do DDL para declarar um gatilho, e levaria um tempo significativo para tratar todas as permutações possíveis, portanto, por uma questão de brevidade, falaremos apenas de um subconjunto deles minimamente necessário em exemplos que siga usando esta sintaxe resumida:
CREATE TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
    ON table_name
    FOR EACH ROW EXECUTE PROCEDURE function_name()

where event can be one of:

    INSERT
    UPDATE [ OF column_name [, ... ] ]
    DELETE
    TRUNCATE

Os elementos configuráveis ​​necessários além de um nome são os quando , o por que , o onde , e o o que , ou seja, o tempo para o código de gatilho ser invocado em relação à ação de gatilho (quando), o tipo específico de instrução DML de gatilho (por que), a tabela ou tabelas atuadas (onde) e o código de função armazenado para executar (que).

Declarando uma função


A declaração de gatilho acima requer a especificação de um nome de função, portanto, tecnicamente, a declaração de gatilho DDL não pode ser executada até que a função de gatilho tenha sido definida anteriormente. A sintaxe DDL geral para uma declaração de função também tem muitas opções, portanto, para gerenciamento, usaremos essa sintaxe minimamente suficiente para nossos propósitos aqui:
CREATE [ OR REPLACE ] FUNCTION
    name () RETURNS TRIGGER
  { LANGUAGE lang_name
    | SECURITY DEFINER
    | SET configuration_parameter { TO value | = value | FROM CURRENT }
    | AS 'definition'
  }...

Uma função de gatilho não recebe parâmetros e o tipo de retorno deve ser TRIGGER. Falaremos sobre os modificadores opcionais conforme os encontramos nos exemplos abaixo.

Um esquema de nomenclatura para gatilhos e funções


O respeitado cientista da computação Phil Karlton foi atribuído por declarar (em forma parafraseada aqui) que nomear as coisas é um dos maiores desafios para as equipes de software. Vou apresentar aqui um gatilho fácil de usar e uma convenção de nomenclatura de função armazenada que me serviu bem e encorajo você a considerar adotá-lo para seus próprios projetos RDBMS. O esquema de nomenclatura nos exemplos deste artigo segue um padrão de uso do nome da tabela associada com o sufixo de uma abreviação indicando o gatilho declarado quando e por que atributos:A primeira letra do sufixo será um “b”, “a” ou “i” (para “antes”, “depois” ou “em vez de”), o próximo será um ou mais de um “i” , “u”, “d” ou “t” (para “inserir”, “atualizar”, “excluir” ou “truncar”), e a última letra é apenas um “t” para disparar. (Eu uso uma convenção de nomenclatura semelhante para regras e, nesse caso, a última letra é “r”). Assim, por exemplo, as várias combinações mínimas de atributos de declaração de gatilho para uma tabela chamada “my_table” seriam:
|-------------+-------------+-----------+---------------+-----------------|
|  TABLE NAME |  WHEN       |  WHY      |  TRIGGER NAME |  FUNCTION NAME  |
|-------------+-------------+-----------+---------------+-----------------|
|  my_table   |  BEFORE     |  INSERT   |  my_table_bit |  my_table_bit   |
|  my_table   |  BEFORE     |  UPDATE   |  my_table_but |  my_table_but   |
|  my_table   |  BEFORE     |  DELETE   |  my_table_bdt |  my_table_bdt   |
|  my_table   |  BEFORE     |  TRUNCATE |  my_table_btt |  my_table_btt   |
|  my_table   |  AFTER      |  INSERT   |  my_table_ait |  my_table_ait   |
|  my_table   |  AFTER      |  UPDATE   |  my_table_aut |  my_table_aut   |
|  my_table   |  AFTER      |  DELETE   |  my_table_adt |  my_table_adt   |
|  my_table   |  AFTER      |  TRUNCATE |  my_table_att |  my_table_att   |
|  my_table   |  INSTEAD OF |  INSERT   |  my_table_iit |  my_table_iit   |
|  my_table   |  INSTEAD OF |  UPDATE   |  my_table_iut |  my_table_iut   |
|  my_table   |  INSTEAD OF |  DELETE   |  my_table_idt |  my_table_idt   |
|  my_table   |  INSTEAD OF |  TRUNCATE |  my_table_itt |  my_table_itt   |
|-------------+-------------+-----------+---------------+-----------------|

O mesmo nome exato pode ser usado tanto para o gatilho quanto para a função armazenada associada, o que é totalmente permitido no PostgreSQL porque o RDBMS mantém o controle dos gatilhos e das funções armazenadas separadamente pelos respectivos propósitos, e o contexto no qual o nome do item é usado torna claro a qual item o nome se refere.

Assim, por exemplo, uma declaração de gatilho correspondente ao cenário da primeira linha da tabela acima seria vista implementada como
CREATE TRIGGER my_table_bit 
    BEFORE INSERT
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_bit();

No caso de um gatilho ser declarado com vários por que atributos, apenas expanda o sufixo adequadamente, por exemplo, para um inserir ou atualizar gatilho, o acima se tornaria
CREATE TRIGGER my_table_biut 
    BEFORE INSERT OR UPDATE
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_biut();

Mostre-me algum código já!


Vamos torná-lo real. Começaremos com um exemplo simples e depois expandiremos para ilustrar outros recursos. As instruções DDL de gatilho requerem uma função pré-existente, como mencionado, e também uma tabela sobre a qual agir, então primeiro precisamos de uma tabela para trabalhar. Para fins de exemplo, digamos que precisamos armazenar dados básicos de identidade da conta
CREATE TABLE person (
    login_name varchar(9) not null primary key,
    display_name text
);

Algumas imposição de integridade de dados podem ser tratadas simplesmente com DDL de coluna adequada, como neste caso um requisito de que o nome_do_login exista e não tenha mais de nove caracteres. As tentativas de inserir um valor NULL ou um valor muito longo de login_name falham e relatam mensagens de erro significativas:
INSERT INTO person VALUES (NULL, 'Felonious Erroneous');
ERROR:  null value in column "login_name" violates not-null constraint
DETAIL:  Failing row contains (null, Felonious Erroneous).

INSERT INTO person VALUES ('atoolongusername', 'Felonious Erroneous');
ERROR:  value too long for type character varying(9)

Outras imposições podem ser tratadas com restrições de verificação, como exigir um comprimento mínimo e rejeitar determinados caracteres:
ALTER TABLE person 
    ADD CONSTRAINT PERSON_LOGIN_NAME_NON_NULL 
    CHECK (LENGTH(login_name) > 0);

ALTER TABLE person 
    ADD CONSTRAINT person_login_name_no_space 
    CHECK (POSITION(' ' IN login_name) = 0);

INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  new row for relation "person" violates check constraint "person_login_name_non_null"
DETAIL:  Failing row contains (, Felonious Erroneous).

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  new row for relation "person" violates check constraint "person_login_name_no_space"
DETAIL:  Failing row contains (space man, Major Tom).

mas observe que a mensagem de erro não é tão totalmente informativa quanto antes, transmitindo apenas o que está codificado no nome do gatilho, em vez de uma mensagem textual explicativa significativa. Ao implementar a lógica de verificação em uma função armazenada, você pode usar uma exceção para emitir uma mensagem de texto mais útil. Além disso, as expressões de restrição de verificação não podem conter subconsultas nem fazer referência a variáveis ​​que não sejam colunas da linha atual nem outras tabelas do banco de dados.

Então, vamos eliminar as restrições de verificação
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;

e continue com gatilhos e funções armazenadas.

Mostre-me mais alguns códigos


Temos uma mesa. Passando para a função DDL, definimos uma função de corpo vazio, que podemos preencher posteriormente com um código específico:
CREATE OR REPLACE FUNCTION person_bit() 
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    SET search_path = public
    AS '
    BEGIN
    END;
    ';

Isso nos permite finalmente chegar ao gatilho DDL conectando a tabela e a função para que possamos fazer alguns exemplos:
CREATE TRIGGER person_bit 
    BEFORE INSERT ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

O PostgreSQL permite que funções armazenadas sejam escritas em uma variedade de linguagens diferentes. Neste caso e nos exemplos seguintes, estamos compondo funções na linguagem PL/pgSQL que é projetada especificamente para PostgreSQL e suporta o uso de todos os tipos de dados, operadores e funções do RDBMS PostgreSQL. A opção SET SCHEMA define o caminho de pesquisa do esquema que será usado durante a execução da função. Definir o caminho de pesquisa para cada função é uma boa prática, pois evita a necessidade de prefixar objetos de banco de dados com um nome de esquema e protege contra certas vulnerabilidades relacionadas ao caminho de pesquisa.

EXEMPLO 0 - Validação de dados


Como primeiro exemplo, vamos implementar as verificações anteriores, mas com mensagens mais amigáveis.
CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;
    RETURN NEW;
    END;
    $$;

O qualificador “NEW” é uma referência à linha de dados prestes a ser inserida. É uma das várias variáveis ​​especiais disponíveis em uma função de gatilho. Apresentaremos alguns outros abaixo. Observe também que o PostgreSQL permite a substituição das aspas simples que delimitam o corpo da função por outros delimitadores, neste caso seguindo uma convenção comum de usar cifrões duplos como delimitador, já que o próprio corpo da função inclui caracteres de aspas simples. As funções de gatilho devem sair retornando a NOVA linha a ser inserida ou NULL para abortar silenciosamente a ação.

As mesmas tentativas de inserção falham conforme o esperado, mas agora com mensagens amigáveis:
INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  Login name must not be empty.

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  Login name must not include white space.

EXEMPLO 1 - Registro de auditoria


Com funções armazenadas, temos ampla latitude sobre o que o código invocado faz, incluindo referenciar outras tabelas (o que não é possível com restrições de verificação). Como um exemplo mais complexo, percorreremos a implementação de uma tabela de auditoria, ou seja, mantendo um registro, em uma tabela separada, de inserções, atualizações e exclusões em uma tabela principal. A tabela de auditoria normalmente contém os mesmos atributos da tabela principal, que são usados ​​para registrar os valores alterados, além de atributos adicionais para registrar a operação executada para fazer a alteração, bem como um carimbo de data e hora da transação e um registro do usuário que fez a alteração. mudança:
CREATE TABLE person_audit (
    login_name varchar(9) not null,
    display_name text,
    operation varchar,
    effective_at timestamp not null default now(),
    userid name not null default session_user
);

Nesse caso, implementar a auditoria é muito fácil, simplesmente modificamos a função de gatilho existente para incluir DML para efetuar a inserção da tabela de auditoria e, em seguida, redefinimos o gatilho para acionar atualizações e inserções. Observe que optamos por não alterar o sufixo do nome da função de gatilho para “biut”, mas se a funcionalidade de auditoria fosse um requisito conhecido no tempo de design inicial, esse seria o nome usado:
CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- New code to record audits

    INSERT INTO person_audit (login_name, display_name, operation) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP);

    RETURN NEW;
    END;
    $$;


DROP TRIGGER person_bit ON person;

CREATE TRIGGER person_biut 
    BEFORE INSERT OR UPDATE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

Observe que introduzimos outra variável especial “TG_OP” que o sistema define para identificar a operação DML que disparou o gatilho como “INSERT”, “UPDATE”, “DELETE”, ou “TRUNCATE”, respectivamente.

Precisamos lidar com exclusões separadamente de inserções e atualizações, pois os testes de validação de atributos são supérfluos e porque o valor especial NOVO não é definido na entrada para um antes de excluir função de gatilho e, assim, definir a função armazenada e o gatilho correspondentes:
CREATE OR REPLACE FUNCTION person_bdt()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN

    -- Record deletion in audit table

    INSERT INTO person_audit (login_name, display_name, operation) 
      VALUES (OLD.login_name, OLD.display_name, TG_OP);

    RETURN OLD;
    END;
    $$;
        
CREATE TRIGGER person_bdt 
    BEFORE DELETE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bdt();

Observe o uso do valor especial OLD como referência à linha que está prestes a ser excluída, ou seja, a linha como ela existe antes a exclusão acontece.

Fazemos algumas inserções para testar a funcionalidade e confirmar que a tabela de auditoria inclui um registro das inserções:
INSERT INTO person VALUES ('dfunny', 'Doug Funny');
INSERT INTO person VALUES ('pmayo', 'Patti Mayonnaise');

SELECT * FROM person;
 login_name |   display_name   
------------+------------------
 dfunny     | Doug Funny
 pmayo      | Patti Mayonnaise
(2 rows)

SELECT * FROM person_audit;
 login_name |   display_name   | operation |        effective_at        |  userid  
------------+------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny       | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise | INSERT    | 2018-05-26 18:48:07.698623 | postgres
(2 rows)

Em seguida, fazemos uma atualização em uma linha e confirmamos que a tabela de auditoria inclui um registro da alteração adicionando um nome do meio a um dos nomes de exibição do registro de dados:
UPDATE person SET display_name = 'Doug Yancey Funny' WHERE login_name = 'dfunny';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 pmayo      | Patti Mayonnaise
 dfunny     | Doug Yancey Funny
(2 rows)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-26 18:48:07.698623 | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-26 18:48:07.707284 | postgres
(3 rows)

E, por último, exercitamos a funcionalidade de exclusão e confirmamos que a tabela de auditoria também inclui esse registro:
DELETE FROM person WHERE login_name = 'pmayo';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 dfunny     | Doug Yancey Funny
(1 row)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-27 08:13:22.747226 | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-27 08:13:22.74839  | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-27 08:13:22.749495 | postgres
 pmayo      | Patti Mayonnaise  | DELETE    | 2018-05-27 08:13:22.753425 | postgres
(4 rows)

EXEMPLO 2 - Valores derivados


Vamos dar um passo adiante e imaginar que queremos armazenar algum documento de texto de formato livre em cada linha, digamos, um currículo formatado em texto simples ou um documento de conferência ou um resumo de personagem de entretenimento, e queremos oferecer suporte ao uso da poderosa pesquisa de texto completo capacidades do PostgreSQL nesses documentos de texto de formato livre.

Primeiro adicionamos dois atributos para suportar o armazenamento do documento e de um vetor de pesquisa de texto associado à tabela principal. Como o vetor de pesquisa de texto é derivado por linha, não faz sentido armazená-lo na tabela de auditoria, se adicionarmos a coluna de armazenamento de documentos à tabela de auditoria associada:
ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Em seguida, modificamos a função de gatilho para processar esses novos atributos. A coluna de texto simples é tratada da mesma forma que outros dados inseridos pelo usuário, mas o vetor de pesquisa de texto é um valor derivado e, portanto, é tratado por uma chamada de função que reduz o texto do documento a um tipo de dados tsvector para uma pesquisa eficiente.
CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- Modified audit code to include text abstract

    INSERT INTO person_audit (login_name, display_name, operation, abstract) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP, NEW.abstract);

    -- New code to reduce text to text-search vector

    SELECT to_tsvector(NEW.abstract) INTO NEW.ts_abstract;

    RETURN NEW;
    END;
    $$;

Como teste, atualizamos uma linha existente com algum texto de detalhes da Wikipedia:
UPDATE person SET abstract = 'Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd.' WHERE login_name = 'dfunny';

e, em seguida, confirme se o processamento do vetor de pesquisa de texto foi bem-sucedido:
SELECT login_name, ts_abstract  FROM person;
 login_name |                                                                                                                ts_abstract                                                                                                                
------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 dfunny     | '11':11 '12':13 'an':5 'and':9 'as':4 'boy':16 'crowd':24 'depicted':3 'doug':1 'fit':20 'gullible':10 'in':21 'insecure':8 'introverted':6 'is':2 'later':12 'old':15 'quiet':7 'the':23 'to':19 'wants':18 'who':17 'with':22 'year':14
(1 row)

EXEMPLO 3 - Acionadores e visualizações


O vetor de pesquisa de texto derivado do exemplo acima não se destina ao consumo humano, ou seja, não é inserido pelo usuário e nunca esperamos apresentar o valor a um usuário final. Se um usuário tentar inserir um valor para a coluna ts_abstract, qualquer coisa fornecida será descartada e substituída pelo valor derivado internamente à função de gatilho, para que tenhamos proteção contra envenenamento do corpus de pesquisa. Para ocultar a coluna completamente, podemos definir uma visão resumida que não inclui esse atributo, mas ainda temos o benefício da atividade do gatilho na tabela subjacente:
CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;

Para uma visualização simples, o PostgreSQL o torna automaticamente gravável para que não precisemos fazer mais nada para inserir ou atualizar dados com sucesso. Quando o DML entra em vigor na tabela subjacente, os gatilhos são ativados como se a instrução fosse aplicada diretamente à tabela, de modo que ainda temos o suporte à pesquisa de texto executado em segundo plano, preenchendo a coluna do vetor de pesquisa da tabela pessoa, bem como anexando o alterar as informações para a tabela de auditoria:
INSERT INTO abridged_person VALUES ('skeeter', 'Mosquito Valentine', 'Skeeter is Doug''s best friend. He is famous in both series for the honking sounds he frequently makes.');


SELECT login_name, ts_abstract FROM person WHERE login_name = 'skeeter';
 login_name |                                                                                   ts_abstract                                                                                    
------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 skeeter    | 'best':5 'both':11 'doug':3 'famous':9 'for':13 'frequently':18 'friend':6 'he':7,17 'honking':15 'in':10 'is':2,8 'makes':19 's':4 'series':12 'skeeter':1 'sounds':16 'the':14
(1 row)


SELECT login_name, display_name, operation, userid FROM person_audit ORDER BY effective_at;
 login_name |    display_name    | operation |  userid  
------------+--------------------+-----------+----------
 dfunny     | Doug Funny         | INSERT    | postgres
 pmayo      | Patti Mayonnaise   | INSERT    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 pmayo      | Patti Mayonnaise   | DELETE    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 skeeter    | Mosquito Valentine | INSERT    | postgres
(6 rows)

Para visualizações mais complicadas que não atendem aos requisitos para serem graváveis ​​automaticamente, o sistema de regras ou em vez de os gatilhos podem fazer o trabalho de suporte a gravações e exclusões.

EXEMPLO 4 - Valores resumidos


Vamos embelezar ainda mais e tratar o cenário onde existe algum tipo de tabela de transações. Pode ser um registro de horas trabalhadas, acréscimos e reduções de estoque de armazém ou estoque de varejo, ou talvez um registro de cheques com débitos e créditos para cada pessoa:
CREATE TABLE transaction (
    login_name character varying(9) NOT NULL,
    post_date date,
    description character varying,
    debit money,
    credit money,
    FOREIGN KEY (login_name) REFERENCES person (login_name)
);

E digamos que, embora seja importante manter o histórico de transações, as regras de negócios envolvem o uso do saldo líquido no processamento do aplicativo, em vez de qualquer detalhe da transação. Para evitar ter que recalcular frequentemente o saldo somando todas as transações toda vez que o saldo for necessário, podemos desnormalizar e manter um valor de saldo atual na tabela pessoa anexando uma nova coluna e usando um gatilho e uma função armazenada para manter o saldo líquido à medida que as transações são inseridas:
ALTER TABLE person ADD COLUMN balance MONEY DEFAULT 0;

CREATE FUNCTION transaction_bit() RETURNS trigger
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    DECLARE
    newbalance money;
    BEGIN

    -- Update person account balance

    UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name
                RETURNING balance INTO newbalance;

    -- Data validation

    IF COALESCE(NEW.debit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Debit value must be non-negative';
    END IF;

    IF COALESCE(NEW.credit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Credit value must be non-negative';
    END IF;

    IF newbalance < 0::money THEN
        RAISE EXCEPTION 'Insufficient funds: %', NEW;
    END IF;

    RETURN NEW;
    END;
    $$;



CREATE TRIGGER transaction_bit 
      BEFORE INSERT ON transaction 
      FOR EACH ROW EXECUTE PROCEDURE transaction_bit();

Pode parecer estranho fazer a atualização primeiro na função armazenada antes de validar a não negatividade dos valores de débito, crédito e saldo, mas em termos de validação de dados a ordem não importa porque o corpo de uma função trigger é executado como um transação de banco de dados, portanto, se essas verificações de validação falharem, toda a transação será revertida quando a exceção for gerada. A vantagem de fazer a atualização primeiro é que a atualização bloqueia a linha afetada durante a transação e, portanto, qualquer outra sessão que tente atualizar a mesma linha é bloqueada até que a transação atual seja concluída. O teste de validação adicional assegura que o saldo resultante não seja negativo, e a mensagem de informação de exceção pode incluir uma variável, que neste caso retornará a linha de transação de inserção tentada ofensiva para depuração.

Para demonstrar que realmente funciona, aqui estão alguns exemplos de entradas e uma verificação mostrando o saldo atualizado em cada etapa:
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name | balance 
------------+---------
 dfunny     |   $0.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-11', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$2780.52', NULL);
ERROR:  Insufficient funds: (dfunny,2018-01-17,"FOR:BGE PAYMENT ACH Withdrawal",,"$2,780.52")

Observe como a transação acima falha em fundos insuficientes, ou seja, produziria um saldo negativo e retrocederia com sucesso. Observe também que retornamos a linha inteira com a variável especial NEW como detalhe extra na mensagem de erro para depuração.
SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$278.52', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,721.48
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal', '$35.29', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

EXEMPLO 5 - Gatilhos e Redux de Visualizações


No entanto, há um problema com a implementação acima, e isso é que nada impede que um usuário mal-intencionado imprima dinheiro:
BEGIN;
UPDATE person SET balance = '1000000000.00';

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

Por enquanto, revertemos o roubo acima e mostraremos uma maneira de criar proteção usando um gatilho para evitar atualizações no valor do saldo.

Primeiro, aumentamos a visão resumida anterior para expor a coluna de saldo:
CREATE OR REPLACE VIEW abridged_person AS
  SELECT login_name, display_name, abstract, balance FROM person;

Isso obviamente permite o acesso de leitura ao saldo, mas ainda não resolve o problema porque para visualizações simples como esta baseadas em uma única tabela, o PostgreSQL automaticamente torna a visualização gravável:
BEGIN;
UPDATE abridged_person SET balance = '1000000000.00';
SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

Poderíamos usar uma regra, mas para ilustrar que os gatilhos podem ser definidos tanto em visualizações quanto em tabelas, seguiremos o último caminho e usaremos um em vez de atualizar acionar na visualização para bloquear DML indesejada, evitando alterações não transacionais no valor do saldo:
CREATE FUNCTION abridged_person_iut() RETURNS TRIGGER
    LANGUAGE plpgsql
    SET search_path TO public
    AS $$
    BEGIN

    -- Disallow non-transactional changes to balance

      NEW.balance = OLD.balance;
    RETURN NEW;
    END;
    $$;

CREATE TRIGGER abridged_person_iut
    INSTEAD OF UPDATE ON abridged_person
    FOR EACH ROW EXECUTE PROCEDURE abridged_person_iut();

The above instead of update trigger and stored procedure discards any attempted updates to the balance value and instead forces use of the value present in the database prior to the triggering update statement:
UPDATE abridged_person SET balance = '1000000000.00';

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

which affords protection against un-auditable changes to the balance value.
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper

EXAMPLE 6 - Elevated Privileges


So far all the example code above has been executed at the database owner level by the postgres login role, so any of our anti-tampering efforts could be obviated… that’s just a fact of the database owner super-user privileges.

Our final example illustrates how triggers and stored functions can be used to allow the execution of code by a non-privileged user at a higher privilege than the logged in session user normally has by employing the SECURITY DEFINER attribute associated with stored functions.

First, we define a non-privileged login role, eve and confirm that upon instantiation there are no privileges:
CREATE USER eve;
\dp
                                  Access privileges
 Schema |      Name       | Type  | Access privileges | Column privileges | Policies 
--------+-----------------+-------+-------------------+-------------------+----------
 public | abridged_person | view  |                   |                   | 
 public | person          | table |                   |                   | 
 public | person_audit    | table |                   |                   | 
 public | transaction     | table |                   |                   | 
(4 rows)

We grant read, update, and create privileges on the abridged person view and read and create to the transaction table:
GRANT SELECT,INSERT, UPDATE ON abridged_person TO eve;
GRANT SELECT,INSERT ON transaction TO eve;
\dp
                                      Access privileges
 Schema |      Name       | Type  |     Access privileges     | Column privileges | Policies 
--------+-----------------+-------+---------------------------+-------------------+----------
 public | abridged_person | view  | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=arw/postgres          |                   | 
 public | person          | table |                           |                   | 
 public | person_audit    | table |                           |                   | 
 public | transaction     | table | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=ar/postgres           |                   | 
(4 rows)

By way of confirmation we see that eve is denied access to the person and person_audit tables:
SET SESSION AUTHORIZATION eve;

SELECT * FROM person;
ERROR:  permission denied for relation person

SELECT * from person_audit;
ERROR:  permission denied for relation person_audit

and that she does have appropriate read access to the abridged_person and transaction tables:
SELECT * FROM abridged_person;
 login_name |    display_name    |                                                            abstract                                                             |  balance  
------------+--------------------+---------------------------------------------------------------------------------------------------------------------------------+-----------
 skeeter    | Mosquito Valentine | Skeeter is Doug's best friend. He is famous in both series for the honking sounds he frequently makes.                          |     $0.00
 dfunny     | Doug Yancey Funny  | Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd. | $1,686.19
(2 rows)

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
(3 rows)

However, even though she has write privilege on the transaction table, a transaction insert attempt fails due to lack of privilege on the person tabela.
SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
ERROR:  permission denied for relation person
CONTEXT:  SQL statement "UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name"
PL/pgSQL function transaction_bit() line 6 at SQL statement

The error message context shows this hold up occurs when inside the trigger function DML to update the balance is invoked. The way around this need to deny Eve direct write access to the person table but still effect updates to the person balance in a controlled manner is to add the SECURITY DEFINER attribute to the stored function:
RESET SESSION AUTHORIZATION;
ALTER FUNCTION transaction_bit() SECURITY DEFINER;

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
 dfunny     | 2018-01-23 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
(4 rows)

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $3,686.19
(1 row)

Now the transaction insert succeeds because the stored function is executed with privilege level of its definer, i.e., the postgres user, which does have the appropriate write privilege on the person table.

Conclusão


As lengthy as this article is, there’s still a lot more to say about triggers and stored functions. What we covered here is a basic introduction with a consideration of pros and cons of triggers and stored functions. We illustrated six use-case examples showing data validation, change logging, deriving values from inserted data, data hiding with simple updatable views, maintaining summary data in separate tables, and allowing safe invocation of code at elevated privilege. Look for a future article on using triggers and stored functions to prevent missing values in sequentially-incrementing (serial) columns.