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

Comprendre les Recordset ADO

Comprendre les Recordset ADO


précédentsommairesuivant

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.

 
Sélectionnez
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 Sub

Il 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é.

 
Sélectionnez
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 Sub

Le 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.

 
Sélectionnez
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.Requery

Comme 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)

 
Sélectionnez
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 Sub

Dans 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

 
Sélectionnez
Private Declare Function GetTickCount Lib "kernel32" () As Long

Pour 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 :

 
Sélectionnez
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 Sub

Tel 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 :

 
Sélectionnez
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 Sub

Chaque 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.

 
Sélectionnez
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 Sub

Dans 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.

 
Sélectionnez
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 Sub

Ce 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.

 
Sélectionnez
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 Sub

Attention 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

 
Sélectionnez
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.

 
Sélectionnez
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 Sub

Dans 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 :

 
Sélectionnez
MonRs.Properties("Update Resync") = adResyncAutoIncrement

VIII-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

Image non disponible 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
With Adodc1
    .RecordSource = "SELECT * FROM Authors"
    .Refresh
End With

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 :

 
Sélectionnez
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 = MonRs

Si 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 :

 
Sélectionnez
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 = MonRs

Vous 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

tab_Mere
Champ Type
Me_Id NumeroAuto
Nom Texte (50)
Prenom Texte (50)
   
 
tab_Fille
Champ Type
Fil_Id NumeroAuto
Adresse Texte (255)
Ville Texte (50)
Me_Id Long

La requête reqAdresse de mon exemple précédent correspondant à la requête SQL :

 
Sélectionnez
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_id

Si 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 :

 
Sélectionnez
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.PubID

J'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.

 
Sélectionnez
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 Sub

Comme 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 :

 
Sélectionnez
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 Sub

Ce 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"

 
Sélectionnez
Private Sub Command6_Click()

While Not MonRs.EOF
    If MonRs.Fields("nom").Value = "Durand" Then MonRs.Delete adAffectCurrent
    MonRs.MoveNext
Wend

End Sub

Le 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 :

 
Sélectionnez
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 Sub

Dans 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.

 
Sélectionnez
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 Sub

Le 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.

 
Sélectionnez
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 Sub

Selon 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 :

 
Sélectionnez
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 Sub

Comme 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 :

 
Sélectionnez
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 Sub

Le 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.

 
Sélectionnez
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 Sub

Comme 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

 
Sélectionnez
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 Sub

Comme 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 :

 
Sélectionnez
MonRs.Open "d:\monrs.adtg", "Provider=MSPersist;", adOpenStatic, adLockBatchOptimistic, adCmdFile

Quelques 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.

 
Sélectionnez
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 If

Je vais profiter de ce chapitre, pour vous montrer une erreur assez facile à faire. Examinons le code suivant :

 
Sélectionnez
Set MonRs1 = New ADODB.Recordset
MonRs1.ActiveConnection = cnn1
MonRs1.Open "SELECT * FROM Authors", cnn1, adOpenStatic, adLockOptimistic, adCmdText
MonRs1![Year born] = 1921
MonRs1.Resync adAffectCurrent, adResyncUnderlyingValues

Aprè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

 
Sélectionnez
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 Function

Cette 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.

 
Sélectionnez
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
 
Sélectionnez
'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 With

J'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.

 
Sélectionnez
'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
Wend

Ici j'ai fais mes modifications, je procède maintenant au traitement par lot

 
Sélectionnez
'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 Sub

Regardons 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.


précédentsommairesuivant

Copyright © 2003 bidou. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.