I have a couple of functions in VBA that run a series of queries in Access to download data from various sharepoints, manipulate it and then delete some data from SP, move data between SPs and finally upload new data. (it compiles data from various SP, and then because we have a 5k row limit on our SPs, moves historical versions of the result to archive SP, result is 1500 rows so archive has 3 old versions, and uploads new version). When it is run it quite often fails and my error handling reports that file is read only, I presume NW issues with SP connection and stops, but I cannot tell which step has failed. I need to restart but only from the step that failed so as to not lose a historical data set from the archive. In my shortest Fn I just inc a variable after each query and report this with the error so I can see. But my longest Fn has 100's of queries, and I really don't want to have to go through line by line and add a variable count. Is there a way of getting the error messge to include the query name that failed. My short function with variable included is below.
Function Export_to_sp()
On Error GoTo Export_to_sp_Err
Dim progress As Integer
DoCmd.SetWarnings False
DoCmd.OpenQuery "6-delete_C", acViewNormal, acEdit
Let Progress = 1
DoCmd.OpenQuery "6-rename_B_to_C", acViewNormal, acEdit
Let Progress = 2
DoCmd.OpenQuery "6-rename_A_to_B", acViewNormal, acEdit
Let Progress = 3
DoCmd.OpenQuery "6-rename_Active_to_A", acViewNormal, acEdit
Let Progress = 4
DoCmd.OpenQuery "6- export_to_Sp", acViewNormal, acEdit
Let Progress = 5
DoCmd.OpenQuery "6- move_A_to_backup", acViewNormal, acEdit
Let Progress = 6
DoCmd.OpenQuery "6-delete_A", acViewNormal, acEdit
Let Progress = 7
DoCmd.OpenQuery "upload_size_tracker_to_SP_new", acViewNormal, acEdit
Let Progress = 8
DoCmd.OpenQuery "Clear_old_for_current_SP"
Let Progress = 9
DoCmd.OpenQuery "3_open_op", acViewNormal, acEdit
Let Progress = 10
DoCmd.OpenQuery "export change log to new sp", acViewNormal, acEdit
Let Progress = 11
DoCmd.OpenQuery "clear_sp_table_size", acViewNormal, acEdit
Let Progress = 12
DoCmd.OpenQuery "count_local_std_res", acViewNormal, acEdit
Let Progress = 13
DoCmd.OpenQuery "count_local_res", acViewNormal, acEdit
Let Progress = 14
DoCmd.OpenQuery "count_sp_std_res", acViewNormal, acEdit
Let Progress = 15
DoCmd.OpenQuery "count_sp_ol_res", acViewNormal, acEdit
Let Progress = 16
DoCmd.OpenQuery "count_sp_backup_ol_result", acViewNormal, acEdit
Let Progress = 17
DoCmd.OpenQuery "count_audit_log", acViewNormal, acEdit
Let Progress = 18
DoCmd.OpenQuery "count_row_tracker", acViewNormal, acEdit
Let Progress = 19
DoCmd.OpenQuery "SP_export_sumary", acViewNormal, acEdit
Let Progress = 10
DoCmd.SetWarnings True
Export_to_sp_Exit:
Exit Function
Export_to_sp_Err:
MsgBox Error$
MsgBox Progress
Resume Export_to_sp_Exit
End Function
CodePudding user response:
The classic way of doing this is to avoid the very limited old scool way of dealing with errors and instead implement a Tryxxx function. The Role of the TryXXX function is to return True of False to indicate the success or fail of an action. One of the parameters (usually the last) is declared as ByRef so that the result of the action can be passed back via the parameter.
I'd also restructure how you provide the strings for the queries so that you separate them from the code that does the querying
XXXQuery is the name of the query for which you have the sequence of query statments.
SOmething along these lines
Public Sub DoXXXQuery(ByRef ipQuerySeq As Collection)
Dim myItem As Variant
For Each myItem In ipQuerySeq
Dim myResult as Variant 'as an example
If Not TryDoQuery(myItem, acviewnormal, acedit, myresult) Then
MsgBox "Query execution stopped unexpectedly at '" & myItem & "'"
'put code here for action after failure
End If
Next
End Sub
' ipViewNormal, ipEdit, and opResult are variants as the OP
' doesn't provide enough information to define more appropriately
Public Function TryDoQuery(ByVal ipCommand As String, ByVal ipViewNormal, ByVal ipEdit, ByVal opResult As Variant) As Boolean
TryDoQuery = False
On Error Resume Next
DoCmd.OpenQuery ipCommand, ipViewNormal, ipEdit
If Err.Number <> 0 Then
Err.Clear
Exit Function
End If
err.clear
opResult = ????? ' its not clear what the result of the query is
TryDoQuery = True
End Function
Public Function GetXXXQuerySeq() As Collection
Dim myC As Collection
Set myC = New Collection
With myC
.Add "6-delete_C", acviewnormal, acedit
.Add "6-rename_B_to_C"
.Add "6-rename_A_to_B"
.Add "6-rename_Active_to_A"
.Add "6- export_to_Sp"
.Add "6- move_A_to_backup"
.Add "6-delete_A"
.Add "upload_size_tracker_to_SP_new"
.Add "Clear_old_for_current_SP"
.Add "3_open_op"
.Add "export change log to new sp"
.Add "clear_sp_table_size"
.Add "count_local_std_res"
.Add "count_local_res"
.Add "count_sp_std_res"
.Add "count_sp_ol_res"
.Add "count_sp_backup_ol_result"
.Add "count_audit_log"
.Add "count_row_tracker"
.Add "SP_export_sumary"
End With
Set GetXXXQuerySeq = myC
End Function
CodePudding user response:
Just going with your constraints taken at face value, have you considered something like the following... I woudln't recommend this approach, but if nothing else about your situation can be changed, this might work for you... Considering the following requirements
- Single-line operations
- Detect failed line
- Be able to restart on-or-around failed line ( /- 1, etc).
Public Sub RunCode(Optional ByVal StartLine As Integer = 0)
On Error GoTo Handle
If StartLine > 0 Then
If StartLine = 10 Then GoTo 10
If StartLine = 20 Then GoTo 20
If StartLine = 30 Then GoTo 30
If StartLine = 40 Then GoTo 40
End If
10: Debug.Print "10"
20: Debug.Print "20"
30: Err.Raise -1, "Jones"
40: Debug.Print "40"
Exit Sub
Handle:
Debug.Print "Handle [" & Erl & "]: " & Error
End Sub
The trick used is the Erl
statement from VBA... It's generally not used, because it requires the explicit line numbers, seen above... But, since you ALREADY effectively numbered you lines, it would seem like a better option than what you're currently doing.
Second, if you NEED to restart AT or around the line that failed, you need a goto label anyway... That is, you would have to have a third line on each clause the way you're doing it, so you can do a GoTo
to it. That is the only way to restart in the middle of your control structure, as you have it. By combining the line numbering as the labels, you effectively reduce this, hopefully making your life easier in the mid-term (not the long term, since numbering your lines is inherently problematic).
The Erl
will give you the line that failed, which can be stored somewhere like either a return value or a Global
(or wherever you can store and retrieve it). What you have left is the requirement to restart at the given line. Unfortunately, to the best of my knowledge, you can't simply do GoTo StartLine
, as would be the easiest, so, as you see, each line is enumerated twice (but, you already had that, right?).. The function, via Optional
parameter, can be effectively re-entered at any given point.
This, then, is the most straight-forward way to accomplish your three goals. With that said, you could consider some other options... For example, you could wrap your call to DoCmd.OpenQuery "...", acViewNormal, acEdit
in some function, and attempt to apply some retry-logic on the same... That is,
Public Function RetryMyQuery(ByVal Query As String) As Boolean
Const MaxRetries As Long = 5
DoRetry:
On Error Goto QueryFailed
Dim FailureCount as Long
DoCmd.OpenQuery Query, acViewNormal, acEdit
RetryMyQuery = True
Exit Function
QueryFailed:
FailureCount = FailureCount 1
If FailureCount >= MaxRetries Then Exit Function
If MsgBox("Query Failed [" & Query & "]: " & Err.Description, vbRetryCancel, "Query Failed") = vbRetry Then Resume DoRetry
End Function
Each line in the main function would simply call this function with the query name:
If Not RetryMyQuery("SP_export_sumary") Then MyErrorLine = 19
What this is doing is attempting to retry each operation up to 5 times (configurable), with a message box between each retry to force the user to interact. They can double-check the connection, re-open anything as needed, and try again, or, simply cancel the operation.
The stub function encapsulates the retries and returns True on success, so in the main loop, you can still detect the error, and even record the line that failed, if desired. However, again, if you NEED to re-enter the control logic at any given point, you likely need something like solution #1. Other alternatives to this would be to put the list of operations in some kind of String Array and loop through them, and then you're not re-entering hard-coded logic lines, you just start at an arbitrary point in the list. It depends on your needs.
Hopefully something in this would be of assistance. It is not always possible to change a lot of the control structures on existing code, and while I wouldn't ever set out and design something like the first example, if you merely need to modify what's there, a down-and-dirty way is sometimes the best. Just remember, maintaining line numbers is terrible. BASIC (Before VB or VBA, or even QuickBasic) had line numbers and always had this problem. They aren't used any more for a reason. It should be considered a last resort, but, if it gets things up and going, I've done worse.
NOTE: It may also be helpful to point out that GoTo Labels are only scoped to the current function. Pre-pending the name of the function (Export_to_sp_Err:
) only results in more characters being typed, and never being able to remember what the handler is named by the time you get to the end of a long function. Just a tip!