This question is basically the same as this other StackOverflow answer. I based our solution off the one in the link using parameters, and altered it to an ADO command instead. Using parameters and an ADO command allows you to exceed the 255 character limit with DAO parameters, and if you end up trying to track an RTF field, you won't have the headache of trying to parse HTML/markdown/whatever into a safe SQL String (and is also more resistant to SQL injection attacks if users enter such data into your form). You'll find I used a "longText" field for our old/new values as this facilitates using memo fields and a more reusable field.
Using an ADO command vs recordset is orders of magnitude faster when logging field changes, as you don't need to do anything except insert to the data.
Note the following:
- This solution requires linked tables and fields. It does not handle detection for non-linked fields.
- This solution ignores getting user details (username) in a safe manner. Using the Environ variable isn't super secure, but I left it.
- I found caching the command for later makes the command run an order of magnitude faster vs building it each time. When you're logging all fields on a form routinely (eg, for auditing), this makes a big difference at not a lot of cost to memory or connections.
- I assumed all the fields were "text". That's probably not the case, so you'll need to change your field types to match the correct types and sizes.
The Code:
Option Compare Database
Option Explicit
Private m_strUserID as String
Private m_StoredCMD as ADODB.Command
Private Property Get StrUserID as String
If m_struserID = vbNullString then m_strUserID = Environ("USERNAME")
StrUserID = m_struserID
End Property
Public Sub AuditChanges(ByRef FormToProcess as Access.Form, Byref RecordIDField as String)
Dim TimeStamp as DateTime
Dim CtrlCheck as Access.Control
Dim RecordIDFieldCtrl as Access.Control
Set RecordIDFieldCtrl = FormToProcess.Controls(RecordIDField)
TimeStamp = Now()
For Each CtrlCheck In FormToProcess
If IsChanged(CtrlCheck) And CtrlCheck.Tag = "Audit" Then
AddLogEntry (CtrlChanged, RecordIDFieldCtrl.Value)
End If
Next CtrlCheck
End Sub
Private Sub AddLogEntry (ByRef CtrlChanged as Control, ByRef RecordIDFieldCtrl as Access.Control)
Dim TimeStamp as DateTime
Dim adoCMD = ADODB.Command
TimeStamp = Now()
If IsChanged(CtrlChanged) Then ' Verify anything actually changed. Check twice because it doesn't cost anything.
Set adoCMD = GetLogCommand ' Note, it will be much faster to put this into a module stored command, but
With If adoCMD
(.ActiveConnection.State And adStateOpen) <> adStateOpen Then .ActiveConnection.Open
.Parameters("[pDateTime]") = TimeStamp
.Parameters("[pUserName]") = StrUserID
.Parameters("[pFormName]") = CtrlChanged.Parent.Name
.Parameters("[pRecordID]") = RecordIDFieldCtrl.Value
.Parameters("[pFieldName]") = CtrlChanged.Name
.Parameters("[pNewValue]") = CtrlChanged.Value
.Parameters("[pOldValue]") = CtrlChanged.OldValue
.Execute
End If
End Sub
Public Function GetLogCommand() As ADODB.Command
Dim cnn as ADODB.Connection
Dim SQLCommand as String
If m_StoredCMD Is Nothing Then
' Note: Verify these field type assumptions are correct and alter as needed.
' Note2: I use "LongText" Fields for values, because Access's VarChar Fields are limited to 255 charachters.
' If you're using any
SQLCommand = "PARAMETERS [pDateTime] DateTime, [pUserName] VARCHAR(255), " & _
"[pFormName] VARCHAR(255), [pRecordID] VARCHAR(255), [pFieldName] VARCHAR(255)," & _
"[pOldValue] LONGTEXT, [pNewValue] LONGTEXT;
INSERT INTO tblAuditTrail (DateTime,UserName,FormName,RecordID,FieldName,OldValue,NewValue) " & _
"VALUES ([pDateTime], [pUserName], [pFormName], [pRecordID], [pFieldName], [pOldValue], [pNewValue]); "
Set m_StoredCMD = New ADODB.Command
With m_StoredCMD
Set .ActiveConnection = CurrentProject.Connection
.CommandText = SQLString.GetStr
.CommandType = adCmdText
.Prepared = True
.Parameters.Append .CreateParameter("[pDateTime]", adDBTimeStamp, adParamInput, 255)
.Parameters.Append .CreateParameter("[pUserName]", adVarChar, adParamInput, 255)
.Parameters.Append .CreateParameter("[pFormName]", adVarChar, adParamInput, 255)
.Parameters.Append .CreateParameter("[pRecordID]", adVarChar, adParamInput, 255)
.Parameters.Append .CreateParameter("[pFieldName]", adVarChar, adParamInput, 255)
.Parameters.Append .CreateParameter("[pNewValue]", adLongVarChar, adParamInput, 63999)
.Parameters.Append .CreateParameter("[pOldValue]", adLongVarChar, adParamInput, 63999)
End With
End If
Set GetLogCommand = m_StoredCMD
End Function
Public Function IsChanged(ByRef CtrlChanged as Control) As Boolean
' There are a lot of ways to do this, but this keeps code clutter down, and lets you
' alter how you determine if a control was altered or not.
' As this is written, it will ONLY work on bound controls in bound forms.
IsChanged = ((CtrlChanged.OldValue <> CtrlChanged.Value) Or (IsNull(CtrlChanged.OldValue) = Not IsNull(CtrlChanged.Value)))
End Function