VBA: Faster Recordset connection - ms-access

I have the following code to access a recordset from SQL Server 2008.
Dim cnn As Object
Dim rs As Object
Dim strSQL As String
Set cnn = CreateObject("ADODB.Connection") 'ADO Connection
cnn.ConnectionString = "DRIVER=SQL Server;SERVER=" & dbServer & ";" & _
"Trusted_Connection=Yes;DATABASE=" & dbDatabase
cnn.Open
strSQL = "Select * from mytable"
Set rs = CreateObject("ADODB.Recordset")
rs.Open strSQL, cnn, adOpenKeyset, adLockOptimistic
On rs.Open it takes from one second to three seconds. Is there a faster way to access the recordset?
The variables dbServer and dbDatabase are defined in a module as a constant string.

Assuming mytable is a base table rather than a VIEW, the SQL is as trivial as is possible i.e. there is no scope for optimization.
If you are bringing the entire table contents over to do further RBAR ("row by agonizing row" = pejorative) processing in VBA code then consider rewriting your procedural logic as a "set-based" paradigm using more complex SQL e.g. adding a WHERE clause would be a good start.
If you do need to bring the entire table contents over and the table is relatively large then it will take a relatively long time. Again, there is no scope in your code for optimization.
You are currently performing a synchronous fetch, meaning execution of your VBA code will wait at the rs.Open line until the fetch is complete. The effect for your users will be that the application appears frozen, possibly crashed.
An alternative approach is to use an asynchronous fetch. Relocate your code class module, declare the recordset as WIthEvents (needs to be relocated to module level) and specify the adAsyncFetch option on the recordset before opening it (you may need to alter your approach slightly to accommodate this). This will cause execution to continue past the rs.Open as soon as the request has been sent (so you will need to relocate any code that closes the connection/recordset to a separate sub procedure). You can then use the FetchProgress and FetchComplete events to give feedback to your end users in the application. For more details, see this MSDN article.
I've never been able to get any meaningful 'percent completed' style progress out of the Access engine/provider. But you can at least show a marquee style progress bar to users to reassure them that data is being fetched. They will be able to still interact with the UI (e.g. click on controls) and be reassured that the application has not crashed.

You have 2 operations:
open connection
open recordset using that connection
Where is the delay?
You could add line for Debug.Print Now() immediately before the two open statements, and another after the recordset open.
If opening the connection is the bottleneck, see whether using the native client provider speeds things up. www.connectionstrings.com: SQL Server Native Client 10.0 OLE DB Provider
If opening the recordset is the bottleneck, you have several options. If you don't actually need a dynamic/editable recordset, use adOpenStatic instead of adOpenKeyset. You could try CommandTypeEnum adCmdTable to open the table instead of using a SELECT statement which returns everything. Or with the SELECT statement, include an explicit field list instead of "*" ... because "*" requires an extra trip to the server to figure out which fields it represents. (Although that's probably not a big bottleneck.) And you could also try a WHERE clause to select a single record based on a primary key value.

Related

Records cannot be deleted with this form access error

I have made a search box in Access that will set the Access form's recordset to ADO recordset once the result is found.
The code is
rs.Open "select * from main where Name= '" & Me.txtSearch.Value & "';", CurrentProject.Connection, adOpenDynamic, adLockOptimistic
Set Me.Recordset = rs
This lets me find the records much faster than default Access ctrl+f method which is slower with linked tables.
However, it looks like I cannot delete the records that were found using the search field that I made.
If i try to delete it, I get:
Records cannot be deleted with this form access
at the bottom of the form.
Is there a way to have deletable ADO record bound to Access form?
The reason the ADO bound form was not editable is not because of cursor type or lock type but because of cursor location. Adding
rs.CursorLocation = adUseClient
did the job for me.
Also in this discussion thread Dirk Goldgar writes:
One possible consideration is that, if you're binding an Access form
to an ADO recordset, I've found that using a server-side cursor makes
the form read-only.
So far, I have not found any explanation on why this is the case. Please reply if anyone knows why server side cursor bound to Access form makes it uneditable.
EDIT 1:
Also Made Sure that you are using currentproject.connection and not an ADO connection string

Classic ASP VBScript need dropin replacement for MyConn.Execute( query )

I have a site running on ASP VBScript, and the original code never closes a DB connection. It opens connections as part of "startup" for any given page, then does whatever it does and stops -- but never explicitly closes connections. This is now causing problems where things are crashing at the web server level -- presumably from the lack of garbage collection.
So I want to make a function that acts as a drop-in replacement for all the MyConn.Execute( sqlQuery ) commands throughout the site. I've found some good candidates, but none of them seem to quite work. The most promising appears to be the code below, but when I try to actually use the recordset returned I get an error.
Function GetRS(strSQL)
'this function returns a disconnected RS
'Set some constants
Const adOpenStatic = 3
Const adUseClient = 3
Const adLockBatchOptimistic = 4
'Declare our variables
Dim oConn
Dim strSQL
Dim oRS
'Open a connection
Set oConn = Server.CreateObject("ADODB.Connection")
oConn.ConnectionString = "Driver={MySQL ODBC 5.3 Unicode Driver};Server=localhost;User=foo;Password=bar;Database=baz"
oConn.Open
'Create the Recordset object
Set oRS = Server.CreateObject("ADODB.Recordset")
oRS.CursorLocation = adUseClient
'Populate the Recordset object with a SQL query
oRS.Open strSQL, oConn, adOpenStatic, adLockBatchOptimistic
'Disconnect the Recordset
Set oRS.ActiveConnection = Nothing
'Return the Recordset
Set GetRS = oRS
'Clean up...
oConn.Close
oRS.Close
Set oConn = Nothing
Set oRS = Nothing
End Function
'call the function
strSQL = "SELECT * FROM Authors"
set RS = GetRS(strSQL)
(source: https://web.archive.org/web/20211020134116/https://www.4guysfromrolla.com/webtech/080101-1.shtml)
Here's my test code:
Set rs = GetRS( "SELECT `first_name` FROM `users` WHERE `id`=123" )
x = rs( "first_name" )
response.write x
I get the error:
ADODB.Recordset error '800a0cc1'
Item cannot be found in the collection corresponding to the requested name or ordinal.
/test.asp, line 25
Using an ordinal -- rs(0) -- returns the same error.
Looks like an empty recordset to me, but it's a legit Query that does return a record.
Does anyone know why this isn't working, or can point me to other code that will do the job? (Especially with practical usage examples, which the 4guys article lacks.)
My understanding is that a a Recordset is tied to the datasource. When you execute a query, by default, the client (your program) doesn't get the entire contents of the query, but will wait until you actually request the data. That way, you can choose a specific page size, page offset etc. to efficiently select rows from the database without transferring potentially millions of rows over the wire.
As a side-effect of this, if you close the database connection, you will no longer be able to use the Recordset. You must leave the connection open until you are done with it. In the same way, closing the Recordset itself will stop you from being able to interact with it further.
You have two options: copy the data out of the Recordset into your own variables/arrays before closing, or use a different technique to manage your connection. I'll talk about the latter option here.
There is technique which will allow you to open the DB connection once, and ensure it is closed properly by VBScript when it terminates.
Class DbConnectionManager
Private Sub Class_Initialize()
Set oConn = Server.CreateObject("ADODB.Connection")
oConn.ConnectionString = "Driver={MySQL ODBC 5.3 Unicode Driver};Server=localhost;User=foo;Password=bar;Database=baz"
oConn.Open
End Sub
Private Sub Class_Terminate()
oConn.Close
End Sub
End Class
Dim connMgr : Set connMgr = New DbConnectionManager
This code snippet is untested, but the general principle is that you start your program by defining a class and creating an instance of it. When a class instance is created, Class_Initialize is called, and when your program ends (or the instance gets removed and garbage-collected), then Class_Terminate will be called. That means oConn.Close should always be called before your program ends, even in the event of an error.
This is a very basic example of how classes work, but you could actually extend the class further and insert your Execute functions into the class itself to encapsulate the database connection details for easier maintenance. If you haven't used classes in VBScript yet but you have a basic understanding of how Object-Oriented programming works, I would highly recommend you try that.
Bonus extra: It looks like you're passing in raw SQL strings. To avoid SQL injection vulnerabilities, don't build your SQL queries dynamically. Instead, use ADO and parameters so any user-created content can be safely passed into the query without security risks. How do I run a parameterized SQL query in classic ASP? And is it secure? W3Schools also has a section on how to use ADO.

Usage of DoEvents() in VB 6.0

I am having one case and need your advice whether to use DoEvents() / Any Other / Nothing to use.
I have developed an application in VB 6.0 before 5-6 Years and is working fine.
Now From some time as Data is increased (MS Access), It gives some unexpected result.
I am using DbName.Execute "...Query to Update Tables..." and then after this line I had Used DoEvents(), to let DbName.Execute Query to be completed first and then go ahead with the rest of the code.
So Is it the right use of DoEvents(), as I had monitored that in some Advanced CPU, the problems occurs due to non executing the Query / Query is Running Still the rest of the code is executed
I actually want to Stop executing further code, once the Query Executed Completely, Then I want to execute further code
Please Guide !
The DAO and ADO .Execute methods both operate synchronously by default. In other words, the next line of code does not execute until the query finishes processing. You do not need to use DoEvents or While Loops or anything else.
You can force the Execute method to run asynchronously by setting the option flag dbRunAsync in DAO or adAsyncExecute in ADO. If either of these flags is set (you don't specify whether you are using DAO or ADO) then simply deleting them from the method call will force your code to wait until the query is done before going to the next line.
I think you can ensure your Execute operation has been completed and written to disk by operating it within a transaction. Here's what Access 2003 Execute Method help topic says about transactions:
For best performance in a Microsoft Jet workspace, especially in a multiuser environment, nest the Execute method inside a transaction. Use the BeginTrans method on the current Workspace object, then use the Execute method, and complete the transaction by using the CommitTrans method on the Workspace. This saves changes on disk and frees any locks placed while the query is running.
See if an approach likes this eliminates your unexpected result. If not, describe the unexpected result you're hoping to avoid.
Dim objWorkspace As DAO.Workspace
Dim db As DAO.Database
Dim strSql As String
On Error GoTo ErrorHandler
Set objWorkspace = DBEngine.Workspaces(0)
Set db = CurrentDb
objWorkspace.BeginTrans
strSql = "UPDATE YourTable SET some_field = Null;"
db.Execute strSql, dbFailOnError
'* additional db.Execute operations if desired *'
objWorkspace.CommitTrans
ExitHere:
On Error GoTo 0
Set db = Nothing
Set objWorkspace = Nothing
Exit Sub
ErrorHandler:
objWorkspace.Rollback
GoTo ExitHere
Yes, I got the same idea. I would do the following as you asked "whether or not to use DoEvents()
Dim i as long
i = 0
Do
i = i + 1
DoEvents() ' Is this what you were asking?
Loop Until i = 5000

Learning MS Access

i am new to ms-access, and i have started to look at code that works. unfortunately it is completely uncommented.
Private Sub OKbut_Click()
Dim dt As Date
dt = Now()
Set rstOrder = New ADODB.Recordset
rstOrder.Open "tblUsers", CurrentProject.Connection, adOpenStatic, adLockOptimistic
If rstOrder.Supports(adAddNew) Then
With rstOrder
.AddNew
.Fields("title") = title
.Fields("first") = first
.Fields("last") = last
.Fields("gender") = gender
.Fields("date_submitted") = dt
.Update
End With
End If
rstOrder.Close
Set rstOrder = Nothing
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("SELECT id FROM tblUsers WHERE date_submitted=#" & dt & "#")
duser = rs.Fields("id")
rs.Close
Set rs = Nothing
Do While Not user_defined(duser)
DoCmd.OpenForm "define_user_frm", , , , , acDialog
Loop
'Forms(0).user_lst.RowSource = "select * from users where id=" & duser
Me.SetFocus
DoCmd.Close
End Sub
what does the function Now() do?
ADODB.Recordset is just a way to connect to a table?
adOpenStatic, adLockOptimistic what are these?
why are we checking this: if rstOrder.Supports(adAddNew) ?
why do we need to do this: Set rstOrder = Nothing?
what does this do:? Do While Not user_defined(duser)
DoCmd.OpenForm "define_user_frm", , , , , acDialog
Loop
Approaching existing code like this isn't the best way to learn to program. There are plenty of online tutorials that will help you get started and progress in a linear manner. The other advice I'd give is to try things out for yourself. Write a small method that contains "MsgBox Now()" and run it to see what happens. Then run it a couple of seconds later.
I can't over-recommend finding a beginners tutorial.
However that said . . .
Now - returns the computers system date and time, at the moment it's called
Yes - Among other things.
Hints to the query engine. Open Static says you don't want to see rows added after the record set is retrieved, optimistic locking tells the engine which record locking option to use (the other is pessemistic)
Checking this because you can't add records to all resordsets (e.g. complex queries joining several tables)
Not stricly necessary, but better for memory management, otherwise the recordset object (and all associated resources - memory, handles etc) wouldn't be disposed until the end of the method.
Opens an ms access form until the condition "user_defined(duser)" returns true
Hope this helps.
what does the function Now() do?
Now() returns the current system date and time
ADODB.Recordset is just a way to connect to a table?
Yes - more importantly, it is a good way to iterate through records of a dataset individually. Like a cursor in SQL Server. You could do something like
While not rstOrder.EOF
'a.k.a. while there are still records left to iterate through
'Do something
End While
adOpenStatic, adLockOptimistic what are these?
From http://www.dotnetspider.com/forum/16958-In-VB-What-difference-between-adopendynamic.aspx
adOpenForwardOnly - this is the default cursor if no other is specified. This cursor allows only forward movement through a recordset
adOpenKeyset - this cursor supports forwards as well as backwards navigation. It also allows you to update a recordset and all changes will be reflected in other users recordsets. The cursor also supports bookmarking
adOpenDynamic - this cursor supports forward and backward navigation but bookmarks may not be supported (ie Access). Any changes made to data are immediately visible with no need to resynchronise the cursor with the database
adOpenStatic - this cursor uses a static copy of data from the database and therefore no changes to the data are visible and supports forward and backward navigation
why are we checking this: if rstOrder.Supports(adAddNew) ?
It's a way of writing more robust code - i.e. before attempting to add a new record, first check whether the recordset supports the addition of new records.
why do we need to do this: Set rstOrder = Nothing?
Frees the recordset from memory. Not absolutely necessary but again, makes for more robust code.
what does this do:? Do While Not user_defined(duser) DoCmd.OpenForm "define_user_frm", , , , , acDialog Loop
Checks for existence of a user and if it doesn't exist, it opens a form called "define_user_frm" which I assume allows for creating a new user.
what does the function Now() do?
Place the cursor on the word Now in your subroutine, then press the F1 key. Is Access' help not helpful for answering your question?
I'm trying to suggest it should be quicker for you to use the built-in Help feature for a question like that, instead of posting to Stack Overflow and waiting for responses. Then you only need post the questions that Help doesn't answer adequately for you.
I'm very rusty with VB/Access but:
1) Now returns the current date and time
2) Recordset is a set of records - from memory doesn't have to be a table, could be a query.
3) Those are flags to indicate how the table (as it is a table in this case) should be accessed can't remember static but the Lock says that we're going to use optimistic locking i.e. we will basically assume that the record won't be changed by anyone else.
4) Because you can open recordsets in a number of different ways - so it may well be the case that you have a read-only set of data. In the context of the example its notionally redundant but in terms of good practice its a reasonable bit of defensive programming.
5) Probably don't but again its sensible defensive programming and it ensures that all resources associated with the recordset are released. rstOrder is coming from somewhere external to the sub so not leaving it in an open state is sensible.
6) The loop repeats until the function user_defined(duser) returns true. duser being the - erk, that looks horrid - ID returned by the query but that doesn't look safe. DoCmd.Open form is popping up the "define_user_form" as a dialog so... if the user doesn't exist then fire up the dialog to define the user then check again that the user has been defined. That's also a bit questionable (although of course without a bit more context its hard to tell).
Basically VBA in Access is mostly VB6 but with sufficient differences to drive one slightly mad... there's a whole pile of stuff that you can assume to exist in access that you have to be rather more explicit about in VB.OLD
what does the function Now() do?
Returns a Variant (Date) specifying the current date and time according your computer's system date and time.
ADODB.Recordset is just a way to connect to a table?
The ADO Recordset object is used to hold a set of records from a database table. A Recordset object consist of records and columns (fields). It can be used to open tables, queries, custom SQL statements, etc.
adOpenStatic, adLockOptimistic what are these?
adOpenStatic: Provides a static copy of the records (you can't see additions, changes or deletions by other users), but all types of movement are enabled (e.g. .moveNext, .MoveFirst, .moveLast, etc).
adLockOptimistic: Record locking is performed only when you call the Update method.
why are we checking this: if rstOrder.Supports(adAddNew) ?
Checking whether the recordset allows new records to be added.
why do we need to do this: Set rstOrder = Nothing?
When you're completely finished with rstOrder, setting it equal to Nothing removes it from memory. You can think of it as a simple version of garbage collection.
what does this do:? Do While Not user_defined(duser) DoCmd.OpenForm
"define_user_frm", , , , , acDialog
Loop
Assuming user_defined() is a custom function, it loops through the record just added and opens a form that allows you to define the user.
Reference:
http://www.w3schools.com/ado/ado_ref_recordset.asp
what does the function Now() do? - Creates a DateTime for the current date and time the function is being run.
ADODB.Recordset is just a way to connect to a table? This is an object or saying that rstOrder is not a ABDODM.Recordset with a method to open the connection to the DB.
why do we need to do this: Set rstOrder = Nothing? - This is cleaning up the connection so that the computer does not keep the memory and for rstOrder. Other wise after running your computer might slow down or have less memory.
what does this do:? Do While Not user_defined(duser) DoCmd.OpenForm "define_user_frm", , , , , acDialog Loop - This is a loop that will be repeated and will Open an Access form as long as user_defined is not duser.
Now() just returns the current date and time
ADODB.Recordset is in this instance being used as a way to connect to a table. It is generally used to connect to an external (read SQL) dataset. And can be populated from a query or stored procedure. Similar to the access builtin Recordset
These are switches used to set certain options in the connection. I would recommend reading over the MSDN documentation.
Not every recordset supports adding records (such as that returned from a query) this simply checks that before you attempt to add a record. It is part of writing code that won't throw errors.
This is simply a way to clean up the memory. As far as I know VBA is garbage collected but if you have a large recordset this can free up some memory sooner.
I'm not sure about this here. but it looks like it's calling a user defined function and it returns false it opens a form.

How to populate a ComboBox with a Recordset using VBA

There is some literature available at expert's exchange and at teck republic about using the combobox.recordset property to populate a combobox in an Access form.
These controls are usually populated with a "SELECT *" string in the 'rowsource' properties of the control, referencing a table or query available on the client's side of the app. When I need to display server's side data in a combobox, I create a temporary local table and import requested records. This is time consuming, specially with large tables.
Being able to use a recordset to populate a combobox control would allow the user to directly display data from the server's side.
Inspired by the 2 previous examples, I wrote some code as follow:
Dim rsPersonne as ADODB.recordset
Set rsPersonne = New ADODB.Recordset
Set rsPersonne.ActiveConnection = connexionActive
rsPersonne.CursorType = adOpenDynamic
rsPersonne.LockType = adLockPessimistic
rsPersonne.CursorLocation = adUseClient
rsPersonne.Open "SELECT id_Personne, nomPersonne FROM Tbl_Personne"
fc().Controls("id_Personne").Recordset = rsPersonne
Where:
connexionActive: is my permanent ADO connection to my database server
fc(): is my current/active form
controls("id_Personne"): is the
combobox control to populate with
company's staff list
Access version in 2003
Unfortunately, it doesn't work!
In debug mode, I am able to check that the recordset is properly created, with requested columns and data, and properly associated to the combobox control. Unfortunately, when I display the form, I keep getting an empty combobox, with no records in it! Any help is highly appreciated.
EDIT:
This recordset property is indeed available for the specific combobox object, not for the standard control object, and I was very surprised to discover it a few days ago.
I have already tried to use combobox's callback function, or to populate a list with the "addItem" method of the combobox,. All of these are time consuming.
To set a control that accepts a rowsource to a recordset you do the following:
Set recordset = currentDb.OpenRecordset("SELECT * FROM TABLE", dbOpenSnapshot)
Set control.recordset = recordset
Works with DAO Recordsets for sure, I haven't tried ADO recordsets because I don't have any real reason to use them.
When done this way, a simple requery will not work to refresh the data, you must do a repeat of the set statement.
As was said, you have to get the RowSourceType to "Table/Query" (or "Table/RequĂȘte" if in french) in order to show query results in the combobox.
Your memory problems arise from opening the recordset (rsPersonne) without closing it. You should close them when closing/unloading the form (but then again you would have scope problems since the recordset is declared in the function and not in the form).
You could also try to create and save a query with Access's built-in query creator and plug that same query in the RowSource of your combobox. That way the query is validated and compiled within Access.
I found the trick ... the "rowSourceType" property of the combobox control has to be set to "Table/Query". Display is now ok, but I have now another issue with memory. Since I use these ADO recordsets on my forms, memory usage of Access is increasing each time I browse a form. Memory is not freed either by stopping the browsing or closing the form, making MS Access unstable and regularly freezing. I will open a question if I cannot solve this issue
good method with using the Recordset property, thanks for that hint!
Patrick, the method you shown on your page has a big disadvantage (I tried that too on my own): The value list can only be 32 KB, if you exceed this limit the function will throw an error.
The callback method has the big disadvantage that it is very slow and it is called once for every entry which makes it unuseable for a longer list.
Using the recordset method works very well. I needed this because my SQL string was longer than 32 KB (lot of index values for WHERE ID IN(x,x,x,x,x...)).
Here's a simple function which uses this idea to set a recordset to the combobox:
' Fills a combobox with the result of a recordset.
'
' Works with any length of recordset results (up to 10000 in ADP)
' Useful if strSQL is longer than 32767 characters
'
' Author: Christian Coppes
' Date: 16.09.2009
'
Public Sub fnADOComboboxSetRS(cmb As ComboBox, strSQL As String)
Dim rs As ADODB.Recordset
Dim lngCount As Long
On Error GoTo fnADOComboboxSetRS_Error
Set rs = fnADOSelectCommon(strSQL, adLockReadOnly, adOpenForwardOnly)
If Not rs Is Nothing Then
If Not (rs.EOF And rs.BOF) Then
Set cmb.Recordset = rs
' enforces the combobox to load completely
lngCount = cmb.ListCount
End If
End If
fnADOComboboxSetRS_Exit:
If Not rs Is Nothing Then
If rs.State = adStateOpen Then rs.Close
Set rs = Nothing
End If
Exit Sub
fnADOComboboxSetRS_Error:
Select Case Err
Case Else
fnErr "modODBC->fnADOComboboxSetRS", True
Resume fnADOComboboxSetRS_Exit
End Select
End Sub
(The function fnADOSelectCommon opens an ADO recordset and gives it back. The function fnErr shows a message box with the error, if there was one.)
As this function closes the opened recordset there should be no problem with the memory. I tested it and didn't saw any increasing of memory which wasn't released after closing the form with the comboboxes.
In the Unload Event of the form you can additionaly use a "Set rs=Me.Comboboxname.Recordset" and then close it. This should not be necessary regarding memory, but it may be better to free up open connections (if used with a backend database server).
Cheers,
Christian
A combo box control does not have a recordset property. It does have a RowSource property but Access is expecting a SQL string in there.
You can change the RowSourceType to the name of a user defined "callback" function. Access help will give you more information including sample code by positioning yourself on the RowSourceType and pressing F1. I use this type of function when I want to give the users a list of available reports, drive letters, or other data that is not available via a SQL query.
I don't understand what you mean by your third paragraph with respect to using data directly from the server side. Or rather I don't understand what the problem is with using standard queries.
In MS Access, it's ok, but in VB, you may use something like this using adodc (Jet 4.0):
Private sub Form1_Load()
with Adodc1
.commandtype = adcmdtext
.recordsource = "Select * from courses"
.refresh
while not .recordset.eof
combo1.additem = .recordset.coursecode
.recordset.movenext
wend
end with
End Sub