VIII. Exemple d'utilisation▲
VIII-A. Ouvrir un recordset▲
Dans l'exemple suivant, je vais ouvrir trois fois le même recordset avec trois méthodes différentes. Nous discuterons ensuite des différences.
Private Sub Form_Load()
Dim Cnn1 As ADODB.Connection, Cmd1 As ADODB.Command, MonRs As ADODB.Recordset
Set Cnn1 = New ADODB.Connection
With Cnn1
.Provider = "Microsoft.Jet.OLEDB.4.0;"
.ConnectionTimeout = 30
.Mode = adModeShareExclusive
.Open "Data Source=D:\Biblio.mdb ;User Id=Admin; Password="
End With
'par Connection.Execute
Set MonRs = Cnn1.Execute("SELECT * From Authors", , adCmdText)
MonRs.Close
'par command.execute
Set Cmd1 = New ADODB.Command
With Cmd1
.ActiveConnection = Cnn1
.CommandType = adCmdText
.CommandText = "SELECT * From Authors"
End With
Set MonRs = Cmd1.Execute
MonRs.Close
'par recordset.open
Set MonRs = New ADODB.Recordset
MonRs.Open "SELECT * From Authors", Cnn1, , , adCmdText
End SubIl n'y a pas de différence entre créer le recordset par la méthode Open ou par Command.Execute, dans les deux cas, l'objet Recordset à une référence à ActiveCommand et ActiveConnection. Par contre, le recordset créé par Connection.Execute n'a pas de référence à ActiveCommand. Cela vient du fait que la méthode Execute de l'objet Connection utilise une commande "volatile" qui ne crée pas d'objet Command.
Dans cet exemple je n'ai précisé aucun paramètre, j'ai donc trois fois un curseur serveur, en avant seulement et en lecture seule.
Nous allons voir maintenant l'ouverture d'un recordset configuré.
Private Sub Form_Load()
Dim Cnn1 As ADODB.Connection, Cmd1 As ADODB.Command, MonRs As ADODB.Recordset
Set Cnn1 = New ADODB.Connection
With Cnn1
.Provider = "Microsoft.Jet.OLEDB.4.0;"
.ConnectionTimeout = 30
.IsolationLevel = adXactIsolated
.Mode = adModeReadWrite
.Open "Data Source=D:\Biblio.mdb ;User Id=Admin; Password="
End With
'Set Cmd1 = New ADODB.Command
'With Cmd1
' .CommandType = adCmdText
' .CommandText = "SELECT * From Authors"
'End With
Set MonRs = New ADODB.Recordset
With MonRs
.CursorLocation = adUseClient
.LockType = adLockBatchOptimistic
.CursorType = adOpenStatic
.CacheSize = 30
.Source = "SELECT * From Authors"
Set .ActiveConnection = Cnn1
' Set .ActiveCommand = Cmd1
.Open , , , , adCmdText
End With
End SubLe code que je vous ai mis en commentaire déclenchera une erreur s'il est utilisé. En effet on ne peut pas affecter d'objet Command à la propriété ActiveCommand. Si je veux passer l'objet Command, j'utiliserai Recordset.Open avec le nom de l'objet Command comme source de mon Recordset.
En résumé donc, aucune méthode n'est équivalente. On utilise donc le choix selon les critères suivant :
Connection.Execute -> Crée des requêtes volatiles (non réutilisable), en lecture seule
Command.Execute -> Crée des jeux d'enregistrement toujours en lecture seule.
Recordset.Open -> Permet l'accès aux curseurs disponibles. C'est la seule méthode à utiliser pour agir sur la source de données par l'intermédiaire d'un recordset.
Voyons enfin le cas de la création d'un jeu d'enregistrement par l'intermédiaire d'une requête paramétrée.
Dim Cnn1 As ADODB.Connection, MonRs As ADODB.Recordset, MaCommand As ADODB.Command
Dim Param1 As ADODB.Parameter
Set Cnn1 = New ADODB.Connection
Cnn1.CursorLocation = adUseClient
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=d:\biblio.mdb ;User Id=Admin; Password="
Set MaCommand = New ADODB.Command
With MaCommand
.ActiveConnection = Cnn1
.CommandType = adCmdText
.CommandText = "PARAMETERS Annee Long;SELECT Authors.Au_ID, Authors.Author, Authors.[Year Born] FROM Authors WHERE [Year Born]=Annee"
Set Param1 = .CreateParameter("Annee", adInteger, adParamInput, , 1941)
.Parameters.Append Param1
End With
Set MonRs = New ADODB.Recordset
MonRs.Open MaCommand, , adOpenKeyset, adLockOptimistic
MonRs.ActiveCommand.Parameters(0).Value = 1940
MonRs.RequeryComme nous le voyons, il n'est nécessaire de créer le paramètre qu'une fois. Après cela, on peut modifier la valeur par l'intermédiaire de la propriété ActiveCommand de l'objet Recordset. De même, nous voyons que je n'ai pas besoin de spécifier la propriété CursorLocation de mon Recordset puisqu'il va hériter de celle de la connexion.
VIII-B. Nombre d'enregistrement, position et signet▲
Dans cet exemple nous allons travailler sur la navigation sans les méthodes "Move" dans un Recordset. Pour cela nous allons utiliser deux propriétés du recordset : RecordCount et AbsolutePosition. Avant la version 2.6 d'ADO il fallait impérativement utiliser un curseur côté client pour utiliser certaines de ces propriétés, et cela reste encore le cas avec certains fournisseurs (Jet 3.51 par exemple).
Dans notre cas, la seule contrainte est d'obtenir un curseur bidirectionnel puisque les curseurs en avant seulement ne valorisent pas les propriétés RecordCount et AbsolutePosition.
Globalement la position dans un recordset est soit sur un enregistrement, soit en position BOF c'est à dire avant le premier enregistrement ou EOF c'est à dire après le dernier. La valeur de AbsolutePosition va de 1 à RecordCount.
Un signet (Bookmark) sert à identifier un enregistrement de manière à pouvoir le retrouver facilement. Ne confondez pas signet et position, ils ne sont pas identiques.
Dans cet exemple nous allons faire une ProgressBar qui indique la position de l'enregistrement courant en pourcentage. (équivalent au PercentPosition DAO)
J'ajoute donc à mon projet un contrôle DataCombo qui me permettra de sélectionner un auteur dans la liste et un contrôle ProgressBar. (COMCTL32.OCX)
Option Explicit
Private WithEvents Cnn1 As ADODB.Connection
Private WithEvents MonRs As ADODB.Recordset
Private Sub Form_Load()
Set Cnn1 = New ADODB.Connection
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\Biblio.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
MonRs.Open "SELECT * From Authors", Cnn1, adOpenKeyset, adLockReadOnly, adCmdText
With DataCombo1
Set .RowSource = MonRs
.ListField = "Author"
.SelText = MonRs.Fields("Author").Value
End With
End Sub
Private Sub DataCombo1_Change()
If IsNull(DataCombo1.SelectedItem) Then Exit Sub
MonRs.Bookmark = DataCombo1.SelectedItem
ProgressBar1.Value = CInt((MonRs.AbsolutePosition * 100) / MonRs.RecordCount)
End SubDans le "Load" de la feuille, je crée mon recordset. Notez que je n'ai pas précisé la position du curseur, il sera donc côté serveur. J'affecte ensuite mon recordset comme source de données du DataCombo, et je précise quel champ servira pour le remplissage de la liste. J'utilise enfin la propriété SelText du DataCombo, pour afficher le premier enregistrement.
Tout se passe donc dans l'événement Change du DataCombo. La première ligne me permet de sortir de la procédure quand aucun élément n'est sélectionné.
La deuxième ligne positionne l'enregistrement courant sur l'élément sélectionné. On utilise la propriété Bookmark du recordset car la propriété SelectedItem du DataCombo renvoie un signet sur l'élément choisi. En clair, le fait de choisir un élément dans un DataCombo ne change pas l'enregistrement courant. Par contre, comme j'affecte la valeur du signet à la propriété Bookmark du recordset, celui-ci modifie l'enregistrement courant.
Enfin je fais mon calcul en pourcentage.
VIII-C. Comparaison SQL Vs Recordset▲
Dans cet exemple nous allons faire un petit concours de vitesse qui va nous permettre de mieux considérer les raisons d'un choix entre les recordset et l'utilisation du SQL.
Le concours est simple, nous allons extraire un jeu d'enregistrement correspondant à tous les auteurs dont le nom commence par "S" et le trier, puis ensuite extraire les enregistrements ou le champ [année de naissance] est rempli. Pour cela je vais utiliser deux méthodes. D'un côté nous allons utiliser des clauses SQL, de l'autre uniquement des fonctionnalités de l'objet Recordset.
Pour mesurer le temps, je vais utiliser l'API GetTickCount qui renvoie le nombre de millisecondes écoulées depuis le début de la session Windows. Par différence, j'obtiendrais le temps nécessaire. Nous sommes en mode synchrone.
Je déclare donc mon API
Private Declare Function GetTickCount Lib "kernel32" () As LongPour rester sensiblement comparable, je n'utiliserais que des curseurs bidirectionnels en lecture seule. Dans le premier test je ne vais travailler qu'avec des ordres SQL. Je vais donc utiliser un curseur coté serveur. Je vais utiliser un curseur à jeu de clé (KeySet) qui est un peu plus rapide qu'un curseur statique. La connexion ne sera pas incluse dans le calcul du temps. Le code sera le suivant :
Private Sub Command1_Click()
Dim TStart As Long
Set Cnn1 = New ADODB.Connection
Cnn1.CursorLocation = adUseServer
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\Biblio.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
MonRs.CursorLocation = adUseServer
TStart = GetTickCount
MonRs.Open "SELECT * From Authors WHERE Author LIKE 'S*' ORDER BY Author, _
[Year Born] DESC", Cnn1, adOpenKeyset, adLockReadOnly, adCmdText
MsgBox "temps = " & GetTickCount - TStart
MonRs.Close
TStart = GetTickCount
MonRs.Open "SELECT * From Authors WHERE Author LIKE 'S*' AND NOT [Year Born] IS NULL ORDER BY Author, [Year Born] DESC", _
Cnn1, adOpenKeyset, adLockReadOnly, adCmdText
MsgBox "temps = " & GetTickCount - TStart
End SubTel que nous le voyons, je procède à deux extractions successives.
Dans mon second test, je ne vais travailler qu'avec les fonctionnalités du recordset. Je vais donc extraire l'ensemble de la table, puis travailler sur le jeu d'enregistrement. La propriété Sort demande un curseur côté client. De ce fait le curseur sera statique. Le code utilisé sera le suivant :
Private Sub Command2_Click()
Dim TStart As Long
Set Cnn1 = New ADODB.Connection
Cnn1.CursorLocation = adUseClient
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\Biblio.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
MonRs.CursorLocation = adUseClient
TStart = GetTickCount
MonRs.Open "SELECT * From Authors", Cnn1, adOpenStatic, adLockReadOnly, adCmdText
MonRs.Filter = "Author LIKE 'S*'"
MonRs.Sort = "Author ASC, [Year Born] DESC"
MsgBox "temps = " & GetTickCount - TStart
TStart = GetTickCount
MonRs.Filter = adFilterNone
MonRs.Filter = "Author LIKE 'S*' AND [Year Born] <> NULL"
MsgBox "temps = " & GetTickCount - TStart
End SubChaque programme affichera donc deux temps, le temps qu'il lui faut pour obtenir un jeu d'enregistrement trié contenant tous les auteurs dont le nom commence par "S" et temps qu'il met pour obtenir le même jeu dont le champ "année de naissance est rempli.
Les temps ne seront jamais parfaitement reproductible car de nombreux paramètre peuvent jouer, mais on constate que le code SQL va de 3 à 20 fois plus vite que le filtrage et le tri du recordset (premier temps). Ceci est du au fait que le curseur client rapatrie l'ensemble des valeurs dans son cache, puis applique le filtre et tri les enregistrements. Il est à noter que le tri par la méthode sort du recordset est très pénalisant.
Par contre, pour le deuxième temps, le second code est toujours plus rapide (deux à trois fois plus). Cela vient du fait qu'il n'est plus nécessaire de ré extraire les données, celles-ci étant présente dans le cache du client ce qui permet une exécution très rapide.
Tout cela montre bien qu'il ne faut pas a priori rejeter les fonctionnalités recordset comme on le lit trop souvent. Ce qui pénalise beaucoup les curseurs client, c'est la nécessite de passer dans le cache l'ensemble des valeurs (Marshaling) mais une fois l'opération faite, le travail devient très rapide. Une fois encore, tout est question de besoin.
VIII-D. Les recherches▲
Il existe donc deux méthodes de recherches sur un jeu d'enregistrement, plus deux autres qui peuvent être apparentées à une recherche, les filtres.
Commençons par ses derniers. Filtrer un jeu d'enregistrement (en SQL ou par la méthode Filter) revient à faire une recherche de tous les enregistrements répondant à certains critères. Nous avons vu dans l'exemple précédent comment les utiliser, et nous verrons dans les traitements par lot d'autres exemples.
Dans cet exemple nous allons voir les deux méthodes de recherches standard d'ADO, Seek et Find.
VIII-D-1. Recherche avec Seek▲
Normalement cette méthode demande l'utilisation d'une table indexée qu'on ouvre avec l'option adCmdTableDirect, on précise l'index dans le recordset et on utilise la méthode Seek. Le code suivant montre un exemple.
Private Sub Command3_Click()
Dim MonRs1 As ADODB.Recordset, maprop As Property
Set Cnn1 = New ADODB.Connection
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\biblio.mdb ;User Id=Admin; Password="
Set MonRs1 = New ADODB.Recordset
MonRs1.CursorLocation = adUseServer
MonRs1.ActiveConnection = Cnn1
MonRs1.Index = "PrimaryKey"
MonRs1.Open "Authors", Cnn1, adOpenKeyset, adLockReadOnly, adCmdTableDirect
If MonRs1.Supports(adSeek) Then
MonRs1.Seek InputBox("Entrez le numéro de l'auteur"), adSeekFirstEQ
End If
End SubDans ce code j'utilise la clé primaire comme index pour la recherche. Je fais un test pour vérifier que le recordset supporte bien la méthode Seek avant de l'utiliser.
Cette méthode de recherche est extrêmement rapide, puisqu'elle porte sur des champs indexés mais encore faut-il que ceux-ci le soient. Si je regarde ma table 'Authors' le seul champ indexé est la clé primaire 'Au_Id'. Or on connaît rarement la valeur du champ clé lorsque celui-ci est non informationnel. Donc la possibilité d'utilisation de cette méthode repose sur la qualité de la source de données.
Recherche avec Find▲
Rappelons que la méthode Find ne fonctionne que sur un champ. A la différence de DAO, il n'existe pas de propriété NoMatch pour dire que la recherche a échoué. Lorsqu'il n'y a pas de correspondances, le recordset se place en position BOF ou EOF selon le sens de la recherche.
Le code suivant fait une recherche des enregistrements dont l'année de naissance est celle choisie par l'utilisateur.
Private Sub cmdFind_Click()
Dim AnneeCherchee As Integer
AnneeCherchee = Val(InputBox("Entrez l'année de naissance", , 1947))
MonRs.Find "[year born]=" & AnneeCherchee, , adSearchForward, 1
Do While Not MonRs.EOF
MsgBox "nom de l'auteur -->" & MonRs!Author
MonRs.Find "[year born]=" & AnneeCherchee, 1, adSearchForward
Loop
End SubCe code est l'équivalent d'une recherche DAO avec FindFirst et FindNext. Je pourrais écrire l'inverse avec une recherche à l'envers pour peu que le curseur soit bidirectionnel.
Private Sub cmdFind_Click()
Dim AnneeCherchee As Integer, Signet As Variant
AnneeCherchee = Val(InputBox("Entrez l'année de naissance", , 1947))
MonRs.MoveLast
Signet = MonRs.Bookmark
MonRs.Find "[year born]=" & AnneeCherchee, , adSearchBackward, Signet
Do While Not MonRs.BOF
MsgBox "nom de l'auteur -->" & MonRs!Author
MonRs.Find "[year born]=" & AnneeCherchee, 1, adSearchBackward
Loop
End SubAttention de bien utiliser un signet ou la position courante pour démarrer la recherche, si vous utiliser une position votre code risque de ne pas se comporter comme vous le désirez.
On peut donc faire une analogie entre la méthode Find ADO et les méthodes DAO.
| FindFirst | Find Critères, , adSearchForward, 1 |
| FindLast | Find Critères, , adSearchBackward, Signet |
| FindNext | Find Critères, 1, adSearchForward |
| FindPrevious | Find Critères, 1, adSearchBackward |
Enfin n'oubliez pas que l'opérateur du critère doit être =, <, >, <=, >=, <> ou Like. N'essayez donc pas une recherche du genre MonRs.Find "[year born] IS NOT NULL" qui déclenchera une erreur, utilisez MonRs.Find "[year born] <> NULL".
Si le recordset contient de nombreux enregistrements et que l'on travaille côté client, il peut être très rentable d'ajouter un index temporaire sur le champ de la recherche. Pour cela on utilise la propriété dynamique "Optimize". Notez bien que cet index n'existe que dans l'objet Recordset, il n'est pas créé dans la source de données. Par exemple
Set cnn1 = New ADODB.Connection
cnn1.CursorLocation = adUseClient
cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\biblio.mdb ;User Id=Admin; Password="
Set MonRs1 = New ADODB.Recordset
MonRs1.ActiveConnection = cnn1
MonRs1.Open "SELECT * FROM Authors", cnn1, adOpenStatic, adLockReadOnly, adCmdText
MonRs1.Fields("Author").Properties("Optimize") = True
MonRs1.Find "Author='Boddie, John'"VIII-E. Récupérer une clé auto-incrémentée▲
Lorsqu'on crée un nouvel enregistrement il peut être parfois intéressant de récupérer la valeur de la clé auto-incrémentée correspondantes, pour pouvoir par exemple créer des enregistrements ayant cette valeur comme clé étrangère. Dans l'exemple suivant nous allons regarder le principe.
Private Sub Command4_Click()
Dim recup As Long
Set Cnn1 = New ADODB.Connection
Cnn1.CursorLocation = adUseServer
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\Biblio.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
With MonRs
.CursorLocation = adUseServer
.Open "SELECT * From Authors", Cnn1, adOpenKeyset, adLockPessimistic, adCmdText
.AddNew
!Author = "Rabilloud ,J-M"
![year born] = 1967
.Update
recup = !Au_id
End With
End SubDans ce cas il n'y à rien de particulier à faire puisque le curseur est côté serveur. Notons toutefois que le curseur doit être de type "KeySet" pour que cela fonctionne.
Avec un curseur côté client, nous avons vu qu'il faut utiliser la propriété dynamique donc ajouter dans le code :
MonRs.Properties("Update Resync") = adResyncAutoIncrementVIII-F. Contrôles Visual Basic▲
C'est contrôles aussi contiennent leurs lots de pièges divers et variés. Nous allons regarder deux exemples pour parcourir certains de ces problèmes.
VIII-F-1. Le contrôle ADO (ADODC)▲
Ce contrôle possède quelques astuces de fonctionnement, mais son principal défaut est d'être utilisé à tort et à travers. En effet à l'origine, il est fait pour servir de fournisseur de données pour d'autres contrôles de la feuille tout en permettant la navigation par de simples clics. Or très souvent on le retrouve en utilisation invisible, et alors une simple classe ferait mieux l'affaire, comme fournisseur de recordset, et là il ne fonctionne pas.
Piège n°1 : Pas de recordset▲
Le contrôle ADO crée le recordset uniquement si au moins un autre contrôle de la feuille l'utilise comme DataSource et comme DataField. Dans le cas contraire, le recordset ne sera jamais créé, quand bien même cela serait explicitement demandé par le code.
Piège n°2 : Le curseur▲
|
Dans sa page de propriétés, le contrôle ADO contient toutes les informations qui lui sont utile pour créer sa connexion et son recordset. Outre le piège standard de définir un curseur non disponible, il y a aussi la possibilité de définir une propriété à la création incompatible avec le code de manipulation du recordset. L'exemple suivant va expliciter mon propos. Imaginons un contrôle ADODC configuré comme dans l'exemple ci-dessus. Si dans mon code je fais : Sélectionnez |
Je vais avoir une erreur "Erreur de syntaxe dans la clause FROM". Ceci vient du fait que la propriété CommandType du contrôle n'est plus valide.
Piège n°3 : L'asynchronisme▲
La troisième source d'erreurs classique vient du fait qu'un contrôle ADO est toujours asynchrone. Il faut donc gérer sa programmation évènementielle. Ceci est indispensable car le changement de l'enregistrement courant valide l'ensemble des modifications effectuées sur l'enregistrement courant précédent.
VIII-F-2. Le contrôle DataGrid▲
Affichage des données▲
Je m'arrête sur ce point car il peut être assez déroutant. En utilisant comme fournisseur JET 3.51 tous les curseurs acceptants les signets (c'est à dire n'étant pas en avant seulement) peuvent être affectés comme source de données du contrôle DataGrid. Ainsi le code suivant affiche bien les données dans la grille :
Set Cnn1 = New ADODB.Connection
Cnn1.CursorLocation = adUseServer
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.3.51;Data Source=D:\user\jmarc\bd6.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
MonRs.CursorLocation = adUseServer
MonRs.Open "reqAdresse", Cnn1, adOpenKeyset, adLockPessimistic, adCmdTable
Set DataGrid1.DataSource = MonRsSi j'utilise le même code avec le fournisseur Jet 4.0 plus rien ne s'affiche dans la grille. Ceci vient du fait que les fonctionnalités du curseur ont changé. Pour qu'un curseur côté serveur puisse être utilisé comme source de données d'un contrôle DataGrid avec Jet 4.0, je dois utiliser la propriété dynamique IrowsetIdentity. Mon code deviendra :
Set Cnn1 = New ADODB.Connection
Cnn1.CursorLocation = adUseServer
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\user\jmarc\bd6.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
MonRs.CursorLocation = adUseServer
MonRs.ActiveConnection = Cnn1
MonRs.Properties("IrowsetIdentity") = True
MonRs.Open "reqAdresse", , adOpenKeyset, adLockPessimistic, adCmdTable
Set DataGrid1.DataSource = MonRsVous noterez que j'ai légèrement changé la structure de mon code. En effet, pour pouvoir valoriser une propriété dynamique, je dois :
- spécifier le fournisseur
- Donner la valeur avant l'ouverture de l'objet
Requête multi table▲
Le contrôle DataGrid permet pour peu qu'il soit correctement paramétré de faire des ajouts, suppression, mises à jour dans la base de données. Si ceci fonctionne sans poser de problèmes lorsque la requête ne concerne qu'une table, il en va souvent autrement lorsque la grille affiche des données provenant de tables mises en relation.
Envisageons les deux tables suivantes
|
|
|||||||||||||||||||||||||
La requête reqAdresse de mon exemple précédent correspondant à la requête SQL :
SELECT tab_mere.Nom, tab_mere.Prenom, tab_fille.Adresse, tab_fille.Ville
FROM tab_mere
INNER JOIN tab_fille
ON tab_mere.Me_id = tab_fille.Me_idSi dans mon contrôle DataGrid je modifie un enregistrement tous se passe bien, par contre si j'en ajoute un nouveau, j'obtiens l'erreur 6153 "Vous ne pouvez pas ajouter ou modifier un enregistrement car l'enregistrement associé est requis dans la table tab_mere".
Pour contourner ce problème il faut mettre en place toute une stratégie de récupération des valeurs saisies, pas nécessairement très lourde d'ailleurs. Néanmoins, la programmation du DataGrid peut vite devenir laborieuse sur les Recordset multi table.
Nous allons voir ici une des méthodes de traitement possible pour les jeux d'enregistrements multi tables avec le contrôle Datagrid. Il en existe de nombreuses variantes, celle ci est peu coûteuse en ressource mais ne fonctionnera pas avec tous les fournisseurs.
J'ai donc sur ma feuille un contrôle ADO qui pointe sur ma base "biblio" avec la requête suivante :
SELECT Publishers.PubID, Titles.PubID, Publishers.Name, Publishers.[Company Name], Titles.Title, Titles.ISBN, Titles.[Year Published]
FROM Publishers
INNER JOIN Titles
ON Publishers.PubID = Titles.PubIDJ'ai configuré mon Datagrid afin que les deux champs PubID (clé primaire de la table Publishers et clé étrangère de la table Titles) n'apparaissent pas, puisque ce n'est pas à l'utilisateur de rentrer la valeur PubID de l'éditeur. Comme je l'ai dit plus haut, si un utilisateur entre une ligne dans le Datagrid, celui-ci va déclencher une erreur 6153. Je vais donc programmer l'événement Error du contrôle DataGrid.
Private Sub DataGrid1_Error(ByVal DataError As Integer, Response As Integer)
Dim MaComm As ADODB.Command, Recup
If DataError = 6153 Then
Set MaComm = New ADODB.Command
With MaComm
.ActiveConnection = Adodc1.Recordset.ActiveConnection
.CommandText = "INSERT INTO Publishers (Name,[Company Name]) VALUES('" & Adodc1.Recordset!Name & "','" _
& Adodc1.Recordset![Company Name] & "')"
.Execute
.CommandText = "SELECT @@IDENTITY"
Recup = .Execute
.CommandText = "INSERT INTO Titles (Title,ISBN,[Year Published],PubId) VALUES('" & Adodc1.Recordset!Title & _
"','" & Adodc1.Recordset!ISBN & "','" & Adodc1.Recordset![Year Published] & "','" & Recup(0).Value & "')"
.Execute
End With
Set DataGrid1.DataSource = Nothing
Adodc1.Recordset.CancelUpdate
Adodc1.Recordset.Requery
Set DataGrid1.DataSource = Adodc1
Response = 0
End If
End SubComme vous le voyez, je vais utiliser un objet Command pour remplacer le moteur de curseur. Le recordset du contrôle ADO contient toutes les informations entrées par l'utilisateur dans son enregistrement en cours. En résumé, je passe la requête ajout dans la table parent, je récupère la valeur de la clé primaire et je passe ma seconde requête.
Ensuite je dois décrocher le DataGrid de sa source de données pour pouvoir annuler les modifications du contrôle ADO avant de refaire le lien.
Mise à jour▲
Une autre question qui revient souvent est au sujet de la non mise à jour de la grille par rapport au recordset sous-jacent. A cela, deux erreurs classiques reviennent souvent.
- Vous travailler avec un curseur côté client donc statique. Comme le recordset ne reflète pas les modifications de la base, la grille n'est pas mise à jour.
- Vous utiliser le recordset que vous avez affecté au lieu du recordset sous jacent.
Explication du problème.
Regardons le code suivant :
Private Sub Form_Load()
Set Cnn1 = New ADODB.Connection
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\user\jmarc\bd6.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
MonRs.CursorLocation = adUseServer
MonRs.ActiveConnection = Cnn1
MonRs.Properties("IrowsetIdentity") = True
MonRs.Open "tab_Mere", , adOpenKeyset, adLockPessimistic, adCmdTable
Set DataGrid1.DataSource = MonRs
End SubCe code est le même que le précédent si ce n'est que la source est une table. A ce propos, vous vous demandez peut être pourquoi une requête stockée dans la base est ouverte avec le paramètre adCmdTable. La réponse se trouve là -> "Utilisation d'ADOX"
Private Sub Command6_Click()
While Not MonRs.EOF
If MonRs.Fields("nom").Value = "Durand" Then MonRs.Delete adAffectCurrent
MonRs.MoveNext
Wend
End SubLe code ci-dessus supprime tous les enregistrements dont le nom est Durand. Pourtant votre grille continue d'afficher les Durand. Cela vient du fait que plus rien ne lie le Recordset MonRs avec le contrôle grille. Pour que l'effet de cette suppression de la base soit reflété automatiquement par la grille il faut :
Private Sub Command6_Click()
Dim TempRs As ADODB.Recordset
Set TempRs = DataGrid1.DataSource
While Not MonRs.EOF
If TempRs.Fields("nom").Value = "Durand" Then TempRs.Delete adAffectCurrent
MonRs.MoveNext
Wend
End SubDans ce code, je récupère le recordset qui est dans la propriété DataSource de la grille. Dans ce cas là, celle-ci reflètera les modifications du à la manipulation du Recordset.
VIII-G. Programmation évènementielle▲
Celle-ci n'est pas plus complexe que la programmation évènementielle des contrôles. Il faut toutefois garder à l'esprit que le changement de l'enregistrement courant valide les modifications.
Comme un simple Find fait changer l'enregistrement courant, on devine facilement l'intérêt de la programmation asynchrone pour éviter des actions masquées.
Connection et command asynchrone▲
L'exemple suivant est une opération traditionnelle de modification de données. Comme elle peut être lourde, on la traite de manière asynchrone.
Private Sub Form_Load()
Set Cnn1 = New ADODB.Connection
Cnn1.CursorLocation = adUseServer
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\nwind.mdb ;User Id=Admin; Password=", , , adAsyncConnect
…………
End Sub
Private Sub Cnn1_ConnectComplete(ByVal pError As ADODB.Error, adStatus As ADODB.EventStatusEnum, _
ByVal pConnection As ADODB.Connection)
Dim MaCommand As ADODB.Command
If adStatus = adStatusErrorsOccurred Then
MsgBox "Erreur de connexion " & pError.Description
Unload Me
Else
Set MaCommand = New ADODB.Command
MaCommand.ActiveConnection = Cnn1
MaCommand.CommandText = "UPDATE Clients Set Pays = 'USA' WHERE Pays = 'États-Unis'"
Set MonRs = MaCommand.Execute(, , adAsyncExecute)
End If
End Sub
Private Sub Cnn1_ExecuteComplete(ByVal RecordsAffected As Long, ByVal pError As ADODB.Error, _
adStatus As ADODB.EventStatusEnum, ByVal pCommand As ADODB.Command, _
ByVal pRecordset As ADODB.Recordset, ByVal pConnection As ADODB.Connection)
MsgBox RecordsAffected & " modification(s) exécutée(s)"
End SubLe cheminement est simple, dans le load de la feuille je démarre une connexion asynchrone, lorsque celle-ci est complète elle lance s'il n'y a pas d'erreur la commande. Lorsque celle ci est terminée, un message précise le nombre de modifications effectuées.
Extractions bloquantes & non bloquantes▲
Il y a ces deux sortes d'extractions asynchrones. Il s'agit juste d'une différence de comportement lorsqu'on fait appel à des enregistrements qui n'ont pas été encore extraits. Une extraction bloquante rend la main lorsque la réponse exacte peut être donnée, une extraction non bloquante rend la main immédiatement et une réponse temporairement exacte. Un petit exemple rendra cela plus clair.
Private Sub Command7_Click()
Dim Recup As String, MonSignet As Variant
Set Cnn1 = New ADODB.Connection
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\biblio.mdb ;User Id=Admin; Password=" ', , , adAsyncConnect
Set MonRs = New ADODB.Recordset
With MonRs
.CursorLocation = adUseClient
.ActiveConnection = Cnn1
.Properties("Initial Fetch Size") = 50
.CursorType = adOpenStatic
.LockType = adLockBatchOptimistic
.Open "Titles", , , , adAsyncFetch 'adAsyncFetchNonBlocking
End With
MonRs.MoveLast
Recup = MonRs!Title
MonSignet = MonRs.Bookmark
Set DataGrid1.DataSource = MonRs
MonRs.Bookmark = MonSignet
MsgBox Recup & " " & MonRs.AbsolutePosition
End SubSelon le mode choisi, je récupérerai un signet sur le cinquantième enregistrement (adAsyncFetchNonBlocking) ou sur le dernier (adAsyncFetch). Aussi faut-il se méfier du type d'extraction que l'on choisit.
Suivre l'extraction▲
Nous allons, dans cet exemple, utiliser une ProgressBar pour suivre l'extraction d'un recordset asynchrone. Pour cela nous allons utiliser l'événement FetchProgress. Attention il faut travailler avec un curseur côté client. Le code est le suivant :
Private Sub cmdSuiv_Click()
Dim MaComm As ADODB.Command
ProgressBar1.Visible = True
ProgressBar1.Min = 0
Set MonRs = Cnn1.Execute("SELECT Count(Titles.Title) AS NbRes
FROM (Publishers INNER JOIN Titles ON Publishers.PubID =
Titles.PubID) INNER JOIN (Authors INNER JOIN [Title Author]
ON Authors.Au_ID = [Title Author].Au_ID) ON Titles.ISBN = [Title Author].ISBN")
ProgressBar1.Max = MonRs!NbRes
MonRs.Close
Set MonRs = Nothing
Set MonRs = New ADODB.Recordset
With MonRs
.CursorLocation = adUseClient
.ActiveConnection = Cnn1
.Properties("Initial Fetch Size") = 100
.Properties("Background Fetch Size") = 1000
.Open "[All Titles]", , adOpenStatic, adLockReadOnly, adCmdTable + adAsyncFetchNonBlocking
End With
End Sub
Private Sub MonRs_FetchComplete(ByVal pError As ADODB.Error, _
adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)
ProgressBar1.Visible = False
End Sub
Private Sub MonRs_FetchProgress(ByVal Progress As Long, ByVal MaxProgress As Long, _
adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)
ProgressBar1.Value = Progress 'Debug.Print Progress 'Int((Progress / pRecordset.RecordCount) * 100) '
End SubComme vous le voyez, j'utilise une requête pour déterminer le nombre d'enregistrements attendu afin de valoriser correctement le maximum de la ProgressBar. La propriété dynamique " Initial Fetch Size" doit être utilisée pour un comportement correct du code.
Gestion des modifications▲
Idéalement on utilise uniquement l'événement WillChangeRecord pour le contrôle des modifications. En effet en réalisant juste un test sur l'argument adReason on connaît la cause de la demande de validation. La structure du code est la suivante :
Private Sub MonRs_WillChangeRecord(ByVal adReason As ADODB.EventReasonEnum, _
ByVal cRecords As Long, adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)
Dim bCancel As Boolean
Select Case adReason
Case adRsnAddNew
Case adRsnClose
Case adRsnDelete
Case adRsnFirstChange
Case adRsnMove
Case adRsnRequery
Case adRsnResynch
Case adRsnUndoAddNew
Case adRsnUndoDelete
Case adRsnUndoUpdate
Case adRsnUpdate
End Select
If bCancel Then adStatus = adStatusCancel
End SubLe problème de se style de programmation est la difficulté à bien identifier la cause de la modification. Ainsi, si vous changez deux champs d'un même enregistrement par l'intermédiaire d'une grille, l'événement se produira deux fois, dans le premier cas avec l'argument adRsnFirstChange, ensuite avec adRsnUpdate. C'est pour cela que dans certains cas on préfère travailler sur l'événement WillMove en testant la valeur EditMode du Recordset.
Dans cet exemple, nous allons afficher un message de confirmation de validation à chaque mouvement dans le jeu d'enregistrement.
Private Sub MonRs_WillMove(ByVal adReason As ADODB.EventReasonEnum, _
adStatus As ADODB.EventStatusEnum, ByVal pRecordset As ADODB.Recordset)
If pRecordset.EditMode = adEditInProgress Or adEditAdd Or adEditDelete Then
If MsgBox("Voulez vous enregistrer les modifictions", vbQuestion + vbYesNo) = vbYes Then
pRecordset.Update
Else
pRecordset.CancelUpdate
End If
End If
End SubComme nous le voyons, ce sont des codes assez simples qui gèrent en général la programmation évènementielle.
Je ne vais pas vous donner plus d'exemple de programmation évènementielle car elle doit être adaptée à votre programme.
VIII-H. Recordset persistant▲
Un recordset persistant est en fait un fichier de type "datagram" ou XML que l'on peut créer à partir d'un Recordset ou ouvrir comme un Recordset. Ceci peut être très intéressant dans les cas suivants :
- Comme sécurité (comme nous allons le voir plus loin)
- Pour alléger la connexion (puisqu'on peut le remettre à jour par re synchronisation)
- Comme base de travail, si la mise à jour n'est pas primordiale
Dans le principe de fonctionnement c'est assez simple
Private Sub Form_Load()
Set Cnn1 = New ADODB.Connection
Cnn1.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\biblio.mdb ;User Id=Admin; Password="
Set MonRs = New ADODB.Recordset
MonRs.CursorLocation = adUseClient
MonRs.ActiveConnection = Cnn1
MonRs.Open "Authors", , adOpenStatic, adLockBatchOptimistic, adCmdTable
End Sub
Private Sub cmdSave_Click()
If Dir("d:\monrs.adtg") <> "" Then Kill "d:\monrs.adtg"
MonRs.Save "d:\monrs.adtg", adPersistADTG
End Sub
Private Sub cmdOpen_Click()
If MonRs.State = adStateOpen Then MonRs.Close
MonRs.Open "d:\monrs.adtg", Cnn1, adOpenStatic, adLockBatchOptimistic, adCmdFile
End SubComme vous le voyez, il faut toujours s'assurer qu'il n'existe pas de fichier du même nom sur le disque. Notez aussi qu'un recordset persistant garde l'ensemble des données et des méta-données, on peut ainsi rendre persistant un recordset ayant des modifications en attente, puis le rouvrir pour appliquer ces modifications sur la base.
Dans mon exemple, j'utilise une connexion pour ouvrir mon Fichier. Cela n'est pas obligatoire, car ADO propose un fournisseur de service pour faire cela, je pourrais donc l'ouvrir avec :
MonRs.Open "d:\monrs.adtg", "Provider=MSPersist;", adOpenStatic, adLockBatchOptimistic, adCmdFileQuelques précisions sont encore nécessaires :
- Si le recordset a une propriété Filter définie sans que celle-ci soit une constante prédéfinie, seules les lignes accessibles avec ce filtre sont sauvegardées
- Pour sauvegarder plusieurs fois votre recordset, précisez la destination seulement la première fois. Si la deuxième fois, vous redonnez la même destination vous obtiendrez une erreur, si vous en donnez une autre vous obtiendrez deux fichiers et le premier restera ouvert. La fermeture du fichier n'a lieu que lors de la fermeture du recordset.
- En programmation asynchrone, Save est toujours une méthode bloquante.
- Un recordset persistant garde ses possibilités de modifications sur la base. Toutefois, elles ne peuvent concerner qu'une table si le curseur est côté serveur. De plus, ce type de recordset ne pourra pas être synchronisé.
Si votre recordset contient des champs de type "Variant" le recordset sauvegardé ne sera pas exploitable.
VIII-I. Synchronisation▲
Lorsqu'on utilise un Recordset sans mise à jour, de type statique ou en avant seulement, il est possible de réactualiser les données de celui-ci. On appelle cette technique la synchronisation. Celle-ci n'est pas possible sur les recordset côté client en lecture seule. Cette méthode est souvent mal utilisée car son comportement peut sembler étrange, aussi beaucoup de développeurs préfère utiliser, à tort, la méthode Requery. Un des problèmes de cette méthode est qu'en fait, elle contient deux méthodes fondamentalement différentes selon les paramètres qu'on lui passe.
Synchronisation des données sous-jacentes▲
De la forme :
MonRs1.Resync adAffectAllChapters, adResyncUnderlyingValues
Elle ne va concerner que les enregistrements dont vous avez modifié au moins un champ. La valeur remise à jour sera la propriété UnderlyingValue des objets Fields. Dans certains cas, elle peut provoquer une erreur si la synchronisation n'est pas possible.
Synchronisation des données▲
De la forme :
MonRs1.Resync adAffectAllChapters, adResyncAllValues
Elle va porter sur tous les enregistrements. Par contre elle annule aussi toutes les modifications en cours de votre recordset.
Ces deux méthodes ne s'appliquent pas dans la même situation. Nous verrons le premier cas un peu plus loin dans les traitements par lot, le code suivant déclenche une alerte si un enregistrement a été supprimé par un autre utilisateur.
On Error Resume Next
MonRs1.Resync adAffectAllChapters, adResyncAllValues
If Err.Number = -2147217885 Then
MonRs1.Filter = adFilterConflictingRecords
MsgBox MonRs1.RecordCount & " enregistrements ont été supprimés", vbCritical + vbOKOnly, "Erreur"
Err.Clear
Exit Sub
End IfJe vais profiter de ce chapitre, pour vous montrer une erreur assez facile à faire. Examinons le code suivant :
Set MonRs1 = New ADODB.Recordset
MonRs1.ActiveConnection = cnn1
MonRs1.Open "SELECT * FROM Authors", cnn1, adOpenStatic, adLockOptimistic, adCmdText
MonRs1![Year born] = 1921
MonRs1.Resync adAffectCurrent, adResyncUnderlyingValuesAprès l'exécution de la synchronisation, vous aurez soit une erreur de verrouillage, soit la valeur de l'année de naissance sera de 1921. Ceci vient du fait que l'appel de la méthode Resync crée toujours un repositionnement de l'enregistrement en cours, et comme nous l'avons vu, un changement de position valide les modifications en attentes. Voilà pourquoi on utilise plutôt les synchronisations en mode Batch, que nous allons voir maintenant.
VIII-J. Traitement par lot▲
Comme nous allons le voir, voilà un passage "sensible" de la programmation ADO. L'intérêt du travail par lot n'est plus à démontrer mais il faut gérer soigneusement la mise à jour de la base afin de ne pas endommager celle-ci. Grosso modo, il existe trois sortes de traitement par lot :
- Le traitement sécurisé : Il se fait par une connection exclusive à la source de données (en général en dehors des heures d'accès). Il n'est dans ce cas pas nécessaire de gérer la concurrence et il peut contenir des procédures assez lourdes.
- Le traitement sans contrôle : Il n'y a pas de contrôles sur les mises à jour en échec. Ce type de traitement est assez rare, mais ne demande aucune gestion par le code.
- Le traitement transactionnel : C'est le cas le plus fréquent mais aussi le plus délicat à gérer. Nous allons donc étudier un exemple.
Remarque préliminaire :
Décomposition de Status
La valeur de la propriété Status peut être la somme de plusieurs des constantes données plus haut. Nous allons donc utiliser la fonction de décomposition donnée ci-dessous
Private Function Decomp_Status(ByVal ValStatus As Long) As Long()
Dim PEnt As Integer, Retour() As Long
ReDim Retour(0 To 0)
Do While ValStatus > 0
PEnt = Int(Log(ValStatus) / Log(2))
Retour(UBound(Retour)) = 2 ^ PEnt
ReDim Preserve Retour(0 To UBound(Retour) + 1)
ValStatus = ValStatus - 2 ^ PEnt
Loop
Retour(UBound(Retour)) = 0
Decomp_Status = Retour
End FunctionCette fonction renvoie un tableau de long contenant les valeurs des constantes de la propriété Status.
Transaction
On est parfois tenté d'englober le traitement par lot dans une transaction afin de pouvoir revenir en arrière en cas d'échec partiel du traitement. Malheureusement cette méthode assez simple ne fonctionne pas, du moins avec certains fournisseurs. On doit donc écrire l'ensemble du code qui gère l'intégrité de la source de données.
Règles de traitement
Avant toute écriture de code il faut avoir défini les règles de comportement que le traitement par lot doit suivre. Cette phase est indispensable afin de ne jamais pouvoir endommager la source de données. Dans l'exemple ci-dessous, je vais faire un traitement qui suit les règles suivantes.
- Si un enregistrement concerné par le traitement a été supprimé, la mise à jour de cet enregistrement est annulée.
- Si une mise à jour échoue de la faute du verrouillage, l'enregistrement sera retraité à la volée
- Si une autre erreur se produit, toute l'opération sera annulée
Exemple
Pour voir le fonctionnement de la résolution des conflits, nous allons simuler par le code, une action de deux utilisateurs. Dans cet exemple nous allons voir de nombreuses techniques expliquées au-dessus, n'hésitez pas à vous y reporter de nouveau.
Dim Reponse() As Long, compteur As Long
'première connexion
Set AutreCnn = New ADODB.Connection
With AutreCnn
.CursorLocation = adUseServer
.Mode = adModeShareDenyNone
.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\biblio.mdb ;User Id=Admin; Password="
End With
Set AutreRs = New ADODB.Recordset
With AutreRs
.CursorLocation = adUseServer
.ActiveConnection = AutreCnn
.CursorType = adOpenKeyset
.LockType = adLockPessimistic
.Source = "SELECT * From Authors WHERE [year born] IS NOT NULL"
.Open
End With'seconde connexion
Set MaCnn = New ADODB.Connection
With MaCnn
.CursorLocation = adUseClient
.Mode = adModeShareDenyNone
.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\biblio.mdb ;User Id=Admin; Password="
End With
Set MonRs = New ADODB.Recordset
With MonRs
.CursorLocation = adUseClient
.ActiveConnection = MaCnn
.CursorType = adOpenStatic
.LockType = adLockBatchOptimistic
.Source = "SELECT * From Authors WHERE [year born] IS NOT NULL"
.Open
End WithJ'ai donc créé deux connexions, une côté serveur qui va me servir à perturber mon traitement, et une côté client que j'utilise pour le traitement par lot.
'modification de perturbation
While Not AutreRs.EOF
'modification de quelques dates
If AutreRs.Fields("year born").Value = 1947 Then AutreRs.Fields("year born").Value = 1932
AutreRs.MoveNext
Wend
AutreRs.MoveLast
'suppression du dernier enregistrement
AutreRs.Delete adAffectCurrent
'modification de mon utilisateur par lot
While Not MonRs.EOF
If MonRs.Fields("year born").Value < 1951 Then MonRs.Fields("year born").Value = MonRs.Fields("year born").Value + 1
MonRs.MoveNext
WendIci j'ai fais mes modifications, je procède maintenant au traitement par lot
'Vérification préliminaire
MonRs.Resync adAffectAllChapters, adResyncUnderlyingValues
MonRs.Filter = adFilterPendingRecords
'traitement préliminaire (non utilisé dans l'exemple)
'*****DEBUT TRAITEMENT*****
'While Not MonRs.EOF
' If MonRs.Fields("year born").OriginalValue <> MonRs.Fields("year born").UnderlyingValue Then
' MsgBox "Au moins un enregistrement a été modifié par un autre utilisateur" & -
' vbCrLf & "Opération annulée", vbCritical + vbOKOnly
' MonRs.CancelBatch
' Exit Sub
' End If
' MonRs.MoveNext
'Wend
'*****FIN TRAITEMENT*****
'création d'un recordset de sauvegarde
If Dir("d:\RsSauve.adtg") <> "" Then Kill "d:\RsSauve.adtg"
MonRs.Save "d:\RsSauve.adtg", adPersistADTG
'mise à jour + résolution des conflits
MonRs.Properties("Update Resync") = adResyncConflicts
On Error Resume Next
MonRs.UpdateBatch
If MonRs.ActiveConnection.Errors.Count > 0 Then
MonRs.Filter = adFilterConflictingRecords
Do While Not MonRs.EOF
Reponse = Decomp_Status(MonRs.Status)
For compteur = LBound(Reponse) To UBound(Reponse)
Select Case Reponse(compteur)
Case adRecOK, adRecModified 'mise à jour réussie
Case adRecConcurrencyViolation, adRecCantRelease 'enregistrement modifié ou verrouillé
If MonRs.Fields("year born").UnderlyingValue < 1951 Then
MonRs.Fields("year born").Value = MonRs.Fields("year born").UnderlyingValue + 1
End If
Case adRecDBDeleted 'enregistrement supprimé
MonRs.CancelUpdate
Exit For
Case Else 'Autres erreurs
Annulation = True
Exit For
End Select
Next compteur
If Annulation Then Exit Do
MonRs.MoveNext
Loop
If Annulation Then 'restaure les valeurs d'origine
MonRs.Close
Set MonRs = New ADODB.Recordset
MonRs.Open "d:\RsSauve.adtg", MaCnn, , , adCmdFile
MonRs.Resync adAffectAllChapters, adResyncUnderlyingValues
MonRs.Filter = adFilterAffectedRecords
Do While Not MonRs.EOF
If MonRs.Fields("year born").UnderlyingValue = MonRs.Fields("year born").Value Then
MonRs.Fields("year born").Value = MonRs.Fields("year born").OriginalValue
End If
MonRs.MoveNext
Loop
End If
'exécute la validation ou l'annulation
MonRs.Properties("Update Criteria") = adCriteriaKey
MonRs.UpdateBatch adAffectGroup
End If
Err.Clear
On Error GoTo 0
End SubRegardons tout cela en détail. Je vous ai mis en commentaires au début un exemple de traitement préliminaire. Dans cet exemple pourtant simple, car les modifications ne portent que sur un champ d'une seule table, le codage est déjà assez lourd. Beaucoup de développeurs préfèrent garder un modèle transactionnel strict (atomicité) est rejette l'ensemble du traitement si un enregistrement au moins ne peut être mis à jour. Dans ce cas, le traitement préliminaire permet de détecter une cause d'annulation avant le début des mises à jour.
Ensuite j'applique la stratégie générale qui consiste à garder une copie du recordset avant la transmission des modifications. Je vais donc créer un recordset persistant. Le fait de garder un recordset persistant peut paraître aberrant, mais il est obligatoire lors de traitement par lot avec un curseur client. Nous avons vu que le moteur de curseur utilise les propriétés OriginalValue des champs pour écrire ses requêtes actions. C'est propriétés, qui pour le coup porte mal leurs noms se remettent à jour lors de la réussite d'une modification. Après l'appel de UpdateBatch, il n'est donc plus possible de connaître la valeur qu'avait le champ avant la mise à jour. Le recordset persistant permet de pouvoir restaurer.
Avant d'appeler la méthode UpdateBatch je déclenche la récupération d'erreur. En effet nous avons dit que l'échec d'une mise à jour provoque l'apparition d'une erreur ainsi que la valorisation de la collection Errors de l'objet Connection. Si au moins une erreur apparaît dans la collection je rentre alors dans mon code de gestion.
La procédure est alors simple. Je filtre pour obtenir un recordset des enregistrements ayant échoué leurs mises à jour et je décompose leur propriété Status. Cette décomposition est obligatoire puisque tous les enregistrements seront au moins marqué comme la somme du statut modifié et de l'erreur. Je gère ensuite les cas.
Vous pouvez noter aussi que pour contourner le verrouillage je modifie la propriété "Update Criteria".
Cet exemple ne présente qu'une des techniques de gestion d'erreurs. Il est plus fréquent de rencontrer des traitements à l'aide de l'objet Command.


