IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Contribuez .NET Discussion :

Imbriquer des transactions en C#


Sujet :

Contribuez .NET

  1. #1
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 149
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 149
    Points : 7 392
    Points
    7 392
    Billets dans le blog
    1
    Par défaut Imbriquer des transactions en C#
    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 : 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();
    }
    Code qui marche :
    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
    }
    Voici le code, que j'ai essayé de documenter au maximum :
    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) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    MyTransaction tranA = new MyTransaction(maSqlConnexion);
    Création d'une transaction imbriquée :
    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();
    Ces trois syntaxes sont absolument équivalentes.
    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 :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    maSqlConnexion.CreateCommand();
    // ou
    tranA.CreateCommand();
    Les deux syntaxes sont absolument équivalentes, l'objet IDbCommand créé n'étant absolument pas lié à l'objet MyTransaction qui l'a créé.

    Début de la transaction :
    Validation de la transaction :
    Annullation de la transaction :
    Et un exemple complet d'utilisation :
    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);
            }
        }
    }
    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
     
    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
    On ne jouit bien que de ce qu’on partage.

  2. #2
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Points : 39 749
    Points
    39 749
    Par défaut
    Bien vu

    Mais est-ce qu'on ne peut pas déjà faire des transactions imbriquées en utilisant TransactionScope ? (la question n'a rien de rhétorique, je ne connais pas la réponse...)

    Sinon, pour le problème de ta classe statique : effectivement ce n'est pas très propre... Autres approches possibles :

    - Tu peux marquer le champ Level avec l'attribut ThreadStatic. Chaque thread aura sa propre copie de la valeur, et donc plus de problème. Ca reste pas très propre à mon avis, mais ça ne demande quasiment aucune modif par rapport à ton code actuel

    - Quand tu crées une transaction imbriquée, tu peux lui passer en paramètre la transaction parente. Comme ça, plutôt que de vérifier le niveau, tu peux simplement vérifier s'il y a une transaction parente. Si ce n'est pas le cas, c'est que tu es au niveau 0. A mon avis c'est plus propre que de se reposer sur un état global.

  3. #3
    Membre chevronné Avatar de Er3van
    Homme Profil pro
    Architecte Logiciel
    Inscrit en
    Avril 2008
    Messages
    1 430
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Architecte Logiciel
    Secteur : Industrie

    Informations forums :
    Inscription : Avril 2008
    Messages : 1 430
    Points : 2 227
    Points
    2 227
    Par défaut
    A priori c'est possible d'imbriquer des TransactionScope, il faudrait que je teste pour être sûr. Il faut peut-être jouer sur les TransactionScopeOption pour avoir ce que l'on souhaite.

    Si j'ai le temps je teste dans la journée !
    One minute was enough, Tyler said, a person had to work hard for it, but a minute of perfection was worth the effort. A moment was the most you could ever expect from perfection.

    -- Chuck Palahniuk, Fight Club, Chapter 3 --

  4. #4
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 149
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 149
    Points : 7 392
    Points
    7 392
    Billets dans le blog
    1
    Par défaut
    Merci pour ta réponse.

    Apparement, TransactionScope (que je ne connais pas) permettrait, aux dire de certains autres forumeurs, de faire des transactions imbriquées.

    Au détail près que SQL Server par exemple, ne supporte pas, même en interne, les transactions imbriquées : il doit se baser en interne sur un artifice du même genre que le mien.

    Aussi, comme j'ai déjà répondu dans un autre topic, ce qui me dérange fortement avec TransactionScope, c'est qu'il ne soit pas dans le namespace "System.Data".
    Pour moi, cela implique plusieurs limitations qui ne m'encouragent pas à l'utiliser :
    - La transaction n'est pas forcément gérée par le SGBD (en effet, c'est Microsoft Transaction Server qui gère la transaction, et la délègue au SGBD)
    - Le TransactionScope n'est pas lié à la connexion à la base de données, et vice-versa. En cas d'utilisation de plusieurs connexions à différentes bases, on ne peut pas choisir quelle connexion utilisera des transaction et telle autre pas.
    - De la même manière, on ne peux pas rendre une partie du code "non transactionnelle".
    - Et surtout : le fait que TransactionScope ne soit pas dans le namespace Data montre qu'il a été mis en place dans l'optique éventuellement de gérer aussi d'autres éléments en transaction (système de fichier, mémoire, etc. ?) même si ces fonctionnalité ne sont pas prises en charge actuellement.

    Pour en revenir à ThreadStatic, merci pour l'astuce

    Quant au fait de passer la transaction en paramètre, oui et non.

    Dans l'absolu, pour gérer de vraies transactions imbriquées, il faudrait en effet pouvoir passer la transaction mère en paramètre à la transaction fille : plus propre, et sémantiquement plus logique.
    Le seul souci, c'est que derrière, SQL Server ne supporte pas les transactions imbriquées, et par conséquent, je suis obligé de gérer un niveau d'imbrication "séquentiel" global au thread. En effet, il ne faut pas que lorsque je suis au niveau 3, je puisse créer une transaction de niveau 2 : car en réalité, elle sera au niveau 4, et il n'y a aucun artifice à ma connaissance qui permette d'éviter ça (mise à part éventuellement passer par une connexion séparée, mais on va se retrouver avec des locks et autres joyeusetés).

    Plus généralement, même Oracle qui semble être un des rares SGBD a supporter les transactions imbriquées ne semble pas accepter qu'on modifie une transaction parente lorsqu'une transaction fille est active... quoi que... la phrase, en anglais issue de la documentation, porte à confusion et voici comme je l'ai comprise :
    "Quand une transaction mère ouvre une transaction imbriquée, elle ne [g]devrait[/g] (may) plus effectuer de modifications autre que commit, rollback ou begin nested transaction."
    Il n'y a pas d'explication sur le "may" :
    - Est-ce un abus de langage et "must" est plus approprié ? J'en doute
    - A mon avis c'est simplement que la transaction mère risque de se prendre un lock à cause de la transaction fille, et ainsi se retrouver dans état de deadlock. C'est à vérifier.

    Enfin bref, de toute façon, j'ai pour le moment écrit cette librairie plutôt pour SQL Server (puisqu'avec Oracle on peut faire de vraies transactions imbriquées, on n'a pas besoin de faire de SAVE). Et donc dans la réalité, on ne peux faire que ça (en T-SQL) :

    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
    16
     
    BEGIN TRANSACTION A
    [...]
    SAVE TRANSACTION B
    [...]
    SAVE TRANSACTION C
    [...]
    SAVE TRANSACTION D
    [...]
    SAVE TRANSACTION E
    [...]
    ROLLBACK TRANSACTION D
    [...]
    SAVE TRANSACTION F
    [...]
    COMMIT TRANSACTION A
    => C'est parfaitement séquentiel, et il n'y a aucun moyen après le "SAVE TRANSACTION D" d'aller modifier des données au niveau du scope de B (c'est à dire que si on veut rollbacker D, on est obligé de rollbacker tout ce qui a été fait après D, sans aucun moyen de faire autrement).
    On ne jouit bien que de ce qu’on partage.

  5. #5
    Membre chevronné Avatar de Er3van
    Homme Profil pro
    Architecte Logiciel
    Inscrit en
    Avril 2008
    Messages
    1 430
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Architecte Logiciel
    Secteur : Industrie

    Informations forums :
    Inscription : Avril 2008
    Messages : 1 430
    Points : 2 227
    Points
    2 227
    Par défaut
    Citation Envoyé par StringBuilder Voir le message
    - Et surtout : le fait que TransactionScope ne soit pas dans le namespace Data montre qu'il a été mis en place dans l'optique éventuellement de gérer aussi d'autres éléments en transaction (système de fichier, mémoire, etc. ?) même si ces fonctionnalité ne sont pas prises en charge actuellement.
    Je te confirme que tu gères d'autres transactions, et c'est bien le but.

    Je m'en sers actuellement dans un programme qui consomme des messages en provenance d'une file MQ (IBM MQSeries), et insère les données en base après traitement (la version courte).

    Le principe avec MQ, c'est qu'un message consommé est sorti de la file, et a priori perdu.
    Avec le TransactionScope, je peux gérer ça très facilement : je consomme le message, et si pour une raison ou une autre ma requête SQL se vautre, par exemple à cause d'un problème réseau, alors mon message est remis dans la file à la même position qu'il était (car l'ordre peut jouer au point de vue applicatif), et donc aucune perte.
    Le programme s'execute ensuite de nouveau et si tout fonctionne, tout est commité (côté base, et également la consommation du message MQ).

    Alors, oui, c'est peut-être un bien pour un mal et ça ne s'adapte pas à tous les cas, mais de mon point de vue c'est exactement ce que je cherche.

    EDIT : J'ai mis un +1 quand même pour l'idée !
    One minute was enough, Tyler said, a person had to work hard for it, but a minute of perfection was worth the effort. A moment was the most you could ever expect from perfection.

    -- Chuck Palahniuk, Fight Club, Chapter 3 --

  6. #6
    Rédacteur/Modérateur


    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2004
    Messages
    19 875
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 42
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2004
    Messages : 19 875
    Points : 39 749
    Points
    39 749
    Par défaut
    Citation Envoyé par StringBuilder Voir le message
    - Le TransactionScope n'est pas lié à la connexion à la base de données, et vice-versa. En cas d'utilisation de plusieurs connexions à différentes bases, on ne peut pas choisir quelle connexion utilisera des transaction et telle autre pas.
    - De la même manière, on ne peux pas rendre une partie du code "non transactionnelle".
    Bah déjà ça permet de faire des transactions distribuées sur plusieurs bases par exemple, ce qui est plus compliqué avec des transactions "classiques". Et le fait de ne pas pouvoir faire de traitement hors transaction sur une DB pendant que tu fais un traitement dans une transaction sur une autre me semble une bonne chose, puisque ça permet de garantir la cohérence des données... mais effectivement il y a sans doute des scénarios où c'est gênant

  7. #7
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 149
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 149
    Points : 7 392
    Points
    7 392
    Billets dans le blog
    1
    Par défaut
    Citation Envoyé par tomlev Voir le message
    Et le fait de ne pas pouvoir faire de traitement hors transaction sur une DB pendant que tu fais un traitement dans une transaction sur une autre me semble une bonne chose, puisque ça permet de garantir la cohérence des données... mais effectivement il y a sans doute des scénarios où c'est gênant
    Un exemple classique où c'est gênant :
    J'ai un trigger qui effectue une vérification fonctionnelle durant son traitement.
    Je souhaite conserver la trace dans une table de LOG des actions effectuées par le trigger.
    => En cas d'erreur fonctionnelle, le trigger déclenche une erreur, qui sera traduite sour forme d'un ROLLBACK dans la transaction appelante.
    => Il faut que l'insertion dans la table de LOG ne soit pas prise en compte dans la transaction, sinon elle perd tout son intérêt.

    SQL Server, en interne, gère lui aussi des choses hors transaction à l'intérieur d'une transaction : si tu fais un INSERT dans une table avec un champ IDENTITY, alors même en cas le ROLLBACK, l'identifiant attribué restera réservé, et le compteur incrémenté. Si on souhaite par exemple gérer son propre compteur à l'aide d'une PS par exemple, on peut souhaiter aussi laisser le trou, afin de mettre en évidence le fait qu'on a bel et bien essayé d'insérer une ligne, mais qu'elle a échouée.

    Bon, en même temps, la librairie que j'ai écrit ne permet pas non plus, au sein de la transaction, de décider si une ligne est prise en compte ou non dans la transaction. La seule solution sera d'ouvrir une seconde connexion (et serrer les fesses pour en pas avoir de LOCK), ou utiliser, comme ça existe sous Oracle, une instruction qui permet de rendre non transactionnelle une requête à l'intérieur d'une transaction.
    On ne jouit bien que de ce qu’on partage.

  8. #8
    Expert éminent
    Avatar de StringBuilder
    Homme Profil pro
    Chef de projets
    Inscrit en
    Février 2010
    Messages
    4 149
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 45
    Localisation : France, Rhône (Rhône Alpes)

    Informations professionnelles :
    Activité : Chef de projets
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2010
    Messages : 4 149
    Points : 7 392
    Points
    7 392
    Billets dans le blog
    1
    Par défaut
    Je viens de :
    - Enrichir la librairie avec un second constructeur pour MyTransaction ainsi que les méthodes CreateNestedTransaction() et CreateCommand()
    Ces deux méthodes ne sont là que pour commodité (et lisibilité du code). Elles n'apportent rien d'un point de vue fonctionnel, contrairement à ce qu'on pourrait croire !
    - Améliorer la documentation
    - Donner des exemples unitaires d'utilisation des méthodes
    On ne jouit bien que de ce qu’on partage.

Discussions similaires

  1. Réponses: 5
    Dernier message: 24/08/2005, 11h21
  2. Apropos des Transactions au sein d'un Stored Procedure
    Par Sarbacane dans le forum Connexion aux bases de données
    Réponses: 6
    Dernier message: 16/11/2004, 08h21
  3. Annuler des transactions
    Par sgire dans le forum ASP
    Réponses: 2
    Dernier message: 04/05/2004, 09h31
  4. gestion des transactions
    Par viny dans le forum Requêtes
    Réponses: 2
    Dernier message: 26/03/2004, 21h53
  5. Réponses: 12
    Dernier message: 18/03/2004, 15h09

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo