02 - Database DB2 for i02a - SQL04g - Varie sistemistica

SQL – Audit trigger … chi ha spostato il mio formaggio!

I sorgenti del trigger e del test di questo Post sono disponibile sul nostro GIST di Github:

Avete letto il libro di Spencer Johnson “Chi ha spostato il mio formaggio?” … se non lo avete fatto vi consiglio di farlo … è una semplice parabola sul cambiamento, nella vita, nel lavoro, nella famiglia. Nel libro, il formaggio rappresenta i nostri desideri, un buon lavoro, una bella famiglia, un po’ di soldi sul conto corrente … ma le cose cambiano e il problema è che le scorte di formaggio prima o poi finiscono.

Ma cosa c’entra il formaggio con un trigger SQL per l’Audit dei cambiamenti in una tabella? Niente, non c’entra niente, ma il titolo potrebbe fare molto “click-baiting” stimolando proprio la curiosità!

E’ solo che questo post nasce in realtà da un piccola discussione con un cliente (C):

  • (C) “Voglio sapere chi ha cambiato il codice pagamento su quell’ordine!”
  • (io) “OK, io, invece, vorrei sapere chi ha spostato il mio formaggio”
  • (C) “Ma cosa c’entra il formaggio … voglio sapere chi ha cambiato quel pagamento!”

Sulla tabella degli ordini non c’era nessun log, solo data, ora e utente che ha fatto l’ultima modifica … ma non c’era nessuna certezza che fosse colui che ha cambiato il codice di pagamento.

OK, la modifica può essere fatta da diversi programmi, non voglio mettere le mani a tutti, potrei attivare un trigger con la procedura normale dei trigger ADDPFTRG … ma la tabella degli ordini è sempre in uso, o dagli utenti o dai web service del B2B e della Intranet …

E’ una ottima occasione per pensare ad un trigger SQL che, con un trucchetto suggerito da Scott Forstie, è possibile attivare anche con la tabella in uso.

Non vi riporterò l’esempio reale sulla mia tabella degli ordini, cercherò invece di creare un trigger di Audit sulla solita tabella EMPLOYEE della libreria SAMPLEDB che utilizzo spesso in questo Blog.

Il sorgente SQL di questo post, se volete fare del copia-incolla, lo trovate anche in questo Github Gist, copiate da lì che non viene “rovinato” da WordPress. essenziali:

Prerequisiti per l’esempio: SAMPLEDB

L’esempio qui riportato si basa sulla tipica tabella EMPLOYEE di una SAMPLEDB, una libreria (SQL SCHEMA) con semplici esempi di tabelle relazionate tra loro


CALL QSYS.CREATE_SQL_SAMPLE(‘SAMPLEDB’)

Passo 1, la tabella di LOG

Iniziamo a creare la nostra tabella di Log … dove andremo a salvare gli eventi e le modifiche della effettuate sulla nostra tabella principale

CREATE OR REPLACE TABLE sampledb.Employee_AuditLog (
MYKEY VARCHAR(300),
EVENT CHAR(1),
USERNAME CHAR(10),
JOBNAME  VARCHAR(50),
DATA_CHANGES VARCHAR(300),
ADDSTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP);

Passo 2, creiamo il nostro Trigger

Creiamo il trigger sulla tabella EMPLOYEE che monitorizza gli eventi di INSERT/DELETE/UPDATE: Per l’INSERT e il DELETE andremo semplicemente a loggare utente e job che ha fatto l’operazione e il riferimento al nostro EMPNO, codice dipendete. Per l’UPDATE andiamo invece in dettaglio a controllare alcuni campi specifici della nostra tabella, rendendo il file di AUDIT leggibile dall’utente:

---------------------------------------------
-- Then: Create an AUDIT Trigger on my TABLE
---------------------------------------------
  CREATE OR REPLACE TRIGGER sampledb.Employee_trg
  AFTER INSERT OR DELETE OR UPDATE ON sampledb.Employee
  REFERENCING NEW ROW AS N OLD ROW AS O
  FOR EACH ROW MODE DB2ROW
  BEGIN
  DECLARE V_USERNAME CHAR ( 10 ) ;
  SET     V_USERNAME = CURRENT_USER ; 
  -- WHEN INSERTING A NEW RECORD WILL LOG USER/JOB and
  -- JUST A FEW INFO
    IF INSERTING THEN
      INSERT INTO sampledb.Employee_AuditLog
               (mykey, event, username, jobname, data_changes)
               VALUES('EMPNO: ' concat n.empno,
                      'I',
                      current user,
                      JOB_NAME,
                      'INSERT NEW EMPLOYEE: ' 
                         CONCAT trim(N.EMPNO)
                         CONCAT ' '
                         CONCAT trim(N.LASTNAME)
                         CONCAT ' '
                         CONCAT TRIM(N.FIRSTNME)
                      ) ;
    END IF ;
    -- WHEN DELETING 
    IF DELETING THEN
      INSERT INTO sampledb.Employee_AuditLog
               (mykey, event, username, jobname, data_changes)
               VALUES('EMPNO: ' concat o.empno,
                      'D',
                      current user,
                      JOB_NAME,
                      'DELETED EMPLOYEE: ' 
                         CONCAT trim(O.EMPNO)
                         CONCAT ' '
                         CONCAT trim(O.LASTNAME)
                         CONCAT ' '
                         CONCAT TRIM(O.FIRSTNME)
                      ) ;
    END IF ;

    -- WHEN UPDATING a ROW WILL LOG ONLY CHANGES 
    -- TO SOME FIELDS
    IF UPDATING THEN
      -------------------------
      -- FISTNAME or LASTNAME
      -------------------------
      IF O.FIRSTNME<> N.FIRSTNME OR
         O.LASTNAME<> N.LASTNAME THEN
         INSERT INTO sampledb.Employee_AuditLog
               (mykey, event, username, jobname, data_changes)
               VALUES('EMPNO: ' concat N.empno,
                      'U',
                      current user,
                      JOB_NAME,
                      'NAME CHANGE: ' 
                         CONCAT trim(O.LASTNAME)
                         CONCAT ' '
                         CONCAT trim(O.FIRSTNME)
                         CONCAT ' --> '
                         CONCAT trim(N.LASTNAME)
                         CONCAT ' '
                         CONCAT TRIM(N.FIRSTNME)
                      ) ;
         
      END IF;   
      -------------------------
      -- WORKDEPT and JOB
      -------------------------
       IF O.WORKDEPT<> N.WORKDEPT OR 
          O.JOB <> N.JOB THEN
         INSERT INTO sampledb.Employee_AuditLog
               (mykey, event, username, jobname, data_changes)
               VALUES('EMPNO: ' concat N.empno,
                      'U',
                      current user,
                      JOB_NAME,
                      'WORKDEPT/JOB CHANGE: '
                         CONCAT 'DEPT:' 
                         CONCAT trim(O.WORKDEPT)
                         CONCAT ' JOB:' 
                         CONCAT trim(O.JOB)
                         
                         CONCAT ' --> '
                         CONCAT ' DEPT:' 
                         CONCAT trim(N.WORKDEPT)
                         CONCAT ' JOB:' 
                         CONCAT trim(O.JOB)

                      ) ;
         
      END IF;   
      -------------------------
      -- SALARY OR BONUS
      -------------------------
       IF O.SALARY<> N.SALARY OR
          O.BONUS <> N.BONUS THEN
         INSERT INTO sampledb.Employee_AuditLog
               (mykey, event, username, jobname, data_changes)
               VALUES('EMPNO: ' concat N.empno,
                      'U',
                      current user,
                      JOB_NAME,
                      'SALARY/BONUS CHANGE: '
                         CONCAT 'S: ' 
                         CONCAT to_char(O.SALARY, '99,999.99') -- Italian Number Format (Decimal point)
                         CONCAT ' B: ' 
                         CONCAT to_char(O.BONUS,  '99,999.99')
                         CONCAT ' --> '
                         CONCAT 'S: ' 
                         CONCAT to_char(N.SALARY, '99,999.99')
                         CONCAT ' B: ' 
                         CONCAT to_char(N.BONUS,  '99,999.99')
                      ) ;
         
      END IF;   
      

   END IF ;
  END ;

Passo 3, testiamo il trigger

Facciamo un piccolo test del trigger, inserendo un dipendente, facendone alcune modifiche e quindi cancellandolo dalla tabella dei dipendenti

---------------------------------------
-- NOW WE CAN TRY OUT AUDIT LOG TRIGGER
----------------------------------------

-- 1 Check EMPNO 999999 (Doesn't exists)
select * from sampledb.employee 
  where empno='999999';
  
-- 2 Insert EMPNO 999999
insert into sampledb.employee
 (empno, firstnme, midinit, lastname, workdept, phoneno, hiredate, job, edlevel, sex, salary, bonus, comm)
values
 ('999999', 'Roberto','',  'De Pedrini', 'B01', '0343', current date, 'CLEANER', '4', 'M', 1000, 1, 0);

-- 3 WOW! I got promoted!
update sampledb.employee
 set JOB='VICE-P'
 where empno='999999';   
 
-- 4 OK now I want some more money!
update sampledb.employee
 set salary=salary+5000
 where empno='999999';   

-- 5 Oh my God, the boss will fire me!
delete from sampledb.employee
where empno='999999';

Passo 4: Leggiamo la nostra tabella di AUDIT … ecco, è stato lui!

-- NOW LET'S CHECK OUR AUDIT LOG
select * from sampledb.employee_auditlog;

Ma se la tabella (File) è in uso non posso attivare il trigger

Il grosso vantaggio del Trigger SQL è che può essere “attaccato” e “staccato” anche con la tabella in uso, come indicato in questo GIST di Scott Forstie (Change trigger program in production)… forzando, solo per un attimo, le regole del motore DB2 SQL

Se cerco di creare un trigger, cancellarlo o anche solo modificarlo con la tabella in uso ottengo l’errore SQL “[SQL0913] Row or Object in use”, ma con il trick di Scotto Forstie possiamo inserire in QAQQINI una regola momentanea di ALLOW_DDL_CHANGES_WHILE_OPEN: non è fantastico?

-- HOW TO ACTIVATE/DISACTIVATE AND CHANGE TRIGGER WHEN TABLE IS "IN USE"
-- Prepare to break the rules
call qsys2.override_qaqqini(1, '', '');
-- Rules broken
call qsys2.override_qaqqini(2,  
                            'ALLOW_DDL_CHANGES_WHILE_OPEN', 
                            '*YES');
 
-- Create/Delete or change your Trigger here
--..
        
-- Done breaking the rules
call qsys2.override_qaqqini(3, '', '');
    

Conclusione e riferimenti

La creazione di un trigger come questo è molto semplice, bastano 5 minuti ma dobbiamo decidere quali campi controllare (nel caso di update). Se volessimo un trigger generale, che controlla la modifica di qualsiasi campo della tabella, ma sempre con una logica simile a questo trigger c’è questo ottimo post di Michael Sansoterra su ITJungle, ancora del 2013: “Create a generic auditing trigger with SQL che con la tecnica degli SQL Descriptor passa tutti i campi della tabella.

Anche “Simple SQL Trigger” di Simon Huthinson su RPGPGM.COM può essere un ottimo spunto per gli SQL.

L’articolo vi è piaciuto? Avete qualche altro suggerimento?

Oltre a leggervi i post indicati sopra vi invito anche a leggervi i libri di Spencer Johnson, “Chi ha spostato il mio formaggio” e la sua continuazione … vi metto qui sotto i link Amazon se volete acquistarli o scaricarli sul vostro Kindle.

#AndraTuttoBene

--- Roberto De Pedrini Faq400.com
About author

Founder di Faq400 Srl, IBM Champion, ideatore del sito Faq400.com e del Blog blog.faq400.com. Sviluppatore RPG da quando avevo i pantaloni corti, forte sostenitore della piattaforma IBM i (ex AS400), ho sempre cercato di convididere le mie conoscenze con gli altri tramite forum, eventi e corsi. Oggi, tramite Faq400 Srl, cerchiamo di aiutare le aziende a sfruttare al meglio questa fantastica piattaforma IBM i.

Rispondi

%d blogger hanno fatto clic su Mi Piace per questo: