Je me permet de bumper ce topic, car je suis très impatient d'avoir une réponse sur le comment faire une transaction imbriquée avec C# et Sql Server.
En effet, d'après mes tests, pas moyen de créer une transaction alors qu'une transaction est déjà existante.
Pas moyen non plus de spécifiquer s'il s'agit d'une transaction concurrente ou imbriquée (d'un autre côté, vu que je peux pas en créer une seconde, j'imagine que c'est normal que je ne puisse pas dire comment elle doit se comporter)
Pour ma part, quand j'ai à le faire (je parle bien en transaction impriquée au sein d'une seule connexion) je le fais via le SQL (rappel : on peut balancer un script complet à un Execute* de ADO.Net en séparant les commandes par des ";"). Faudrait que je retrouve un code d'un ancien projet, mais là je n'en ai pas sous la main. (je pense à un projet en particulier, chez mon précédent client, je dois pouvoir avoir accès aux codes réalisés).
Sur l'objet Transaction de ADO.Net, c'est possible (je vais vérifier, un peu plus tard).En effet, d'après mes tests, pas moyen de créer une transaction alors qu'une transaction est déjà existante.
Habituellement, j'ai besoin de transactions imbriquées plutôt en fonction de l'IHM.
Genre l'utilisateur crée une commande (début de la transaction).
Il ajoute des produits (transaction imbriquée).
Il modifie la quantité du produit, et des propriétés (couleur, etc.) dans une transaction imbriquée.
Et en cas de problème (pas de disponibilité de son produit en rose avec des stickers Hello Kitty par exemple), selon une fenêtre d'invite, il doit pouvoir rollbacker les différents niveaux d'imbrication de transaction : revenir au coloris de base, retirer le produit, ou carrément annuler sa commande.
Alors avec des Save(), si la cinématique est séquentielle, on s'en sort, mais si par exemple le contrôle de stock se fait une fois l'intégralité de la commande saisie, c'est plus complexe d'aller annuler la ligne 2 sur 4 de la commande si on a utilisé des Save().
Il faudrait que je regarde si OleDbConnnexion supporte les transactions imbriquées. Puisque le drivers OLEDB pour SQL Server 7 le supportait parfaitement depuis VB Script ! (d'où ma surprise de ne pas retrouver le support avec SqlConnection)
Effectivement tu soulèves une question.
De fait, on peut le gérer via les TransactionScope avec le paramètre TransactionScopeOption.RequiresNew pour forcer l'utilisation d'une portée nouvelle, mais cela implique une deuxième connexion sur la base (sans escalader à DTC dans ce cas).
Vais essayer de trouver un moment (sauf que j'ai pas de Sql Server là où je suis présentement, le client étant de Delphes)
Mais je maintiens que tu peux le gérer aisément via des ExecuteNonQuery, à condition de ne pas fermer la connexion entre deux commandes.
En revanche, effectivement, je ne vois pas trop de syntaxe pour le gérer via l'objet SqlTransaction (à la limite c'est un détail).
Pseudo code :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12 ExecuteNonQuery "BEGIN TRANSACTION trans_extern" /* modif data ici */ ExecuteNonQuery "BEGIN TRANSACTION trans_intern" /* modif data trans interne ici */ ExecuteNonQuery "ROLLBACK TRANSACTION trans_intern"; /* on laisse tomber modif interne */ /* on commit modif externe */ ExecuteNonQuery "COMMIT TRANSACTION trans_extern"
Tu peux laisser une transaction ouverte avec un ExecuteNonQuery ?
Décidément, j'ai pas la même vision que Microsoft
Pour moi il était évident que la transaction ouverte et non committée explicitement dans un ExecuteNonQuery était rollbackée
Si ton exemple fonctionne, effectivement, c'est un moyen pour pallier à cette limitation du connecteur SQL Server.
Cela dit, à moins d'encapsuler ça dans des objets bien pensés, c'est hyper crade comme approche, on a vite fait de plus savoir où on en est![]()
En tout cas, OleDbConnexion ne prends pas plus en charge les transactions imbriquées/parallèles :/
Exception quand on ouvre une transaction à l'intérieur d'une première :
Les transactions parallèles ne sont pas prises en charge par OleDbConnection.
J'essaie d'implémenter ta méthode, et le résutlat est TRES mitigé.
Je ne comprends pas une seconde ce qu'il se passe
Voici les ExecuteQuery envoyés à la base (1 ligne = 1 ExecuteQuery)
Ert ça plante sur la dernière ligne : il me dit que la transaction guvu n'existe pas !
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 BEGIN TRANSACTION bhri insert into test (name) values (@name) COMMIT TRANSACTION bhri BEGIN TRANSACTION yffx insert into test (name) values (@name) ROLLBACK TRANSACTION yffx BEGIN TRANSACTION wudt insert into test (name) values (@name) BEGIN TRANSACTION jdnj insert into test (name) values (@name) COMMIT TRANSACTION jdnj ROLLBACK TRANSACTION wudt BEGIN TRANSACTION gkeu insert into test (name) values (@name) BEGIN TRANSACTION guvu insert into test (name) values (@name) ROLLBACK TRANSACTION guvu
Pour le reste, ça se passe comme prévu :
La transaction "bhri" a bien été commitée
La transaction "yffx" a bien été rollbackée
La transaction "wudt" a bien été rollbackée et a annulé la transaction "jdnj"
Mais là, étrange, impossible de rollbacker une transaction imbriquée (???)
Mon code :
Program.cs
Et MagicTransaction.cs :
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181 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(); Console.WriteLine(); /* Console.WriteLine("Cas 1 :"); Console.WriteLine("- La transaction A est rollbackée"); Console.WriteLine("- La transaction B est committée"); Console.WriteLine("On s'attend à ce que tout soit rollbacké"); */ MonObj obj0 = new MonObj(cnx); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); obj0.MethodA("Cas -1, transaction A"); tranA.Commit(); } using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); obj0.MethodA("Cas 0, transaction A"); tranA.Rollback(); } MonObj obj1 = new MonObj(cnx); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); obj1.MethodA("Cas 1, transaction A"); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); obj1.MethodB("Cas 1, transaction B"); tranB.Commit(); } tranA.Rollback(); } Console.WriteLine(); /* Console.WriteLine("Cas 2 :"); Console.WriteLine("- La transaction A est committée"); Console.WriteLine("- La transaction B est rollbackée"); Console.WriteLine("On s'attend à ce que B soit rollbackée, mais que A soit committée"); */ MonObj obj2 = new MonObj(cnx); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); obj2.MethodA("Cas 2, transaction A"); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); obj2.MethodB("Cas 2, transaction B"); tranB.Rollback(); } tranA.Commit(); } Console.WriteLine(); /* Console.WriteLine("Cas 3 :"); Console.WriteLine("- La transaction A est committée"); Console.WriteLine("- La transaction B est committée"); Console.WriteLine("On s'attend à ce que les deux transactions soient committées"); */ MonObj obj3 = new MonObj(cnx); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); obj3.MethodA("Cas 3, transaction A"); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); obj3.MethodB("Cas 3, transaction B"); tranB.Commit(); } tranA.Commit(); } Console.WriteLine(); /* Console.WriteLine("Cas 4 :"); Console.WriteLine("- La transaction A est rollbackée"); Console.WriteLine("- La transaction B est rollbackée"); Console.WriteLine("On s'attend à ce que les deux transactions soient rollbackées"); */ MonObj obj4 = new MonObj(cnx); using (MyTransaction tranA = new MyTransaction(cnx)) { tranA.Begin(); obj4.MethodA("Cas 4, transaction A"); using (MyTransaction tranB = new MyTransaction(cnx)) { tranB.Begin(); obj4.MethodB("Cas 4, transaction B"); tranB.Rollback(); } tranA.Rollback(); } Console.WriteLine(); Console.WriteLine("Résultat :"); using (SqlCommand cmd = cnx.CreateCommand()) { cmd.CommandText = "select id, name from test order by id"; 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); } } class MonObj { SqlConnection _c; public MonObj(SqlConnection c) { _c = c; } public void MethodA(string name) { using (SqlCommand cmd = _c.CreateCommand()) { cmd.CommandText = "insert into test (name) values (@name)"; cmd.Parameters.Add("name", SqlDbType.VarChar, 50).Value = name; Console.WriteLine(cmd.CommandText); cmd.ExecuteNonQuery(); } } public void MethodB(string name) { using (SqlCommand cmd = _c.CreateCommand()) { cmd.CommandText = "insert into test (name) values (@name)"; cmd.Parameters.Add("name", SqlDbType.VarChar, 50).Value = name; Console.WriteLine(cmd.CommandText); cmd.ExecuteNonQuery(); } } } }
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 using System; using System.Data; namespace CustomTransaction { public 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> /// Objet qui permet d'effectuer des transactions imbriquées. /// On ne catch absolument aucune erreur volontairement. /// Attention, les transactions imbriquées ne sont pas supportées par tous les SGBD ! /// Aussi, la syntaxe peut éventuellement changer d'un SGBD à l'autre. /// </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 stock la connexion Cnx = cnx; // On crée un nom aléatoire pour la transaction string lettres = "abcdefghijklmnopqrstuvwxyz"; char[] name = new char[NAME_LENGTH]; for (int i = 0; i < NAME_LENGTH; i++) { name[i] = lettres[MyRandom.Next(0, 26)]; } Name = string.Concat(name); //Console.WriteLine("Transaction créée : {0}", Name); } /// <summary> /// Débute une transaction /// </summary> public void Begin() { lock (this) { using (IDbCommand cmd = Cnx.CreateCommand()) { cmd.CommandText = string.Format("BEGIN TRANSACTION {0}", Name); Console.WriteLine(cmd.CommandText); cmd.ExecuteNonQuery(); } IsActive = true; } } /// <summary> /// Valide la transaction /// </summary> public void Commit() { lock (this) { if (IsActive) { using (IDbCommand cmd = Cnx.CreateCommand()) { cmd.CommandText = string.Format("COMMIT TRANSACTION {0}", Name); Console.WriteLine(cmd.CommandText); cmd.ExecuteNonQuery(); } IsActive = false; } } } /// <summary> /// Annule la transaction /// </summary> public void Rollback() { lock (this) { if (IsActive) { using (IDbCommand cmd = Cnx.CreateCommand()) { cmd.CommandText = string.Format("ROLLBACK TRANSACTION {0}", Name); Console.WriteLine(cmd.CommandText); cmd.ExecuteNonQuery(); } IsActive = false; } } } /// <summary> /// Libère les ressources. /// Rollback la transaction si elle est encore active. /// </summary> public void Dispose() { //Console.WriteLine("Dispose {0}", Name); // Le test pour savoir si la transaction est active est déjà pris en compte dans la méthode Rollback() Rollback(); } } }
Oui mais créer un fichier MDB avec l'explorateur Windows, c'est plus facile que d'installer un serveur Oracle, donc pour faire des tests, c'est toujours une solution de repli, même si c'est tout pourri![]()
Bon, j'ai pris 10 mn pour installer Sql Express
Et je confirme mes dires :
le DDL de deux tables de test :
Le code (fait à l'arrache, hein) :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 CREATE TABLE [dbo].[Table_2]( [t2_id] [int] IDENTITY(1,1) NOT NULL, [t2_text] [nvarchar](50) NULL ) ON [PRIMARY] CREATE TABLE [dbo].[Table_1]( [t1_id] [int] IDENTITY(1,1) NOT NULL, [t1_text] [nvarchar](50) NULL ) ON [PRIMARY]
A la fin, la ligne de Table_1 est bien insérée, mais celle de Table_2 est "rollbackée".
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 SqlConnection connection = new SqlConnection("Data Source=localHost;Initial Catalog=TEST;Integrated Security=true"); connection.Open(); using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "begin transaction trans_1;"; command.ExecuteNonQuery(); } using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "insert into Table_1 values('test 111');"; command.ExecuteNonQuery(); } using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "begin transaction trans_2;"; command.ExecuteNonQuery(); } using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "save transaction trans_2;"; command.ExecuteNonQuery(); } using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "insert into Table_2 values('test222');"; command.ExecuteNonQuery(); } using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "rollback transaction trans_2;"; command.ExecuteNonQuery(); } using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "commit transaction trans_1;"; command.ExecuteNonQuery(); } using (SqlCommand command = connection.CreateCommand()) { command.CommandText = "commit;"; command.ExecuteNonQuery(); } connection.Close();
Pourquoi fais-tu un SAVE de ta transaction 2 avant de t'en servir ?
Si ma mémoire est bonne on ne peut pas faire un rollback sur une transaction imbriquée (donc celle qui n'est pas la plus extérieure) sans sauvegarder le point de synchro.
Le rollback doit s'effectuer normalement sur la plus externe, donc le save permet de ramener cela "au premier plan".
Voir les détails dans la doc, car ça fait un moment que j'ai pas pratiqué Sql Server, et je cite de mémoire un truc pas utilisé très souvent.
En tout cas, avec le SAVE, ça débloque mon problème.
(Faudra m'expliquer pourquoi mais bon ^^)
Par contre, j'en ai un autre... Qui semble venir de SAVE
Honnêtement, je ne comprends absolument pas ce qu'il se passe dans la tête de SQL Server !
=> C'est ce que je joue dans mon programme de test.
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 BEGIN TRANSACTION [lzsy] SAVE TRANSACTION [lzsy] insert into test (name) values ('Cas 1, transaction seule'); COMMIT TRANSACTION [lzsy] BEGIN TRANSACTION [zpnn] SAVE TRANSACTION [zpnn] insert into test (name) values ('Cas 2, transaction seule'); ROLLBACK TRANSACTION [zpnn] BEGIN TRANSACTION [jbjx] SAVE TRANSACTION [jbjx] insert into test (name) values ('Cas 3, transaction parente'); BEGIN TRANSACTION [sdot] SAVE TRANSACTION [sdot] insert into test (name) values ('Cas 3, transaction fille'); COMMIT TRANSACTION [sdot] ROLLBACK TRANSACTION [jbjx] BEGIN TRANSACTION [emed] SAVE TRANSACTION [emed] insert into test (name) values ('Cas 4, transaction parente'); BEGIN TRANSACTION [nsze] SAVE TRANSACTION [nsze] insert into test (name) values ('Cas 4, transaction fille'); ROLLBACK TRANSACTION [nsze] COMMIT TRANSACTION [emed] BEGIN TRANSACTION [glye] SAVE TRANSACTION [glye] insert into test (name) values ('Cas 5, transaction parente'); BEGIN TRANSACTION [givs] SAVE TRANSACTION [givs] insert into test (name) values ('Cas 5, transaction fille'); COMMIT TRANSACTION [givs] COMMIT TRANSACTION [glye] BEGIN TRANSACTION [zolw] SAVE TRANSACTION [zolw] insert into test (name) values ('Cas 6, transaction parente'); BEGIN TRANSACTION [kkub] SAVE TRANSACTION [kkub] insert into test (name) values ('Cas 6, transaction fille'); ROLLBACK TRANSACTION [kkub] ROLLBACK TRANSACTION [zolw]
Et je me rends compte que seule la transaction 1 ([lzsy]) est bien terminée.
En effet, si je lis la table par la suite, seule la ligne de la transaction 1 est bien sauvegardée !
Et quand je lance depuis Entreprise Manager, il me dit que des transactions ne sont pas terminées.
J'imagine que les SAVE sont responsable. Pourtant, si je ne les met pas, il ne veut pas rollbacker une transaction fille.
Bref, c'est quoi le binz ?
J'ai essayé de virer le "SAVE" quand j'étais au premier niveau (comme dans l'exemple de Bluedeep), et je retrouve mon problème initial lors du rollback de la transaction fille du cas 6... (alors que les 5 autres cas sont passés, notamment le 4 qui est strictement identique au 6 par rapport à là où ça plante)
=> Le 4 et le 6 ont se bout de code, strictement identique (c'est la ligne suivante qui plante).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 begin insert begin save insert rollback
Pourtant, le 4 passe, et pas le 6.
Je sens que je vais péter un plomb
Code sql : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 BEGIN TRANSACTION [lwvh] insert into test (name) values ('Cas 4, transaction parente'); BEGIN TRANSACTION [ghpo] SAVE TRANSACTION [ghpo] insert into test (name) values ('Cas 4, transaction fille'); ROLLBACK TRANSACTION [ghpo] COMMIT TRANSACTION [lwvh] BEGIN TRANSACTION [vais] insert into test (name) values ('Cas 6, transaction parente'); BEGIN TRANSACTION [dxjx] SAVE TRANSACTION [dxjx] insert into test (name) values ('Cas 6, transaction fille'); ROLLBACK TRANSACTION [dxjx]
C'est quoi la différence ?
(on ne parle pas du commit final du cas 4, puisqu'on plante sur le rollback...)
Partager