Nota di Diversinines:questo blog è stato pubblicato postumo alla morte di Berend Tober il 16 luglio 2018. Onoriamo i suoi contributi alla comunità di PostgreSQL e auguriamo pace al nostro amico e scrittore ospite.
In un articolo precedente abbiamo discusso dello pseudo-tipo seriale PostgreSQL, utile per popolare i valori delle chiavi sintetiche con numeri interi incrementali. Abbiamo visto che l'impiego della parola chiave del tipo di dati seriali in un'istruzione DDL (table data definition language) viene implementata come una dichiarazione di colonna di tipo intero che viene popolata, su un inserimento nel database, con un valore predefinito derivato da una semplice chiamata di funzione. Questo comportamento automatizzato di invocare il codice funzionale come parte della risposta integrale all'attività del linguaggio di manipolazione dei dati (DML) è una potente caratteristica dei sofisticati sistemi di gestione di database relazionali (RDBMS) come PostgreSQL. In questo articolo approfondiremo un altro aspetto più capace di invocare automaticamente codice personalizzato, ovvero l'uso di trigger e funzioni memorizzate. Introduzione
Usa casi per trigger e funzioni memorizzate
Parliamo del motivo per cui potresti voler investire nella comprensione dei trigger e delle funzioni memorizzate. Creando codice DML nel database stesso, è possibile evitare l'implementazione duplicata del codice relativo ai dati in più applicazioni separate che possono essere create per interfacciarsi con il database. Ciò garantisce l'esecuzione coerente del codice DML per la convalida dei dati, la pulizia dei dati o altre funzionalità come il controllo dei dati (ad esempio, la registrazione delle modifiche) o il mantenimento di una tabella di riepilogo indipendentemente da qualsiasi applicazione chiamante. Un altro uso comune dei trigger e delle funzioni memorizzate consiste nel rendere scrivibili le viste, ovvero per consentire inserimenti e/o aggiornamenti su viste complesse o per proteggere determinati dati di colonna da modifiche non autorizzate. Inoltre, i dati elaborati sul server anziché nel codice dell'applicazione non attraversano la rete, pertanto vi è un rischio minore di esposizione dei dati a intercettazioni e una riduzione della congestione della rete. Inoltre, in PostgreSQL le funzioni memorizzate possono essere configurate per eseguire codice a un livello di privilegio superiore rispetto all'utente della sessione, che ammette alcune potenti capacità. Faremo alcuni esempi in seguito.
Il caso contro i trigger e le funzioni memorizzate
Una revisione del commento sulla mailing list generale di PostgreSQL ha rivelato alcune opinioni sfavorevoli verso l'uso dei trigger e delle funzioni memorizzate che menziono qui per completezza e per incoraggiare te e il tuo team a valutare i pro ei contro della vostra implementazione.
Tra le obiezioni c'era, ad esempio, la percezione che le funzioni memorizzate non siano facili da mantenere, richiedendo quindi una persona esperta con competenze e conoscenze sofisticate nell'amministrazione di database per gestirle. Alcuni professionisti del software hanno segnalato che i controlli delle modifiche aziendali sui sistemi di database sono in genere più rigorosi rispetto al codice dell'applicazione, quindi se le regole aziendali o altre logiche vengono implementate all'interno del database, apportare modifiche man mano che i requisiti evolvono è proibitivo. Un altro punto di vista considera i trigger come un effetto collaterale inaspettato di qualche altra azione e, in quanto tali, possono essere oscuri, facilmente ignorabili, difficili da eseguire il debug e frustranti da mantenere e quindi di solito dovrebbero essere l'ultima scelta, non la prima.
Queste obiezioni potrebbero avere qualche merito, ma se ci pensi, i dati sono una risorsa preziosa e quindi probabilmente in realtà desideri una persona o un team competente ed esperto in ogni caso responsabile dell'RDBMS in un'organizzazione aziendale o governativa e, allo stesso modo, Cambia Le schede di controllo sono una componente comprovata della manutenzione sostenibile di un sistema informativo di registrazione e l'effetto collaterale di una persona è anche la potente comodità di un'altra, che è il punto di vista adottato per il bilancio di questo articolo.
Dichiarazione di un trigger
Iniziamo a imparare i dadi e i bulloni. Ci sono molte opzioni disponibili nella sintassi generale DDL per dichiarare un trigger e ci vorrebbe molto tempo per trattare tutte le possibili permutazioni, quindi per brevità parleremo solo di un sottoinsieme minimamente richiesto di esse negli esempi che segui usando questa sintassi ridotta:
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
Gli elementi configurabili richiesti oltre a un nome sono i quando , il perché , il dove e il cosa , ovvero la tempistica per il codice di attivazione da invocare rispetto all'azione di attivazione (quando), il tipo specifico di istruzione DML di attivazione (perché), la tabella o le tabelle agite (dove) e il codice funzione memorizzato da eseguire (cosa).
Dichiarazione di una funzione
La dichiarazione di trigger sopra richiede la specifica di un nome di funzione, quindi tecnicamente la dichiarazione di trigger DDL non può essere eseguita fino a quando la funzione di trigger non è stata definita in precedenza. Anche la sintassi DDL generale per una dichiarazione di funzione ha molte opzioni, quindi per la gestibilità useremo questa sintassi minimamente sufficiente per i nostri scopi qui:
CREATE [ OR REPLACE ] FUNCTION
name () RETURNS TRIGGER
{ LANGUAGE lang_name
| SECURITY DEFINER
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
}...
Una funzione trigger non accetta parametri e il tipo restituito deve essere TRIGGER. Parleremo dei modificatori facoltativi come li incontriamo negli esempi seguenti.
Uno schema di denominazione per trigger e funzioni
Il rispettato scienziato informatico Phil Karlton ha dichiarato (in forma parafrasata qui) che dare un nome alle cose è una delle maggiori sfide per i team di software. Presenterò qui un trigger di facile utilizzo e una convenzione di denominazione delle funzioni memorizzata che mi è stata utile e ti incoraggerò a considerare l'adozione per i tuoi progetti RDBMS. Lo schema di denominazione negli esempi di questo articolo segue uno schema di utilizzo del nome della tabella associato suffisso con un'abbreviazione che indica il trigger dichiarato quando e perché attributi:la prima lettera del suffisso sarà una "b", "a" o "i" (per "prima", "dopo" o "invece di"), la successiva sarà una o più di una "i" , "u", "d" o "t" (per "inserire", "aggiorna", "elimina" o "troncare") e l'ultima lettera è solo una "t" per trigger. (Uso una convenzione di denominazione simile per le regole, e in tal caso l'ultima lettera è "r"). Quindi, ad esempio, le varie combinazioni di attributi di dichiarazione di trigger minimo per una tabella denominata "my_table" sarebbero:
|-------------+-------------+-----------+---------------+-----------------|
| 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 |
|-------------+-------------+-----------+---------------+-----------------|
Lo stesso identico nome può essere utilizzato sia per il trigger che per la funzione memorizzata associata, il che è completamente consentito in PostgreSQL perché l'RDBMS tiene traccia dei trigger e delle funzioni memorizzate separatamente per i rispettivi scopi e il contesto in cui viene utilizzato il nome dell'elemento rende deselezionare a quale elemento si riferisce il nome.
Quindi, ad esempio, una dichiarazione trigger corrispondente allo scenario della prima riga della tabella sopra verrebbe vista implementata come
CREATE TRIGGER my_table_bit
BEFORE INSERT
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_bit();
Nel caso in cui un trigger venga dichiarato con più perché attributi, basta espandere il suffisso in modo appropriato, ad esempio per un inserimento o aggiornamento trigger, quanto sopra diventerebbe
CREATE TRIGGER my_table_biut
BEFORE INSERT OR UPDATE
ON my_table
FOR EACH ROW EXECUTE PROCEDURE my_table_biut();
Mostrami già del codice!
Rendiamolo reale. Inizieremo con un semplice esempio e poi lo espanderemo per illustrare ulteriori funzionalità. Le istruzioni DDL trigger richiedono una funzione preesistente, come accennato, e anche una tabella su cui agire, quindi prima abbiamo bisogno di una tabella su cui lavorare. Ad esempio, supponiamo di dover archiviare i dati di base dell'identità dell'account
CREATE TABLE person (
login_name varchar(9) not null primary key,
display_name text
);
Alcune imposizioni dell'integrità dei dati possono essere gestite semplicemente con un DDL di colonna appropriato, come in questo caso un requisito che il login_name esista e non contenga più di nove caratteri. I tentativi di inserire un valore NULL o un valore troppo lungo di login_name non riescono e segnalano messaggi di errore significativi:
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)
Altre imposizioni possono essere gestite con vincoli di controllo, come la richiesta di una lunghezza minima e il rifiuto di determinati caratteri:
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).
ma si noti che il messaggio di errore non è completamente informativo come prima, trasmettendo solo quanto è codificato nel nome del trigger piuttosto che un messaggio testuale esplicativo significativo. Implementando invece la logica di controllo in una funzione memorizzata, è possibile utilizzare un'eccezione per emettere un messaggio di testo più utile. Inoltre, le espressioni di vincolo di controllo non possono contenere sottoquery né fare riferimento a variabili diverse dalle colonne della riga corrente o da altre tabelle del database.
Quindi lasciamo perdere i vincoli di controllo
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;
e continua con i trigger e le funzioni memorizzate.
Mostrami altro codice
Abbiamo un tavolo. Passando alla funzione DDL, definiamo una funzione a corpo vuoto, che possiamo compilare in seguito con codice specifico:
CREATE OR REPLACE FUNCTION person_bit()
RETURNS TRIGGER
SET SCHEMA 'public'
LANGUAGE plpgsql
SET search_path = public
AS '
BEGIN
END;
';
Questo ci permette di arrivare finalmente al trigger DDL collegando la tabella e la funzione così possiamo fare alcuni esempi:
CREATE TRIGGER person_bit
BEFORE INSERT ON person
FOR EACH ROW EXECUTE PROCEDURE person_bit();
PostgreSQL consente di scrivere funzioni memorizzate in una varietà di linguaggi diversi. In questo caso e negli esempi seguenti, stiamo componendo funzioni nel linguaggio PL/pgSQL che è progettato specificamente per PostgreSQL e supporta l'uso di tutti i tipi di dati, operatori e funzioni di PostgreSQL RDBMS. L'opzione SET SCHEMA imposta il percorso di ricerca dello schema che verrà utilizzato per la durata dell'esecuzione della funzione. L'impostazione del percorso di ricerca per ogni funzione è una buona pratica, in quanto evita di dover anteporre agli oggetti del database un nome di schema e protegge da alcune vulnerabilità relative al percorso di ricerca.
ESEMPIO 0 - Convalida dei dati
Come primo esempio, implementiamo i controlli precedenti, ma con messaggi più a misura d'uomo.
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;
$$;
Il qualificatore “NEW” è un riferimento alla riga di dati che sta per essere inserita. È una delle numerose variabili speciali disponibili all'interno di una funzione trigger. Ne presenteremo alcuni altri di seguito. Si noti inoltre che PostgreSQL consente la sostituzione delle virgolette singole che delimitano il corpo della funzione con altri delimitatori, in questo caso seguendo una convenzione comune di utilizzare il segno del doppio dollaro come delimitatore, poiché il corpo stesso della funzione include virgolette singole. Le funzioni di trigger devono uscire restituendo la NUOVA riga da inserire o NULL per interrompere silenziosamente l'azione.
Gli stessi tentativi di inserimento falliscono come previsto, ma ora con messaggi amichevoli:
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.
ESEMPIO 1 - Registrazione audit
Con le funzioni memorizzate, abbiamo un'ampia libertà su ciò che fa il codice invocato, incluso il riferimento ad altre tabelle (cosa non possibile con i vincoli di controllo). Come esempio più complesso, esamineremo l'implementazione di una tabella di controllo, ovvero il mantenimento di un record, in una tabella separata, di inserimenti, aggiornamenti ed eliminazioni in una tabella principale. La tabella di controllo contiene in genere gli stessi attributi della tabella principale, che vengono utilizzati per registrare i valori modificati, più attributi aggiuntivi per registrare l'operazione eseguita per apportare la modifica, nonché un timestamp della transazione e un record dell'utente che effettua il cambia:
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
);
In questo caso, l'implementazione del controllo è molto semplice, modifichiamo semplicemente la funzione di trigger esistente per includere DML per effettuare l'inserimento della tabella di controllo, quindi ridefiniamo il trigger per attivare gli aggiornamenti e gli inserimenti. Si noti che abbiamo scelto di non modificare il suffisso del nome della funzione di attivazione in "biut", ma se la funzionalità di controllo fosse stata un requisito noto in fase di progettazione iniziale, quello sarebbe il nome utilizzato:
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();
Si noti che abbiamo introdotto un'altra variabile speciale "TG_OP" che il sistema imposta per identificare l'operazione DML che ha attivato il trigger rispettivamente come "INSERT", "UPDATE", "DELETE" o "TRUNCATE".
Dobbiamo gestire le eliminazioni separatamente dagli inserimenti e dagli aggiornamenti poiché i test di convalida degli attributi sono superflui e poiché il NUOVO valore speciale non è definito all'ingresso in un prima dell'eliminazione funzione trigger e quindi definire la funzione memorizzata e il trigger corrispondenti:
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();
Nota l'uso del valore speciale VECCHIO come riferimento alla riga che sta per essere eliminata, ovvero la riga come esiste prima l'eliminazione avviene.
Facciamo un paio di inserimenti per testare la funzionalità e confermiamo che la tabella di controllo include un record degli inserimenti:
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)
Quindi aggiorniamo una riga e confermiamo che la tabella di controllo include un record della modifica aggiungendo un secondo nome a uno dei nomi visualizzati del record di dati:
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 infine esercitiamo la funzionalità di eliminazione e confermiamo che la tabella di controllo include anche quel record:
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)
ESEMPIO 2 - Valori derivati
Facciamo un ulteriore passo avanti e immaginiamo di voler archiviare un documento di testo in formato libero all'interno di ogni riga, ad esempio un curriculum formattato in testo normale o un documento per conferenze o un abstract di carattere di intrattenimento, e vogliamo supportare l'uso della potente ricerca full-text capacità di PostgreSQL su questi documenti di testo in formato libero.
Per prima cosa aggiungiamo due attributi per supportare l'archiviazione del documento e di un vettore di ricerca testo associato alla tabella principale. Poiché il vettore di ricerca del testo è derivato per riga, non ha senso memorizzarlo nella tabella di controllo, se aggiungiamo la colonna di archiviazione dei documenti alla tabella di controllo associata:
ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;
ALTER TABLE person_audit ADD COLUMN abstract TEXT;
Quindi modifichiamo la funzione trigger per elaborare questi nuovi attributi. La colonna di testo normale viene gestita allo stesso modo degli altri dati inseriti dall'utente, ma il vettore di ricerca del testo è un valore derivato e quindi è gestito da una chiamata di funzione che riduce il testo del documento a un tipo di dati tsvector per una ricerca efficiente.
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;
$$;
Come test, aggiorniamo una riga esistente con del testo dettagliato 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 quindi conferma che l'elaborazione del vettore di ricerca del testo è andata a buon fine:
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)
ESEMPIO 3 - Trigger e visualizzazioni
Il vettore di ricerca del testo derivato dall'esempio precedente non è destinato al consumo umano, ovvero non è inserito dall'utente e non ci si aspetta mai di presentare il valore a un utente finale. Se un utente tenta di inserire un valore per la colonna ts_abstract, tutto ciò che viene fornito verrà scartato e sostituito con il valore derivato internamente alla funzione trigger, quindi abbiamo protezione contro l'avvelenamento del corpus di ricerca. Per nascondere completamente la colonna, possiamo definire una vista ridotta che non include quell'attributo, ma otteniamo comunque il vantaggio dell'attività di attivazione sulla tabella sottostante:
CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;
Per una visualizzazione semplice, PostgreSQL lo rende automaticamente scrivibile in modo da non dover fare nient'altro per inserire o aggiornare correttamente i dati. Quando il DML ha effetto sulla tabella sottostante, i trigger si attivano come se l'istruzione fosse applicata direttamente alla tabella, quindi otteniamo comunque sia il supporto per la ricerca di testo eseguito in background, popolando la colonna del vettore di ricerca della tabella person, sia aggiungendo il modificare le informazioni nella tabella di controllo:
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)
Per viste più complicate che non soddisfano i requisiti per essere scrivibili automaticamente, il sistema di regole o invece di i trigger possono svolgere il lavoro per supportare scritture ed eliminazioni.
ESEMPIO 4 - Valori di riepilogo
Abbelliamo ulteriormente e trattiamo lo scenario in cui è presente un tipo di tabella delle transazioni. Potrebbe essere un registro delle ore lavorate, aggiunte di inventario e riduzioni di magazzino o stock al dettaglio, o forse un registro degli assegni con addebiti e crediti per ogni persona:
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 supponiamo che, sebbene sia importante conservare la cronologia delle transazioni, le regole aziendali implicano l'utilizzo del saldo netto nell'elaborazione dell'applicazione anziché di qualsiasi dettaglio della transazione. Per evitare di dover ricalcolare frequentemente il saldo sommando tutte le transazioni ogni volta che è necessario il saldo, possiamo denormalizzare e mantenere un valore di saldo corrente proprio lì nella tabella delle persone aggiungendo una nuova colonna e utilizzando un trigger e una funzione memorizzata per mantenere il saldo netto al momento dell'inserimento delle transazioni:
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();
Può sembrare strano eseguire l'aggiornamento prima nella funzione memorizzata prima di convalidare la non negatività dei valori di debito, credito e saldo, ma in termini di convalida dei dati l'ordine non ha importanza perché il corpo di una funzione trigger viene eseguito come un transazione del database, quindi se i controlli di convalida falliscono, viene eseguito il rollback dell'intera transazione quando viene sollevata l'eccezione. Il vantaggio di eseguire prima l'aggiornamento è che l'aggiornamento blocca la riga interessata per la durata della transazione e quindi qualsiasi altra sessione che tenta di aggiornare la stessa riga viene bloccata fino al completamento della transazione corrente. L'ulteriore test di convalida assicura che il saldo risultante non sia negativo e il messaggio di informazioni sull'eccezione può includere una variabile, che in questo caso restituirà la riga della transazione di inserimento tentata incriminata per il debug.
Per dimostrare che funziona davvero, ecco alcune voci di esempio e un controllo che mostra il saldo aggiornato ad ogni passaggio:
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")
Nota come la transazione di cui sopra non riesce a causa di fondi insufficienti, ovvero produrrebbe un saldo negativo e verrà ripristinata con successo. Si noti inoltre che abbiamo restituito l'intera riga con la NUOVA variabile speciale come dettaglio aggiuntivo nel messaggio di errore per il debug.
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)
ESEMPIO 5 - Trigger e Views Redux
C'è un problema con l'implementazione di cui sopra, tuttavia, ed è che nulla impedisce a un utente malintenzionato di stampare denaro:
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;
Per ora abbiamo annullato il furto di cui sopra e mostreremo un modo per creare una protezione contro l'utilizzo di un trigger su una vista per impedire gli aggiornamenti del valore del saldo.
Per prima cosa aumentiamo la vista ridotta di prima per esporre la colonna del saldo:
CREATE OR REPLACE VIEW abridged_person AS
SELECT login_name, display_name, abstract, balance FROM person;
Questo ovviamente consente l'accesso in lettura al saldo, ma non risolve comunque il problema perché per viste semplici come questa basate su un'unica tabella, PostgreSQL rende automaticamente scrivibile la vista:
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;
Potremmo usare una regola, ma per illustrare che i trigger possono essere definiti sia sulle viste che sulle tabelle, prenderemo quest'ultima strada e useremo un invece di un aggiornamento 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.
Scarica il whitepaper oggi Gestione e automazione di PostgreSQL con ClusterControlScopri cosa devi sapere per distribuire, monitorare, gestire e ridimensionare PostgreSQLScarica il whitepaperEXAMPLE 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 table.
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.
Conclusione
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.