Transactions

publicité
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).
Téléchargement