Transactions Vous utilisez les transactions quand vous avez à faire une série d’opérations individuelles mais dépendantes les unes des autres, en ayant la possibilité, une fois que toutes les commandes ont été exécutées, d’accepter ou d’annuler d’un bloc toutes les opérations effectuées. L’exemple le plus classique est le transfert d’un compte à un autre à la banque. Si quelque chose survenait entre le moment où vous retirez l’argent du premier compte et le moment où il est déposé dans l’autre, l’argent se retrouverait « dans les limbes ». Ça prend un mécanisme qui permet de faire les deux opérations comme s’il ne s’agissait que d’une seule, et de pouvoir l’annuler en un bloc si un problème est détecté. Une transaction servant habituellement à assurer une cohérence entre plusieurs opérations différentes sur plusieurs tables différentes, elle doit s’appliquer sur une connexion. Comme dans .NET on travaille en mode déconnecté, le mécanisme peut s’appliquer à plusieurs niveaux. Lignes individuelles Au niveau des lignes individuelles, une série de méthodes BeginEdit / EndEdit / CancelEdit correspond aux plus familiers BeginTran / Commit / RollBack. Elles peuvent être utilisées pour développer un petit mécanisme de transactions très localisé. Quand vous chargez une table dans un DataSet, chaque DataRow contient une ligne qu’on dit Original. Dim ds As Dataset Dim row As DataRow Dim tb As DataTable DataAdapterQuelconque.Fill(ds) tb = DataSet.Tables("Clients") row = tb.Rows(2) Debug.Write (row.ToString) 'Donne par exemple "Québec" Quand vous modifiez la ligne, l’Original est conservé, et une copie appelée Current est créée. Toutes les opérations sur la DataRow se font sur cette copie. row.Item("Ville")= "Montréal" ou row("Ville")= "Montréal" Si vous appelez la méthode BeginEdit sur la ligne, une troisième copie appelée Proposed est créée, et à partir de ce moment, les opérations se font sur la Proposed plutôt que sur la Current. row.BeginEdit() row.Item("Ville")= "Laval" Pour accéder aux données « cachées », vous pouvez utiliser un overload de la propriété Item : row.Item("Ville",DataRowVersion.Original) -> Québec row.Item("Ville",DataRowVersion.Current) -> Montréal row.Item("Ville",DataRowVersion.Proposed) -> Laval Si vous faites un CancelEdit à ce moment, vous éliminez simplement la valeur Proposed : row.CancelEdit() row.Item("Ville",DataRowVersion.Original) -> Québec row.Item("Ville",DataRowVersion.Current) -> Montréal row.Item("Ville",DataRowVersion.Proposed) -> Nothing Par contre, si vous faites un EndEdit, les changements apportés sont enregistrés, la valeur Proposed devient la Current. La ligne originale reste toujours disponible : row.EndEdit() row.Item("Ville",DataRowVersion.Original) -> Québec row.Item("Ville",DataRowVersion.Current) -> Laval row.Item("Ville",DataRowVersion.Proposed) -> Nothing Notez que bien que notre démonstration soit sur un seul champ, le mécanisme joue sur l’ensemble des champs de la ligne. Dans la table Gérer les transactions ligne par ligne peut devenir fastidieux, alors la table possède deux méthodes qui permettent de faire le travail d’un coup sur toutes les lignes actuellement en cours d’édition. RejectChanges appelle CancelEdit sur toutes les lignes de la table, et en plus, assume que tous les changements faits depuis le début des opérations sur la table sont rejetés. Vous vous retrouvez en fait avec le DataSet tel qu’il était juste au moment de le créer en appelant un Fill sur un DataAdapter : tb.RejectChanges() row.Item("Ville",DataRowVersion.Original) -> Québec row.Item("Ville",DataRowVersion.Current) -> Québec row.Item("Ville",DataRowVersion.Proposed) -> Nothing AcceptChanges appelle EndEdit sur toutes les lignes de la table et en plus, assume que tous les changements apportés deviennent final. tb.AcceptChanges() row.Item("Ville",DataRowVersion.Original) -> Laval row.Item("Ville",DataRowVersion.Current) -> Laval row.Item("Ville",DataRowVersion.Proposed) -> Nothing row.RowState() -> Unchanged Important Toutes les lignes ajoutées ou modifiées deviennent permanentes et les lignes détruites le sont vraiment. Si vous déclenchez un Update, sur-le-champ, il ne se passera rien. Et si vous déclenchez un Update plus tard, il risque d’y avoir des conflits parce que les enregistrements « originaux » du DataSet ne correspondront plus à ceux de la base de données. AcceptChanges n’est donc habituellement appelé qu’après avoir fait un Update. Dans le DataSet Le DataSet possède aussi des méthodes AcceptChanges et RejectChanges, qui appellent les méthodes correspondantes sur toutes les tables faisant partie du DataSet. Objet Transaction sur la Connection Pour des transactions plus globales et plus traditionnelles, qui jouent au moment d’écrire les données dans la base de données plutôt que dans le DataSet en mémoire, vous devez utiliser un objet Transaction. Notez que le mécanisme est très variable dépendant de la base de données. Certaines sont pessimistes et vont accumuler les transactions en mémoire en attendant d’avoir le OK avant de les écrire dans la BD. D’autres sont plus optimistes et assument qu’il est rare d’annuler une transaction. Elles vont donc écrire les changements dans la BD, en disposant d’un mécanisme permettant de revenir en arrière advenant une annulation. Tout ça est relativement transparent pour le programmeur ADO.NET. La technique est la même dans tous les cas. C’est le fournisseur ADO qui s’occupe des détails.1 Mais pour concevoir des transactions efficaces, vous devez bien connaître le mécanisme de transactions dans la base de données que vous utilisez. Si vous avez besoin d’un contrôle très serré des transactions, vous devrez vous référer à la documentation de votre fournisseur de base de données. 1 Vous créez tout d’abord une transaction en appelant la méthode BeginTransaction de la Connection que vous allez utiliser. La Connection doit être préalablement ouverte. Dim cn As New OleDbConnection(pcConnectionString) Dim tr As OleDbTransaction cn.Open() tr = cn.BeginTransaction() Vous assignez ensuite la transaction à la propriété Transaction d’un objet Command. La transaction s’appliquera à toutes les opérations effectuées sur cet objet. Dim cmd1 as New OleDBCommand("INSERT …", cn, tr) Dim cmd2 as New OleDBCommand("DELETE …", cn) cmd2.Transaction = tr Comme vous pouvez le constater, la même transaction peut être appliquée à plusieurs commandes. Ainsi, pour avoir une transaction lors d’un Update d’un DataSet, il faut avoir associé chacune des commandes du DataAdapter (InsertCommand, DeleteCommand et UpdateCommand) avec la même transaction. Toutes les opérations subséquentes sur ces objets Command font partie de la transactiont : cmd1.ExecuteNonQuery cmd2.ExecuteNonQuery Vous terminez avec un Commit sur l’objet Transaction dans le cas où toutes les opérations se sont déroulées selon les prévisions, ou avec un RollBack dans le cas où un problème a été détecté. If OK Then tr.Commit() Else tr.Rollback() End If Si vous avez une commande qui crée 12 enregistrements et une autre qui efface 35 enregistrements et qu’une erreur survient au sixième DELETE – un enregistrement verrouillé par un autre utilisateur par exemple – un Rollback annule tout, incluant l’ajout des 12 et la suppression des 5 premiers enregistrements. Les transactions imbriquées sont possibles. La compréhension complète des mécanismes de transaction est un cours en soi, et relève plus de la gestion des bases de données que de la programmation comme telle2. Nous n’insisterons pas beaucoup plus, sauf pour dire aux initiés qu’il est possible de déterminer le niveau d’isolation3 soit en créant la transaction. tr = cn.BeginTransaction(IsolationLevel.Serializable) Si vous travaillez avec SQL Server, notez que l’objet SqlTransaction permet beaucoup plus de choses que le OleDbTransaction. Si vous êtes familier avec les notions de transactions nommées et de SavePoints en Transact-SQL par exemple, et que vous voulez utiliser ce mécanisme dans votre code .NET, vous devrez absolument passer par des objets Sql, les objets OleDb n’offrant pas cette possibilité. Transact-SQL permet par ailleurs de contrôler les transactions directement dans vos commandes SQL si vous le désirez. Utiliser ces commandes dans vos procédures stockées pourrait être intéressant si vous devez absolument utiliser des objets OleDb (vous supportez peut-être un BD en plusieurs formats) et que vous voulez obtenir un niveau de contrôle plus avancé dans vos transactions sur SQL Server. Transactions distribuées Une transaction distribuée en est une qui travaille sur des opérations effectuées sur plusieurs ressources, par exemple, quand vous travaillez simultanément avec deux bases de données, en implémentant un mécanisme de réplication entre une base de données maître et une base de données locale. Vous pourriez aussi vouloir établir une entre plusieurs classes et/ou dll roulant peut-être sur des ordinateurs différents. C’est un mécanisme qui s’applique aux membres des classes impliquées plutôt à des bases de données, mais si les classes développées de cette façon créent des connexions ADO, la transaction implémentée sur la classe contrôle indirectement les transactions sur ces (un Rollback sur la classe va s’appliquer à la Connection). Elles ne sont supportées que dans des environnements très avancés (oubliez ça avec Access) et impliquent généralement l’intervention du système d’opération. Les versions modernes de Windows supportent les transactions distribuées au travers des Component Services, une composante de COM+. Une excellente référence pour tout ce qui a trait à SLQ Server, le livre de Karen Delaney, dont vous trouverez les coordonnées dans la bibliographie, à la fin du manuel. 2 Le niveau d’isolation gère comment les locks (verrouillage) vont se faire pendant la transaction. Par exemple, un niveau Serializable comme celui de notre ligne de code implique que tous les enregistrements du DataSet sont verrouillées pour les autres utilisateurs le temps que dure la transaction. Il faut que la base de données supporte ce mécanisme. 3 Leur étude déborde des cadres de ce cours. Nous nous permettrons cependant de guider ceux qui pourraient avoir besoin de ces techniques vers les ressources pertinentes. Vous pouvez recherchez l’aide en ligne et l’Internet pour trouver des détails et exemples sur les mots-clés retrouvés dans la description très sommaire qui suit. Dans .NET, vous implémentez le mécanisme dans un librairie de classes (dll) qui héritent de la classe ServicedComponent, après avoir référencé System.EntrepriseServices. Les classes devant participer aux transactions doivent être marquées de l’attribut TransactionOption qui détermine pour chaque classe si elle va fonctionner dans une transaction ou non. Ces classes contiennent un objet appelé ContextUtil qui possède des méthodes SetAbort (Rollback) et SetComplete (Commit) permettant de définir si l’on rejette ou accepte toutes les opérations faites par la classe. Vous pouvez en plus automatiser les SetComplete en définissant des procédures possédant l’attribut AutoComplete. Les procédures AutoComplete vont automatiquement déclencher un SetAbort si elles détectent une Exception, ou un SetComplete si elles se terminent sans avoir rencontré d’erreur. La librairie doit être compilée avec un strong name et enregistrée comme un service avec l’aide de RegSvcs.exe. Notez que les classes fonctionnant dans ce mode participent automatiquement au connection pooling et au object pooling (l’ancien MTS - Microsoft Transaction Services - qui est maintenant transparent).