DataWright Information Services

Consulting and Resources for Excel and Access

Tracking data changes in Access

There are situations where you want to be able to track changes to records in a multi-user database. This page contains articles covering two options: storing when a record was changed and by whom; and storing the old value of a field before it changed. The first option only keeps a record of the last change, and won't tell you what was changed on the form. The second will give you an audit trail, and you can track the full history of changes for any field.

Simple tracking: Recording the last change made to a record

If you are only interested in knowing who created the record, and who changed it, you can do it by adding four fields to the table that underlies the form (and, if the form is based on a query, adding the new fields to the query). The fields are:

Field Data Type Comment
CreateBy Text
CreateDate DateTime Default value =Now()
ModBy Text
ModDate DateTime  

To use the fields, you need to add code to the Current and BeforeUpdate events of the form, as shown below:

Private Sub Form_BeforeUpdate(Cancel As Integer)
    ModBy = Environ("username")
    ModDate = Now()
End Sub

Private Sub Form_Current()
    If Me.NewRecord Then
        CreateBy = Environ("username")
    End If
End Sub

When you go to a record, the Current event fires. You can check to see if it is a new record and, if so, write the current logged-in user's name to the CreateBy field. CreateDate is filled automatically by setting its default value.

When you save a record, one of the events that fires is BeforeUpdate. This is just before the record is actually saved, so you can write the current user's name and the timestamp to the two Mod fields.

Although this is simple to set up, you need to add the fields to every table / query whose changes you want to track. The second method takes a bit more explaining but centralises all of the logging to a custom table, and allows you to build reports covering the history.

Audit trail: Logging all changes to a field

Let's assume that you want to keep a full history of changes to a field, in case you want to restore a value at a later date. This situation requires that you keep multiple records (one for each change that you track) and because of that, it makes sense to create a custom table for the purpose. In this example the log table will be called ztblDataChanges, and it will have these fields:

Field Data Type Comment
LogID AutoNumber Primary Key
FormName Text
ControlName Text
FieldName Text  
RecordID Number (Long Integer)
UserName Text
OldValue Text
NewValue Text
TimeStamp DateTime Default value =Now()

Most of the fields are text, including the OldValue and NewValue fields. The reason for this is that text fields will accept numeric data, but number fields will not accept text. You can always convert back to the correct data type when you restore the value. The exception is the RecordID, and I have made that a Number because all of my tables use AutoNumber primary keys. If you use any text-based primary keys, make this field text too.

The code uses a custom function which needs to be put into a standard code module. The code is shown below:

Function LogChanges(lngID As Long, Optional strField As String = "")
    Dim dbs As DAO.Database
    Dim rst As DAO.Recordset
    Dim varOld As Variant
    Dim varNew As Variant
    Dim strFormName As String
    Dim strControlName As String
    varOld = Screen.ActiveControl.OldValue
    varNew = Screen.ActiveControl.Value
    strFormName = Screen.ActiveForm.Name
    strControlName = Screen.ActiveControl.Name
    Set dbs = CurrentDb()
    Set rst = dbs.TableDefs("ztblDataChanges").OpenRecordset
    With rst
        !FormName = strFormName
        !ControlName = strControlName
        If strField = "" Then
            !FieldName = strControlName
            !FieldName = strField
        End If
        !RecordID = lngID
        !UserName = Environ("username")
        If Not IsNull(varOld) Then
            !OldValue = CStr(varOld)
        End If
        !NewValue = CStr(varNew)
    End With
    'clean up
    Set rst = Nothing
    Set dbs = Nothing
End Function

Explaining the code

Of the seven fields that we populate in ztblDataChanges, only one has to be directly passed to the function; the ID or the current record. Everything else is retrieved automatically using Screen.ActiveControl, Screen.ActiveForm, or the Environ function. There is an option to provide the field name where it doesn't match the name of the control.

When populating the fields, there are two If statements to handle exceptions. The first one checks to see whether the second parameter has been provided. If not, the code assumes that the control and field names are the same (usually the case if you add a bound control to a form), and uses the control name to populate the field name. If not, the user-provided field name is used.

        If strField = "" Then
            !FieldName = strControlName
            !FieldName = strField
        End If

The second checks to see whether an OldValue existed. If not (eg. for a new record, or for an existing record where a field is being populated for the first time), this field is skipped to avoid errors.

        If Not IsNull(varOld) Then
            !OldValue = CStr(varOld)
        End If

Using the function

To call the function use either of the following syntax options. Assuming that the form's key field is CustomerID, and the field and control names match, use this syntax:

Private Sub Address1_BeforeUpdate(Cancel As Integer)
    Call LogChanges(CustomerID)
End Sub

If the field and control names do not match, use this instead:

Private Sub txtAddress1_BeforeUpdate(Cancel As Integer)
    Call LogChanges(CustomerID, "Address1")
End Sub

Potential uses for the logging data

Without going overboard you can track changes to critical fields on any form in your database. The information stored in the table can be used to report on a range of things such as:

  • Usage patterns -- who uses which forms
  • How often are particular forms used? If you want to rationalise your database, this can be useful information
  • Obviously, the stored data can be used to restore changed values if necessary

Another advantage of this method is that, once the table is created and the code has been placed in a module, you don't have to make structural changes to any other tables in order to track usage.