MINI CORSO DI MySQL - Stored Procedure - Triggers

http://www.mysqltutorial.org/mysql-stored-procedure-tutorial.aspx - vers. 12/12/2011 - rev. 15/12/2011

Prima di cominciare occorre creare il DB di prova utilizzato in questo breve manuale.

/* Creazione DB */
DROP DATABASE IF EXISTS DBProva;
CREATE DATABASE DBProva DEFAULT CHARACTER SET latin1;
USE DBProva
/* Creo la tabella */
DROP TABLE IF EXISTS Studenti;
CREATE TABLE Studenti
(
    IdStudente INT Auto_increment PRIMARY KEY,
    Nominativo VARCHAR(60) NOT NULL,
    NatoIl DATETIME,
    Ripetente BIT DEFAULT 0,
    Sesso ENUM('Maschio', 'Femmina'),
    Eta TINYINT UNSIGNED,
    AnnoDiIscrizione INT,
    RedditoDiRiferimentoInLire INT,
    ConsiderazioniSulloStudente MEDIUMTEXT,
    Foto MEDIUMBLOB,
    RedditoDiRiferimento DOUBLE,
    MediaVoti FLOAT,
    ImportoTassePagato DECIMAL(12,2),
    Classe CHAR(2),
    CodiceFiscale CHAR(16) UNIQUE,
    FULLTEXT (Nominativo, ConsiderazioniSulloStudente)
) ENGINE=MyISAM;
/* Popolo la tabella con dei dati */
INSERT INTO Studenti (IdStudente, Nominativo, Classe, Sesso, NatoIl, Ripetente, Eta, MediaVoti)
values
(1,'Bianchi Bruna','5N','Femmina','1994-01-15 00:00:00',1,17,6.11),
(2,'Neri Bianca','6N','Femmina','1995-04-25 00:00:00',0,16,3.32),
(3,'Rossi Mario','5N','Maschio','1995-11-13 00:00:00',0,16,9.21),
(4,'Verdi Aginulfo','5N','Maschio','1995-11-13 00:00:00',0,16,9.21),
(5,'Gialli Primo','5N','Maschio','1995-11-13 00:00:00',0,16,9.21),
(6,'Azzurro Celestina','5N','Femmina','1995-11-13 00:00:00',0,16,9.21),
(7,'Blu Bruno','5N','Maschio','1995-09-23 00:00:00',0,16,7.31);

Per creare l'archivio di prova basta copiare le seguenti istruzioni SQL ed incollarle dentro l'interfaccia a linea di comando di MySQL.

LE STORED PROCEDURE E FUNCTION

Una "Stored Procedure" è una sequenza di istruzioni SQL salvata all'interno del catalogo del DB e che può essere richiamata da:
- un programma
- un'altra Stored Procedure.

Una Stored Procedure che richiama se stessa si dice ricorsiva. La gran parte dei RDBMS (Relational DataBase Management System) supportano le Stored Procedure Ricorsive ma in MySQL, per default, questa modalità risulta disabilitata.
Per abilitare la ricorsione occorre cambiare il valore della variabile server max_sp_recursion_depth impostandola ad un valore maggiore di 0.

Occorre quindi digitare questi comandi:

show variables like 'max_sp_recursion_depth';

oppure (@@ indica una variabile globale):

SELECT @@max_sp_recursion_depth;

per verificare il valore di max_sp_recursion_depth e poi

set global max_sp_recursion_depth = 255;

per impostare il nuovo valore

La modifica della variabile rimane attiva fino al primo arresto del DB server (quindi dopo un riavvio del PC oppure se digito da Dos net stop mysql la variabile ritorna al suo valore originale!).  Per rendere la modifica permanente occorre modificare il file my.ini, nella sezione relativa al server [mysqld], come mostrato nella successiva figura:

I vantaggi delle Stored Procedure (SP da questo momento in poi) sono:

- Le SP aumentano le performance delle applicazioni. Una volta creata la SP viene compilata e salvata nel catalogo del DB. E' quindi più veloce di un comando SQL equivalente non compilato.
- Le SP riducono il traffico tra Server DB ed applicazione client poiché invece di inviare una sequenza (più o meno lunga) di comandi SQL si limita a mandare solo il nome della procedura ed eventuali parametri.
- Le SP sono riusabili e trasparenti a qualsiasi applicazione. Con le SP  è possibile creare un'interfaccia comune che consenta l'uso corretto del DB a tutte le applicazioni, senza che i programmatori delle applicazioni client debbano riscrivere le funzionalità fornite dalle SP disponibili.

- Le SP sono sicure grazie all'uso dei diritti di accesso forniti dal
RDBMS.

Gli svantaggi delle SP sono:

- Le SP generano un carico ulteriore, sia per quanto riguarda la RAM che la CPU, del Server DB. Le SP non si limitano a richiedere il recupero dei dati ma solitamente svolgono una serie di operazioni logiche che non sempre si adattano alle funzionalità per le quali il server DB è stato progettato.
- Le SP contengono un set di istruzioni SQL non adatto a procedure caratterizzate da una logica complessa. In altri termini non hanno la stessa potenza computazionale di linguaggi come il C++, C# etc.
- Generalmente gli
RDBMS forniscono funzionalità di debugging limitate o addirittura nessuna. Per chi sviluppa questo è un problema da non sottovalutare.
- La scrittura e la manutenzione di una SP richiede una competenza che non tutti gli sviluppatori hanno.

La sintassi generica di una SP è la seguente:

CREATE PROCEDURE nome_funzione ([IN|OUT|INOUT parametro[,...]])
BEGIN
   corpo;
END;

Scrittura della prima Stored Procedure

Consideriamo la seguente SP che estrae tutti i nominativi dalla tabella Studenti.

DELIMITER //
DROP PROCEDURE IF EXISTS BlindaTuttiINominativi//
CREATE PROCEDURE BlindaTuttiINominativi()
BEGIN
  SELECT Nominativo FROM Studenti;
END //
DELIMITER ;

Il primo comando  DELIMITER // non è collegato alla SP. Serve solo a dire a MySQL che il terminatore standard di una istruzione SQL non è più ; ma un'altra sequenza di caratteri. Nel caso specifico da ; a //. In questo modo posso avere più comandi SQL all'interno della SP che possono essere separati dal ;. Dopo l'esecuzione dell'istruzione END //  l'istruzione DELIMITER ; consente il ripristino del terminatore di default.

Il secondo comando DROP PROCEDURE IF ... serve a eliminare un'eventuale versione precedente della procedura.

Il corpo della SP deve essere delimitato dalle istruzioni BEGIN ed END. Nel corpo abbiamo la sequenza di istruzioni SQL che implementano le funzionalità richieste alla SP. Per invocare una SP dobbiamo utilizzare il seguente comando.

CALL Stored_Procedure_Name();

La SP del nostro primo esempio potrà quindi essere richiamata con il comando:

CALL BlindaTuttiINominativi();

 

Nel caso la SP non abbia parametri è possibile omettere le parentesi () finali:

CALL BlindaTuttiINominativi;

E' possibile visualizzare l'elenco delle SP create mediante il comando:

SHOW PROCEDURE STATUS;
SHOW PROCEDURE STATUS WHERE db='NomeDB';

Le Stored Function

Le Stored Functions sono simili alle SP, ma hanno uno scopo più semplice, cioè quello di definire nuove funzioni, in aggiunta a quelle già fornite da MySQL. Esse restituiscono un valore, e non possono quindi restituire un resultset (insieme di record) come invece avviene per le SP. Nelle versioni di MySQL precedenti alla 5.0 esistevano le "user-defined functions", che venivano memorizzate esternamente al server. Ora queste funzioni sono ancora supportate, ma è sicuramente consigliabile utilizzare le nuove Stored Functions. La sintassi generica di una Stored Function è la seguente:

CREATE FUNCTION nome_funzione ([parametro[,...]])
RETURNS tipo
BEGIN
   corpo;
   RETURN Valore;
END;

Ecco la Stored Function che determina il sesso dello studente a partire dal nominativo.

DELIMITER //
DROP FUNCTION IF EXISTS SessoDi//
CREATE FUNCTION SessoDi(VarNominativo VARCHAR(60))
RETURNS ENUM('Maschio', 'Femmina')
BEGIN
    DECLARE VarSesso ENUM('Maschio', 'Femmina');
    SELECT Sesso INTO VarSesso FROM Studenti WHERE Nominativo=VarNominativo;
    RETURN VarSesso;
END //
DELIMITER ;

Per utilizzare la funzione digitiamo il comando:

SELECT SessoDi('Bianchi Bruna');

E' possibile visualizzare l'elenco delle Stored Function create mediante il comando:

SHOW FUNCTION STATUS;
SHOW FUNCTION STATUS WHERE db='NomeDB';

La dichiarazione delle variabili

Le variabili sono utilizzate nelle SP per memorizzare i dati  da manipolare e i risultati. Il nome di una variabile non deve essere identico a quello di una delle tabelle o uno dei campi presenti nel DB in cui è memorizzata la SP. Il tipo di dato è tra quelli supportati dal RDBMS.

In MySQL i tipi di dato principali sono:

- Numerici:
BIT[(M)],
TINYINT[(M)] [UNSIGNED] [ZEROFILL],
SMALLINT[(M)] [UNSIGNED] [ZEROFILL],
MEDIUMINT[(M)] [UNSIGNED] [ZEROFILL],
INT[(M)] [UNSIGNED] [ZEROFILL],
BIGINT[(M)] [UNSIGNED] [ZEROFILL],
FLOAT[(M,D)] [UNSIGNED] [ZEROFILL],
DOUBLE[(M,D)] [UNSIGNED] [ZEROFILL],
DECIMAL[(M[,D])] [UNSIGNED] [ZEROFILL]

- Temporali:
DATE,
DATETIME,
TIMESTAMP[(M)],
TIME,
YEAR[(2|4)]

Stringhe:
[NATIONAL] CHAR(M) [BINARY | ASCII | UNICODE],
[[NATIONAL] VARCHAR(M) [BINARY],
BINARY(M),
VARBINARY(M),
TINYBLOB,
TINYTEXT,
BLOB[(M)],
TEXT[(M)],
MEDIUMBLOB,
MEDIUMTEXT,
LONGBLOB,
LONGTEXT ENUM('valore1','valore2',...),
SET('valore1','valore2',...)

La sintassi da utilizzare per la dichiarazione di una variabile è la seguente:

DECLARE variable_name datatype(size) DEFAULT default_value;

Vediamo un esempio:

DELIMITER //
DROP PROCEDURE IF EXISTS DichiaraEMostraDelleVariabili//
CREATE PROCEDURE DichiaraEMostraDelleVariabili()
BEGIN
    DECLARE Nominativo VARCHAR(20) DEFAULT 'Sechi Marco';
    DECLARE SonoNatoIl DATE DEFAULT '1995-11-13';
    DECLARE Eta TINYINT UNSIGNED DEFAULT 47;
    DECLARE Cap SMALLINT ZEROFILL DEFAULT 23;
    DECLARE Adesso DATETIME DEFAULT NOW();
    DECLARE Sesso ENUM('Maschio','Femmina') DEFAULT 'Maschio';
    DECLARE Frase TEXT DEFAULT '';
    /* Compongo la frase di output */
    SET Frase=CONCAT('\nNominativo: ',Nominativo,'\nNato il:',SonoNatoIl,'\nEta:',Eta);
    SET Frase=CONCAT(Frase,'\nSesso:',Sesso,'\nAdesso:',Adesso,'\nCap:',Cap);
    SELECT Frase;
END //
DELIMITER ;
Call DichiaraEMostraDelleVariabili;

L'esecuzione della SP produce il seguente output:

Una volta dichiarate le variabili possono essere valorizzate mediante il comando SET.

E' possibile assegnare un valore ad una variabile anche con il comando SELECT ... INTO :

DECLARE NrStudenti INT DEFAULT 0;
SELECT COUNT(*) INTO NrStudenti FROM Studenti;

Le variabili hanno un proprio livello di visibilità. Se la variabile è dichiarata all'interno di una SP essa non sarà più accessibile dopo  aver eseguito l'END della SP. E' possibile dichiarare diverse variabili con lo stesso nome purché in differenti SP. Una variabile che inizia con @ è una variabile di sessione ed esiste finché la sessione non termina (ad esempio con il comando EXIT nell'interfaccia a linea di comando). Una variabile che inizia con @@ è una variabile globale e rimane visibile fino al restart del server DB.

Uso dei parametri in una SP

I parametri consentono una maggior flessibilità ed usabilità delle SP. In Mysql i parametri sono inseriti con 3 modalità: IN , OUT e INOUT.
IN
è la modalità di default.

- IN: indica che il parametro è passato alla SP ma non può essere modificato dalla SP.

- OUT: questo modalità consente la modifica del parametro che viene quindi usato per restituire un valore di ritorno.

- INOUT: è una combinazione delle 2 modalità. Posso passare un valore ad una SP e ricevere il risultato prodotto dall'esecuzione della SP utilizzando lo stesso parametro.

La sintassi per il passaggio dei parametri è la seguente:

MODE Nome_Parametro Tipo_Parametro(Dimens_Param)

- MODE: assume il valore  IN , OUT o INOUT a seconda.
-
Nome_Parametro: è il nome del parametro. Se all'interno della SP abbiamo dei comandi SQL che coinvolgono delle tabelle, tale nome non deve essere uguale a quello di una tabella o campo del DB in uso.
-
Tipo_Parametro(Dimens_Param): il tipo e la dimensione del parametro.

Nel caso siano presenti più parametri questi vanno separati con la virgola.

Analizziamo ora alcuni esempi:

Esempio 1:

DELIMITER //
USE DBProva//
DROP PROCEDURE IF EXISTS MostraINominativiDellaClasse//
CREATE PROCEDURE MostraINominativiDellaClasse(IN VarClasse VARCHAR(255))
BEGIN
  SELECT Nominativo FROM Studenti WHERE Classe=VarClasse;
END //
DELIMITER ;

Il parametro VarClasse (mode:IN) contiene la sigla della classe dalla quale si vuole estrarre l'elenco degli studenti. Ad esempio per estrarre tutti gli studenti della fulgida '6N' devo digitare il seguente comando:

CALL MostraINominativiDellaClasse('6N');

Esempio 2:

DELIMITER $$
USE DBProva$$
DROP PROCEDURE IF EXISTS ContaGliStudentiDellaClasse//
CREATE PROCEDURE ContaGliStudentiDellaClasse(IN VarClasse VARCHAR(255), OUT NrStud INT)
BEGIN
  SELECT Count(*) INTO NrStud FROM Studenti WHERE Classe=VarClasse;
END $$
DELIMITER ;

In VarClasse (mode:IN) viene impostata la classe mentre nella variabile  NrStud (mode:OUT) viene restituito il numero degli studenti della classe indicata nel primo parametro. Per conoscere il numero di studenti presenti nella fulgida '6N' basta digitare il seguente comando:

CALL ContaGliStudentiDellaClasse('6N',@quanti);
SELECT @quanti AS Numero_Studenti;

Esempio 3:

DELIMITER $$
USE DBProva$$
DROP PROCEDURE IF EXISTS RendiPrimaLetteraMaiuscolaDiOgniParola$$
CREATE PROCEDURE RendiPrimaLetteraMaiuscolaDiOgniParola(INOUT stringa VARCHAR(1024))
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE myc, pc CHAR(1);
    DECLARE outstr VARCHAR(1000) DEFAULT stringa;
    WHILE i <= CHAR_LENGTH(stringa) DO
        SET myc = SUBSTRING(stringa, i, 1);
        SET pc = CASE WHEN i = 1 THEN ' ' ELSE SUBSTRING(stringa, i - 1, 1) END;
        IF pc IN (' ', '&', '''', '_', '?', ';', ':', '!', ',', '-', '/', '(', '.') THEN
            SET outstr = INSERT(outstr, i, 1, UPPER(myc));
        END IF;
        SET i = i + 1;
    END WHILE;
    SET stringa = outstr;
END$$
DELIMITER ;

Questa SP rende maiuscola la prima lettera di ogni parola contenuta nel parametro Stringa che ho passato. Quindi digitando la seguente sequenza di comandi:

SET @frase = 'sechi marco';
CALL RendiPrimaLetteraMaiuscolaDiOgniParola(@frase);
SELECT @frase;

ottengo il seguente risultato:

L'istruzione IF

Nelle SP è possibile utilizzare l'istruzione IF. La sintassi è la seguente:

IF Condizioni THEN
    Comandi
ELSEIF Condizioni THEN
    Comandiandi
...
ELSE
    Comandi
END IF

Vediamo un esempio di utilizzo dell'istruzione IF all'interno di una FUNCTION.

DELIMITER //
CREATE FUNCTION SimpleCompare(n INT, m INT)
RETURNS VARCHAR(20)
BEGIN
    DECLARE s VARCHAR(20);
    IF n > m THEN
        SET s = '>';
    ELSEIF n = m THEN
        SET s = '=';
    ELSE
        SET s = '<';
    END IF;
    SET s = CONCAT(n, ' ', s, ' ', m);
    RETURN s;
END //
DELIMITER ;

Per richiamare la funzione possiamo utilizzare un'istruzione SELECT:

SELECT SimpleCompare(4,12) AS Relazione UNION
SELECT SimpleCompare(4,2) UNION
SELECT SimpleCompare(4,4);

L'istruzione CASE

Quando ho più condizioni posso usare in alternativa all'IF il construtto CASE (che peraltro risulta più leggibile). La sintassi dell'istruzione CASE è la seguente:

CASE
    WHEN Condizioni1 THEN Comandi1
    WHEN Condizioni2 THEN Comandi2
    ...
    ELSE ComandiN
END CASE

Ecco un esempio di Stored Function che utilizza l'istruzione CASE:

DELIMITER //
DROP FUNCTION IF EXISTS EstraiStudente//
CREATE FUNCTION EstraiStudente(Estrai VARCHAR(50))
RETURNS VARCHAR(100)
BEGIN
    DECLARE Risposta VARCHAR(100) DEFAULT '';
    CASE Estrai
        WHEN 'Con ID + piccolo' THEN
            SELECT Nominativo INTO Risposta FROM Studenti
            ORDER BY IdStudente ASC LIMIT 1;
        WHEN 'Con ID + grande' THEN
            SELECT Nominativo INTO Risposta FROM Studenti
            ORDER BY IdStudente DESC LIMIT 1;
        WHEN '+ Giovane' THEN
            SELECT Nominativo INTO Risposta FROM Studenti
            ORDER BY NatoIl ASC LIMIT 1;
        WHEN '+ Vecchio' THEN
            SELECT Nominativo INTO Risposta FROM Studenti
            ORDER BY NatoIl DESC LIMIT 1;
        WHEN 'Primo Alfabeticamente' THEN
            SELECT Nominativo INTO Risposta FROM Studenti
            ORDER BY Nominativo ASC LIMIT 1;
        WHEN 'Ultimo Alfabeticamente' THEN
            SELECT Nominativo INTO Risposta FROM Studenti
            ORDER BY Nominativo DESC LIMIT 1;
        ELSE
            SET Risposta='Statistica non prevista';
    END CASE;
    SET Risposta=CONCAT(Estrai,': ',Risposta);
    RETURN Risposta;
END //
DELIMITER ;

Per richiamare la funzione utilizziamo un'istruzione SELECT:

SELECT EstraiStudente('Con ID + piccolo') AS Statistica UNION
SELECT EstraiStudente('Con ID + grande') UNION
SELECT EstraiStudente('+ Giovane') UNION
SELECT EstraiStudente('+ Vecchio') UNION
SELECT EstraiStudente('Primo Alfabeticamente') UNION
SELECT EstraiStudente('Ultimo Alfabeticamente') UNION
SELECT EstraiStudente('+ Intelligente');

Ora un'altro esempio di utilizzo dell'istruzione CASE in una SP:

USE DBProva;
-- I parametri ? non possono essere usati al posto di
-- parole chiave del linguaggio SQL. Neanche al posto dei
-- di un nome di un campo.
DELIMITER //
DROP PROCEDURE IF EXISTS EstraiStudente//
CREATE PROCEDURE EstraiStudente(IN Estrai VARCHAR(50),INOUT Uscita VARCHAR(1024))
BEGIN
    SET @CmdSQL="SELECT ? AS Statistica, Nominativo AS Riferimento FROM Studenti ORDER BY ";
    SET @NrRighe=1;
    SET @Statistica=Estrai;
    CASE Estrai
        WHEN 'Con ID + piccolo' THEN
            SET @Fld="IdStudente";
            SET @OrderSQL=" ASC LIMIT ?";
        WHEN 'Con ID + grande' THEN
            SET @Fld="IdStudente";
            SET @OrderSQL=" DESC LIMIT ?";
        WHEN '+ Giovane' THEN
            SET @Fld="NatoIl";
            SET @OrderSQL=" DESC LIMIT ?";
        WHEN '+ Vecchio' THEN
            SET @Fld="NatoIl";
            SET @OrderSQL=" ASC LIMIT ?";
        WHEN 'Primo Alfabeticamente' THEN
            SET @Fld="Nominativo";
            SET @OrderSQL=" ASC LIMIT ?";
        WHEN 'Ultimo Alfabeticamente' THEN
            SET @Fld="Nominativo";
            SET @OrderSQL=" DESC LIMIT ?";
        ELSE
            SET @CmdSQL="SELECT ? AS Statistica, 'Statistica non prevista' AS Riferimento FROM DUAL LIMIT ?";
            SET @Fld="";
            SET @OrderSQL="";
    END CASE;
    SET @CmdSQL=CONCAT(@CmdSQL,@Fld,@OrderSQL," INTO @Campo1,@Campo2");
    PREPARE hCmdSQL FROM @CmdSQL;
    EXECUTE hCmdSQL USING @Statistica, @NrRighe;
    DEALLOCATE PREPARE hCmdSQL;
    SET Uscita=CONCAT(Uscita,RPAD(@Campo1,25," "),": ",@Campo2,"\n");
END //
DELIMITER ;

Per utilizzare questa procedura possiamo scrivere:

SET @Frase="\n";
Call EstraiStudente('Con ID + piccolo',@Frase);
Call EstraiStudente('Con ID + grande',@Frase);
Call EstraiStudente('+ Giovane',@Frase);
Call EstraiStudente('+ Vecchio',@Frase);
Call EstraiStudente('Primo Alfabeticamente',@Frase);
Call EstraiStudente('Ultimo Alfabeticamente',@Frase);
Call EstraiStudente('+ Intelligente',@Frase);
SELECT @Frase\G

L'istruzione WHILE

La programmazione delle SP in MySQL supporta il loop consentendo l'esecuzione dei comandi iterativi. La prima istruzione iterativa è WHILE la cui sintassi è:

WHILE Condizione DO
    Comandi
END WHILE

I comandi verranno eseguiti fino a che la condizione resta vera (TRUE). Poiché il WHILE controlla subito la condizione segue che la particella iterativa COMANDI, nel caso la Condizione sia inizialmente FALSE, non venga assolutamente eseguita.
Ecco ora un esempio che utilizza questa istruzione iterativa.

DELIMITER $$
DROP PROCEDURE IF EXISTS WhileLoopProc$$
CREATE PROCEDURE WhileLoopProc()
BEGIN
    DECLARE x INT DEFAULT 1;
    DECLARE str VARCHAR(255) DEFAULT '|';
    WHILE x <= 5 DO
        SET str = CONCAT(str,x,'|');
        SET x = x + 1;
    END WHILE;
    SELECT str;
END$$
DELIMITER ;

In questa SP, viene visualizzata una stringa contenente la sequenza di numeri 1, 2 , 3, 4, 5 separati dal simbolo pipe |.  Per eseguire la SP digitiamo:

CALL WhileLoopProc;

Attenzione! La condizione X<=5 risulta sempre vera quando la variabile X non viene inizializzata. In questo caso si potrebbe verificare un loop infinito che porta al crash del server RDBMS.

L'istruzione REPEAT

La programmazione delle SP in MySQL supporta anche il REPEAT la cui sintassi è:

REPEAT
    Comandi;
UNTIL Condizione END REPEAT

La sequenza COMANDI verrà eseguita fino a che la Condizione resta FALSE (opposto del WHILE). La particella iterativa COMANDI verrà eseguita almeno una volta.
La SP precedente riscritta con
REPEAT diventa:

DELIMITER $$
DROP PROCEDURE IF EXISTS RepeatLoopProc$$
CREATE PROCEDURE RepeatLoopProc()
BEGIN
    DECLARE x INT DEFAULT 1;
    DECLARE str VARCHAR(255) DEFAULT '|';
    REPEAT
        SET str = CONCAT(str,x,'|');
        SET x = x + 1;
    UNTIL x > 5 END REPEAT;
    SELECT str;
END$$
DELIMITER ;

Si osservi che non ho alcun ; dopo la condizione X>5. Per eseguire la SP digitiamo:

CALL RepeatLoopProc;

Istruzioni LEAVE, ITERATE e LOOP

L'istruzione LEAVE consente l'uscita immediata dal loop. Ricorda come comportamento l'istruzione break del C++, Java e C#.
L'istruzione
ITERATE consente invece di ripetere completamente l'ultimo loop. Assomiglia al continue del C++, Java, C#.
MySQL supporta inoltre l'istruzione
LOOP che permette di eseguire delle istruzione ripetutamente ma con maggior flessibilità. Vediamo degli esempi con queste istruzioni

Esempio con LEAVE:

DELIMITER $$
DROP PROCEDURE IF EXISTS LOOPLoopLeaveProc$$
CREATE PROCEDURE LOOPLoopLeaveProc()
BEGIN
    DECLARE x INT DEFAULT 1;
loop_label:
    LOOP
        IF x > 5 THEN
            LEAVE loop_label;
        END IF;
        SET x = x + 1;
    END LOOP;
    SELECT x;
END$$
DELIMITER ;
CALL RepeatLoopLeaveProc;

Questa SP visualizza il numero 6.

Esempio con ITERATE:

DELIMITER $$
DROP PROCEDURE IF EXISTS LOOPIterateProc$$
CREATE PROCEDURE LOOPIterateProc()
BEGIN
    DECLARE x INT DEFAULT 0;
    DECLARE str VARCHAR(255) DEFAULT '|';
loop_label:
    LOOP
        SET x = x + 1;
        IF x MOD 2 = 0 THEN
            ITERATE loop_label;
        END IF;
        IF x > 10 THEN
            LEAVE loop_label;
        END IF;
        SET str = CONCAT(str,x,'|');
    END LOOP;
    SELECT str;
END$$
DELIMITER ;
CALL LOOPIterateProc;

Questa SP visualizza la stringa |1|3|5|7|9|. Quando la variabile X è maggiore di 10 il ciclo si interrompe per via dell'istruzione LEAVE. Se X è dispari l'istruzione ITERATE consente di ignorare la concatenazione del numero X alla stringa str che invece è eseguita nel caso X sia pari. Per eseguire la SP digitiamo:

CALL LOOPIterateProc;

I Cursor

MySQL consente l'uso dei Cursor nelle SP. Un Cursor consente di operare singolarmente su ogni riga restituita durante l'esecuzione di una query. Questo permette di processare in modo distinto ogni singola riga. Attualmente nelle versioni 5.x di MySQL i cursori hanno le seguenti proprietà:

> ReadOnly: non possono essere quindi aggiornati
> Non Scrollabili: l'attraversamento avviene solo in una sola direzione (per cui non è possibile muoversi a ritroso, saltare un record, etc.)
> ASensitive: Non bloccano la tabella per cui se i dati vengono modificati da altre applicazioni allora si potrebbero ottenere risultati inattesi (in altre parole il server non effettua una copia dei risultati estratti dal cursore).

MySQL supporta i seguenti comandi:

1) dichiarazione del cursore:

DECLARE cursor_name CURSOR FOR SELECT_statement;

2) apertura del cursore:

OPEN cursor_name;

3) estrazione della riga corrente:

FETCH cursor_name INTO variable list;

4) chiusura del cursore per liberare la memoria associata.

CLOSE cursor_name;

Quando si utilizzano i cursori occorre sempre controllare lo stato di NOT FOUND per evitare errori fatali come "no data to fetch". Questa SP mostra l'utilizzo delle SP.

DELIMITER $$
DROP PROCEDURE IF EXISTS CursorProc$$
CREATE PROCEDURE CursorProc()
BEGIN DECLARE basta_studenti INT DEFAULT 0;
    DECLARE std_nominativo VARCHAR(60);
    DECLARE std_classe VARCHAR(2);
    DECLARE Stringa MEDIUMTEXT DEFAULT '\n';
    -- Il cursore va dichiarato prima dell'handler
    DECLARE cur_studenti CURSOR FOR SELECT Classe, nominativo FROM Studenti;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET basta_studenti = 1;
    OPEN cur_studenti;
    REPEAT
        FETCH cur_studenti INTO std_classe, std_nominativo;
        IF (basta_studenti = 0) THEN
            SET Stringa=CONCAT(Stringa,std_nominativo,' [',std_classe,']','\n');
        END IF;
    UNTIL (basta_studenti = 1) END REPEAT;
    CLOSE cur_studenti;
    SELECT Stringa;
END$$
DELIMITER ;
Call CursorProc;

Attenzione! Ricordarsi di dichiarare prima il cursore e poi l'handler NOT FOUND altrimenti viene visualizzato un errore (ERROR 1338 (42000): Cursor declaration after handler declaration).

I TRIGGER

I Trigger sono comandi SQL registrati nel catalogo del DB. I Trigger vengono attivati da eventi sul database come UPDATE, DELETE o INSERT.
I Triggers possono essere eseguiti prima o dopo l'attivazione dell'evento associato.

Mentre per le SP l'esecuzione deve essere esplicitamente richiesta nei Trigger questa si avvia automaticamente quando si manifesta l'evento associato. I Trigger risultano utili quando devo svolgere automaticamente una particolare attività prima/dopo la modifica di un record.

I vantaggi dei Trigger SQL sono:

> vengono attivati in automatico per cui li posso utilizzare come metodo di controllo alternativo sull'integrità dei dati.
> li posso utilizzare per attivare dei task pianificati.
 

Gli svantaggi dei Trigger SQL sono:

> I Trigger SQL forniscono meccanismi di validazione generici che non possono rimpiazzare qualsiasi meccanismo di validazione.
> Alcuni meccanismi di validazione risultano più efficienti se attuati direttamente sull'applicazione client (ad esempio i controlli JavaScript)
- I Trigger girano in modo invisibile ed asincrono rispetto alle applicazioni client per cui è difficile immaginare gli effetti sui risultati finali.
- I Trigger girano ad ogni aggiornamento e quindi generano un sovraccarico del server che risulta evidente in caso di numerosi aggiornamenti al DB.

MySQL, a partire dalla versione 5.0.2, supporta i Trigger. La sintassi utilizzata è quella di SQL:2003. Quando si crea un Trigger nella cartella del DB (select @@datadir) si crea un file con estensione .TRG con un nome percorso che rispetta questa regola:

@@datadir/database_name/table_name.trg

e che quindi può essere modificato con un normale editor.

Nella definizione di un Trigger non è permesso:
- richiamare una SP.
- non è possibile creare un Trigger associato ad una vista o una tabella temporanea.
- all'interno di un Trigger non è possibile gestire una Transaction.
- l'istruzione
RETURN è disabilitata.
- per ogni tripla (
BEFORE/AFTER, INSERT/DELETE/UPDATE, tabella) è possibile attivare un solo Trigger.
- I Trigger di un db devono avere nomi univoci anche se relativi a differenti tabelle.
- La creazione di un Trigger su una tabella rende inusabile la query cache (la query cache consente di memorizzare il risultato di una query e il corrispondente comando SQL. Se in seguito viene richiesta la stessa query allora il DB engine riutilizzerà i dati in cache fornendo una risposta più veloce).

Per creare un Trigger si usa solitamente la seguente convenzione:

(BEFORE|AFTER)_tableName_(INSERT|UPDATE|DELETE);

Scrittura del primo Trigger

Vediamo un esempio. Partiamo dalla tabella Studenti definita all'inizio di questo manuale. Immaginiamo di voler tener traccia delle modifiche sui dati nella tabella Studenti utilizzando un'altra tabella che chiameremo Studenti_Audit.

DROP TABLE IF EXISTS Studenti_Audit;
CREATE TABLE Studenti_Audit
(
    IdAudit INT NOT NULL AUTO_INCREMENT,
    IdStudente INT NOT NULL,
    OldNominativo VARCHAR(60) NOT NULL,
    NewNominativo VARCHAR(60) NOT NULL,
    CambiatoIl datetime DEFAULT NULL,
    Azione varchar(50) DEFAULT NULL,
    PRIMARY KEY (IdAudit)
);

Per registrare le modifiche nella tabella di audit creiamo un Trigger che tenga conto delle variazioni sul nominativo dello studente. Creiamo un Trigger che viene attivato quando effettuiamo un qualsiasi UPDATE sulla tabella. Ecco il suo codice:

DELIMITER $$
DROP TRIGGER IF EXISTS Studenti.Before_Studenti_Update$$
CREATE TRIGGER Before_Studenti_Update
    BEFORE UPDATE ON Studenti
    FOR EACH ROW
    BEGIN
        INSERT INTO studenti_Audit SET
        Azione = 'UPDATE',
        IdStudente = OLD.IdStudente,
        OldNominativo = OLD.Nominativo,
        NewNominativo = NEW.Nominativo,
        CambiatoIl = NOW();
    END$$
DELIMITER ;

Per testare il Trigger digitiamo la seguente sequenza di comandi sul tool a linea di comando di MySql.

DELETE FROM Studenti_Audit;
SELECT * FROM Studenti_Audit;
UPDATE Studenti SET Nominativo = 'Bianchi Bruno' WHERE IdStudente = 7;
UPDATE Studenti SET Nominativo = 'Blu Bruno' WHERE IdStudente = 7;
SELECT * FROM Studenti_Audit;

La dichiarazione di un Trigger si effettua in questo modo:

CREATE TRIGGER nome_del_trigger
{ BEFORE | AFTER }
{ INSERT | UPDATE | DELETE }
ON nome_della_tabella
FOR EACH ROW
BEGIN
    codice_SQL_del_trigger
END;
 

Come possiamo vedere si contano diverse sezioni corrispondenti ognuna ad una linea della dichiarazione: analizziamole una per una.

- La prima linea indica il nome del Trigger che stiamo creando. Non possono esistere due Trigger con lo stesso nome in un solo database visto che lo scope del trigger stesso è relativo all'intero database e non alla singola tabella di riferimento. Il nome, analogamente ai nomi di tabella o di database, deve essere lungo al massimo 64 caratteri ed è consigliabile inserirlo tra backtick (`:ASCII 96) per evitare di riscontrare errori sull'uso di spazi o di caratteri non ASCII.

- La seconda linea determina quando eseguire il Trigger rispetto all'evento di modifica associato. Un Trigger infatti può agire prima dell'esecuzione della query oppure dopo, ed essere quindi rispettivamente definito come AFTER o BEFORE.

- La terza linea definisce il tipo di query SQL che genererà l'esecuzione del Trigger. La query può essere solo di tipo esecutivo quindi INSERT, DELETE, UPDATE ma non SELECT. In base al tipo di query ci troveremo ad intervenire sul record già presente nel database (nel caso ad esempio di una query UPDATE o DELETE) oppure su un record nuovo che andremo ad inserire o a sostituire (query UPDATE o INSERT). Da notare che INSERT non rappresenta soltanto le query di tipo INSERT bensì tutte le query che inseriscono dati ex-novo come LOAD DATA o REPLACE in caso di un record nuovo. Allo stesso modo vanno interpretati UPDATE e DELETE.

- La quarta linea specifica la tabella da monitorare per attivare il Trigger. È possibile avere un solo trigger attivo per lo stesso tipo di query (BEFORE/AFTER, INSERT/DELETE/UPDATE, tabella) sulla stessa tabella.

- Le restanti righe infine rappresentano il codice SQL da eseguire all'attivazione dell'evento. Il codice SQL da eseguire deve essere una singola istruzione seguita da punto e virgola. Il codice da eseguire deve essere quindi un monoblocco, cioè una singola istruzione. Per coniugare questa esigenza con il bisogno di codici più complessi della singola riga si introduce l'uso di blocchi logici BEGIN/END BEGIN e l'END verrà interpretato come una singola istruzione. Le istruzioni comprese tra BEGIN e END devono necessariamente terminare con il punto e virgola..

SELECT * FROM Information_Schema.TRIGGERS
WHERE Trigger_schema = 'dbprova'
AND Event_object_table = 'Studenti'\G

posso vedere la definizione completa del Trigger.

Per modificare un Trigger occorre prima eliminarlo e poi ricrearlo ex-novo. MySQL non fornisce un comando in grado di modificare un Trigger esistente come invece avviene per le tabelle e per le procedure.