Cannot open ADO Recordset more than once - vba

I have a function that is passed a SQL string, queries a SQL server, and return the results via a an array. This function works perfectly the first time, but I try to call it again it get:
Operation is not allowed when the object is closed
Oddly enough this error occures right after the Open statement. I've Googled about everything I can think of, with no good results.
Public Function SQL_Query(SQLCommand As String) As Variant
Dim cn As New ADODB.Connection
Dim SQLrs As New ADODB.Recordset
cn.ConnectionString = "Provider=SQLNCLI11;Server=10.XXX.XXXX.XXX;DataBase=Database1;Trusted_Connection=yes;"
cn.Open
SQLrs.CursorLocation = adUseClient
Call SQLrs.Open(SQLCommand, cn, adOpenStatic, adLockBatchOptimistic)
SQLrs.MoveFirst
SQL_Query = RecordSet2Array(SQLrs.GetRows)
SQLrs.Close
Set SQLrs = Nothing
cn.Close
Set cn = Nothing
End Function

If I'm reading right - you're opening the SQL connection again, but not the recordset (SQLrs). You have to create a new instance each time on open and close both when done, or you tend to get these issues on re-running.

I've not experienced it personally, but I've been told that Dim x as NEW y can lead to issues.
Try this change to see if it helps:
Public Function SQL_Query(SQLCommand As String) As Variant
'changes here vvvvvvvv
Dim cn As ADODB.Connection
Dim SQLrs As ADODB.Recordset
Set cn = New ADODB.Connection
Set SQLrs = New ADODB.Recordset
'to here ^^^^^^^^^^^^^^
cn.ConnectionString = "Provider=SQLNCLI11;Server=10.XXX.XXXX.XXX;DataBase=Database1;Trusted_Connection=yes;"
cn.Open
SQLrs.CursorLocation = adUseClient
Call SQLrs.Open(SQLCommand, cn, adOpenStatic, adLockBatchOptimistic)
SQLrs.MoveFirst
SQL_Query = RecordSet2Array(SQLrs.GetRows)
SQLrs.Close
Set SQLrs = Nothing
cn.Close
Set cn = Nothing
End Function

So, I've finally tracked this to SQL query I was using in my second call to the function. It creates a temp table and returns records from that temp table. This is apparently and known issue. However it's easily corrected by SET NOCOUNT ON. So I've updated my code to the following and everything is working fine now.
Public Function SQL_Query(SQLCommand As String) As Variant
Dim cn As New ADODB.Connection
Dim SQLrs As New ADODB.Recordset
cn.ConnectionString = "Provider=SQLNCLI11;Server=10.XXX.XXXX.XXX;DataBase=Database1;Trusted_Connection=yes;"
cn.Open
cn.Execute "SET NOCOUNT ON"
SQLrs.CursorLocation = adUseClient
Call SQLrs.Open(SQLCommand, cn, adOpenStatic, adLockBatchOptimistic)
SQLrs.MoveFirst
SQL_Query = RecordSet2Array(SQLrs.GetRows)
SQLrs.Close
Set SQLrs = Nothing
cn.Close
Set cn = Nothing
End Function

Related

If i create a disconnected ADO recordset from scratch in VBA how do i set the base table information for UpdateBatch?

I have been using disconnected recordsets for a few weeks now, typically retrieving data from SQL Server, disconnecting the rs and filtering/formatting in VBA. Now i'm trying to do the reverse and create a new ADO recordset from scratch, and then connect it to my database and use UpdateBatch to insert the recordset into the database without using a loop. I have a fully populated recordset at this point, hooked it back up to my connection string, and try UpdateBatch. Understandably, it has no information at this point about what table I'm trying to update (only Data Source and Initial Catalog via the connection string). Is there a recordset property that I use to provide the table in question? Additionally, the table I'm trying to import into has a GUID field (first field) that I have left blank on purpose in my disconnected recordset assuming that upon import, SQL Server would assign this GUID/primary key automatically.
The specific error I'm getting after "rs.UpdateBatch" is
Run-time error '-2147467259 (80004005)'"
Insufficient base table information for updating or refreshing.
I know I could use a loop and a SQL command "INSERT INTO ...". I'd like to use a recordset object though since those provide much more functionality as a container for data. One thing I haven't tried is to first retrieve a recordset from the table in question, then clear it and re-populate it with the new data so that the recordset itself retains all of the original database and table properties. If that's the only/best approach I can try that route too. I just wanted to see if it was possible to create an ADO recordset, populate it, and then insert it into a matching table of my choice.
dim rs as ADODB.Recordset
set rs = New ADODB.Recordset
With rs.Fields
.append "alias", adVarChar, 255
.append "textA", adVarChar, 255
.append ......
End With
rs.Open
rs.AddNew Array(0, 1, 2, ..., n), Array(val0, val1, val2, ..., valn)
rs.Update
call importRS(rs)
rs.close
set rs = nothing
After rs.update above some recordsets may need to go to a database, other recordset objects are just used to expedite filtering and sorting so I just use them as a convenient container and they'd never go to importRS()
However, IF I need to send the disconnected recordset to a database, i'd like to just pass the recordset object to another function that serves the purpose of opening the connection, sending the update, and closing the connection. The code below would serve that purpose which is why i'd like to wait to establish a connection until this point, right at the end after my rs is populated.
sub importRS(byref rs as ADODB.Recordset)
dim cn as ADODB.Connection
set cn = New ADODB.Connection
cn.ConnectionString = strConnection 'my connection string variable'
cn.Open
rs.ActiveConnection = cn
rs.UpdateBatch '-------error message appears on this line
cn.close
set cn = nothing
You can get the data, (wherever it may be) into an array and add to the recordset using a loop. Then then when the loop is finished, you do rs.updatebatch as follows:
Private Sub SaveToSQLSever()
Dim lngLastRow As Long
Dim arrySheet As Variant
Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Dim strCn As String
Set cn = New ADODB.Connection
Set rs = New ADODB.Recordset
strCn= "Provider=VersionOfSQL;User ID=*********;Password=*********;"
& _ "Data Source=ServerName;Initial Catalog=DataBaseName"
cn.Open strCn
On Error Goto exiting
'*********************************************************
'If the data is coming from a sheet
'Set to your Range
With Sheets("SheetName")
lngLastRow = .Range("A2").CurrentRegion.Rows _
(.Range("A2").CurrentRegion.Rows.Count).Row
arrySheet = .Range("A1:G" & lngLastRow).Value2
End With
'Else populate the array and pass it to this Sub
'*************************************************************
'Note the property parameters
'.Source = Table That you want to populate
With rs
.ActiveConnection = cn
.Source = "Select * from TableName"
.CursorType = adOpenDynamic
.CursorLocation = adUseClient
.LockType = adLockBatchOptimistic
.Open
End With
For i = LBound(arrySheet,1) To UBound(arrySheet,1)
rs.AddNew
For j = LBound(arrySheet,2) To UBound(arrySheet,2)
rs.Fields(j).Value = arrySheet(i,j)
Next j
rs.MoveNext
Next i
rs.UpdateBatch 'Updates the table with additions from the array
i = 0
'******************************************************************
'Note that you can also refer to the Field Names Explicitly Like So:
For i = LBound(arryData,1) To UBound(arryData,1)
With rs
.AddNew
.Fields("FieldName1").Value = arryData(i,1)
.Fields("FieldName2").Value = arryData(i,2)
.Fields("FieldName3").Value = arryData(i,3)
.Fields("FieldName4").Value = arryData(i,4)
.Fields("FieldName5").Value = arryData(i,5)
.Fields("FieldName6").Value = arryData(i,6)
.Fields("FieldName7").Value = arryData(i,7)
End With
Next i
rs.UpdateBatch
'******************************************************************
MsgBox "The data has successfully been saved to the SQL Server", _
vbInformation + vbOKOnly,"Alert: Upload Successful"
exiting:
If cn.State > 0 Then cn.Close
If rs.State > 0 Then rs.Close
Set cn = Nothing
Set rs = Nothing
End Sub
Edit: As per OP's request to pass an existing recordset to a SQL table, below should do so:
Private Sub SendRcrdsetToSQL(ByRef rsIn As ADODB.Recordset)
Dim arrySheet As Variant
Dim rsSQL As ADODB.Recordset
Dim cn As ADODB.Connection
Dim strCn As String
Set cn = New ADODB.Connection
strCn= "Provider=VersionOfSQL;User ID=*********;Password=*********;"
& _ "Data Source=ServerName;Initial Catalog=DataBaseName"
cn.Open strCn
On Error Goto exiting
Set rsSQL = New ADODB.Recordset
With rsSQL
.ActiveConnection = cn
.Source = "Select * from TableName"
.CursorType = adOpenDynamic
.CursorLocation = adUseClient
.LockType = adLockBatchOptimistic
.Open
End With
'disconnect the recordset and close the connection
Set rsSQL.ActiveConnection = Nothing
cn.Close
Set cn = Nothing
rsIn.MoveFirst
rsSQL.MoveLast
'Add the records from the passed recordset to the SQL recordset
Do While Not rsIn.EOF
With rsSQL
.AddNew
.Fields("FieldName1").Value = rsIn.Fields("FieldName1").Value
.Fields("FieldName2").Value = rsIn.Fields("FieldName2").Value
.Fields("FieldName3").Value = rsIn.Fields("FieldName3").Value
.Fields("FieldName4").Value = rsIn.Fields("FieldName4").Value
.Fields("FieldName5").Value = rsIn.Fields("FieldName5").Value
.Fields("FieldName6").Value = rsIn.Fields("FieldName6").Value
.Fields("FieldName7").Value = rsIn.Fields("FieldName7").Value
End With
rsIn.MoveNext
Loop
rsSQL.UpdateBatch
MsgBox "The data has successfully been saved to the SQL Server", _
vbInformation + vbOKOnly,"Alert: Upload Successful"
exiting:
If cn.State > 0 Then cn.Close
If rsIn.State > 0 Then rsIn.Close
If rsSQL.State > 0 Then rsSQL.Close
Set cn = Nothing
Set rsIn = Nothing
Set rsSQL = Nothing
End Sub
The only way I was able to get this to work was by running a query to build the structure of my Recordset. So your code becomes something like this:
Private Sub Command1_Click()
Dim cn As ADODB.Connection
Set cn = New ADODB.Connection
cn.ConnectionString = "<your connection string>"
cn.CursorLocation = adUseClient
cn.Open
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
Set rs.ActiveConnection = cn
rs.Open "select * from states where 1<>1", , adOpenStatic, adLockBatchOptimistic
rs.AddNew Array("Abbrev", "Name", "Region", "SchoolDataDirect"), Array("TN", "TestName", "MyRegion", 1)
Set rs.ActiveConnection = Nothing
cn.Close
ImportRS rs
End Sub
Private Sub ImportRS(ByRef rs As ADODB.Recordset)
Dim cn As ADODB.Connection
Set cn = New ADODB.Connection
cn.ConnectionString = "<your connection string>"
cn.CursorLocation = adUseClient
cn.Open
Set rs.ActiveConnection = cn
rs.UpdateBatch
Set rs.ActiveConnection = Nothing
cn.Close
End Sub

ADO Connection and Recordset with HTTP data source

I have the below Macro reading from a Database table stored as a txt file on the local C drive and returning an SQL query.
Public Function getData(fileName As String) As ADODB.Recordset
Dim cN As ADODB.Connection
Dim RS As ADODB.Recordset
Set cN = New ADODB.Connection
Set RS = New ADODB.Recordset
cN.Open ("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\Cloud\;Persist
Security Info=False;Extended Properties=""text; HDR=Yes; FMT=Delimited;
IMEX=1;""")
RS.ActiveConnection = cN
RS.Source = "select top 10 * from " & fileName
Set getData = RS
End Function
I can then call the function and return data using the below, so far so good...
Sub Cloud()
Dim a As ADODB.Recordset
Set a = getData("file.txt")
a.Open
MsgBox (a.GetString())
a.Close
End Sub
BUT now I would like to move 'file.txt' from C:\Cloud\ to a HTTP location, ie http://it.wont.work/
How would I amend the above for this to work? I've searched and tested but nothing seems to work... I either get internet login failed or ISAM not found.
Many thanks

VBA New Database Connection

How to change the code below to prevent what you see in the screenshot.
I am running a macro with the following code
Dim conn As ADODB.Connection
Dim rec1 As ADODB.Recordset
Dim thisSql As String
Set conn = New ADODB.Connection
Dim sConn As String
sConn = "Provider=SQLOLEDB;Trusted_Connection=Yes;Server=xyz;Database=xyz;UID=xyz;PWD=xyz"
conn.Open sConn
' this is creating multiple connections.
Set rec1 = New ADODB.Recordset
rec1.Open thisSql, conn
which runs a SQL Server query (which is around 20 lines long and contains 4 joins). Everything is fine except for the fact that after a couple times of running it my DB admin says that my query is loading up the DB too much.
Now, my query could be causing the problem, or it could be that Excel is starting to run multiple connections at once. Some evidence for this is the screenshot below and the fact that the load on the database appears to increase with time.
How do I establish a DB connection without constantly creating new connections?
Has anyone had similar problems working with Excel DB macros?
UPDATE
While the answers below were very useful (especially for someone starting out in VBA), it seems that the main reason my query was taking up load was a combination of multiple connections and having overlooked a line in my code:
With Sheets("FVols").QueryTables.Add(Connection:=rec1, Destination:=Sheets("FVols").Range("A1"))
.name = "data"
.FieldNames = True
.Refresh BackgroundQuery:=True <<<<<<<<<<<<<<<<<<<<<<<-----
End With
You only need to open the connection once. That literally means you can execute multiple queries on that one active connection. You must close the connection and free the reference (specially with ADODB) to avoid running into collisions and other connection related problems.
If you know the queries you are going to be executing you can create an array (or collection) and add queries to the queue.
While you already have an open connection to work with you can keep executing queries.
Scan through code there is not much difference between yours and mine so you should be able to see what is going on and where. Please, ask questions in the comments if anything is unclear
Sub DbConnection()
Dim cn As ADODB.Connection
Set cn = New ADODB.Connection
Dim rs As ADODB.Recordset
Dim strConn As String
strConn = "Driver={SQL Server};Server=; Database=; UID=; PWD="
cn.Open strConn
Dim queryArr, i
queryArr = Array("SELECT * FROM [MyTable]", "SELECT * FROM [MyOtherTable]")
For i = LBound(queryArr) To UBound(queryArr)
ExecuteQuery queryArr(i), cn, rs
Next i
cn.Close
Set cn = Nothing
End Sub
Private Sub ExecuteQuery(query As Variant, ByRef cn As ADODB.Connection, ByRef rs As ADODB.Recordset)
Set rs = New ADODB.Recordset
With rs
.ActiveConnection = cn
.Open CStr(query)
Sheets(1).Range("A1").CopyFromRecordset rs
.Close
End With
Set rs = Nothing
End Sub
Now, you only need to execute the DBConnection() once and all the queries you listed in the array will be executed.
Alternatively, if your queries are created at run-time you can pass it to the DbConnection() as a parameter.
Sub DbConnection(queryQueue As Collection)
Dim cn As ADODB.Connection
Set cn = New ADODB.Connection
Dim rs As ADODB.Recordset
Dim strConn As String
strConn = "Driver={SQL Server};Server=HELIUM\PRI; Database=sourcedata; UID=tabula; PWD=Tabula123!"
cn.Open strConn
For i = 1 To queryQueue.Count
ExecuteQuery queryQueue.Item(i), cn, rs
Next i
cn.Close
Set cn = Nothing
End Sub
Private Sub ExecuteQuery(query As Variant, ByRef cn As ADODB.Connection, ByRef rs As ADODB.Recordset)
Set rs = New ADODB.Recordset
With rs
.ActiveConnection = cn
.Open CStr(query)
Sheets(1).Range("A1").CopyFromRecordset rs
.Close
End With
Set rs = Nothing
End Sub
Update:
You can declare your connection as a Global Variable. Now you can run the DBConnection() as many times as you like and you will not be creating a new connection each time. Instead you will be using the global connection object.
Option Explicit
Public cn As ADODB.Connection
Sub DbConnection()
Set cn = New ADODB.Connection
Dim rs As ADODB.Recordset
Dim strConn As String
strConn = "Driver={SQL Server};Server=; Database=; UID=; PWD="
cn.Open strConn
Set rs = New ADODB.Recordset
With rs
.ActiveConnection = cn
.Open "SELECT * FROM [MyTable]"
Sheets(1).Range("A1").CopyFromRecordset rs
.Close
End With
Set rs = Nothing
cn.Close
Set cn = Nothing
End Sub
Are you releasing the connection variable when you've finished with it? i.e.
Set rec1 = Nothing
The connection won't close fully if not.

Access Recordset Yielding NAME?

I have an access database that connects to an SQL database via ADODB. The recordset for the access table view is set via the recordset property in the method below. This method is called from the Form_Load function for the form it is viewable in. The form is accessed via a tab on a main form.
Unfortunately, the recordset doesn't seem to be updating correctly between machines. On one machine (Access 2010) it loads up fine. On the second (Access 2010) it loads only the first line as Name?. Sometimes I can get it to load on the second machine if I open the form on it's own, then open the tab.
Any help would be appreciated. Thanks in advance!
Function LoadTblEmployeesADOtoForm()
Dim sqlStr As String
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim p As ADODB.Property
Const ConnStr = "PROVIDER=SQLOLEDB;Server=SERVER;Database=DB;User ID=ID;Password=PWD;"
Set cn = New ADODB.Connection
cn.Open ConnStr
Set rs = New ADODB.Recordset
With rs
.CursorLocation = adUseClient
.CursorType = adOpenKeyset
.LockType = adLockPessimistic
'SELECT
sqlStr = "SELECT * FROM tblEmployees ORDER BY NetworkID"
Debug.Print sqlStr
.Source = sqlStr
Set .ActiveConnection = cn
.Properties("Preserve on Abort") = True
.Properties("Preserve on Commit") = True
End With
'cn.BeginTrans
rs.Open
Debug.Print rs.RecordCount
Dim temp As Integer
Set Form_frmManagetblEmployees.Recordset = rs
cn.Close
Set rs = Nothing
End Function
When the form is used as a subform, you cannot refer to:
Set Form_frmManagetblEmployees.Recordset = rs
However, Me will work for both a form and a subform, so:
Set Me.Recordset = rs

VBA ODBC Update

This is the code I'm using to update an SQL database:
Public Sub main()
Dim cnn As ADODB.Connection
Dim rst As ADODB.Recordset
Set cnn = New ADODB.Connection
Set rst = New ADODB.Recordset
cnn.Open "ConnectionName"
rst.ActiveConnection = cnn
rst.CursorLocation = adUseServer
rst.Source = "Update Table ..."
rst.Open
Set rst = Nothing
Set cnn = Nothing
End Sub
What I want to know is if and how I should deal with the rst object after opening it. Do I close it? When I try doing rst.Close, I get the error: "Operation is not allowed when the object is closed". The code works fine without rst.Close, I'm wondering if there are any dangers to not closing the object.
An UPDATE operation does not return a resultset. Therefore, if executed with a Recordset object, it results in an empty and closed recordset. It cannot be closed anyway because it has never been opened.
A rule of thumb is:
if rst.State <> adStateClosed then rst.Close
But, because you are executing a command that's not going to return data anyway, the preferred way is:
dim cm as ADODB.Command
set cm = new adodb.command
set cm.activeconnection = cnn
cm.commandtype = adCmdText
cm.commandtext = "UPDATE ..."
cm.execute ,, adExecuteNoRecords
Or, if your SQL is a fixed string that doesn't have parameters,
cnn.execute "UPDATE ...",, adExecuteNoRecords
Also, please change rst.ActiveConnection = cnn to Set rst.ActiveConnection = cnn.
An update returns no rows, so no need to open a recordset.
You can execute the statement directly using:
Connection.Execute "Update Table ...", [flags]
To answer your question, after .Open the recordset is closed (its .state will be adStateClosed) as no data has been returned, so setting it to nothing is sufficent.