Error in using QueryDef to define parameters in a query - vba

I'm working in Access and trying to use a query with parameters in VBA. I have several queries that I need to use, so I added a routine to generalize the process:
Public Function Execute_query(query) As Recordset
Dim qdf As QueryDef
Set qdf = CurrentDb.QueryDefs(query)
For Each prm In qdf.Parameters
prm.Value = Eval(prm.Name)
Next prm
If (qdf.Type = 80) Then
qdf.Execute
Else: Set Execute_query = qdf.OpenRecordset
End If
End Function
I'm still testing this so there may be other issues, but my immediate question is why the Eval(prm.name) line isn't working. The paramater is [R_Yr] which I've declared as a public variable and have assigned a value - which I can verify in the watch window. But I get an error code 2482 - Access cannot find the name 'R_yr"
This same code seems to work when the parameter value is coming from a form instead of a variable - which is why I had to set it up in the first place - I couldn't access a form control in a query run from VBA.

Forgive me, but I'm having a bit of trouble seeing the real benefit from this extra level of indirection. You have to create variables that correspond to the parameters for the particular query you are planning to invoke, and then you call a generic function to invoke it. Why not just create a QueryDef, pass it the parameters, and invoke it in place? It seems like essentially the same amount of work, and it makes the code easier to follow because "everything is right there".

Eval() takes the string you give it and uses the "expression service" to process it. The problem you're facing is the expression service doesn't know anything about VBA variables. If you're really determined, you may be able to figure out a workaround which builds the variable's value rather than the variable's name into the string you give Eval() to ... um ... evaluate.
But for what you're doing, I suggest you ditch Eval(). Instead give the function a data structure such as a Scripting.Dictionary or VBA Collection which contains the parameter values with your former variable names as keys.
Here is a VBA Collection example ...
Dim MyCol As Collection
Set MyCol = New Collection
MyCol.Add CLng(10), "R_Yr"
MyCol.Add "foo", "MyString"
Debug.Print MyCol("R_Yr"), TypeName(MyCol("R_Yr"))
Debug.Print MyCol("MyString"), TypeName(MyCol("MyString"))
That code gives me this output in the Immediate window ...
10 Long
foo String
So consider building a similar collection in the calling code and passing that collection to a modified Execute_query function.
Public Function Execute_query(ByVal pQdf As String, _
ByRef pCol As Collection) As Recordset
Dim qdf As QueryDef
Set qdf = CurrentDb.QueryDefs(pQdf)
For Each prm In qdf.Parameters
prm.Value = pCol(prm.Name)
Next prm
If (qdf.Type = 80) Then
qdf.Execute
Else
Set Execute_query = qdf.OpenRecordset
End If
End Function

Related

VBA using function return values instead of variables

I've tried searching the internet for a definitive answer to this two-part scenario but couldn't find anything conclusive. I've been writing VBA procedures for sometime now in both Access and Excel and while trying to streamline some code I came across something of a dilemma
The first part is about using functions to return objects. The example below is generally what I've seen on the web for a function to return an ADODB.Recordset (I've simplified the code so no error handling etc.).
Public Function CreateADORecordset(SQL As String, Connection As ADODB.Connection) As ADODB.Recordset
Dim rst As ADODB.Recordset
Set rst = New ADODB.Recordset
Call rst.Open(SQL, Connection)
Set CreateADORecordset = rst
End Function
The first part of the question is, why do I need a variable called rst when I could rewrite the function as this:
Public Function CreateADORecordset(SQL As String, Connection As ADODB.Connection) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, Connection)
End Function
Is there anything fundamentally wrong with the above rewrite of the function? As the function returns an ADO recordset, why declare a variable separately?
I can take this a step further:
Public Function CreateADOConnection(ConnectionString As String) As ADODB.Connection
Set CreateADOConnection = New ADODB.Connection
Call CreateADOConnection.Open(ConnectionString)
End Function
Public Function CreateADORecordset(SQL As String, ConnectionString As String) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, CreateADOConnection(ConnectionString))
End Function
Again, is this a particularly bad thing to do by using function return objects over declaring objects within the procedures via Dim?
In the grand scheme of things, I've been writing VBA code to transfer the contents of a recordset via GetRows into an Excel range. The function declaration line is:
Public Sub TransferRecordsetArray(GetRows As Variant, Destination As Range)
As TransferRecordsetArray works correctly, I've not included the code.
The dilemma I'm in now is in this scenario, I've reached a point where I don't need to declare any variables for the code to run correctly and I'm unsure how much of a good or bad thing this in terms of functions returning objects, scope and variables, etc.
In order to run the code correctly, I only need one of two lines without variables:
Call TransferRecordsetArray(CreateADOConnection(ConnectionString).Execute(SQL).GetRows, Target)
or
Call TransferRecordsetArray(CreateADORecordset(SQL, CreateADOConnection(ConnectionString)).GetRows, Target)
Any advice/recommendations on this way of writing VBA code would be greatly appreciated. I have used the task manager to keep an eye on memory usage on both methods and it doesn't seem to greatly differ and, it appears that VBA destroys the function return objects after a while, despite them not being explicitly destroyed by setting them to Nothing.
Many thanks.
The first part of the question is, why do I need a variable called rst when I could rewrite the function as this
Public Function CreateADORecordset(SQL As String, Connection As ADODB.Connection) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, Connection)
End Function
You don't need a separate variable. Your code is perfectly fine.
I can take this a step further:
Public Function CreateADOConnection(ConnectionString As String) As ADODB.Connection
Set CreateADOConnection = New ADODB.Connection
Call CreateADOConnection.Open(ConnectionString)
End Function
Public Function CreateADORecordset(SQL As String, ConnectionString As String) As ADODB.Recordset
Set CreateADORecordset = New ADODB.Recordset
Call CreateADORecordset.Open(SQL, CreateADOConnection(ConnectionString))
End Function
Yes, absolutely. Nothing wrong with that.
I've reached a point where I don't need to declare any variables for the code to run correctly
Congratulations, keep it up. :)
Further reading: Is there a need to set Objects to Nothing

VBA: Interact with Access from within Outlook

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

Constructor in VBA - Runtime error 91 "Object variable not set"

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.

Too few parameters in OpenRecordset code

I have two sets of code, that are the same I just change variables to another set that exist and now with the ones I changed I get an error saying "Run-time error '3061': Too few parameters. Expected 6."
This is the changed code:
Dim rec As Recordset
Dim db As Database
Dim X As Variant
Set db = CurrentDb
Set rec = db.OpenRecordset("UnitMoreInfoQ")
Const msgTitle As String = "Open Explorer"
Const cExplorerPath As String = "C:\WINDOWS\EXPLORER.EXE"
Const cExplorerSwitches As String = " /n,/e"
cFilePath = rec("ProjFilePath")
It highlights this line:
Set rec = db.OpenRecordset("UnitMoreInfoQ")
This is the first code:
Dim rec As Recordset
Dim db As Database
Dim X As Variant
Set db = CurrentDb
Set rec = db.OpenRecordset("ProjectMoreInfoQ")
Const msgTitle As String = "Open Explorer"
Const cExplorerPath As String = "C:\WINDOWS\EXPLORER.EXE"
Const cExplorerSwitches As String = " /n,/e"
cFilePath = rec("ProjFilePath")
As you can see, the line has the same amount of parameters:
Set rec = db.OpenRecordset("ProjectMoreInfoQ")
This has gotten me quite confused for awhile because of this. How do I fix this error?
I didn't get the same result as you when testing your db, and I still don't understand the difference. However, maybe we can still get you something which works in spite of my confusion.
The query contains 6 references to form controls, such as [Forms]![WorkOrderDatabaseF]![Text71]. Although you're certain that form is open in Form View when you hit the "too few parameters" error at db.OpenRecordset("UnitMoreInfoQ"), Access doesn't retrieve the values and expects you to supply them.
So revise the code to supply those parameter values.
Dim rec As DAO.Recordset
Dim db As DAO.database
Dim prm As DAO.Parameter
Dim qdf As DAO.QueryDef
Dim X As Variant
Set db = CurrentDb
'Set rec = db.OpenRecordset("UnitMoreInfoQ")
Set qdf = db.QueryDefs("UnitMoreInfoQ")
For Each prm In qdf.Parameters
prm.value = Eval(prm.Name)
Next
Set rec = qdf.OpenRecordset(dbOpenDynaset) ' adjust options as needed
I'm leaving the remainder of this original answer below in case it may be useful for anyone else trying to work through a similar problem. But my best guess is this code change will get you what you want, and it should work if that form is open in Form View.
Run this statement in the Immediate window. (You can use Ctrl+g to open the Immediate window.)
DoCmd.OpenQuery "UnitMoreInfoQ"
When Access opens the query, it will ask you to supply a value for the first parameter it identifies. The name of that parameter is included in the parameter input dialog. It will ask for values for each of the parameters.
Compare those "parameter names" to your query's SQL. Generally something is misspelled.
Using the copy of your db, DoCmd.OpenQuery("UnitMoreInfoQ") asks me for 6 parameters.
Here is what I see in the Immediate window:
? CurrentDb.QueryDefs("UnitMoreInfoQ").Parameters.Count
6
for each prm in CurrentDb.QueryDefs("UnitMoreInfoQ").Parameters : _
? prm.name : next
[Forms]![WorkOrderDatabaseF]![Text71]
[Forms]![WorkOrderDatabaseF]![ClientNameTxt]
[Forms]![WorkOrderDatabaseF]![WorkOrderNumberTxt]
[Forms]![WorkOrderDatabaseF]![TrakwareNumberTxt]
[Forms]![WorkOrderDatabaseF]![WorkOrderCompleteChkBx]
[Forms]![WorkOrderDatabaseF]![WorkOrderDueDateTxt]
Make sure there is a form named WorkOrderDatabaseF open in Form View when you run this code:
Set rec = db.OpenRecordset("UnitMoreInfoQ")
Does the [UnitMoreInfoQ] query execute properly on its own? If you mistype a field in access it will treat that field as a parameter.
ProjectMoreInfoQ and UnitMoreInfoQ are different queries... it sounds like one takes 6 parameters and the other doesn't. Look at the queries in Access and see if either have parameters defined.

How can I evaluate a string into an object in VBA?

In my previous question, How do I assign a value to a property where the property name is supplied at runtime in VBA?, I learned to use CallByName to set a property in a class at run time.
This time, however, I'm trying to figure out how to get an object at run time from a string.
For example, let's say I have a string with the following data: Worksheets("RAW DATA").Range("A1").QueryTable.
Here's what I might try to do where the data above is the input for strParam below:
Function GetObject(strParam As String) As Object
GetObject = SomeFunction(strParam)
End Function
In this case, GetObject should return a QueryTable when evaluated against Worksheets("RAW DATA").Range("A1").QueryTable. Is there anything in VBA that could take the place of SomeFunction from the example above?
Active Scripting Engine can help you. Instantiate ScriptControl ActiveX, use .AddObject() method to add reference to Excel's Application object to the script control's execution environment, set the third parameter to True to make all Application's members accessible too. Then just use .Eval() method to evaluate any property or method, which is the Application's member. The example below shows evaluation of Worksheets() property:
Sub TestQueryTable()
Dim objQueryTable As QueryTable
Dim strEvalContent As String
strEvalContent = "Worksheets(""RAW DATA"").Range(""A1"").QueryTable"
Set objQueryTable = EvalObject(strEvalContent)
objQueryTable.Refresh
MsgBox objQueryTable.Connection
End Sub
Function EvalObject(strEvalContent As String) As Object
With CreateObject("ScriptControl")
.Language = "VBScript"
.AddObject "app", Application, True
Set EvalObject = .Eval(strEvalContent)
End With
End Function
If you are on 64-bit Office, this answer may help you to get ScriptControl to work.
This time you're out of luck. There is no VBA equivalent of eval (not in Excel anyway...there is in Access VBA).
(Application.Evaluate() evaluates strings as Excel expressions, not as VBA code.)
There's the "Evaluate" method (or [ ] brackets). I don't think it will do exactly what you expect - as in run VBA code found in a string. You can look it up in the VBA help menu.