Storing recordset as instance of a class? - excel

Have the following scenario. I have a few form, which essentially have a few dropboxes, lists etc. I populate them with records from a ms sql db. However, is there a way to query the database only once and store the records as an instance of a class throughout the life of the application rather than querying each time the user opens the form?
Connection is as this:
Sub connection_test()
Dim Cn As ADODB.Connection
Dim Rs As ADODB.Recordset
Dim stSQL As String
stSQL = "SELECT * FROM dbo.Client"
Set Cn = New ADODB.Connection
With Cn
.CursorLocation = adUseClient
.Open CONNECTION_STRING
.CommandTimeout = 0
Set Rs = .Execute(stSQL)
End With
Rs.Close
Cn.Close
Set Rs = Nothing
Set Cn = Nothing
End Sub
Can someone suggest a solution for me? I can see all the problems from heavy traffic and mostly unnecessary.

If you just want the recordset available, dim the variable as public in a standard module
Public Rs As ADODB.Recordset
Sub connection_test()
Dim Cn As ADODB.Connection
Dim sSQL As String
If Rs.State = adStateClosed Then
sSQL = "SELECT * FROM dbo.Client"
Set Cn = New ADODB.Connection
With Cn
.CursorLocation = adUseClient
.Open CONNECTION_STRING
.CommandTimeout = 0
Set Rs = .Execute(sSQL)
End With
End If
End Sub
Now Rs will be available anywhere in the project. You can run connection_test whenever you need to and, if the recordset it closed, it will create it. If not, you're good to go.
Generally my approach to this is to create custom classes. I would create a CClient class, fill it from a recordset (or somewhere else), manipulate the objects with the business logic, then write the new values back to the database. That way, none of my business logic relies on the fact that I'm using ado. I could switch to a text file or an Excel worksheet as a data store and wouldn't have to worry about dependencies everywhere in the code.
For instance, suppose I have an Access table:
ClientID, Autonumber
ContactFirst, String
ContactLast, String
Company, String
CityState, String
Volume, Double
I create a CClient class with a property for each field in my table. I also create a CClients class to hold all of the CClient instances. In a standard module, you might have something like this
Public gclsClients As CClients
Sub Main()
Set gclsClients = New CClients
'Fill the class
gclsClients.FillFromRS
'change some value
gclsClients.Client(1).Volume = 100
'write back to the database
gclsClients.WriteToDB
End Sub
Where I change the volume of one client, you would have a lot more code calling your userform, etc. The basics are, load up the class, do whatever you need, then write the class data back to the db. I won't show you all the class code, but in CClients
Public Sub FillFromRS()
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim clsClient As CClient
Const sSQL As String = "SELECT * FROM tblClient"
Set cn = New ADODB.Connection
cn.Open msCON
Set rs = cn.Execute(sSQL)
If Not rs.BOF And Not rs.EOF Then
rs.MoveFirst
Do While Not rs.EOF
Set clsClient = New CClient
With clsClient
.ClientID = rs.Fields("ClientID").Value
.ContactFirst = rs.Fields("ContactFirst").Value
.ContactLast = rs.Fields("ContactLast").Value
.Company = rs.Fields("Company").Value
.CityState = rs.Fields("CityState").Value
.Volume = rs.Fields("Volume").Value
End With
Me.Add clsClient
rs.MoveNext
Loop
End If
End Sub
This method gets the data from the database and fills a bunch of CClient instances. Also in CClients
Public Sub WriteToDB()
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim clsClient As CClient
Dim sSQL As String
Set cn = New ADODB.Connection
cn.Open msCON
For Each clsClient In Me
sSQL = BuildUpdateSQL(clsClient)
cn.Execute sSQL
Next clsClient
End Sub
Private Function BuildUpdateSQL(clsClient As CClient)
Dim sReturn As String
With clsClient
sReturn = "UPDATE tblClient SET ContactFirst = '" & .ContactFirst & "',"
sReturn = sReturn & " ContactLast = '" & .ContactLast & "',"
sReturn = sReturn & " Company = '" & .Company & "',"
sReturn = sReturn & " CityState = '" & .CityState & "',"
sReturn = sReturn & " Volume = " & .Volume
sReturn = sReturn & " WHERE ClientID = " & .ClientID & ";"
End With
BuildUpdateSQL = sReturn
End Function
This method loops through all of the CClient instances, creates an UPDATE sql statement and executes it. You'll want to implement some sort of IsDirty property in CClient so that you only update those client where something is changed. The rest of CClients and CClient are basic class module stuff.
You can call WriteToDB a lot or a little. In some apps, I write it whenever something changes. In others, I only write back to the database when the workbook is closed. It kind of depends on the flow of your application. The real beauty is that if you change from, say, an Access database to a text file for data storage, you only have to change to methods in CClients. All of the rest of your code consumes CClients and doesn't care where the data lives.
You can see the workbook and Access database here http://www.dailydoseofexcel.com/excel/ClientClassExample.zip

Read up on Disconnected Recordsets here.
One thing that the article does include (in the sample code), but doesn't emphasize is that you have to use the adLockBatchOptimistic. You do not have to use adOpenForwardOnly, as they do. And, of course, your Recordset object has to have a scope outside the sub. I'd do it this way:
Function connection_test() as ADODB.Recordset
Dim Cn As ADODB.Connection
Dim Rs As ADODB.Recordset
Dim stSQL As String
stSQL = "SELECT * FROM dbo.Client"
Set Cn = New ADODB.Connection
With Cn
.Open CONNECTION_STRING
.CommandTimeout = 0
End With
With Rs
.CursorLocation = adUseClient
.LockType = adLockBatchOptimistic
.Open stSQL
Set .ActiveConnection = Nothing
End With
Set connection_test = Rs
'DON'T Close the Recordset
'Rs.Close
Cn.Close
'Destroying the local instance is fine, though
Set Rs = Nothing
Set Cn = Nothing
End Function
And of course you want to add error handling ;). Then have a Recordset object declared in the calling code, and instantiate it by calling the Function.

Related

Updating MS Access record using Excel vba with record Id

I am trying to update 4 cells in Excel 2010 to my MS Access 2010 using VBA the following code. The problem is that sometimes it update those fields in the table record and other times it is not. I wonder if anyone can shed some light on my code on what is causing to be inconsistent.
'Add a reference to the Microsoft ActiveX Data 2.8 or later Object Library
'via the Tool | References... in the VB-editor
Dim cnt As ADODB.Connection
Dim rst As ADODB.Recordset
Dim stDB As String
Dim stCon As String
'Instantiate the ADO COM's objects.
Set cnt = New ADODB.Connection
Set rst = New ADODB.Recordset
'Pathway and name of the database
stDB = "P:\Quote Log.mdb"
'Create the connectionstring.
stCon = "Provider=Microsoft.Ace.OLEDB.12.0; Persist Security Info = False;" & _
"Data Source=" & stDB & ";"
'Open the connection
cnt.Open stCon
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'Find and Update the record in Access
With rst
.Index = "PrimaryKey"
.CursorLocation = adUseServer
.Open "tbQuote", cnt, 1, 3, adCmdTableDirect
.Seek Range("dx32").Value
If Not rst.EOF Then
.Fields("QuoteNum") = Sheets("Quote Notice").Range("dx32").Value
.Fields("OtherCost") = Sheets("Cost").Range("b3").Value
.Fields("StocklistCost") = Sheets("Cost").Range("b4").Value
.Fields("DesignHrs") = Sheets("Cost").Range("b5").Value
.Fields("ProductionHrs") = Sheets("Cost").Range("b6").Value
.Update
Else
MsgBox "No such record...not updating."
End If
End With
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'Close the recordset and close the connection.
rst.Close
cnt.Close
Set cnt = Nothing
ExitSub:
Set rs = Nothing
'...and set to nothing if no such record
Exit Sub
thank you,
hughv
Okay, discovered my problem...this line needs to reference to the sheet and cell correctly.
.Seek Range("dx32").Value
to:
.seek Worksheet("Quote Notice").range("dx32").Value

VBA: How to add entire row of recordset into form list

I have Excel VBA code that runs a select query on an access database and returns a recordset. The connection works fine. When I get the recordset, I can access each field in each row fine. Here is what I am currently using:
Sub accessSelect(sql)
Set rs = New ADODB.Recordset
Call accessConnection
With rs
.Open sql, conn
End With
Do While Not rs.EOF
'rtvEditForm.rtvList is a list box.
rtvEditForm.rtvList.AddItem rs(1) & " - " & rs(2)
rs.MoveNext
Loop
End Sub
I want to be able to throw the whole rs in the rtvEditForm.rtvList without having to refer to each field. I've tried rtvEditForm.rtvList.AddItem rs, but that does not work because I get a "Type Mismatch" error.
Seems simple but I just can't figure this out. How would I do that with my current code?
No need to do any looping. A listbox has a recordset property which can be bound to an ADO recordset.
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Set cn = New ADODB.Connection
Set cn = CurrentProject.Connection
Set rs = New ADODB.Recordset
With rs
Set .ActiveConnection = cn
.Source = sql
.LockType = adLockOptimistic
.CursorType = adOpenKeyset
.Open
End With
Set Me.rtvList.Recordset = rs
rtvList.ColumnCount = rs.Fields.Count

When I call a query from Access using VBA, I can't know how many records are on that query. Why?

I did a query on MS Access 2010, with the Query Builder, now I'm calling it from VBA. But when I try to use the recordcount to know how many records are on this query, the result is -1.
The code:
Public Function teste(Optional ByVal consulta As String, Optional ByVal cbAR_valor As String, Optional ByVal cbAR_valor2 As String)
Dim DB As ADODB.Connection
Dim RST As ADODB.Recordset
Dim Query As ADODB.Command
Dim parametro1 As ADODB.Parameter
Dim parametro2 As ADODB.Parameter
On Error GoTo trataErro
DB_Dir = ThisWorkbook.Path & "\" & "DB2.accdb"
Set DB = New ADODB.Connection
DB.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & DB_Dir & ";Persist Security Info=False"
Set Query = New ADODB.Command
Set Query.ActiveConnection = DB
Query.CommandText = consulta
Query.CommandType = adCmdStoredProc
' Pego o valor do param. e adiciono no command
Set parametro1 = Query.CreateParameter("AR1", adChar, adParamInput, 255)
Query.Parameters.Append parametro1
parametro1.Value = cbAR_valor
' Pego o valor do param. e adiciono no command
Set parametro2 = Query.CreateParameter("AR2", adChar, adParamInput, 255)
Query.Parameters.Append parametro2
parametro2.Value = cbAR_valor2
Set RST = Query.Execute
a = RST.RecordCount
linha = 1
coluna = 1
ActiveWorkbook.Sheets("Plan2").Cells(CInt(linha), CInt(coluna)).CopyFromRecordset RST
DB.Close
Set DB = Nothing
Exit Function
trataErro:
MsgBox ("Erro: " & Err.Description)
End Function
RecordCount is tricky. Depending on the provider, data source type, and options, it may not be reliable.
For an ADO recordset, you are more likely to get a reliable RecordCount with a client-side cursor:
Set DB = New ADODB.Connection
DB.CursorLocation = adUseClient '<- add this
DB.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & DB_Dir & ";Persist Security Info=False"
I assumed your procedure's consulta argument is a SQL statement. If that is correct, use adCmdText (instead of adCmdStoredProc) for CommandType:
Set Query = New ADODB.Command
'Set Query.ActiveConnection = DB ' Set not needed here ...
Query.ActiveConnection = DB
Query.CommandText = consulta
'Query.CommandType = adCmdStoredProc
Query.CommandType = adCmdText
If those changes were not sufficient to get you a reliable RecordCount, use MoveLast to ensure the recordset is fully populated before asking for RecordCount:
Set RST = Query.Execute
RST.MoveLast
MsgBox "RecordCount: " & RST.RecordCount
Add RST.MoveFirst afterward if you need the recordset pointer to be on the first row in preparation for your remaining operations.
When I've had trouble with counting records before, the issue was with the way the recordset was opened, not the connection. The cursor type needs to be static for it to work. This may be very similar to what HansUp was referring to with the cursorlocation in the connection object, though.
rec.Open querystr, con, 3
rec.movelast
cnt = rec.RecordCount
rec.movefirst

Using ADO to connect to Access database from Excel error 3704

When I use the code below to connect to an access database from Excel, I get error 3704 "Operation is not allowed when the object is closed." at line
Call .Offset(1, 0).CopyFromRecordset(rstRecordSet)
I can 'fix' this problem by commenting out the line
adoConnection.Close
but I really don't like that, or understand why it solves the problem.
Can anyone explain what's wrong and how to fix it?
Thanks
Private Const constStrDBPath As String = "H:\Projects\DP.mdb"
Private Const constStrConnection As String = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
"Data Source=" & constStrDBPath & ";" & _
"Jet OLEDB:Engine Type=5;" & _
"Persist Security Info=False;"
Public Function SelectStatement(strCommandText As String) As Object
Dim adoConnection As New ADODB.Connection
Dim adoCommand As New ADODB.Command
Dim rstRecordSet As New ADODB.Recordset
adoCommand.CommandText = strCommandText
adoConnection.Open constStrConnection
adoCommand.ActiveConnection = adoConnection
'create the recordset by executing command string
Set rstRecordSet = adoCommand.Execute(, , adadoCommandText)
Set SelectStatement = rstRecordSet
' clean up
adoConnection.Close
Set rstRecordSet = Nothing
Set adoConnection = Nothing
Set adoCommand = Nothing
End Function
Sub TestSelect()
Dim rstRecordSet As Object
Dim lngField As Long
Set rstRecordSet = SelectStatement("SELECT * FROM tblSystem")
If Not rstRecordSet Is Nothing Then
With Sheet1.Range("A1")
For lngField = 1 To rstRecordSet.Fields.Count
.Cells(1, lngField).Value = rstRecordSet.Fields(lngField - 1).Name
Next lngField
Call .Offset(1, 0).CopyFromRecordset(rstRecordSet)
End With
End If
End Sub
Your function SelectStatement is not fetching (reading) anything from the database.
In plain terms your actual SelectStatement() opens a channel with Access, prepares the query, sets a cursor and then throws away all.
The cleanup phase has to be postponed after having read the last recordset.

How to return a recordset from a function

I'm building a data access layer in Excel VBA and having trouble returning a recordset. The Execute() function in my class is definitely retrieving a row from the database, but doesn't seem to be returning anything.
The following function is contained in a class called DataAccessLayer. The class contains functions Connect and Disconnect which handle opening and closing the connection.
Public Function Execute(ByVal sqlQuery As String) As ADODB.recordset
Dim rs As ADODB.recordset
Set rs = New ADODB.recordset
Dim recordsAffected As Long
' Make sure we're connected to the database.
If Connect Then
Set command = New ADODB.command
With command
.ActiveConnection = connection
.CommandText = sqlQuery
.CommandType = adCmdText
End With
'Set rs = command.Execute(recordsAffected)
'Set Execute = command.Execute(recordsAffected)
rs.Open command.Execute(recordsAffected)
rs.ActiveConnection = Nothing
Set Execute = rs
Set command = Nothing
Call Disconnect
End If
End Function
Here's a public function that I'm using in cell A1 of my spreadsheet for testing.
Public Function Scott_Test()
Dim Database As New DataAccessLayer
'Dim rs As ADODB.recordset
'Set rs = CreateObject("ADODB.Recordset")
Set rs = New ADODB.recordset
Set rs = Database.Execute("SELECT item_desc_1 FROM imitmidx_sql WHERE item_no = '11001'")
'rs.Open Database.Execute("SELECT item_desc_1 FROM imitmidx_sql WHERE item_no = '11001'")
'rs.Open
' This never displays.
MsgBox rs.EOF
If Not rs.EOF Then
' This is displaying #VALUE! in cell A1.
Scott_Test = rs!item_desc_1
rs.Close
End If
rs.ActiveConnection = Nothing
Set rs = Nothing
End Function
What am I doing wrong?
The problem was with setting the ActiveConnection = Nothing. The following code works:
Public Function Execute(ByVal sqlQuery As String) As ADODB.recordset
Dim rs As ADODB.recordset
Set rs = New ADODB.recordset
Dim recordsAffected As Long
' Make sure we are connected to the database.
If Connect Then
Set command = New ADODB.command
With command
.ActiveConnection = connection
.CommandText = sqlQuery
.CommandType = adCmdText
End With
rs.Open command.Execute(recordsAffected)
Set Execute = rs
Set command = Nothing
Call Disconnect
End If
End Function
Set Execute = recordset
creates a pointer to recordset, which you close on exiting the function.
Thats's why it can't contain anything.
I am also relectant on your variable names which are identical to possibe reserved words (recordset). I generally use rs or rsIn or rsWhateverYouWant...
As mentioned by Patrick, the recordset is a pointer.
The Caller 'Scott_Test' should call recordset.Close instead.
The Execute method CANNOT call recordset.Close, however I believe it is OK to leave the recordset.ActiveConnection = Nothing

Resources