PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Déclencheurs PostgreSQL et principes de base des fonctions stockées

Note de Manynines :Ce blog est publié à titre posthume car Berend Tober est décédé le 16 juillet 2018. Nous honorons ses contributions à la communauté PostgreSQL et souhaitons la paix à notre ami et écrivain invité.

Dans un article précédent, nous avons discuté du pseudo-type série PostgreSQL, qui est utile pour remplir des valeurs de clés synthétiques avec des entiers incrémentés. Nous avons vu que l'utilisation du mot-clé de type de données série dans une instruction DDL (table data definition language) est implémentée sous la forme d'une déclaration de colonne de type entier qui est remplie, lors d'une insertion de base de données, avec une valeur par défaut dérivée d'un simple appel de fonction. Ce comportement automatisé d'appel de code fonctionnel dans le cadre de la réponse intégrale à l'activité du langage de manipulation de données (DML) est une fonctionnalité puissante des systèmes sophistiqués de gestion de bases de données relationnelles (RDBMS) comme PostgreSQL. Dans cet article, nous approfondissons un autre aspect plus capable d'invoquer automatiquement du code personnalisé, à savoir l'utilisation de déclencheurs et de fonctions stockées.Introduction

Cas d'utilisation des déclencheurs et des fonctions stockées

Parlons des raisons pour lesquelles vous pourriez vouloir investir dans la compréhension des déclencheurs et des fonctions stockées. En créant du code DML dans la base de données elle-même, vous pouvez éviter l'implémentation en double du code lié aux données dans plusieurs applications distinctes qui peuvent être conçues pour s'interfacer avec la base de données. Cela garantit une exécution cohérente du code DML pour la validation des données, le nettoyage des données ou d'autres fonctionnalités telles que l'audit des données (c'est-à-dire la journalisation des modifications) ou la maintenance d'un tableau récapitulatif indépendamment de toute application appelante. Une autre utilisation courante des déclencheurs et des fonctions stockées consiste à rendre les vues inscriptibles, c'est-à-dire à permettre des insertions et/ou des mises à jour sur des vues complexes ou à protéger certaines données de colonne contre des modifications non autorisées. De plus, les données traitées sur le serveur plutôt que dans le code d'application ne traversent pas le réseau, il y a donc un risque moindre que les données soient exposées à l'écoute clandestine ainsi qu'une réduction de la congestion du réseau. De plus, dans PostgreSQL, les fonctions stockées peuvent être configurées pour exécuter du code à un niveau de privilège supérieur à celui de l'utilisateur de la session, ce qui admet certaines fonctionnalités puissantes. Nous ferons quelques exemples plus tard.

Le cas contre les déclencheurs et les fonctions stockées

Un examen des commentaires sur la liste de diffusion PostgreSQL General a révélé des opinions défavorables à l'utilisation des déclencheurs et des fonctions stockées que je mentionne ici pour être complet et pour vous encourager, vous et votre équipe, à peser le pour et le contre de votre implémentation.

Parmi les objections figuraient, par exemple, la perception que les fonctions stockées ne sont pas faciles à maintenir, nécessitant ainsi une personne expérimentée avec des compétences et des connaissances sophistiquées en administration de bases de données pour les gérer. Certains professionnels du logiciel ont signalé que les contrôles de modification d'entreprise sur les systèmes de base de données sont généralement plus vigoureux que sur le code d'application, de sorte que si des règles métier ou une autre logique sont implémentées dans la base de données, apporter des modifications à mesure que les exigences évoluent est d'une lourdeur prohibitive. Un autre point de vue considère les déclencheurs comme un effet secondaire inattendu d'une autre action et, en tant que tels, peuvent être obscurs, facilement manqués, difficiles à déboguer et frustrants à maintenir et devraient donc généralement être le dernier choix, pas le premier.

Ces objections peuvent avoir un certain mérite, mais si vous y réfléchissez, les données sont un atout précieux et vous voulez donc probablement une personne ou une équipe qualifiée et expérimentée responsable du SGBDR dans une entreprise ou une organisation gouvernementale de toute façon, et de la même manière, Changer Les tableaux de contrôle sont un élément éprouvé de la maintenance durable d'un système d'information d'enregistrement, et l'effet secondaire d'une personne est tout aussi bien la commodité puissante d'une autre, c'est le point de vue adopté pour le reste de cet article.

Déclarer un déclencheur

Commençons par apprendre les écrous et les boulons. Il existe de nombreuses options disponibles dans la syntaxe DDL générale pour déclarer un déclencheur, et il faudrait beaucoup de temps pour traiter toutes les permutations possibles, donc par souci de brièveté, nous ne parlerons que d'un sous-ensemble minimalement requis d'entre eux dans des exemples qui suivez en utilisant cette syntaxe abrégée :

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

Les éléments configurables requis en plus d'un nom sont les quand , le pourquoi , le , et le quoi , c'est-à-dire le moment où le code de déclenchement doit être invoqué par rapport à l'action de déclenchement (quand), le type spécifique d'instruction DML de déclenchement (pourquoi), la ou les tables d'action (où) et le code de fonction stocké à exécuter (quoi).

Déclarer une fonction

La déclaration de déclencheur ci-dessus nécessite la spécification d'un nom de fonction, donc techniquement, la déclaration de déclencheur DDL ne peut pas être exécutée tant que la fonction de déclencheur n'a pas été définie au préalable. La syntaxe DDL générale pour une déclaration de fonction comporte également de nombreuses options. Par souci de gérabilité, nous utiliserons ici cette syntaxe minimalement suffisante pour nos besoins :

CREATE [ OR REPLACE ] FUNCTION
    name () RETURNS TRIGGER
  { LANGUAGE lang_name
    | SECURITY DEFINER
    | SET configuration_parameter { TO value | = value | FROM CURRENT }
    | AS 'definition'
  }...

Une fonction déclencheur ne prend aucun paramètre et le type de retour doit être TRIGGER. Nous parlerons des modificateurs facultatifs au fur et à mesure que nous les rencontrons dans les exemples ci-dessous.

Un schéma de nommage pour les déclencheurs et les fonctions

L'informaticien respecté Phil Karlton a déclaré (sous une forme paraphrasée ici) que nommer les choses est l'un des plus grands défis pour les équipes de logiciels. Je vais présenter ici un déclencheur facile à utiliser et une convention de dénomination des fonctions stockées qui m'a bien servi et vous encourage à envisager de l'adopter pour vos propres projets RDBMS. Le schéma de dénomination dans les exemples de cet article suit un modèle d'utilisation du nom de table associé suffixé par une abréviation indiquant le déclencheur déclaré quand et pourquoi attributs :la première lettre du suffixe sera soit un « b », un « a » ou un « i » (pour « avant », « après » ou « au lieu de »), la suivante sera un ou plusieurs « i » , "u", "d" ou "t" (pour "insérer", "mettre à jour", "supprimer" ou "tronquer"), et la dernière lettre est juste un "t" pour déclencheur. (J'utilise une convention de dénomination similaire pour les règles, et dans ce cas, la dernière lettre est "r"). Ainsi, par exemple, les différentes combinaisons minimales d'attributs de déclaration de déclencheur pour une table nommée "my_table" seraient :

|-------------+-------------+-----------+---------------+-----------------|
|  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   |
|-------------+-------------+-----------+---------------+-----------------|

Le même nom exact peut être utilisé à la fois pour le déclencheur et la fonction stockée associée, ce qui est tout à fait autorisé dans PostgreSQL car le RDBMS garde une trace des déclencheurs et des fonctions stockées séparément selon les objectifs respectifs, et le contexte dans lequel le nom de l'élément est utilisé rend effacer à quel élément le nom fait référence.

Ainsi, par exemple, une déclaration de déclencheur correspondant au scénario de la première ligne du tableau ci-dessus serait considérée comme implémentée comme

CREATE TRIGGER my_table_bit 
    BEFORE INSERT
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_bit();

Dans le cas où un déclencheur est déclaré avec plusieurs pourquoi attributs, développez simplement le suffixe de manière appropriée, par exemple, pour une insertion ou mise à jour déclencheur, ce qui précède deviendrait

CREATE TRIGGER my_table_biut 
    BEFORE INSERT OR UPDATE
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_biut();

Montrez-moi déjà du code !

Rendons-le réel. Nous commencerons par un exemple simple, puis développerons celui-ci pour illustrer d'autres fonctionnalités. Les instructions DDL de déclenchement nécessitent une fonction préexistante, comme mentionné, ainsi qu'une table sur laquelle agir, nous avons donc d'abord besoin d'une table sur laquelle travailler. Par exemple, disons que nous devons stocker les données d'identité de base du compte

CREATE TABLE person (
    login_name varchar(9) not null primary key,
    display_name text
);

Certaines mesures d'intégrité des données peuvent être gérées simplement avec une colonne DDL appropriée, comme dans ce cas une exigence que le login_name existe et ne comporte pas plus de neuf caractères. Les tentatives d'insertion d'une valeur NULL ou d'une valeur trop longue de login_name échouent et signalent des messages d'erreur significatifs :

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)

D'autres applications peuvent être gérées avec des contraintes de vérification, telles que l'exigence d'une longueur minimale et le rejet de certains caractères :

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

mais notez que le message d'erreur n'est pas aussi complet qu'auparavant, ne transmettant que ce qui est encodé dans le nom du déclencheur plutôt qu'un message textuel explicatif significatif. En implémentant la logique de vérification dans une fonction stockée à la place, vous pouvez utiliser une exception pour émettre un message texte plus utile. De plus, les expressions de contrainte de vérification ne peuvent pas contenir de sous-requêtes ni faire référence à des variables autres que les colonnes de la ligne actuelle ou d'autres tables de base de données.

Alors laissons tomber les contraintes de vérification

ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;

et continuez avec les déclencheurs et les fonctions stockées.

Montrez-moi un peu plus de code

Nous avons un tableau. Passant à la fonction DDL, nous définissons une fonction à corps vide, que nous pourrons remplir ultérieurement avec un code spécifique :

CREATE OR REPLACE FUNCTION person_bit() 
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    SET search_path = public
    AS '
    BEGIN
    END;
    ';

Cela nous permet enfin d'arriver au déclencheur DDL reliant la table et la fonction afin que nous puissions faire quelques exemples :

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

PostgreSQL permet d'écrire des fonctions stockées dans une variété de langages différents. Dans ce cas et les exemples suivants, nous composons des fonctions dans le langage PL/pgSQL qui est conçu spécifiquement pour PostgreSQL et prend en charge l'utilisation de tous les types de données, opérateurs et fonctions du SGBDR PostgreSQL. L'option SET SCHEMA définit le chemin de recherche de schéma qui sera utilisé pendant la durée de l'exécution de la fonction. Définir le chemin de recherche pour chaque fonction est une bonne pratique, car cela évite d'avoir à préfixer les objets de base de données avec un nom de schéma et protège contre certaines vulnérabilités liées au chemin de recherche.

EXEMPLE 0 - Validation des données

Comme premier exemple, implémentons les vérifications précédentes, mais avec une messagerie plus conviviale.

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

Le qualificatif "NEW" est une référence à la ligne de données sur le point d'être insérée. C'est l'une des nombreuses variables spéciales disponibles dans une fonction de déclenchement. Nous en présenterons quelques autres ci-dessous. Notez également que PostgreSQL™ autorise la substitution des guillemets simples délimitant le corps de la fonction par d'autres délimiteurs, dans ce cas en suivant une convention courante consistant à utiliser des signes dollar double comme délimiteur, puisque le corps de la fonction lui-même comprend des guillemets simples. Les fonctions de déclenchement doivent se terminer en renvoyant soit la ligne NEW à insérer, soit NULL pour abandonner silencieusement l'action.

Les mêmes tentatives d'insertion échouent comme prévu, mais maintenant avec une messagerie conviviale :

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.

EXEMPLE 1 - Journalisation d'audit

Avec les fonctions stockées, nous avons une grande latitude quant à ce que fait le code invoqué, y compris le référencement d'autres tables (ce qui n'est pas possible avec les contraintes de vérification). Comme exemple plus complexe, nous allons parcourir l'implémentation d'une table d'audit, c'est-à-dire la conservation d'un enregistrement, dans une table séparée, des insertions, mises à jour et suppressions dans une table principale. La table d'audit contient généralement les mêmes attributs que la table principale, qui sont utilisés pour enregistrer les valeurs modifiées, plus des attributs supplémentaires pour enregistrer l'opération exécutée pour effectuer la modification, ainsi qu'un horodatage de transaction et un enregistrement de l'utilisateur effectuant la modification. modifier :

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

Dans ce cas, la mise en œuvre de l'audit est très simple, nous modifions simplement la fonction de déclencheur existante pour inclure DML afin d'effectuer l'insertion de la table d'audit, puis redéfinissons le déclencheur pour qu'il se déclenche sur les mises à jour ainsi que sur les insertions. Notez que nous avons choisi de ne pas changer le suffixe du nom de la fonction de déclenchement en "biut", mais si la fonctionnalité d'audit avait été une exigence connue au moment de la conception initiale, ce serait le nom utilisé :

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

Notez que nous avons introduit une autre variable spéciale "TG_OP" que le système définit pour identifier l'opération DML qui a déclenché le déclencheur comme "INSERT", "UPDATE", "DELETE", ou "TRUNCATE", respectivement.

Nous devons gérer les suppressions séparément des insertions et des mises à jour car les tests de validation des attributs sont superflus et parce que la valeur spéciale NEW n'est pas définie lors de l'entrée dans un before delete fonction de déclenchement et ainsi définir la fonction stockée correspondante et le déclencheur :

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

Notez l'utilisation de la valeur spéciale OLD comme référence à la ligne qui est sur le point d'être supprimée, c'est-à-dire la ligne telle qu'elle existait avant la suppression se produit.

Nous effectuons quelques insertions pour tester la fonctionnalité et confirmer que le tableau d'audit inclut un enregistrement des insertions :

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)

Ensuite, nous effectuons une mise à jour sur une ligne et confirmons que la table d'audit inclut un enregistrement de la modification en ajoutant un deuxième prénom à l'un des noms d'affichage de l'enregistrement de données :

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)

Et enfin, nous exerçons la fonctionnalité de suppression et confirmons que la table d'audit inclut également cet enregistrement :

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)

EXEMPLE 2 - Valeurs dérivées

Allons un peu plus loin et imaginons que nous voulions stocker un document texte de forme libre dans chaque ligne, disons un CV au format texte brut ou un document de conférence ou un résumé de personnage de divertissement, et nous voulons prendre en charge l'utilisation de la puissante recherche en texte intégral capacités de PostgreSQL sur ces documents texte de forme libre.

Nous ajoutons d'abord deux attributs pour supporter le stockage du document et d'un vecteur de recherche de texte associé à la table principale. Étant donné que le vecteur de recherche de texte est dérivé ligne par ligne, il est inutile de le stocker dans la table d'audit, sinon nous ajoutons la colonne de stockage des documents à la table d'audit associée :

ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Ensuite, nous modifions la fonction de déclenchement pour traiter ces nouveaux attributs. La colonne de texte brut est gérée de la même manière que les autres données saisies par l'utilisateur, mais le vecteur de recherche de texte est une valeur dérivée et est donc géré par un appel de fonction qui réduit le texte du document à un type de données tsvector pour une recherche efficace.

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

À titre de test, nous mettons à jour une ligne existante avec du texte détaillé de Wikipédia :

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

puis confirmez que le traitement du vecteur de recherche de texte a réussi :

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)

EXEMPLE 3 - Déclencheurs et vues

Le vecteur de recherche de texte dérivé de l'exemple ci-dessus n'est pas destiné à la consommation humaine, c'est-à-dire qu'il n'est pas saisi par l'utilisateur et nous ne nous attendons jamais à présenter la valeur à un utilisateur final. Si un utilisateur tente d'insérer une valeur pour la colonne ts_abstract, tout ce qui est fourni sera ignoré et remplacé par la valeur dérivée en interne de la fonction de déclenchement, nous avons donc une protection contre l'empoisonnement du corpus de recherche. Pour masquer complètement la colonne, nous pouvons définir une vue abrégée qui n'inclut pas cet attribut, mais nous bénéficions toujours de l'activité du déclencheur sur la table sous-jacente :

CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;

Pour une vue simple, PostgreSQL le rend automatiquement inscriptible afin que nous n'ayons rien d'autre à faire pour insérer ou mettre à jour les données avec succès. Lorsque le DML prend effet sur la table sous-jacente, les déclencheurs s'activent comme si l'instruction était appliquée directement à la table, de sorte que nous obtenons toujours à la fois le support de recherche de texte exécuté en arrière-plan remplissant la colonne de vecteur de recherche de la table person ainsi que l'ajout du modifier les informations dans la table d'audit :

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)

Pour les vues plus compliquées qui ne répondent pas aux exigences d'écriture automatique, soit le système de règles, soit au lieu de les déclencheurs peuvent faire le travail pour prendre en charge les écritures et les suppressions.

EXEMPLE 4 - Valeurs récapitulatives

Embellissons davantage et traitons le scénario où il existe un type de table de transaction. Il peut s'agir d'un enregistrement des heures travaillées, des ajouts d'inventaire et des réductions de stock d'entrepôt ou de vente au détail, ou peut-être d'un registre de chèques avec des débits et des crédits pour chaque personne :

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

Et disons que s'il est important de conserver l'historique des transactions, les règles commerciales impliquent d'utiliser le solde net dans le traitement des demandes plutôt que les détails de la transaction. Pour éviter d'avoir à recalculer fréquemment le solde en additionnant toutes les transactions chaque fois que le solde est nécessaire, nous pouvons dénormaliser et conserver une valeur de solde actuelle dans la table des personnes en ajoutant une nouvelle colonne et en utilisant un déclencheur et une fonction stockée pour maintenir le solde net au fur et à mesure que les transactions sont insérées :

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

Il peut sembler étrange de faire d'abord la mise à jour dans la fonction stockée avant de valider la non-négativité des valeurs de débit, de crédit et de solde, mais en termes de validation des données, l'ordre n'a pas d'importance car le corps d'une fonction de déclenchement est exécuté comme un transaction de base de données, donc si ces vérifications de validation échouent, la totalité de la transaction est annulée lorsque l'exception est déclenchée. L'avantage de faire la mise à jour en premier est que la mise à jour verrouille la ligne affectée pour la durée de la transaction et ainsi toute autre session tentant de mettre à jour la même ligne est bloquée jusqu'à ce que la transaction en cours se termine. Le test de validation supplémentaire garantit que le solde résultant n'est pas négatif, et le message d'information sur l'exception peut inclure une variable qui, dans ce cas, renverra la ligne de transaction d'insertion incriminée pour le débogage.

Pour démontrer que cela fonctionne réellement, voici quelques exemples d'entrées et une vérification indiquant le solde mis à jour à chaque étape :

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

Notez comment la transaction ci-dessus échoue en raison de fonds insuffisants, c'est-à-dire qu'elle produirait un solde négatif et qu'elle serait annulée avec succès. Notez également que nous avons renvoyé la ligne entière avec la variable spéciale NEW comme détail supplémentaire dans le message d'erreur pour le débogage.

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)

EXEMPLE 5 - Déclencheurs et vues Redux

Il y a cependant un problème avec l'implémentation ci-dessus, et c'est que rien n'empêche un utilisateur malveillant d'imprimer de l'argent :

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;

Nous avons annulé le vol ci-dessus pour l'instant et montrerons un moyen d'intégrer une protection contre en utilisant un déclencheur sur une vue pour empêcher les mises à jour de la valeur du solde.

Nous augmentons d'abord la vue abrégée précédente pour exposer la colonne de solde :

CREATE OR REPLACE VIEW abridged_person AS
  SELECT login_name, display_name, abstract, balance FROM person;

Cela autorise évidemment l'accès en lecture à la balance, mais cela ne résout toujours pas le problème car pour des vues simples comme celle-ci basées sur une seule table, PostgreSQL rend automatiquement la vue accessible en écriture :

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;

We could use a rule, but to illustrate that triggers can be defined on views as well as tables, we will take the latter route and use an instead of update trigger on the view to block unwanted DML, preventing non-transactional changes to the balance value:

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.

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 blanc

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 tableau.

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.

Conclusion

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.