When an sql query makes a call to a vba function and that function raises an error, the error handling code fails to handle the error.
See example below. The call to GetId() in the strSql generates an error when the Set rst = db.OpenRecordset(strSql) is executed.
This error is not handled by the On Error GoTo Err_Test error handler!
Public Function GetId() As Long
Err.Raise 11 'Divide by zero error
End Function
Public Function Test() As String
On Error GoTo Err_Test
Dim db As DAO.Database
Dim rst As DAO.Recordset
Dim strSql As String
Set db = CurrentDb()
strSql = "Select * FROM MyTable WHERE MyTable.Id = GetId()"
Set rst = db.OpenRecordset(strSql)
Test = rst!name
Exit_Test:
If Not rst Is Nothing Then
rst.Close
Set rst = Nothing
End If
Set db = Nothing
Exit Function
Err_Test:
MsgBox Error$
Resume Exit_Test
End Function
Why does the error escape the error handler and is there some way to handle it gracefully when the sql makes a call to a vba function that generates an exception?
I know that removing the function call from the sql string as shown below, will enable the error handler to trap the error.
Dim id as Long
id = GetId()
strSql = "Select * FROM MyTable WHERE MyTable.Id = " & id
Is this the only way? If so, should one avoid using function calls in sql query strings to avoid unhandled exceptions?
My take on the observed behavior is this:
When you run "Select * FROM MyTable WHERE MyTable.Id = GetId()", GetId() is evaluated by the query engine, not by Test(), so the error handler in Test() cannot catch the runtime error. It's the same as if you would put the SQL into a query and run that.
When you do "Select * FROM MyTable WHERE MyTable.Id = " & GetId(), GetId() is evaluated by Test().
This would be the "normal" way to run your example (open a recordset in a VBA function), IMO.
But you can also use VBA functions like GetId() in queries. You only need to make sure that either the function is simple enough that it can't trigger a runtime error, or that the function has its own error handler.
If the function is run only once (in the WHERE clause), a MsgBox is acceptable as error handler. If the function is run for every row (i.e. it's in the SELECT clause), this can make for a truly horrible user experience. :p
So make sure that in this case the function returns an error code or NULL or whatever is applicable, like vacip wrote.
Related
I am trying to create some custom buttons in Outlook that interact with a table contained within an Access database. So far I have my buttons working in Outlook, running code that instantiates a custom data access class which in turn handles opening and closing the connection to the database. So far as I can tell, this much works.
However from this class I cannot even perform a simple select query. Can anyone help me understand why the code below might not work? I always end out with a recordset that has no rows but if I run the same sql using the Access query designer it works fine.
Public Function GetJobID(ByVal xEmailID As String) As Integer
'Returns the JobID associated with a given EmailID from the email link table.
'Returns a fail constant if no link exists.
Dim rs As ADODB.Recordset
Dim sql As String
'Exit if not connected.
'Cast to boolean because VBA doesn't recognise connection state integer as boolean.
If Not CBool(mConn.State) Then
GetJobID = RESULT_FAIL_INTEGER
Exit Function
End If
sql = "SELECT [JobID] FROM [EMAIL_LINK_TABLE] WHERE [EmailID]='xEmailID'"
sql = Replace(sql, "EMAIL_LINK_TABLE", EMAIL_LINK_TABLE)
sql = Replace(sql, "xEmailID", xEmailID)
On Error Resume Next
Set rs = mConn.Execute(sql)
If rs.RecordCount > 0 Then
GetJobID = rs(1).Value
Else
GetJobID = RESULT_FAIL_INTEGER
End If
End Function
I see you've tracked down the issue to .RecordCount returning -1.
This is standard behavior for dynamic cursors, from the docs:
The cursor type of the Recordset object affects whether the number of records can be determined. The RecordCount property will return -1 for a forward-only cursor; the actual count for a static or keyset cursor; and either -1 or the actual count for a dynamic cursor, depending on the data source.
Of course, you can modify your code to use a static cursor, but that will impact performance. Instead, to test if there are records in your recordset, use .EOF (a method returning a boolean to indicate if the recordset is currently at the end of the file). That will save your code from having to load all records, when only loading the first one is required:
Public Function GetJobID(ByVal xEmailID As String) As Integer
'Returns the JobID associated with a given EmailID from the email link table.
'Returns a fail constant if no link exists.
Dim rs As ADODB.Recordset
Dim sql As String
'Exit if not connected.
'Cast to boolean because VBA doesn't recognise connection state integer as boolean.
If Not CBool(mConn.State) Then
GetJobID = RESULT_FAIL_INTEGER
Exit Function
End If
sql = "SELECT [JobID] FROM [EMAIL_LINK_TABLE] WHERE [EmailID]='xEmailID'"
sql = Replace(sql, "EMAIL_LINK_TABLE", EMAIL_LINK_TABLE)
sql = Replace(sql, "xEmailID", xEmailID)
On Error Resume Next
Set rs = mConn.Execute(sql)
If Not rs.EOF Then
GetJobID = rs(0).Value
Else
GetJobID = RESULT_FAIL_INTEGER
End If
End Function
so I'm trying to run this SQL script within a function I call and it's giving me a "Compile Error: Object Required" when I try to run it!
Code calling the function
Private Sub cmdLogin_Click()
Call Load_Username(Username)
End Sub
Function being called
Private Sub Load_Username(Text As String)
Dim SQL As String
Set SQL = "UPDATE tbleUsername " & _
"SET Username = '" & Text & "' " & _
"WHERE ID = 1"
DoCmd.RunSQL SQL
End Sub
Dim SQL As String
Set SQL = "string literal"
You can't use Set to assign a string literal. Use Set to assign object references. That assignment is illegal, because an object is required. Hence, Object required.
Two possible fixes:
Remove the Set keyword and use the wonderful implicit value assignment syntax.
Replace the Set keyword with the obsolete Let keyword for a long-deprecated explicit value assignment syntax. Only suggesting because I'm seeing you use the long-deprecated Call keyword too.
I think that you should define variable Username before call function LoadUsername(UserName)
I am trying to write some code in excel VBA using the Object Oriented Concept. Therefore I wanted to initialize my objects with constructors, like we usually do in Java. However I discovered that the default Class_Initialize() Sub that is offered in VBA does not take arguments. After searching a bit, I found that the answer for this Question proposed a pretty good alternative.
Here is a sample of my Factory Module (I Named it Creator):
Public Function CreateTool(ToolID As Integer) As cTool
Set CreateTool = New cTool
CreateTool.InitiateProperties (ToolID) '<= runtime error 91 here
End Function
The class cTool:
Private pToolID As Integer
Private pAttributes As ADODB.Recordset
Private pCnn As ADODB.Connection
Public Sub InitiateProperties(ToolID As Integer)
Dim sSQL As String
Set pCnn = connectToDB() 'A function that returns a connection to the main DB
pToolID = ToolID
sSQL = "SELECT Tool_ID, Status, Type, Tool_Number " _
& "FROM Tool WHERE Tool_ID = " & pToolID
pAttributes.Open sSQL, pCnn, adOpenKeyset, adLockOptimistic, adCmdText
End Sub
This is how I call the constructor:
Dim tool As cTool
Set tool = Creator.CreateTool(id)
My issue is that when I run the code, I get the following error:
Run-Time error '91' : Object Variable or With Block Variable not Set
The debug highlights the CreateTool.InitiateProperties (ToolID) line of my CreateTool Function.
I know that this usually happens when someone is setting a value to an object without using the keyword Set but it does not seem to be my case.
Any help, advice to resolve this issue would be greatly appreciated!
Thanks.
Might not be the cause of your error, but this:
Public Function CreateTool(ToolID As Integer) As cTool
Set CreateTool = New cTool
CreateTool.InitiateProperties (ToolID) '<= runtime error 91 here
End Function
Is problematic for a number of reasons. Consider:
Public Function CreateTool(ByVal ToolID As Integer) As cTool
Dim result As cTool
Set result = New cTool
result.InitiateProperties ToolID
Set CreateTool = result
End Function
Now, looking at the rest of your code, you're doing the VBA equivalent of doing work in the constructor, i.e. accessing database and other side-effects to constructing your object.
As #Jules correctly identified, you're accessing the unitialized object pAttributes inside InitiateProperties - that's very likely the cause of your problem.
I'd strongly recommend another approach - if you come from Java you know doing work inside a constructor is bad design... the same applies to VBA.
Get your code working, and post it all up on Code Review Stack Exchange for a full peer review.
I have a MS Access 2003 Application to port to Visual Basic 6.0 currently. For this purpose, I have written a copy of the usual DLookup commonly used in Access.
Here is the code so far.
Public Function cDLookup(TargetField As String, TargetTable As String, cTCondition As String) As String
'Eigene Implementation von DLookup
Dim result As String
Dim rs As New ADODB.Recordset
Dim SQL As String
On Error GoTo Fehlerbehandlung
'Zusammenbauen der Query
SQL = "SELECT " & TargetField & " FROM " & TargetTable & " WHERE " & cTCondition
Call dbConn
'Initiate Database connection object cn
rs.Open SQL, cn
If (rs.RecordCount = 1) Then
result = cleanString(rs.GetString)
Debug.Print ("[DLOOKUP] Erfolgreich Einen Datensatz gefunden und konvertiert. Output: " & result)
ElseIf (rs.RecordCount > 1) Then
result = "#ErrRC"
Debug.Print ("[DLOOKUP] Es wurden " & CStr(rs.RecordCount) & " Datensätze statt einem festgestellt. Dies ist nicht erlaubt")
Else
result = "#ErrGen"
Debug.Print ("[DLOOKUP] Es ist ein Fehler in der Abfrage aufgetreten")
End If
rs.Close
cDLookup = result
Exit Function
Fehlerbehandlung:
Debug.Print ("[DLOOKUP] Fehler im Ausführen der Prozedur cDLookup()]")
cDLookup = "#Fehler"
Exit Function
End Function
My main issue is with the generated Queries. If a user types garbage that gets inputted into this, there is a runtime error from ADODB when opening the recordset. Can I verify beforehand that a SQL query is not going to do that and catch it to not crash my entire program somehow?
No, not in the way you want. You need to just try to execute it, and handle the error gracefully. You could do something like create a function called "ValidateSQL" with its own error handler, try to execute it, and return false if the query failed. You could even pass the recordset in byref and set it to have the results if it passes.
It's been a long time so forgive syntax mistakes. Something like this:
Function ValidateSQL(ByRef rs as ADODB.Recordset) as Boolean
On Error GoTo Hell
ValidateSQL = True
'open recordset here
Set rs = ....
If False Then
Hell:
ValidateSQL = False
End If
End Function
If you know you're generating trash, why take the trash to the database and wait for the database to blow up to tell you it's trash? Fail Fast is a thing for a reason.
Hitting a database is not free. Even if it's relatively fast, it's orders of magnitude slower than plain code.
Don't get me wrong, I don't mean "validate that the specified field name does indeed exist in the table with the specified name" and "parse that where statement to see if it makes sense".
However, a few sanity-checks will cost much less than a useless trip to the database. You could:
Verify that the table and field names either don't contain any spaces, or are enclosed in square brackets;
Verify that the table and field names aren't empty;
Verify that the WHERE clause doesn't start with "WHERE", and that it's not empty.
If these simple checks pass, then have the database blow up if they're still wrong.
Can anyone explain what is wrong with this code? There's a runtime error stating that the Object doesn't support this property or method
Set rst = DataFunctions.CheckCompanyID
If IsNull(rst.Fields("ID")) Then 'Error occurs here
ContactID = 0
Else
ContactID = rst!ID
End If
The CheckCompanyID method does as follows
CompanyValue = GetCurrentRecord
CheckData = "Select CompanyID, ID From Contacts Where Contacts.CompanyID = " & CompanyID & ";"
CheckCompanyID = CurrentDB.OpenRecordset(CheckData, dbOpenDynaset) 'Returns The Recordset
The If Is Null(rat.Fields("*FieldName*")) Then has been used elsewhere and works ok. I imagine the error is a result of returning the recordset object?
A suspected in my question comment, your function DataFunctions.CheckCompanyID does not return a recordset object, but Nothing instead. Nothing is VB's null.
This can happen if there is an unhandled error inside the function and On Error Resume Next is in effect, or simply because the function is implemented that way.
As a general tip: You should not use object references without checking that they are valid (i.e. not Nothing) to avoid run-time errors like this one.
You can check for Nothing easily:
Set rst = DataFunctions.CheckCompanyID
ContactID = 0
If Not rst Is Nothing
If IsNull(rst.Fields("ID")) Then
ContactID = rst!ID
End If
End If
Note: Classic VB does not support short-circuiting logical expressions, so you can't use the one-line form that other languages would allow:
If Not rst Is Nothing And Not IsNull(rst.Fields("ID")) Then
' This will cause an error because rst.Fields("ID") is always evaluated!
End If
check your rst, if it gets an recordset returned with results.
and try this:
IF nz(rst("ID"), "") = "" then
instead
If IsNull(rst.Fields("ID")) Then