2 votes

Comment utiliser DataMacros pour exécuter VBA pour enregistrer les changements d'enregistrement

J'ai une base de données MS Access avec plusieurs tables, et chaque table a des champs différents (chaque table a une clé primaire). Chaque fois que quelqu'un modifie un enregistrement existant, je veux que l'état avant/après soit enregistré dans une table "historique", mais j'essaie d'éviter de créer une DataMacro pour plus de 20 champs par table, car cela semble lourd et difficile à gérer.

Pour ce faire, j'ai ajouté des macros "Before Change" et "After Update" qui ont chacune un appel à la fonction SetLocalVar pour exécuter des fonctions vba publiques qui résident dans un module local. La fonction "before" parcourt les champs existants dans la table actuelle pour construire un dictionnaire des valeurs avant modification. La fonction "after" répète le processus pour identifier le champ qui a changé, de sorte que l'information peut être ajoutée à un tableau d'historique (contient le nom de la table, le nom du champ, les valeurs avant/après, l'utilisateur, l'horodatage, etc.)

Le problème que je rencontre est que la fonction "Après la mise à jour" ne révèle que les données antérieures à la modification. Je ne peux pas dire quelle est la nouvelle valeur ou quel champ a été modifié. Je ne peux pas utiliser [Old].[FieldName],[FieldName] comme entrées pour SetLocalVar car je ne sais pas à l'avance quels champs seront mis à jour (et les noms des champs diffèrent selon les tables). Et je ne peux pas appeler Requery ou Refresh car cela entre en collision avec le processus "After Update".

De toutes les questions similaires posées ici et ailleurs que je peux trouver, elles commencent soit par savoir explicitement quel champ va changer, soit par ne pas fournir ce niveau de détail dans leurs événements de journal. Ou peut-être que mon google-fu est faible.

Voici ce que j'ai :

Antes de SetLocalVar (nom de table d'entrée différent par table) :

=SetupLogEvent("Table01",[KeyName])

Après SetLocalVar expression :

=SubmitLogEvent("Table01",[KeyName])

Code du module

Public BeforeFields As Scripting.Dictionary

Public Function SetupLogEvent(ByVal TableName As String, ByVal KeyName As String)
    Set BeforeFields = New Scripting.Dictionary
    Set rs = CurrentDb.OpenRecordset("SELECT * FROM [" & TableName & "] WHERE KeyName='" & KeyName & "'")
    Dim i As Long
    With rs
        .MoveLast
        .MoveFirst
        For i = 0 To rs.Fields.Count - 1
            fName = rs.Fields(i).Name
            fVal = rs.Fields(i).Value
            BeforeFields.Add fName, fVal
        Next i
    End With
    rs.Close
    SetupLogEvent = True
End Function

Public Function SubmitLogEvent(ByVal TableName As String, ByVal KeyName As String)
    DoEvents
    Dim MakeUpdate As Boolean
    MakeUpdate = False
    Set rs = CurrentDb.OpenRecordset("SELECT * FROM [" & TableName & "] WHERE KeyName='" & KeyName & "'")
    With rs
        .MoveLast
        .MoveFirst
        For i = 0 To rs.Fields.Count - 1
            fName = rs.Fields(i).Name
            fVal = rs.Fields(i).Value
            If fVal <> BeforeFields(fName) Then
                Debug.Print ("Modified field is " & fName)
                MakeUpdate = True
                Exit For
            End If
        Next i
    End With
    rs.Close
    If MakeUpdate = True Then
        'Run SQL code to update the history table with the relevant information
        'DoCmd.RunSQL <SQL code here>
    End If
    Set BeforeFields = Nothing
    SubmitLogEvent = True
End Function

Je pense que je devrais utiliser une autre méthode que celle qui consiste à parcourir le jeu d'enregistrements, mais je ne suis pas sûr de ce que je devrais faire à la place. Je suppose que je pourrais ajouter deux douzaines d'entrées facultatives à la fonction after et introduire manuellement chaque [nom de champ] de chaque table dans l'expression SetLocalVar de chaque table, mais je n'arrive pas à imaginer que le système soit inflexible au point d'exiger cela. Il doit y avoir un meilleur moyen, non ?

Edit : J'ai mis à jour le titre de cette question et la raison pour laquelle j'ai évité d'écrire une DataMacro pour chaque champ pour les futurs lecteurs de ce sujet.

0voto

enrikk Points 33

Merci à braX, krish KM, et SunKnight pour vos commentaires ci-dessus.

J'ai développé un processus qui répond à mes propres besoins, mais je vais vous expliquer brièvement mon raisonnement :

Limites des autres méthodes :

  1. Allenbrowne La solution de l'auteur (1) exige que l'utilisateur utilise un formulaire et (2) exige que les clés soient des AutoNumbers (les miennes doivent être des chaînes de caractères).
  2. Scottgem La solution de l'UE exclut l'utilisation de formulaires. Je propose un formulaire à mes utilisateurs pour mettre à jour rapidement les champs avec lesquels ils interagissent fréquemment, mais certaines tables ont plus de 20 champs et tout formulaire que je leur donnerais (plus de 12 tables avec plus de 20 champs semi-uniques) serait plus compliqué que de les laisser interagir avec les tables elles-mêmes (bien que généralement désapprouvée, l'exposition des tables n'est pas un problème dans ce cas, tant que nous avons un certain suivi).
  3. Les DataMacros générées manuellement sont rigides - je ne peux pas en copier-coller une partie dans une nouvelle table pour mettre à jour rapidement les champs que je souhaite suivre (ajout, suppression, modification, etc.). Cette base de données est récente, et je m'attends donc à ce que les priorités et les priorités changent au fur et à mesure que de nouveaux champs sont ajoutés.

Solution :

À la fin de ma question, j'ai mentionné que je pouvais introduire les valeurs mises à jour dans une fonction avec des entrées facultatives, et faire en sorte que la fonction effectue la comparaison avant/après en interne, c'est donc ce que j'ai fait.

Limitations :

  1. Les DataMacros génèrent des erreurs si vous ne traitez pas les valeurs nulles dans l'expression.

  2. Les DataMacros sont limitées à 255 caractères

En raison du point 1 ci-dessus, j'ai dû envelopper les entrées dans des fonctions Nz(). Mon schéma implique également de fournir les entrées à la fonction "after" au format clé/valeur, ce qui nécessite une déclaration verbeuse. Avec la limite de 255 caractères, j'ai décidé de ne suivre que 6 à 10 champs par table à la fois (pour faire plus que cela, je répète simplement le processus dans une deuxième DataMacro).

Public TableChangedName As String
Public TableChangedKey As String
Public SuppressLog As Boolean

Public Function SetupLogEvent(ByVal TableName As String, ByVal KeyName As String)
    'This function reads the recordset as-is to build a 'before' dictionary

    'SuppressLog is True when updating via user form
    'The form will make its own submissions to the history log to specify which form did the update
    If SuppressLog = False Then
        Set BeforeFields = New Scripting.Dictionary
        Set rs = CurrentDb.OpenRecordset("SELECT * FROM [" & TableName & "] WHERE KeyName='" & KeyName & "'")
        Dim i As Long
        Do While Not rs.EOF
            rs.MoveLast
            rs.MoveFirst
            For i = 0 To rs.Fields.Count - 1
                fName = rs.Fields(i).Name
                fVal = rs.Fields(i).Value
                BeforeFields.Add fName, CStr(Nz(fVal, ""))
            Next i
            rs.MoveNext
        Loop
        rs.Close
        TableChangedName = TableName
        TableChangedKey = KeyName
        SetupLogEvent = True
    End If
End Function

Public Function SubmitLogEvent(Optional ByVal Input1 As String = "None", Optional ByVal Input2 As String = "None", Optional ByVal Input3 As String = "None", Optional ByVal Input4 As String = "None", Optional ByVal Input5 As String = "None", Optional ByVal Input6 As String = "None", Optional ByVal Input7 As String = "None", Optional ByVal Input8 As String = "None", Optional ByVal Input9 As String = "None", Optional ByVal Input10 As String = "None", Optional ByVal Input11 As String = "None", Optional ByVal Input12 As String = "None")
    'This function submits an entry to the _History table for each field that was changed, marking the change as "Manually updated" (i.e. no form used)

    'SuppressLog is True when updating via user form
    'The form will make its own submissions to the history log to specify which form did the update
    If SuppressLog = False Then
        Dim InputArray As Variant
        InputArray = Array(Input1, Input2, Input3, Input4, Input5, Input6, Input7, Input8, Input9, Input10, Input11, Input12)
        Set AfterFields = New Scripting.Dictionary
        For i = LBound(InputArray) To UBound(InputArray) Step 2
            If InputArray(i) = "None" Then
                'End of used input fields
                Exit For
            Else
                AfterFields.Add InputArray(i), InputArray(i + 1)
            End If
        Next i
        DoCmd.SetWarnings False
        For Each fName In AfterFields.Keys
            If BeforeFields(fName) <> AfterFields(fName) Then
                strSQL = "INSERT INTO _History ([TableModified],[KeyName],[Field],[From],[To],[User],[TimeStamp],[Method]) VALUES ('" & TableChangedName & "','" & TableChangedKey & "','" & fName & "','" & BeforeFields(fName) & "','" & AfterFields(fName) & "','" & Environ("username") & "','" & Now() & "','Manually updated')"
                Debug.Print ("strSQL = """ & strSQL & """")
                DoCmd.RunSQL strSQL
            End If
        Next fName
        DoCmd.SetWarnings True
    End If
    SubmitLogEvent = True
End Function

Avec ces deux fonctions ci-dessus, la seule chose que je dois mettre à jour par table sont les expressions.

Antes de SetLocalVar expression :

\=SetupLogEvent("Table01", [KeyName])

Après SetLocalVar expression :

\=SubmitLogEvent('Field1',Nz([Field1]), 'Field2',Nz([Field2]), 'Field3',Nz([Field3]), 'Field4',Nz([Field4]))


Si un nouveau champ est ajouté à une ou plusieurs tables et qu'il doit être suivi, je mets à jour la variable FieldList et j'exécute à nouveau la fonction ci-dessous :

Private Function PrintExpression()
    'Enter the list of fields you want to track, separated by commas
    FieldList = "Field_1,Field_2,Field_3,Field_4,Field_5"

    exprString = "=SubmitLogEvent("
    For Each Entry In Split(FieldList, ",")
        exprString = exprString & "'" & Entry & "',Nz([" & Entry & "]),"
    Next
    exprString = RxReplace(exprString, ",$", ")")
    If Len(exprString) > 255 Then
        Call MsgBox("This result is > 255 characters (" & Len(exprString) & ") and will be rejected.", vbExclamation + vbOKOnly, "Input Too Long")
    Else
        Debug.Print ("=SetupLogEvent(""<<< Table Name >>>"",[KeyName])")
        Debug.Print (exprString)
    End If
End Function

Sortie :

\=SetupLogEvent("<<Nom du tableau >>>", [NomClé])

\=SubmitLogEvent('Champ_1',Nz([Champ_1]), 'Champ_2',Nz([Champ_2]), 'Champ_3',Nz([Champ_3]), 'Champ_4',Nz([Champ_4]), 'Champ_5',Nz([Champ_5]))

Je copie/colle la deuxième expression dans n'importe laquelle des tables qui utilisent ces champs, et j'ai terminé. Si j'ai besoin de suivre plus de 6-10 champs par table, je réexécute la fonction ci-dessus avec la 2ème FieldList, et je l'ajoute sous une deuxième DataMacro pour la table. Rincer et répéter si nécessaire.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X