I'm trying to run an Access delete query when I close my Excel workbook. I want the DELETE query to delete any record that is prior to today's date in the "ResDate" field. Below is the code I have written. The workbook closes but the records remain in the database (TR table).
I thought this would work but am striking out so far. Any suggestions on getting this to work would be greatly appreciated. Thanks for the help...…….
Const ConnectionString As String = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=H:\MyDatabase\TRC.mdb;Persist Security Info=False"
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Set Connection = New ADODB.Connection
Connection.ConnectionString = ConnectionString
Connection.Open
VSQL = "DELETE * FROM [TR] WHERE [TR].ResDate < " & Date
Set RecSet1 = Connection.Execute(VSQL, dbrows, adCmdText)
Connection.Close
Set RecSet1 = Nothing
End Sub
Use the Access Date() function in your SQL statement, instead of inserting the date as text into the statement.
Include Option Explicit in your module's Declarations section and run Debug->Compile from the VB Editor's main menu. Fix the first compile error Access complains about, and then Debug->Compile again. Repeat until no more errors.
Opening a recordset based on a DELETE query is not useful because a DELETE does not return records. Simply execute your query instead.
Const ConnectionString As String = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=H:\MyDatabase\TRC.mdb;Persist Security Info=False"
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Dim cn As ADODB.Connection
Dim VSQL As String
Dim dbrows As Long
Set cn = New ADODB.Connection
cn.ConnectionString = ConnectionString
cn.Open
VSQL = "DELETE * FROM [TR] WHERE [TR].ResDate < Date()"
cn.Execute VSQL, dbrows, adCmdText
cn.Close
End Sub
ALWAYS USE PARAMETERS!
When you do VSQL = "DELETE * FROM [TR] WHERE [TR].ResDate < " & Date, depending on your locale, that can result in several issues.
For example, DELETE * FROM [TR] WHERE [TR].ResDate < 2019-11-08, and 2019 minus 11 minus 08 is equal to 2000 and CDate(2000) is 1905-06-22, so this is a valid query, but probably won't delete what you want to delete.
In an alternate locale, it might be 11/08/2019, and 11 divided by 8 divided by 2019 is approximately 0 and CDATE(0) is 1899-12-30.
Instead, pick and choose from How do I use parameters in VBA in the different contexts in Microsoft Access?, for example:
Private Sub Workbook_BeforeClose(Cancel As Boolean)
Set Connection = New ADODB.Connection
Connection.ConnectionString = ConnectionString
Connection.Open
VSQL = "DELETE * FROM [TR] WHERE [TR].ResDate < " & Date
With CreateObject("ADODB.Command")
Set .ActiveConnection = Connection
.CommandText = "DELETE * FROM [TR] WHERE [TR].ResDate < ?"
Set RecSet1 = .Execute(, Array(Date))
End With
Connection.Close
Set RecSet1 = Nothing
End Sub
Related
Please help me. Excel VBA is throwing this error: Multiple-step OLE DB Operation generated errors
on the line : cmd.Parameters("[days]") = ActiveWorkbook.Sheets("MABI_Extracts").Range("B18").Value
I'm trying to run a query in access named "AGBA_Conversion" with an integer parameter. How do I resolve? thanks in advance :)
Sub RECT_MBTCLeads2_Extracts()
Dim cmd As New ADODB.Command, rs As ADODB.Recordset
Dim sht As Worksheet
Dim b As String
Dim d As Long
Dim a As String
Set sht = ActiveWorkbook.Sheets("AGBA_Conversion")
sht.Range("B1").ClearContents
cmd.ActiveConnection = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Users\nemberga\OneDrive - AXA\Documents\Automation\MBTC Conversion.accdb"
cmd.CommandType = adCmdText
cmd.CommandText = "AGBA_Conversion" '<<<--here is where you write your query sql
cmd.Parameters("[days]") = ActiveWorkbook.Sheets("MABI_Extracts").Range("B18").Value
Options = adCmdTable
Set rs = cmd.Execute
sht.Range("A1").CopyFromRecordset rs '--bulk copy to the Excel sheet
rs.Close
cmd.ActiveConnection.Close
MsgBox "All data were successfully retrieved from the queries!", vbInformation, "Done"
End Sub
Parameters is a collection of Parameter. You need first to create the object using CreateParameter, then to add it to the collection using Append.
Replace
cmd.Parameters("[days]") = ActiveWorkbook.Sheets("MABI_Extracts").Range("B18").Value
By
Dim p
Set p = cmd.CreateParameter("days", adInteger, adParamInput)
p.Value = ActiveWorkbook.Sheets("MABI_Extracts").Range("B18").Value
cmd.Parameters.Append p
See https://learn.microsoft.com/fr-fr/sql/ado/reference/ado-api/append-and-createparameter-methods-example-vb?view=sql-server-ver15
I need to move data from Excel sheet to database. To do this, I create ADODB Connection and I am able to execute such an SQL query:
INSERT INTO myTable SELECT * FROM [Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb].[Shee1$A1:C100]
My problem is that the range cannot point further than 255 columns, i.e. column IU. I want to try using named range instead, but I cannot find suitable notation. All examples I found connect directly to the workbook, and use either SELECT * FROM [Sheet1$] reference, or SELECT * FROM myRange as an example of named range. I tried things like
[Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb].[myRange]
[Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb].[myRange$]
[Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb].myRange
[Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb;Name=myRange]
, but without success.
What is the proper way to use named range here? Would it even help working around column number limitation?
I expected [Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb].[myRange] to work, but it throws the following error: 'The Microsoft Access database engine could not find the object 'myRange'. Make sure the object exists (...)'
I can work it around by copying data from source sheet to temporary one, and have it within 255 column limit, but it would be great do it right way.
Not sure if you are going to find a solution for connecting to a named range. I took a look at getting this to work, and I had no luck either, I suspect it's not included in the schema past 255 column, but could be wrong.
I thought you might as well have an efficient solution that doesn't rely on looping for adding data to Access. It's more code than just doing an insert, but I hope it fits your specific problem.
I was able to do an insert of ~2500 records (all integers) in about 3 seconds, so it is fairly quick.
Option Explicit
Private Function GetDisconnectedRecordset(TableName As String) As ADODB.Recordset
Dim conn As ADODB.connection: Set conn = getConn()
Dim rs As ADODB.Recordset: Set rs = New ADODB.Recordset
With rs
.CursorLocation = adUseClient ' <-- needed for offline processing
'Get the schema of the table, don't return anything
.Open "Select * from " & TableName & " where false", conn, adOpenDynamic, adLockBatchOptimistic
End With
rs.ActiveConnection = Nothing
conn.Close
Set conn = Nothing
Set GetDisconnectedRecordset = rs
End Function
'Do an update batch of the data
'Portion used from: https://stackoverflow.com/questions/32821618/insert-full-ado-recordset-into-existing-access-table-without-loop
Sub PopulateDataFromNamedRange()
Dim conn As ADODB.connection
Dim ws As Excel.Worksheet: Set ws = ThisWorkbook.Worksheets("Sheet2") 'Update to your sheet/wb
Dim NamedRange As Excel.Range: Set NamedRange = ws.Range("Test") ' Update to your named range
Dim NamedItem As Excel.Range
Dim rs As ADODB.Recordset: Set rs = GetDisconnectedRecordset("[TestTable]") 'Specify your table name in access
Dim FieldName As String
Dim Row As Long
Dim AddRow As Long
'Add Data to the disconnected recordset
For Each NamedItem In NamedRange
If Not NamedItem.Row = 1 Then
Row = NamedItem.Row
If Not Row = AddRow Then rs.AddNew
AddRow = NamedItem.Row
FieldName = ws.Cells(NamedItem.Row - (NamedItem.Row - 1), NamedItem.Column).Value
rs.Fields(FieldName).Value = NamedItem.Value
End If
Next
'Connect again
Set conn = getConn()
Set rs.ActiveConnection = conn
rs.UpdateBatch '<-- 'Update all records at once to Access
conn.Close
End Sub
Private Function getConn() As ADODB.connection
Dim conn As ADODB.connection: Set conn = New ADODB.connection
conn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Users\Ryan\Desktop\Example.accdb"
Set getConn = conn
End Function
I had the same issue and the solution is pretty easy, even though it works only for named ranges at Workbook level.
The connection has to be done to the Workbook (i.e. [Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb]).
Then in the Query just type: SELECT * FROM [myRange] (Please note the square brackets and the absence of $ sign)
You need to use a construction:
[Excel 12.0 Macro;HDR=Yes;Database=C:\MyPath\MyFile.xlsb].[$myRange]
I am running an SQL query on an oracle DB, it is just a simple select * from table query. This works fine for a small table 900 rows and 5 columns. But when I try it on a table with say 30 columns and 95,000 rows I get the below error. I tried increasing MaxRecords but to no avail.
Error
Code
Public Const sConnect As String = "Driver={Microsoft ODBC for
Oracle};Server=server;Uid=user;Pwd=password"
Sub GetData1()
i = 0
Sheets(1).Range("a1:ao20000").ClearContents
Dim rsConnection As ADODB.Connection
Dim rsRecordset As ADODB.Recordset
Dim sQuery As String
sQuery = "select * from trade"
Set rsConnection = New ADODB.Connection
Set rsRecordset = New ADODB.Recordset
rsConnection.ConnectionString = sConnect
rsConnection.Open
rsRecordset.MaxRecords = 1048575
Set rsRecordset = rsConnection.Execute(sQuery)
Worksheets(1).Range("A2").CopyFromRecordset rsRecordset
For i = 0 To rsRecordset.Fields.Count - 1
Worksheets("Sheet1").Cells(1, i + 1).Value = rsRecordset.Fields(i).Name
Next i
rsConnection.Close
Set rsConnection = Nothing
Set rsRecordset = Nothing
End Sub
Regards,
Tim
After a little more digging I found the answer. It relates to database fields which are TIMESTAMP(6). I have to redo my db query to do a to_char(timestamp_field) and all is OK.
Thanks
Tim
I recently coded something for an assignment and I lost marks for not closing a connection and I'm curious about two things. First do ADODB connections for VBA naturally timeout after a few seconds and would I be overwriting the connection for my code included below or would I end up having multiple ADODB connections? In essence, did I do anything wrong by not closing the connections? Thanks.
Option Explicit
'Declaring all necessary variables - Global saves me from redeclaring and allows to be carried
Public dbMyDatabase As String, CnctSource As String, Src As String
Public rstNewQuery As ADODB.Recordset, cntStudConnection As ADODB.Connection
Public Selected1st As String
Private Sub Cancel_Click()
Unload Me
Worksheets("Question3").Range("D4:E9").Clear
End Sub
Private Sub Clear_Click()
Worksheets("Question3Products").Range("C3:H42").Clear
End Sub
Private Sub UserForm_Initialize()
'Get database and links it
dbMyDatabase = ThisWorkbook.Path & "\SalesOrders.mdb"
Set cntStudConnection = New ADODB.Connection
CnctSource = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=" & dbMyDatabase & ";"
cntStudConnection.Open ConnectionString:=CnctSource
'Commence Query for Categories
Set rstNewQuery = New ADODB.Recordset
rstNewQuery.Open Source:="Categories", ActiveConnection:=cntStudConnection
Range("D4").CopyFromRecordset rstNewQuery
Range("D4:E9").Name = "BufferRange"
TheList.RowSource = "BufferRange"
TheList.Selected(1) = True
'Input Into Listbox
'Decided that clearing the values for connections would be redundant and wasteful.
End Sub
Private Sub FindInfo_Click()
Dim i As Integer
'Switching Sheets and Clearing Previous Variables
Worksheets("Question3Products").Activate
Range("C3:H42").Clear
'Selecting Value user wishes to search for
For i = 0 To TheList.ListCount - 1
If TheList.Selected(i) Then
Selected1st = TheList.List(i)
End If
Next i
'Commence query to search. Decided not to make a function to call upon due to different variables and only 2 instances of use
dbMyDatabase = ThisWorkbook.Path & "\SalesOrders.mdb"
Set cntStudConnection = New ADODB.Connection
CnctSource = "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=" & dbMyDatabase & ";"
cntStudConnection.Open ConnectionString:=CnctSource
Set rstNewQuery = New ADODB.Recordset
Dim StringUse As String
StringUse = "SELECT* From Products WHERE CategoryID = " & Selected1st
rstNewQuery.Open Source:=StringUse, ActiveConnection:=cntStudConnection
Range("C3").CopyFromRecordset rstNewQuery
Range("F3:F42").Style = "Currency"
End Sub
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.