Bonjour,
Cette librairie a �t� con�ue pour �tre utilis�e avec SQL Server.
Elle utilise les interfaces IDbConnection et IDbCommand afin d'�tre compatible aussi avec OleDbConnection et OdbcConnection, mais aussi pour vous premettre de la rendre compatible avec le SGBD de votre choix s'il supporte les transactions imbriqu�es ou les points de sauvegarde de transaction.
L'objet SqlTransaction ne supporte pas d'ouvrir deux transactions imbriqu�es.
Idem avec OleDbTransaction.
Pourtant, lorsqu'on a un long traitement, pouvoir imbriquer des transactions, c'est bien plus pratique que de g�rer manuellement des Save() et ne plus trop savoir � quel savepoint on doit remonter lorsqu'on souhaite annuler une op�ration.
Limitation connues :
- SQL Server ne supporte pas les transactions imbriqu�es. A la place, il supporte les "savepoints". Il s'agit de commit interm�diaires au sein d'une unique transaction, qui permettent, lors d'un rollback, de choisir si on annule toute la transaction, ou seulement ce qui a �t� modifi� depuis un savepoint donn�. Donc une fois "� l'int�rieur" d'une transaction imbriqu�es, vous devez la valider ou l'annuler avant de pouvoir modifier la transaction parente. Pour information, Oracle, qui est un des rares SGBD � supporter les transactions imbriqu�es ne recommande de toute fa�on pas de modifier une transaction parente lorsqu'une transaction imbriqu�e est active : en effet, on peut tr�s ais�ment se retrouver avec un deadlock si la transaction m�re tente de modifier des donn�es modifi�es par la transaction imbriqu�e. Pour que la notion d'imbrication soit lisible, utilisez des using() pour chaque objet MyTransaction.
- Le niveau de transaction en cours est stock� au niveau d'objet IDbConnexion. Donc si vous croyez faire une requ�te en dehors de la transaction en utilisant directement un cnx.CreateCommand, votre requ�te sera malgr� tout ex�cut�e au sein de la transaction en cours.
- MyTransaction.CreateNestedTransaction() retourne une nouvelle transaction [g]� la suite de la transaction en cours[/g], et n'a absolument rien � voir avec l'instance de MyTransaction qui a cr�� la nouvelle transaction. Il n'y a aucun moyen de faire autrement, ou alors demandez � Microsoft de supporter les transactions imbriqu�es
- MyTransaction.CreateCommand() retourne un nouvel objet IDbCommand qui utilise [g]la transaction en cours[/g], et n'a absolument rien � voir avec l'instance de MyTransaction qui a cr�� la nouvelle transaction. Il n'y a aucun moyen de faire autrement, ou alors demandez de nouveau � Microsoft de supporter les transactions imbriqu�es
- Le code devrait �tre threadsafe. Il n'a cependant pas �t� test� dans un tel contexte.
- Il faut imp�rativement faire un Begin() et Commit()/Rollback() pour chaque transaction cr�e.
Code qui ne marche pas : a la sortie du using(cnx), la transaction A n'est pas commit�e. Un rollback se produit alors sur l'ensemble de la transaction ! A droite, le code SQL ex�cut� :
Code qui marche :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11 using (SqlConnexion cnx = new SqlConnexion(cnx_string)) { MyTransaction tranA = new MyTransaction(cnx); tranA.Begin(); BEGIN TRANSACTION [...] MyTransaction tranB = tranA.CreateNestedTransaction(); tranB.Begin(); SAVE TRANSACTION nested [...] tranA.Commit(); }
Voici le code, que j'ai essay� de documenter au maximum :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12 using (SqlConnexion cnx = new SqlConnexion(cnx_string)) { MyTransaction tranA = new MyTransaction(cnx); tranA.Begin(); BEGIN TRANSACTION [...] MyTransaction tranB = tranA.CreateNestedTransaction(); tranB.Begin(); SAVE TRANSACTION nested [...] tranB.Commit(); tranA.Commit(); COMMIT TRANSACTION }
Code c# : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199 using System; using System.Data; namespace CustomTransaction { /// <summary> /// Objet qui permet d'effectuer des transactions imbriquées. /// On ne catch absolument aucune erreur volontairement. /// Cette librairie a été écrite pour SQL Server, même si elle utilise l'interface IDbConnexion. /// Pour créer une transaction imbriquée, on peut uiliser au choix l'un ou l'autre constructeur de MyTransaction, ou la méthode CreateNestedTransaction. /// </summary> public class MyTransaction : IDisposable { // Private attributes IDbConnection Cnx; bool IsActive = false; string Name = string.Empty; const int NAME_LENGTH = 4; /// <summary> /// Créée une transaction liée à la connexion. /// </summary> /// <param name="cnx">Connexion ouverte à la base de données.</param> public MyTransaction(IDbConnection cnx) { // On stocke la connexion. Cnx = cnx; // On crée un nom aléatoire pour la transaction. if (MyTransactionContext.Level > 0) { string lettres = "abcdefghijklmnopqrstuvwxyz"; char[] name = new char[NAME_LENGTH + 2]; name[0] = '['; name[NAME_LENGTH + 1] = ']'; for (int i = 1; i <= NAME_LENGTH; i++) { name[i] = lettres[MyRandom.Next(0, 26)]; } Name = string.Concat(name); } } /// <summary> /// Créée une transaction imbriquée. /// </summary> /// <param name="parent">Transaction mère.</param> public MyTransaction(MyTransaction parent) : this(parent.Cnx) { } /// <summary> /// Débute une transaction /// </summary> public void Begin() { // Si un jour on souhaite que la classe soit threadsafe, c'est mieux de poser un lock lock (this) { using (IDbCommand cmd = Cnx.CreateCommand()) { if (MyTransactionContext.Level == 0) { // On est au premier niveau, on commence donc une transaction réelle cmd.CommandText = "BEGIN TRANSACTION"; } else { // On est dans une transaction imbriquée : en réalité, il s'agit d'un savepoint cmd.CommandText = string.Format("SAVE TRANSACTION {0}", Name); } cmd.ExecuteNonQuery(); } // La transaction est démarrée IsActive = true; MyTransactionContext.Level++; } } /// <summary> /// Valide la transaction /// </summary> public void Commit() { // Si un jour on souhaite que la classe soit threadsafe, c'est mieux de poser un lock lock (this) { if (IsActive) { using (IDbCommand cmd = Cnx.CreateCommand()) { if (--MyTransactionContext.Level == 0) { // On est au premier niveau, on commit donc la transaction cmd.CommandText = "COMMIT"; cmd.ExecuteNonQuery(); } // Si on n'est pas au niveau 0, on ne fait rien de plus, puisque nous sommes dans un savepoint } // La transaction est terminée IsActive = false; } } } /// <summary> /// Annule la transaction /// </summary> public void Rollback() { // Si un jour on souhaite que la classe soit threadsafe, c'est mieux de poser un lock lock (this) { if (IsActive) { using (IDbCommand cmd = Cnx.CreateCommand()) { if (--MyTransactionContext.Level == 0) { // On est au premier niveau, on annule dont toute la transaction cmd.CommandText = "ROLLBACK"; } else { // On est dans une transaction imbriquée, on annule jusqu'au savepoint qui a débuté la transaction cmd.CommandText = string.Format("ROLLBACK TRANSACTION {0}", Name); } cmd.ExecuteNonQuery(); } // La transaction est terminée IsActive = false; } } } /// <summary> /// Crée un IDbCommand à partir de la connexion. Cet objet Command sera utilisé dans la transaction. /// </summary> /// <returns>Nouvel objet Command</returns> public IDbCommand CreateCommand() { return Cnx.CreateCommand(); } /// <summary> /// Crée une transaction imbriquée. /// </summary> /// <returns>Nouvelle transaction imbriquée.</returns> public MyTransaction CreateNestedTransaction() { return new MyTransaction(this); } /// <summary> /// Rollback implicite de la transaction si elle est encore active. /// </summary> public void Dispose() { // Le test pour savoir si la transaction est active est déjà pris en compte dans la méthode Rollback() Rollback(); } /// <summary> /// Classe statique qui permet d'avoir un Random unique pour toutes les classes /// et éviter ainsi de générer systématiquement les mêmes séries /// </summary> private static class MyRandom { private static Random rnd; static MyRandom() { rnd = new Random(); } public static int Next(int min, int max) { return rnd.Next(min, max); } } /// <summary> /// Niveau actuel d'imbrication de transaction /// Attention, si vous avec des connexions dans des threads séparés, vous allez avoir des problèmes ! /// </summary> private static class MyTransactionContext { [ThreadStatic] public static int Level; static MyTransactionContext() { Level = 0; } } } }
Comment l'utiliser :
Cr�ation d'une transaction � partir d'une connexion (qui doit imp�rativement �tre ouverte) :
Cr�ation d'une transaction imbriqu�e :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2 MyTransaction tranA = new MyTransaction(maSqlConnexion);
Ces trois syntaxes sont absolument �quivalentes.
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6 MyTransaction tranB = new MyTransaction(maSqlConnexion); // ou MyTransaction tranB = new MyTransaction(tranA); // ou MyTransaction tranB = tranA.CreateNestedTransaction();
La transaction tranB n'est pas d�pendante de la transaction tranA. Elle est juste ajout�e � la suite des transactions en cours dans l'objet maSqlConnexion.
Cr�ation d'un object IDbCommand utilisant la transaction en cours :
Les deux syntaxes sont absolument �quivalentes, l'objet IDbCommand cr�� n'�tant absolument pas li� � l'objet MyTransaction qui l'a cr��.
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4 maSqlConnexion.CreateCommand(); // ou tranA.CreateCommand();
D�but de la transaction :
Validation de la transaction :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2 tranA.Begin();
Annullation de la transaction :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2 tranA.Begin();
Et un exemple complet d'utilisation :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2 tranA.Rollback();
Ce qui donne :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158 using System; using System.Data; using System.Data.Sql; using System.Data.SqlClient; using CustomTransaction; namespace TestNestedTransaction { class Program { static void Main(string[] args) { Console.WriteLine("Utilisation de transactions imbriquée (B imbriquée dans A)"); Console.WriteLine("On utilise une librairie custom qui simule une transaction."); using (SqlConnection cnx = new SqlConnection("Server=localhost\\SQLEXPRESS;Database=testlock;Trusted_Connection=True;")) { cnx.Open(); using (SqlCommand cmd = cnx.CreateCommand()) { cmd.CommandText = "insert into test (name) values (@name);"; SqlParameter pName = cmd.Parameters.Add("name", SqlDbType.VarChar, 50); Console.WriteLine(); Console.WriteLine("Cas 1 :"); Console.WriteLine("- Une simple transaction committée"); Console.WriteLine("On s'attend à ce que la ligne reste dans la base"); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); pName.Value = "Cas 1, transaction seule"; cmd.ExecuteNonQuery(); tranA.Commit(); } Console.WriteLine(); Console.WriteLine("Cas 2 :"); Console.WriteLine("- Une simple transaction rollbackée"); Console.WriteLine("On s'attend à ce qu'aucune ligne ne reste dans la base"); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); pName.Value = "Cas 2, transaction seule"; cmd.ExecuteNonQuery(); tranA.Rollback(); } Console.WriteLine(); Console.WriteLine("Cas 3 :"); Console.WriteLine("- La transaction parente est rollbackée"); Console.WriteLine("- La transaction fille est committée"); Console.WriteLine("On s'attend à ce qu'aucune ligne ne reste dans la base"); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); pName.Value = "Cas 3, transaction parente"; cmd.ExecuteNonQuery(); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); pName.Value = "Cas 3, transaction fille"; cmd.ExecuteNonQuery(); tranB.Commit(); } tranA.Rollback(); } Console.WriteLine(); Console.WriteLine("Cas 4 :"); Console.WriteLine("- La transaction parente est committée"); Console.WriteLine("- La transaction fille est rollbackée"); Console.WriteLine("On s'attend à ce que seule la ligne de la transaction parente soit dans la base"); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); pName.Value = "Cas 4, transaction parente"; cmd.ExecuteNonQuery(); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); pName.Value = "Cas 4, transaction fille"; cmd.ExecuteNonQuery(); tranB.Rollback(); } tranA.Commit(); } Console.WriteLine(); Console.WriteLine("Cas 5 :"); Console.WriteLine("- La transaction parente est committée"); Console.WriteLine("- La transaction fille est committée"); Console.WriteLine("On s'attend à ce que les deux lignes soient dans la base"); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); pName.Value = "Cas 5, transaction parente"; cmd.ExecuteNonQuery(); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); pName.Value = "Cas 5, transaction fille"; cmd.ExecuteNonQuery(); tranB.Commit(); } tranA.Commit(); } Console.WriteLine(); Console.WriteLine("Cas 6 :"); Console.WriteLine("- La transaction parente est rollbackée"); Console.WriteLine("- La transaction fille est rollbackée"); Console.WriteLine("On s'attend à ce qu'aucune ligne ne soit dans la base"); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); pName.Value = "Cas 6, transaction parente"; cmd.ExecuteNonQuery(); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); pName.Value = "Cas 6, transaction fille"; cmd.ExecuteNonQuery(); tranB.Rollback(); } tranA.Rollback(); } Console.WriteLine(); Console.WriteLine("Résultat :"); cmd.CommandText = "select id, name from test order by id;"; cmd.Parameters.Clear(); Console.WriteLine("ID\tNAME"); Console.WriteLine("------- --------------------------------------------------"); SqlDataReader da = cmd.ExecuteReader(); while (da.Read()) { Console.WriteLine("{0}\t{1}", da.GetInt32(0), da.GetString(1)); } } cnx.Close(); } Console.WriteLine(); Console.WriteLine("Fin"); Console.ReadKey(true); } } }
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 Utilisation de transactions imbriquée (B imbriquée dans A) On utilise une librairie custom qui simule une transaction imbriquée. Cas 1 : - Une simple transaction committée On s'attend à ce que la ligne reste dans la base Cas 2 : - Une simple transaction rollbackée On s'attend à ce qu'aucune ligne ne reste dans la base Cas 3 : - La transaction parente est rollbackée - La transaction fille est committée On s'attend à ce qu'aucune ligne ne reste dans la base Cas 4 : - La transaction parente est committée - La transaction fille est rollbackée On s'attend à ce que seule la ligne de la transaction parente soit dans la base Cas 5 : - La transaction parente est committée - La transaction fille est committée On s'attend à ce que les deux lignes soient dans la base Cas 6 : - La transaction parente est rollbackée - La transaction fille est rollbackée On s'attend à ce qu'aucune ligne ne soit dans la base Résultat : ID NAME ------- -------------------------------------------------- 56 Cas 1, transaction seule 60 Cas 4, transaction parente 62 Cas 5, transaction parente 63 Cas 5, transaction fille Fin
Partager