Petit guide de Transact SQL (v2000)
Date de publication : 16/04/2004
Par
SQLPro (autres articles) (CV)
niveau : intermédiaire
Transact SQL, est une extension du dialecte SQL de SQL Server et constitue un langage procédural pour coder les procédures stockées et les triggers du SGBDR de Microsoft. Le but de cet article est de présenter rapidement les concepts nécessaires à une bonne introduction à ce langage et ses possibilités.
1. Positionnement du langage
1.1. Historique
1.2. Utilisation
1.3. Conclusion
2. Syntaxe
2.1. Identifiant
2.2. Variables
2.3. Structures basiques
2.4. Variable de retour
2.5. Variables "système"
2.6. Flags
2.7. Batch et GO
2.8. Quelques fonctions de base
2.9. Commentaires
3. UDF : fonction utilisateur
3.1. Fonction renvoyant une valeur
3.2. Fonction renvoyant une table
4. Procédures stockées
4.1. Entête de procédure
4.2. Paramètres, variables de retour et ensemble de données
4.3. Gestion des erreurs
4.4. Procédures stockées prédéfinies
4.5. Verrouillage
4.6. Gestion de transactions
4.7. Les curseurs
5. Les triggers
5.1. Mise en place d'un trigger
5.2. Syntaxe d'un trigger MS SQL Server 2000
5.3. Eléments du langage spécifique aux triggers
5.3.1. Pseudo tables INSERTED et DELETED
5.3.2. Fonctions UPDATE et COLUMNS_UPDATED
5.3.3. Annulation des effets d'un trigger
5.4. Exemples de triggers
6. Cryptage du code, liaison au schema et recompilation
7. ANNEXE BIBLIOGRAPHIQUE
1. Positionnement du langage
Transact SQL est un langage procédural (par opposition à SQL qui est un langage déclaratif) qui permet de programmer des algorithmes de traitement des données au sein des SGBDR Sybase Adaptive Server et Microsoft SQL Server.
1.1. Historique
Il a été créé par Sybase INC. à la fin des années 80 pour répondre aux besoins d'extension de la programmation des bases de données pour son SGBDR.
Ce langage est donc commun aux deux produits (MS SQL Server et Sybase Adaptive) avec des différences mineures dans les implémentations.
Il sert à programmer des procédures stockées et des triggeurs (déclencheurs).
Transact SQL n'a aucun aspect normatif contrairement à SQL. C'est bien un "produit" au sein commercial du terme. En revanche depuis SQL 2 et plus fortement maintenant, avec SQL 3, la norme SQL a prévu les éléments de langage procédural normatif propre au langage SQL. Mais il y a une très grande différence entre la norme du SQL procédural et Transact SQL.
D'autres SGBDR utilisent d'autres langages procéduraux pour l'implémentation de procédures stockées et de triggers. C'est le cas par exemple du PL/SQL (Programming Langage) d'Oracle.
Il existe encore peu de SGBDR dont le langage procédural est calé sur la norme. Mais on peut citer OCELOT (SQL 3), PostGreSQL (SQL 2).
Par rapport à la concurrence voici une notation approximative (sur 5) des différents langage procéduraux :
| SGBDR |
Langage |
Respect norme |
Richesse |
| Oracle 8 |
PL/SQL |
3/5 |
5/5 |
| MS SQL Server v7 |
Transact SQL |
2/5 |
3/5 |
| PostGreSQL |
SQL 2 |
4/5 |
4/5 |
| OCELOT |
SQL3 |
5/5 |
4/5 |
| InterBase |
ISQL |
3/5 |
3/5 |
Il s'agit bien entendu d'une notation toute personnelle parfaitement sujette à caution !
1.2. Utilisation
Transact SQL doit être utilisé :
- dans les procédures stockées, pour les calculs portant sur des données internes à la base, sans considération de gestion d'IHM. Exemple pré calculs d'agrégats pour optimisation d'accès...
- dans les triggers, pour définir et étendre des règles portant sur le respect de l'intégrité des données. Exemple formatage de données, liens d'héritage exclusif entre tables filles...
En revanche il faut tenter de ne pas l'utiliser pour le calculs de règles métier, l'idéal étant de déporter cela dans des objets métiers. Enfin l'usage de trigger pour de la réplication est à proscrire.
1.3. Conclusion
Nous retiendrons que ce qui compte dans l'étude de ce langage, c'est plus de comprendre ses grands concepts et son esprit afin de pouvoir le transposer sur d'autres SGBDR plutôt que d'apprendre par cur telle ou telle instruction, d'autant que la documentation sur ce langage est assez fournie.
ATTENTION : nous étudirons ici la version Microsoft du langage Transact SQL
2. Syntaxe
2.1. Identifiant
Voici les règles de codage des identifiants (nom des objets) Les identifiants SQL :
- ne peuvent dépasser 128 caractères.
- Ils doivent commencer par une lettre ou un "underscore".
- Les caractères spéciaux et le blanc ne sont pas admis.
- On se contentera d'utiliser les 37 caractères de base : ['A'..'Z', '0'..'9', '_']
- La casse n'a pas d'importance.
Le symbole @ commence le nom de toute variable. Le symbole dédoublé @@ commence le nom des variables globales du SGBDR.
Le symbole # commence le nom de toute table temporaire (utilisateur)
Exemple :
SELECT CURRENT_TIMESTAMP as DateTime
INTO #tempT
SELECT *
FROM #tempT
Le symbole dédoublé ## commence le nom de toute table temporaire globale. Conséquence : la libération des tables temporaires est consécutif à la libération des utilisteurs.
Nota : SQL Server utilise un base particulière "tempDB" pour stocker la définition des objets temporaires.
Attention : pour des raisons de performances il est fortement déconseillé d'utiliser le SELECT
INTO
qui n'est d'ailleurs pas normalisé.
Un identifiant est dit "qualifié" s'il fait l'objet d'une notation pointée définissant le serveur, la base cible et l'utilisateur :
serveur.base_cible.utilisateur.objet
Exemple :
SELECT * FROM MonServeur.MaBase.MonUser.MaTable
Attention : ceci suppose que les serveurs soient connus les uns des autres (procédure de "linkage") On peut ignorer le serveur, comme l'utilisateur, dans ce cas c'est l'utilisateur courant qui est pris en compte :
base_cible..objet
et au sein de la base :
objet
Un identifiant ne doit pas être un mot clef de SQL, ni un mot clef de SQL Server ni même un mot réservé. Dans le cas contraire il faut utiliser les guillemets comme délimiteur.
La notion de constante n'existe pas dans Transact SQL.
2.2. Variables
Les types disponibles sont ceux de SQL :
bit, int, smallint, tinyint, decimal, numeric, money, smallmoney, float, real, datetime, smalldatetime, timestamp,
uniqueidentifier, char, varchar, text, nchar, nvarchar, ntext, binary, varbinary, image.
Auxquels il faut ajouter le type :
cursor
que nous verrons plus en détail.
Les valeurs de types chaînes doivent être délimitées par des apostrophes.
Une variable est déclarée à tout endroit du code par l'instruction DECLARE : Exemple :
DECLARE @maChaine char(32)
Une variable est assignée par l'instruction SET :
SET @maChaine = 'toto'
Avant toute assignation, une variable déclarée est marquée à NULL.
Remarques :
- il n'existe pas de type "tableau" dans le langage Transact SQL. Cependant, une table temporaire suffit à un tel usage.
- l'ordre SQL SELECT peut aussi servir à assigner une ou plusieurs variable, mais dans ce dernier cas il faut veiller à ce que la réponse à la requête ne produise qu'une seule ligne
Exemple :
SELECT @NomTable = 'TABLES', @NombreLigne = count(*)
FROM INFORMATION_SCHEMA.TABLES
Attention : dans le cas d'une requête renvoyant plusieurs lignes, seule la valeur de la dernière ligne sera récupéré dans la variable.
ASTUCE : si vous désirez concaténer toutes les valeurs d'une colonne dans une seule variable, vous pouvez utiliser la construction suivante :
DECLARE @Colonne varchar(8000)
SET @Colonne = ''
SELECT @Colonne = @Colonne +COALESCE(TABLE_NAME + ', ', '')
FROM INFORMATION_SCHEMA.TABLES
NOTA : la notion de constante n'existe pas.
2.3. Structures basiques
La structure :
BEGIN
...
END
permet de définir des blocs d'instructions.
Les branchements possibles sont les suivants :
IF [NOT] condition instruction [ELSE instruction]
IF [NOT] EXISTS(requête select) instruction [ELSE instruction]
WHILE condition instruction
GOTO etiquette
WAITFOR [DELAY | TIME] temps
Les instructions :
BREAK
CONTINUE
permettent respectivement d'interrompre ou de continuer autoritairement une boucle.
Remarques :
- il n'y a pas de THEN dans le IF du Transact SQL, ni de DO dans le WHILE
- une seule instruction est permise pour les structure IF et WHILE, sauf à mettre en place un bloc d'instructions à l'aide d'une structure BEGIN / END
- un branchement sur étiquette se précise avec en suffixant l'identifiant de l'étiquette avec un caractère deux-points ':'
Exemple :
Il s'agit de donner la liste de tous les nombres premiers entre 1 et 5000.
Première version en code procédural :
/* recherche de tous les nombres premiers de 1 à 5000 */
/* version procédurale (itérations) */
create table #n (n int)
declare @n integer, @i integer, @premier bit
set @n = 1
set nocount on
while @n < 5000
BEGIN
set @premier = 1
set @i = 2
while @i < @n
BEGIN
if (@n / @i) * @i = @n
SET @premier = 0
SET @i = @i + 1
END
if @premier = 1
insert into #n VALUES (@n)
SET @n = @n + 1
END
SELECT * FROM #n
Par ce code procédural, nous avons utilisé la formulation suivante : "n est premier si aucun nombre de 2 à n-1 ne le divise".
Une autre façon de faire est de travailler en logique ensembliste. Si nous disposons d'une table des entiers, il est alors facile de comprendre que les nombres premiers sont tous les nombres, moins ceux qui ne sont pas premiers... Il s'agit ni plus ni moins que de réaliser une différence ensembliste.
/* recherche de tous les nombres premiers de 1 à 5000 */
/* version ensembliste (requêtes) */
DECLARE @max int
SET @max = 5000
SET NOCOUNT ON
CREATE TABLE #n (n int)
DECLARE @i int
SET @i = 0
WHILE @i < @max
BEGIN
INSERT INTO #n VALUES (@i)
SET @i = @i + 1
END
SELECT distinct n
FROM #n
WHERE n not in (SELECT distinct n1.n
FROM #n n1
CROSS JOIN #n n2
WHERE n1.n % n2.n = 0
AND n2.n BETWEEN 2 AND n1.n - 1)
ORDER BY n
Notez la différence de vistesse d'exécution entre les deux manières de faire... Enfin, on peut encore optimiser l'une et l'autre des procédure en limitant le diviseur au maximum à CAST(SQRT(CAST(n2.n AS FLOAT)) AS INTEGER) + 1, car le plus grand des diviseurs d'un nombre ne peut dépasser sa racine carrée.
2.4. Variable de retour
Toute procédure stockée renvoie une variable de type entier pour signaler son état. Si cette variable vaut 0, la procédure s'est déroulée sans anomalie. Tout autre valeur indique un problème. Les valeurs de 0 à -99 sont réservées et celles de 0 à -14 sont prédéfinies. Par exemple la valeur -5 signifie erreur de syntaxe.
On peut assigner une valeur de retour de procédure à l'aide de l'instruction RETURN :
RETURN 1445
2.5. Variables "système"
SQL Server défini un grand nombre de "variables système" c'est à dire des variables définies par le moteur.
En voici quelques unes :
| Variable |
Description |
| @@connections |
nombre de connexions actives |
| @@datefirts |
premier jour d'une semaine (1:lundi à 7:dimanche) |
| @@error |
code de la dernière erreur rencontrée (0 si aucune) |
| @@fetch_status |
état d'un curseur lors de la lecture (0 si lecture proprement exécutée) |
| @@identity |
dernière valeur insérée dans une colonne auto incrémentée pour l'utilisateur en cours |
| @@max_connections |
nombre maximums d'utilisateurs concurrents |
| @@procid |
identifiant de la procédure stockée en cours |
| @@rowcount |
nombre de lignes concernées par le dernier ordre SQL |
| @@servername |
nom du serveur SGBDR courant |
| @@spid |
identifiant du processus en cours |
| @@trancount |
nombre de transaction en cours |
2.6. Flags
Pour manipuler les effets de certaines variables, comme pour paramétrer la base de données, il est nécessaire de recourir à des "flags".
SET NOCOUNT ON / OFF empêche/oblige l'envoi au client de messages pour chaque instruction d'une procédure stockée affichant la ligne « nn rows affected » à la fin d'une instruction (SELECT, INSERT, UPDATE, DELETE). Remarque : il est recommandé de désactiver le comptage dans les procédures stockées afin d'éviter l'envoi intempestif de lignes non lues qui génère du trafic réseau et rallonge les temps d'exécution.
SET ANSI_DEFAULTS ON / OFF Conformation d'une partie de SQL Server à la norme SQL 2
SET DATEFORMAT {format de date} Fixation du format de date par défaut
SET IDENTITY_INSERT nomTable ON / OFF Active, désactive l'insertion automatique de colonne auto incrémentées (identity) dans la table spécifiée.
Exemple : insertion de lignes dont la clef est spécifiée dans une table données pourvue d'un auto incrément :
CREATE TABLE T_CLIENT
(CLI_ID INTEGER IDENTITY NOT NULL PRIMARY KEY,
CLI_NOM VARCHAR(32))
SET IDENTITY_INSERT T_CLIENT ON
INSERT INTO T_CLIENT (CLI_ID, CLI_NOM) VALUES (325, 'DUPONT')
INSERT INTO T_CLIENT (CLI_ID, CLI_NOM) VALUES (987, 'MARTIN')
SET IDENTITY_INSERT T_CLIENT OFF
INSERT INTO T_CLIENT (CLI_ID, CLI_NOM) VALUES (512, 'LEVY')
=> Serveur: Msg 544, Niveau 16, État 1, Ligne 1
Impossible d'insérer une valeur explicite dans la colonne identité de la table 'T_CLIENT' quand IDENTITY_INSERT
est défini à OFF.
2.7. Batch et GO
Un batch est un ensemble d'ordres SQL ou Transact SQL passé en un lot. Il peut être exécuté en lançant un fichier vers le moteur SQL ou encore en l'exécutant dans l'analyseur de requêtes. la plupart du temps un batch est utilisé pour créer les objets d'une base et lancer au coup par coup certaines procédures lourdes (administration notamment).
Le mot clef GO permet de forcer l'exécution de différents ordres SQL d'un traitement par lot. Hors d'un contexte de transaction, le serveur peut choisir dans les différents ordres qui luis ont proposés simultanément d'exécuter telle ou telle demande dans l'ordre que bon lui semble. La présence du mot clef GO dans un fichier d'ordre SQL passé au serveur permet de lui demander l'exécution immédiate de cet ordre ou de l'ensemble d'ordres. Dans ce, cas l'instruction GO doit être spécifié pour chaque ordre atomique ou bien pour chaque ensemble cohérents. En fait le mot clef GO agit comme si chaque ordre était lancé à travers un fichier différent.
Enfin, un batch est exécuté en tout ou rien. Ainsi, en cas d'erreur de syntaxe par exemple, même sur le dernier ordre du lot, aucun des ordres n'est exécuté.
Attention :
- Certains ordres ne peuvent être passés simultanément dans le même lot. Par exemple la suppression d'un objet (DROP) et sa re création immédiatement après, (CREATE) est source de conflits.
- Le mot clef GO n'est valable que pour une exécution par lot (batch). Il n'est donc pas reconnu au sein d'une procédure stockée ni dans le code d'un trigger
2.8. Quelques fonctions de base
USE est une instruction permettant de préciser le nom de la base de données cible. En effet il arrive que l'on soit obligé de travailler depuis une base pour en traiter une autre. C'est le cas notamment lorsque l'on veut créer une base de données et y travailler sur le champ. La seule base en standard dans MS SQL Server est la base "master" qui sert de matrice à toutes les bases créées. Elle contient les procédures inhérentes au serveur mais aussi celles inhérentes aux bases nouvellement créées, ainsi que les méta données spécifiques de la nouvelle base (tables dont le nom commence par "syS...").
Exemple : création d'une base de données depuis la base MASTER et création d'une table dans cette nouvelle base :
CREATE DATABASE NEW_MABASE
ON
( NAME = 'NEW_MABASE_DB',
FILENAME = 'C:\MaBase.DATA',
SIZE = 100,
MAXSIZE = 500,
FILEGROWTH = 50 )
LOG ON
( NAME = 'NEW_MABASE_LOG',
FILENAME = 'C:\MaBase.LOG',
SIZE = 5,
MAXSIZE = 25,
FILEGROWTH = 5 )
GO
USE NEW_MABASE
GO
CREATE TABLE T_CLIENT
(CLI_ID INTEGER IDENTITY NOT NULL PRIMARY KEY,
CLI_NOM VARCHAR(32))
GO
PRINT est une instruction permettant de générer une ligne en sortie de procédure. Elle doit être réservée plus à la mise au point des procédures stockées que pour une utilisation en exploitation.
EXEC est une instruction permettant de lancer une requête ou une procédure stockée au sein d'une procédure ou un trigger. La plupart du temps il n'est pas nécessaire d'utilise l'instruction EXEC, si l'intégralité de la commande SQL ou de la procédure à lancer est connu. Mais lorsqu'il s'agit par exemple d'un ordre SQL contenant de nombreux paramètres, alors il est nécessaire de le définir dynamiquement.
Exemple : voici une procédure cherchant dans toutes les colonnes d'une table l'occurrence d'un mot :
DECLARE
@TableName Varchar(128),
@SearchWord Varchar(32)
Declare @ColumnList varchar(1000)
Declare @SQL varchar(1200)
SELECT @ColumnList = COALESCE(@ColumnList + ' + COALESCE(', 'COALESCE(') + column_name +', '''')'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
AND DATA_TYPE LIKE '%char%'
PRINT 'INFO - @ColumnList value is : ' + @ColumnList
Set @SQL = 'SELECT * FROM '+ @TableName
+ ' WHERE ' + @ColumnList
+ ' LIKE ''%' + @SearchWord +'%'''
PRINT 'INFO - @SQL value is : ' + @SQL
Exec (@SQL)
2.9. Commentaires
Comme dans le cadre du langage SQL de base, pour placer des commentaries il suffit d'utiliser les marqueurs suivants :
| Début |
Fin |
Commentaire |
| -- |
|
pour une ligne de commentaire |
| /* |
*/ |
pour un bloc de ligne ou de caractères |
3. UDF : fonction utilisateur
Une UDF, autrement dit User Define Function ou Function Définie par l'Utilisateur est une fonction que le concepteur de la base écrit pour des besoins de traitement au sein des requêtes et du code des procdures stockées ou des triggers. Elle fait donc partie intégrante de la base ou elle est considérée comme un objet de la base au même titre qu'une table, une vue, un utilisateur ou une procédure stockée.
Il existe deux grands types de fonctions : celles renvoyant une valeur et celles renviyant un jeu de données (table).
3.1. Fonction renvoyant une valeur
La syntaxe est assez simple :
CREATE FUNCTION [ utilisateur. ] nom_fonction
( [ { @parametre1[AS] type [ = valeur_défaut ] } [ , @parametre2 ... ] ] )
RETURNS type_résultant
[ AS ]
BEGIN
code
RETURN valeur_résultante
END
Exemple, calcul de la date de pâque pour une année donnée :
CREATE FUNCTION FN_PAQUE (@AN INT)
RETURNS DATETIME
AS
BEGIN
IF @AN IS NULL
RETURN NULL
DECLARE @G INT
DECLARE @I INT
DECLARE @J INT
DECLARE @C INT
DECLARE @H INT
DECLARE @L INT
DECLARE @JourPaque INT
DECLARE @MoisPaque INT
DECLARE @DimPaque DATETIME
SET @G = @AN % 19
SET @C = @AN / 100
SET @H = (@C - @C / 4 - (8 * @C + 13) / 25 + 19 * @G + 15) % 30
SET @I = @H - (@H / 28) * (1 - (@H / 28) * (29 / (@H + 1)) * ((21 - @G) / 11))
SET @J = (@AN + @AN / 4 + @I + 2 - @C + @C / 4) % 7
SET @L = @I - @J
SET @MoisPaque = 3 + (@L + 40) / 44
SET @JourPaque = @L + 28 - 31 * (@MoisPaque / 4)
SET @DimPaque = CAST(CAST(@AN AS VARCHAR(4)) +
CASE
WHEN @MoisPaque < 10 THEN '0' + CAST(@MoisPaque AS CHAR(1))
ELSE CAST(@MoisPaque AS CHAR(2))
END +
CASE
WHEN @JourPaque < 10 THEN '0' + CAST(@JourPaque AS CHAR(1))
ELSE CAST(@JourPaque AS CHAR(2))
END
AS DATETIME)
RETURN @DimPaque
END
On utilise une telle fonction au sein d'une requête, comme une fonction SQL de base, à ceci près que, pour une raion obscure, il faut la faire précéder du nom du propriétaire.
Exemple :
SELECT dbo.FN_PAQUE (2004) AS PAQUE_2004
|
PAQUE_2004
-----------------------
2004-04-11 00:00:00.000
|
3.2. Fonction renvoyant une table
Il existe deux manières de procéder : soit par requête directe (table en ligne), soit par construction et alimentation d'une table (table multi instruction).
La première syntaxe (table est ligne) est très simple. Voici un exemple, qui construit et renvoi une table des jours de semaine :
CREATE FUNCTION FN_JOUR_SEMAINE ()
RETURNS TABLE
AS
RETURN (SELECT 1 AS N, 'Lundi' AS JOUR
UNION
SELECT 2 AS N, 'Mardi' AS JOUR
UNION
SELECT 3 AS N, 'Mercredi' AS JOUR
UNION
SELECT 4 AS N, 'Jeudi' AS JOUR
UNION
SELECT 5 AS N, 'Vendredi' AS JOUR
UNION
SELECT 2 AS N, 'Samedi' AS JOUR
UNION
SELECT 2 AS N, 'Dimanche' AS JOUR)
|
SELECT *
FROM dbo.FN_JOUR_SEMAINE ()
|
N JOUR
----------- --------
1 Lundi
2 Dimanche
2 Mardi
2 Samedi
3 Mercredi
4 Jeudi
5 Vendredi
|
La seconde syntaxe demande un peu plus de travail, car elle se base sur une variable "table". En voici la syntaxe :
CREATE FUNCTION [ utilisateur. ] nom_fonction
( [ { @parametre1[AS] type [ = valeur_défaut ] } [ , @parametre2 ... ] ] )
RETURNS type_résultant TABLE < definition_de_table >
[ AS ]
BEGIN
code
RETURN
END
Voici un exemple, qui construit une table d'entier limité à MaxInt et comprenant ou non le zéro (entiers naturels) :
CREATE FUNCTION FN_ENTIERS (@MAXINT integer, @NATUREL bit = 0)
RETURNS @integers TABLE
(N int PRIMARY KEY NOT NULL)
AS
BEGIN
DECLARE @N INT
DECLARE @T TABLE (N int)
SET @N = 0
WHILE @N < 10
BEGIN
INSERT INTO @T VALUES (@N)
SET @N = @N + 1
END
SET @N = @N -1
IF @N > @MAXINT
DELETE FROM @T WHERE N > @MAXINT
ELSE
INSERT INTO @T
SELECT DISTINCT 1 * T1.N + 10 * T2.N + + 100 * T3.N + 1000 * T4.N
FROM @T AS T1
CROSS JOIN @T AS T2
CROSS JOIN @T AS T3
CROSS JOIN @T AS T4
WHERE 1 * T1.N + 10 * T2.N + + 100 * T3.N + 1000 * T4.N BETWEEN 10 AND @MAXINT
IF @NATUREL = 1
DELETE FROM @T WHERE N = 0
INSERT INTO @integers
SELECT DISTINCT N FROM @T
RETURN
END
|
SELECT *
FROM dbo.FN_ENTIERS (13, 1)
|
N
-----------
1
2
3
4
5
6
7
8
9
10
11
12
13
|
NOTA : SQL Server interdit l'utilisation de fonctions non déterministe au sein des fonctions utilisateur. Par exemple, la fonction CURRENT_TIMESTAMP qui renvoie la date/heure courante ne peut être utilisée dans le cadre de l'écriture d'une UDF. Il y a cependant moyen de contourner ce problème en utilisant par exemple une vue...
CREATE VIEW V_DATEHEURE_COURANTE
AS
SELECT CURRENT_TIMESTAMP AS DHC
|
CREATE FUNCTION FN_DELTA_MONTH (@MaDate DATETIME)
RETURNS INT
AS
BEGIN
DECLARE @N INT
SELECT @N = DATEDIFF(MONTH, @MaDate, DHC)
FROM V_DATEHEURE_COURANTE
RETURN @N
END
|
SELECT dbo.FN_DELTA_MONTH('20020101')
|
4. Procédures stockées
Le but d'une procédure stockée est triple :
- étendre les possibilités des requêtes, par exemple lorsqu'un seul ordre INSERT ou SELECT ne suffit plus
- faciliter la gestion de transactions
- permettre une exécution plus rapide et plus optimisée de calculs complexes ne portant que sur des données de la base
Dans ce dernier cas la méthode traditionnelle nécessitait de nombreux aller et retour entre le client et le serveur et congestionnait le réseau.
Une procédure stockée est accessible par l'interface de l'Entreprise Manager. Elle peut aussi être créée par l'analyseur de requête.
En créant une nouvelle procédure stockée on se trouve devant une fenêtre de définition de code :
ATTENTION : Les procédures stockées (v7) sont limitées à :
- 128 Mo de code
- 1024 paramètres en argument (y compris curseurs)
4.1. Entête de procédure
Elle commence toujours pas les mots clef CREATE PROCEDURE suivi du nom que l'on veut donner à la procédure stockée. Les paramètres et leur type, s'il y en as suivent le nom. L'entête se termine par le mot clef AS.
Exemple :
CREATE PROCEDURE SP_SEARCH_STRING_ANYFIELD
@TableName Varchar(128),
@SearchWord Varchar(32)
AS
...
4.2. Paramètres, variables de retour et ensemble de données
Une procédure stockée peut accepter de 0 à 1024 paramètres (arguments). Elle a toujours une valeur de retour qui par défaut est le code d'exécution (entier valant 0 en cas de succès). Elle peut retourner des lignes de table comme une requête.
Pour déclarer les paramètres il faut les lister avec un nom de variable et un type. Par défaut les paramètres sont des paramètres d'entrée. Comme pour toutes les listes le séparateur est la virgule. On peut déclarer des valeurs par défaut et spécifier si le paramètre est en sortie avec le mot clef OUTPUT.
Exemple :
CREATE PROCEDURE SP_SYS_DB_TRANSACTION
@TRANS_NUM INTEGER,
@OK BIT = 0 OUTPUT
AS
...
Pour récupérer la valeur de d'un paramètre OUTPUT il faut préciser une variable de récupération de nature OUTPUT dans le lacement de l'exécution.
Exemple :
DECLARE @RetourOK bit
EXEC(SP_SYS_DB_TRANSACTION 127, @RetourOK OUTPUT)
SELECT @RetourOK
Un paramètre de type CURSOR (curseur) est un cas particulier et ne peut être utilisé qu'en sortie et conjointement au mot clef VARYING qui spécifie que sa définition précise n'est pas connue au moment de la compilation (le nombre et le type des colonnes n'est pas défini à cet instant).
CREATE PROCEDURE SEARCH_TEXT @resultTable CURSOR VARYING OUTPUT
AS
Un tel paramètre peut être réutilisé dans une autre procédure stocké ou dans un trigger.
Comme nous l'avons vu, toute procédure stockée renvoie une variable de type entier pour signaler son état. Si cette variable vaut 0, la procédure s'est déroulée sans anomalie. Tout autre valeur indique un problème. Les valeurs de 0 à -99 sont réservées et celles de 0 à -14 sont prédéfinies. Par exemple la valeur -5 signifie erreur de syntaxe. Bien entendu on peut assigner une valeur de retour de procédure à l'aide de l'instruction RETURN.
Une procédure stockée peut en outre renvoyer un jeu de résultat sous la forme d'un ensemble de données à la manière des résultats de requête. Exemple :
CREATE PROCEDURE SP_LISTE_CLIENT
@CLI_ID_DEBUT INTEGER, @CLI_ID_FIN INTEGER
AS
SELECT '
UNION ALL
SELECT CLI_NOM
FROM T_CLIENT
WHERE CLI_ID BETWEEN @CLI_ID_DEBUT AND @CLI_ID_FIN
UNION ALL
SELECT '
4.3. Gestion des erreurs
Il existe différents niveaux d'erreurs et différents moyens de les gérer. Considérons une procédure stockée qui aurait pour effet de supprimer une ligne d'une table en s'assurant de supprimer préalablement toutes les lignes de tous les descendants concernés. L'entête d'une telle procédure pourrait s'écrire :
CREATE PROCEDURE SP_DELETE_CLIENT_RECURSIVE
@CLI_ID INTEGER
Si la ligne considérée n'est pas retrouvée dans la table des clients, comme si la valeur du paramètre est NULL, cette procédure échouera sans indiquer d'anomalie. Il faut donc procéder à des tests préalables (nullité, existence
). Sans cela on dit que l'on à une "exception silencieuse".
Pour les exceptions plus flagrantes, SQL Server fournit la variable globale @@error que l'on peut tester à tout instant. Cette variable est mise à jour à chaque instruction.
Le code continue de s'exécuter même après une erreur.
Pour gérer les erreurs, SQL Server dispose de la levée des erreurs à l'aide de l'instruction RAISERROR et l'utilisation du GOTO combiné à une étiquette de branchement pour la reprise sur erreur, permet de centraliser la gestion des erreurs à un seul et même endroit du programme.
Voici un exemple mettant en uvre ces principes :
/*
| recherche d'une occurrence de mot dans n'importe |
| quelle colonne de type caractères d'une table donnée |
|
| Frédéric BROUARD - COMMUNICATIC SA - 2001-12-17 |
\
CREATE PROCEDURE SP_SEARCH_STRING_ANYFIELD
@TableName Varchar(128),
@SearchWord Varchar(32)
AS
IF @TableName IS NULL OR @SearchWord IS NULL
RAISERROR ('Paramètres NULL impossible à traiter', 16, 1)
IF @@ERROR <> 0 GOTO LBL_ERROR
IF NOT EXISTS(SELECT *
FROM INFORMATION_SCHEMA.tables
WHERE TABLE_NAME = @TableName)
RAISERROR ('Références de table inconnue %s', 16, 1, @TableName)
IF @@ERROR <> 0 GOTO LBL_ERROR
Declare @ColumnList varchar(1000)
Declare @SQL varchar(1200)
SELECT @ColumnList = COALESCE(@ColumnList + ' + COALESCE(', 'COALESCE(') + column_name +', '''')'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
AND DATA_TYPE LIKE '%char%'
IF @ColumnList IS NULL
RAISERROR ('Aucune colonne de recherche trouvé dans la table %s',
16, 1, @TableName)
IF @@ERROR <> 0 GOTO LBL_ERROR
PRINT 'INFO - @ColumnList value is : ' + @ColumnList
Set @SQL =
'SELECT * FROM '+ @TableName
+ ' WHERE ' + @ColumnList
+ ' LIKE ''%' + @SearchWord +'%'''
PRINT 'INFO - @SQL value is : ' + @SQL
Exec (@SQL)
RETURN
LBL_ERROR:
PRINT 'ERREUR LORS DE L''EXÉCUTION DE LA PROCÉDURE STOCKÉE SP_SEARCH_STRING_ANYFIELD'
4.4. Procédures stockées prédéfinies
SQL Server possède des procédures stockées pré établies, un peu à la manière d'une bibliothèque d'utilisation. Elles servent essentiellement à l'administration (gestion des utilisateurs et des bases) et à l'information (dictionnaire des données). Elle se situent toutes dans la base "master".
Ainsi, la procédure stockée "sp_help" fournit une description de n'importe quel objet de la base.
Pour appeler de telles procédures, il n'est pas nécessaire de préciser le nom de la base "master", ce nom est implicite.
Ces procédures stockées permettent :
- de piloter le paramétrage du serveur et de la base de données (respect des normes SQL 2, maximum d'utilisateurs
) - exemple : sp_configure
- de définir les objets de la base (table, index, contraintes, vues
) - exemple : sp_addtype
- d'administrer le serveur, les bases, les accès (rôles, utilisateurs, login
) - exemple : sp_droplogin
- d'obtenir de l'aide et de la documentation (aide en ligne, explications) - exemple : sp_helpsort
- de gérer les agents (alertes et batch) - exemple : sp_add_job
- d'assurer les réplications (entre bases et serveurs) - exemple : sp_deleteMergeConflictRows
- de monitorer le fonctionnement de SQL Server (tracer l'exécution) - exemple : xp_sqlTrace
- ...
Nota : des procédures stockées dites étendues (dont le nom commence généralement par "xp_") font appel à du code C sous forme de DLL. Exemple : xp_cmdShell, permet de lancer un programme en ligne de commande depuis SQL Server (procédure dont l'utilisation est à déconseiller !)
Exemple - création d'un type utilisateur (DOMAINE SQL 2) pour spécification numérique de la valeur d'un mois avec contrôle de validité :
SP_addType TT_MONTH, 'INTEGER', 'NOT NULL'
CREATE RULE R_CHECK_MONTH AS @value BETWEEN 1 AND 12
SP_bindRule R_CHECK_MONTH, TT_MONTH
4.5. Verrouillage
Le verrouillage est la technique de base pour assurer les concurrences d'accès aux ressources. On peut observer les verrous posés lors des manipulations de données à l'aide de la procédure stockée sp_lock. De même on peut savoir qui utilise quoi à l'aide de la procédure stockée sp_who.
SQL Server pose en principe les bons verrous lors des requêtes. Mais il peut arriver que l'on souhaite :
- soit se débarrasser des contraintes de verrouillage, notamment pour faire du "dirty read" (lecture sale)
- soit poser des verrous plus contraignant pour s'assurer du traitement
Dans les deux cas il faut compléter les ordre SQL par une description des verrous attendus.
Voici les paramètres de verrouillage que l'on peut spécifier :
| Verrou |
SELECT |
UPDATE |
| NOLOCK |
aucun verrou |
impossible |
| HOLDLOCK |
maintient du verrou jusqu'à la fin de la transaction |
pas nécessaire |
| UPDLOCK |
pose un verrou de mise à jour au lieu d'un verrou partagé |
redondant |
| PAGLOCK |
force la pose d'un verrou de page |
force la pose d'un verrou de page |
| TABLOCK |
force un verrou partagé sur la table |
force un verrou exclusif sur la table |
| TABLOCKX |
force un verrou exclusif sur la table pendant toute la durée de la transaction |
force un verrou exclusif sur la table pendant toute la durée de la transaction |
Ces paramètres se précisent dans un ordre SQL après le nom de la table et le mot clef WITH en utilisant le parenthèsage.
Exemples :
Accélération d'une extraction de données en demandant la suppression du verrouillage :
SELECT *
FROM T_CLIENT C WITH (NOLOCK)
JOIN T_FACTURE F WITH (NOLOCK)
ON C.CLI_ID = F.CLI_ID
Verrouillage exclusif de la table le temps de la mise à jour :
UPDATE T_CLIENT WITH (TABLOCK)
SET CLI_NOM = REPLACE(CLI_NOM, ' ', '-')
On peut combiner certains paramètres de verrouillage. Exemple :
... WITH (PAGLOCK HOLDLOCK) ...
Attention : la manipulation des verrous est affaire de spécialistes. Une pose de verrous sans étude préalable de la concurrence d'exécution des différentes procédures stockées d'une base, peut conduire à des scénarios de blocage, comme "l'étreinte fatale".
4.6. Gestion de transactions
Toute ordre SQL du DML est une transaction (SELECT INSERT, UPDATE, DELETE).
Parce que SQL Server fonctionne en "AUTOCOMMIT", une combinaison de différents ordres nécessite la pose explicite d'une transaction. Ceci se fait par l'instruction BEGIN TRANSACTION et doit être terminé soit par un ROLLBACK, soit par un COMMIT.
Une transaction peut être anonyme ou nommée. De toute façon SQL Server lui attribue un identifiant interne unique.
Exemple simple. Il s'agit de réserver 3 place dans le vol AF714 pour le client 123, s'il y a bien de la place pour ce vol !
BEGIN TRANSACTION
IF NOT EXISTS(SELECT *
FROM T_VOL_AVION
WHERE VOL_REF = 'AF714'
AND VOL_PLACE_LIBRE > 3)
THEN
BEGIN
ROLLBACK TRANSACTION
RETURN
END
UPDATE T_VOL_AVION
SET VOL_PLACE_LIBRE = VOL_PLACE_LIBRE - 3
WHERE VOL_REF = 'AF714'
IF (@@ERROR <> 0) OR (@@ROWCOUNT = 0)
GOTO ROLLBACK_ON_ERROR
INSERT INTO T_RESERVATION (VOL_REF, CLI_ID, PLACES)
VALUES ('AF714', 123, 3)
IF (@@ERROR <> 0) OR (@@ROWCOUNT = 0)
GOTO ROLLBACK_ON_ERROR
COMMIT TRANSACTION
RETURN
ROLLBACK_ON_ERROR:
ROLLBACK TRANSACTION
SQL Server permet aussi de définir des points de sauvegarde, que l'on peut considérer comme un validation partielle d'une transaction. Pour définir un point de sauvegarde il faut utiliser l'instruction SAVE TRANSACTION avec un nom de préférence (pas obligatoire). Par conséquent pour faire un ROLLBACK partiel il suffit de préciser le nom du point de sauvegarde dans l'ordre ROLLBACK.
L'utilisation de la variable @@TranCount permet de savoir le nombre de transaction ouvertes en cours.
Une bonne transaction ne saurait être bien gérée sans une appréciation claire du niveau d'isolation. SQL Server gère les 4 niveaux de la norme SQL 2. La commande SET TRANSACTION ISOLATION LEVEL dont les options sont READ COMMITTED, READ UNCOMMITTED, REPEATABLE READ et SERIALIZABLE permet de définir le niveau d'isolation d'une transaction.
| Niveau |
Effets |
ID |
READ UNCOMMITTED
|
Implémente la lecture incorrecte, ou le verrouillage de niveau 0, ce qui signifie qu'aucun verrou partagé n'est généré et qu'aucun verrou exclusif n'est respecté. Lorsque cette option est activée, il est possible de lire des données non validées, ou données incorrectes ; les valeurs des données peuvent être modifiées et des lignes peuvent apparaître ou disparaître dans le jeu de données avant la fin de la transaction. Cette option a le même effet que l'activation de l'option NOLOCK dans toutes les tables de toutes les instructions SELECT d'une transaction. Il s'agit du niveau d'isolation le moins restrictif parmi les quatre disponibles. |
0 |
READ COMMITTED
|
Spécifie que les verrous partagés sont maintenus durant la lecture des données pour éviter des lectures incorrectes. Les données peuvent néanmoins être modifiées avant la fin de la transaction, ce qui donne des lectures non renouvelées ou des données fantômes. Cette option est l'option SQL Server par défaut. |
1 |
REPEATABLE READ
|
Des verrous sont placés dans toutes les données utilisées dans une requête, afin d'empêcher les autres utilisateurs de les mettre à jour. Toutefois, un autre utilisateur peut ajouter de nouvelles lignes fantômes dans un jeu de données par un utilisateur ; celles-ci seront incluses dans des lectures ultérieures dans la transaction courante. |
2 |
SERIALIZABLE
|
Place un verrou sur une plage de données, empêchant les autres utilisateurs de les mettre à jour ou d'insérer des lignes dans le jeu de données, jusqu'à la fin de la transaction. Il s'agit du niveau d'isolation le plus restrictif parmi les quatre niveaux disponibles. Utilisez cette option uniquement lorsque cela s'avère nécessaire, car la concurrence d'accès est moindre. Cette option a le même effet que l'utilisation de l'option HOLDLOCK dans toutes les tables de toutes les instructions SELECT d'une transaction. |
3 |
Attention : par défaut SQL Server travaille au niveau READ COMMITTED. Ceci explique sa rapidité comparée a des serveurs qui fonctionnent par défaut au niveau d'isolation maximal mais peut s'avérer catastrophique pour l'intégité des données !!!
CONSEIL : il est toujours préférable d'utiliser la gestion des transactions que de manipuler les verrous, sauf en ce qui concerne la "lecture sale" notamment dans le cadre d'une utilisation client/serveur en mode déconnecté (par exemple dans le cadre de restitutions documentaires pour un site web).
NOTA : SQL Server permet de gérer des transactions distribuées et gère le "commit à deux phases" mais sans permettre l'utilisation de points de sauvegarde.
Exemple : gestion d'un arbre modélisé par intervalle
Pour une illustration plus complète des procédures stockées nous allons montrer les différentes procédures nécessaires pour faire "fonctionner" un arbre modélisé sous forme intervallaire. Pour une développement plus complet de ce sujet, lire l'article sur la représentation intervallaire des arborescences
Dans notre cas nous allons considérer une table permettant de gérer le développement des projet d'une entreprise. Voici la table du développement. Elle contient les informations de chaque noeuds, y compris son identifiant, sojn libellé, ses bornes droite et gauche et son niveau :
CREATE TABLE T_DEVELOPPEMENT_DEV
(DEV_ID INTEGER IDENTITY (1, 1) NOT NULL ,
DEV_NIVEAU SMALLINT NOT NULL ,
DEV_LIBELLE CHAR (32) NOT NULL ,
DEV_BORNE_GAUCHE INTEGER NOT NULL ,
DEV_BORNE_DROITE INTEGER NOT NULL)
Tout d'abord voici la procédure d'insertion d'un fils ainé, c'est à dire d'un fils qui sera le plus proche du sommet au moment de l'insertion. On passe à la procédure l'identifiant du noeud père et le libellé. Une variable de retour permet de conaître l'état de l'insertion.
CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FILS_AINE
@id_pere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS
DECLARE @bg_pere integer
DECLARE @niveau integer
DECLARE @OK integer
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION INSERT_TREE_FILS_AINE
SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere
IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
RETURN
END
SELECT @bg_pere = DEV_BORNE_GAUCHE, @niveau = DEV_NIVEAU + 1
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE > @bg_pere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE > @bg_pere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
SELECT -1
RETURN
END
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE, DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bg_pere + 1, @bg_pere + 2)
SELECT @id_ins = @@identity
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_AINE
SELECT -1
RETURN
END
COMMIT TRANSACTION INSERT_TREE_FILS_IANE
SELECT @id_ins
Pour le fils cadet, la procédure n'est pas plus compliquée :
CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FILS_CADET
@id_pere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS
DECLARE @bd_pere integer
DECLARE @niveau integer
DECLARE @OK integer
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION INSERT_TREE_FILS_CADET
SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere
IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
RETURN
END
SELECT @bd_pere = DEV_BORNE_DROITE, @niveau = DEV_NIVEAU + 1
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_pere
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE >= @bd_pere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE > @bd_pere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
SELECT -1
RETURN
END
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE, DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bd_pere , @bd_pere + 1)
SELECT @id_ins = @@identity
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_FILS_CADET
SELECT -1 as ID
RETURN
END
COMMIT TRANSACTION INSERT_TREE_SON
SELECT @id_ins
Voici maintenant comment on insère un frère à droite d'un autre :
CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FRERE_DROIT
@id_frere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS
DECLARE @bd_frere integer
DECLARE @niveau integer
DECLARE @OK integer
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION INSERT_TREE_RIGHT
SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere
IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
RETURN
END
SELECT @bd_frere = DEV_BORNE_DROITE, @niveau = DEV_NIVEAU
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE > @bd_frere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE > @bd_frere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
SELECT -1
RETURN
END
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE, DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bd_frere + 1, @bd_frere+2)
SELECT @id_ins = @@identity
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_RIGHT
SELECT -1
RETURN
END
COMMIT TRANSACTION INSERT_TREE_RIGHT
SELECT @id_ins
Même insertion en frère à gauche :
CREATE PROCEDURE SP_DEV_INSERTION_ARBRE_FRERE_GAUCHE
@id_frere integer,
@libelle varchar(32),
@id_ins integer OUTPUT AS
DECLARE @bg_frere integer
DECLARE @niveau integer
DECLARE @OK integer
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION INSERT_TREE_LEFT
SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere
IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION INSERT_TREE_LEFT
RETURN
END
SELECT @bg_frere = DEV_BORNE_GAUCHE, @niveau = DEV_NIVEAU
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_frere
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE + 2
WHERE DEV_BORNE_GAUCHE >= @bg_frere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_LEFT
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE + 2
WHERE DEV_BORNE_DROITE > @bg_frere
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION INSERT_TREE_LEFT
SELECT -1
RETURN
END
INSERT INTO T_DEVELOPPEMENT_DEV (DEV_NIVEAU, DEV_LIBELLE, DEV_BORNE_GAUCHE, DEV_BORNE_DROITE)
VALUES (@niveau, @libelle, @bg_frere, @bg_frere+1)
SELECT @id_ins = @@identity
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION
SELECT -1
RETURN
END
COMMIT TRANSACTION INSERT_TREE_LEFT
SELECT @id_ins
Enfin, comment supprimer. Notez l'argument "récursif" passé en paramètre de procédure pour spécifier si l'on tue toute la lignée ou si l'on laisse survivre les descendants !
CREATE PROCEDURE SP_DEV_SUPPRESSION
@id_element integer,
@recursif bit
AS
DECLARE @OK integer
DECLARE @bg_element integer
DECLARE @bd_element integer
DECLARE @intervalle integer
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION DELETE_TREE
SELECT @OK = count(*)
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_element
IF @OK = 0
BEGIN
SELECT -1
ROLLBACK TRANSACTION DELETE_TREE
RETURN
END
SELECT @bd_element = DEV_BORNE_DROITE, @bg_element = DEV_BORNE_GAUCHE
FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_element
IF @recursif = 0
BEGIN
DELETE FROM T_DEVELOPPEMENT_DEV
WHERE DEV_ID = @id_element
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE - 1, DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE - 1
WHERE DEV_BORNE_GAUCHE > @bg_element AND DEV_BORNE_DROITE < @bd_element
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE - 2
WHERE DEV_BORNE_GAUCHE > @bg_element
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE - 2
WHERE DEV_BORNE_DROITE < @bg_element
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
END
IF @recursif = 1
BEGIN
DELETE FROM T_DEVELOPPEMENT_DEV
WHERE DEV_BORNE_GAUCHE >= @bg_element AND DEV_BORNE_DROITE <= @bd_element
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
SET @intervalle = @bd_element - @bg_element + 1
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_GAUCHE = DEV_BORNE_GAUCHE - @intervalle
WHERE DEV_BORNE_GAUCHE > @bg_element
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
UPDATE T_DEVELOPPEMENT_DEV
SET DEV_BORNE_DROITE = DEV_BORNE_DROITE - @intervalle
WHERE DEV_BORNE_DROITE < @bg_element
IF @@ERROR <>0
BEGIN
ROLLBACK TRANSACTION DELETE_TREE
SELECT -1
RETURN
END
END
COMMIT TRANSACTION DELETE_TREE
4.7. Les curseurs
Les curseurs sont des mécanismes de mémoire tampons permettant d'accéder aux données renvoyées par une requête et donc de parcourir les lignes du résultat.
Un curseur se définit dans une instruction DECLARE possédant une requête de type SELECT. Il convient de définir pour chaque colonne renvoyé une variable de type approprié. Pour lancer la requête associée (et donc placer les données dans les buffers appropriés) il faut utiliser l'instruction OPEN. Un curseur doit être refermé avec l'instruction CLOSE. Pour libérer la mémoire utilisée par un curseur, il faut utiliser l'instruction DEALLOCATE.
Pour lire les données de la ligne courante et les associées aux variables du curseur il faut utiliser l'instruction FETCH.
Par défaut l'instruction FETCH navigue en avant d'une ligne à chaque lecture dans l'ensemble des données du résultat. Pour naviguer différemment, on peut qualifier le FETCH avec les mots clef NEXT, PRIOR, FIRST, LAST, ABSOLUTE n et RELATIVE n, mais il faut avoir déclaré le curseur avec l'attribut SCROLL...
Enfin la variable @@fetch_Status permet de savoir si la dernière instruction FETCH passée s'est correctement déroulée (valeur 0), ce qui permet de tester si l'on est arrivé en fin de parcours de l'ensemble de données.
Une boucle traditionnelle de manipulation d'un curseur prend la forme suivante :
DECLARE @Col1 Type1, @Col2 Type2, @Col3, Type3...
DECLARE MyCursor CURSOR
FOR
SELECT COL1, COL2, COL3
FROM MyTable
OPEN MyCursor
FETCH MyCursor INTO @Col1, @Col2, @Col3...
WHILE @@fetch_Status = 0
BEGIN
traitement
FETCH MyCursor INTO @Col1, @Col2, @Col3...
END
CLOSE myCursor
DEALLOCATE myCursor
On constate que l'instruction FETCH apparaît deux fois. Une première fois avant la boucle WHILE une seconde fois à l'intérieur et en dernière ligne de la boucle WHILE. C'est la façon la plus classique et la plus portable d'utiliser des curseurs.
NOTA : les performances sont en baisse lorsque l'on utilise tout autre déplacement que le NEXT.
Remarque : il est possible d'effectuer des mises à jours de données via des curseurs, mais cela n'est pas conseillé. Dans ce cas il faut préciser en fin de déclaration du curseur : FOR UPDATE OF liste_colonne
On peut aussi faire du "dirty read" avec les curseurs de SQL Server en précisant l'attribut INSENSITIVE juste après le nom du curseur.
La syntaxe complète SQL 2 de la déclaration d'un curseur dans MS SQL Server est :
DECLARE nom_curseur [INSENSITIVE] [SCROLL] CURSOR
FOR requête_select
FOR {READ ONLY | UPDATE [OF colonne1 [, colonne2...]]}]
La syntaxe admise par le Transact SQL de MS SQL Server est plus complète mais moins portable :
DECLARE no
|