Handling AddNew Key Violations ADODB.Recordset - excel

My previous Title caused a bit of confusion there...updated
I've encountered this problem recently while using some VBA to bridge Excel and Access.
So I have a spreadsheet containing a table that I need to import to an Access database.
The table is something generic like the following.
EmployeeNumber Unused_Field2 Unused_Field3
1 ### ###
2 ### ###
3 ### ###
The unique key in Access is set to be the EmployeeNumber.
My VBA Code in Excel looks like the following:
Sub test()
Dim con As ADODB.Connection
Dim rst As ADODB.Recordset
strcon = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\temp\mydb.mdb;"
strsql = "SELECT * FROM Table1"
Set con = New Connection
Set rst = New Recordset
con.Open strcon
rst.Open strsql, strcon, adOpenStatic, adLockOptimistic
For i = 0 To n
On Error GoTo Errhdl
rst.AddNew Array("Field1", "Field2", "Field3"), Array(Range("A" & i), Range("B" & i), Range("C" & i))
On Error GoTo 0
Next
Exit Sub
Errhdl:
Debug.Print "Record" & i & "caused an error"
Resume Next
End Sub
Unfortunately the data is not of high quality and I often would have duplicate values causing a key violation all the time.
While I though Resume Next will clear the Error allowing another "AddNew" to run, it did not.
All the subsequent entries after one key violation will return the same Error.
So my question is as follows:
Is there away to clear the Error associated with a recordset? ( For the record I've tried to get the "Error Collection" and used
the method Errors.Clear. That didn't do the trick. The Error Collection is a property specific to the ADO object - MSDN Errors Collection)
Can this be done without closing and re-opening the recordset?
Please let me know if more clarification is needed!

I would not use error handling (handling ADO errors is tricky) but use Find to check each time that the key does not already exist, before performing the insert. If the key is an indexed field (as your description suggests) then you might consider Seek which is more efficient for a large dataset. (However, Seek does not work with a client cursor -adUseClient.)
Here is a code fragment that I found that outlines the steps:
If Not .EOF Then
.MoveFirst
.Find "TPItemNbr='" & m_TPItemNbr & "'", , adSearchForward
End If
If .EOF Then
.AddNew
!TPItemVendorID = m_TPItemVendorID
!TPItemNbr = m_TPItemNbr
!TPItemEUOM = m_TPItemEUOM
!TPItemUOMFactor = m_TPItemUOMFactor
!TPItemPUOM = m_TPItemPUOM
!TPItemDescription = m_TPItemDescription
!TPItemUnitCost = m_TPItemUnitCost
!TPItemUnitLabor = m_TPItemUnitLabor
.Update
That is, perform a Find each time within the loop, if EOF (end-of-file) is true then the key isn't already in the table, so an insert can be performed.
Added in response to further information about the compound key.
I would create a Command object and use Execute. Yes, it requires creating a string, but you can trap and ignore errors for key violations.
The alternatives could be to
Run a separate SQL-statement to obtain a recordset of the list of duplicates
Loop through this storing the values in an array
Perform the AddNew each time checking the array
This seems messy to me, particular the aspect of searching the array.
Pursue the multi-find method linked in a comment.

So I found a solution if a Recordset.AddNew encounters and error.
The trick is to use CancelUpdate - MSDN
One can also use the Status property to check if the operation was successful.
a sample code would be:
Sub test()
Dim con As ADODB.Connection
Dim rst As ADODB.Recordset
strcon = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=c:\temp\mydb.mdb;"
strsql = "SELECT * FROM Table1"
Set con = New Connection
Set rst = New Recordset
con.Open strcon
rst.Open strsql, strcon, adOpenStatic, adLockOptimistic
For i = 0 To n
On Error GoTo Errhdl
rst.AddNew Array("Field1", "Field2", "Field3"), Array(Range("A" & i), Range("B" & i), Range("C" & i))
On Error GoTo 0
Next
Exit Sub
Errhdl:
Debug.Print "Record" & i & "caused an error"
If rst.Status <> 0 Then
rst.CancelUpdate
End If
Resume Next
End Sub

Related

Update Excel worksheet using ADODB from external application

I've found lots of posts on this problem, but so far no solutions have helped.
I'd like to read and write data from/to an Excel worksheet from an external VBA application - so far it reads OK, but I get an error while trying to write values to the ADODB Recordset.
Here's the code:
Sub UpdateFromExcel()
'https://stackoverflow.com/questions/15620080/reading-data-using-oledb-from-opened-excel-file
Dim oConn As New ADODB.Connection
Dim oRS As New ADODB.Recordset
Dim sPath
Dim sSQL As String
sSQL = "SELECT * FROM [Sheet1$A1:N10000]"
sPath = frmExcelSync.txtFilePath
oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source='" & sPath & "';" & _
"Extended Properties='Excel 12.0;HDR=Yes;IMEX=1';"
oRS.Open sSQL, oConn, adOpenDynamic, adLockOptimistic
Do While Not oRS.EOF
'ITERATE THROUGH EACH ROW HERE
'ADD CODE ABOVE
'****GET AN ERROR HERE:****
oRS.Update "Occurrence Name", "Test"
oRS.MoveNext
Loop
oRS.Close
oConn.Close
End Sub
The error is
"Cannot update. Database or object is read-only".
I've tried different lock and cursor types, and I've tried editing the fields then using the .update method, but nothing has worked so far.
Any suggestions?
your update statement is not correct. I believe you want to update the column "Occurrence Name" with the value "Test"
What you should write is.
Do While Not oRS.EOF
oRS![Occurrence Name].value = "Test"
oRS.MoveNext
Loop
oRS.Update
The problem seems to have gone away somehow.
I tried a few different things (different spreadsheets) with mixed success then restarted the application - now it works.
No code changes at all.

Get Data From Access Database with Excel VBA

I am having trouble getting data from an Access Database. I found this code online, and it seems to work (to an extent), but for some reason it will only pull the column headers, and none of the data from the query. I am not too familiar with Access, that is why I pulled one from offline.
Someone had a similar post a while back, where the code they used was the same, and our queries were exactly the same, but we had different issues.
Importing Data From Access Using Excel VBA
Would anyone happen to know why the data won't pull?
Sub getDataFromAccess()
Dim DBFullName As String
Dim Connect As String, Source As String
Dim Connection As ADODB.Connection
Dim Recordset As ADODB.Recordset
Dim Col As Integer
Dim startdt As String
Dim stopdt As String
Dim refresh
refresh = MsgBox("Start New Query?", vbYesNo)
If refresh = vbYes Then
Sheet1.Cells.Clear
startdt = Application.InputBox("Please Input Start Date for Query (MM/DD/YYYY): ", "Start Date")
stopdt = Application.InputBox("Please Input Stop Date for Query (MM/DD/YYYY): ", "Stop Date")
DBFullName = "X:\MyDocuments\CMS\CMS Database.mdb"
' Open the connection
Set Connection = New ADODB.Connection
Connect = "Provider=Microsoft.ACE.OLEDB.12.0;"
Connect = Connect & "Data Source=" & DBFullName & ";"
Connection.Open ConnectionString:=Connect
Set Recordset = New ADODB.Recordset
With Recordset
Source = "SELECT * FROM Tracking WHERE [Date_Logged] BETWEEN " & startdt & " AND " & stopdt & " ORDER BY [Date_Logged]"
.Open Source:=Source, ActiveConnection:=Connection
For Col = 0 To Recordset.Fields.Count - 1
Range(“A1”).Offset(0, Col).Value = Recordset.Fields(Col).Name
Next
Range(“A1”).Offset(1, 0).CopyFromRecordset Recordset
End With
ActiveSheet.Columns.AutoFit
Set Recordset = Nothing
Connection.Close
Set Connection = Nothing
End Sub
An easy way to get data in Excel, especially from Access, is to use the menu "Data > Access". This creates a connection to a table, that you can freely edit.
At the very least, that is a convenient way to limit your investigations to:
the query you typed (the connection string will always be OK, so if you're getting no values, it comes from the query)
or the VBA itself (if the table is returning values but not the corresponding VBA Sub, then you know it comes from the VBA itself, not the SQL).
I'm skipping the creation of connection becuse it's really straightforward; it's better to focus on what you can do once the table has been created.
Edit the connection
When you select the table and go to menu "Data > Properties", then in the window you click on the top right button "Connection properties", you get to the definition of the connection, i.e. some properties in the first tab and the actual definition in the second tab.
If you move the .mdb file, you'll have to change the connection string accordingly. There should be no other events forcing you to alter it.
If you want to type an actual complex query, you'll need to:
Change the command type from "Table" to "SQL"
Type the query in the bottom edit box.
Note if you want to define dynamic parameters in the WHERE clause, you can put question marks (?) instead of hardcoded values. Question marks can be linked to either constants (with a prompt to change their values) or cell.
Use in VBA
Once you checked with the connection that everything works, you have 2 solutions to put that in VBA.
Either use exactly the code you have above; in that case, you can make things easy by simply copying the connection string and the query.
Alternatively and this is what I would recommend, the table we have built previously can be updated very easily in VBA.
Use this piece of code:
WorksheetWithTable.ListObjects(1).QueryTable.Refresh
You really don't need more than this 1 line of code to do the refresh.
If you set your query to automatically refresh when a cell's value is being modified, then you do not even need it at all.
Note #1: Instead of an index in .ListObjects(1), you can use the table name.
Node #2: Refresh has an optional parameters to drive if the query is to be refresh in the background. True means the VBA code will not wait for the execution to end before moving to the next instruction. False, obviously, is the opposite.
The posted code is missing End If line. Perhaps this is just a posting typo because code should not compile and run.
The query SQL needs # delimiters for the date parameters:
Source = "SELECT * FROM Tracking WHERE [Date_Logged] BETWEEN #" & startdt & "# AND #" & stopdt & "# ORDER BY [Date_Logged]"
Text field would need apostrophe delimiters. Number field does not need delimiters.
I solved the answer to my own question after hours, i found a different set of code that worked fine. Thank you all for your help!
Sub getdatamdb()
Dim cn As Object, rs As Object
Dim intColIndex As Integer
Dim DBFullName As String
Dim TargetRange As Range
10 DBFullName = "X:\MyDocuments\CMS\CMS Database.mdb"
20 On Error GoTo Whoa
30 Application.ScreenUpdating = False
40 Set TargetRange = Sheets("Sheet1").Range("A1")
50 Set cn = CreateObject("ADODB.Connection")
60 cn.Open "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=" & DBFullName & ";"
70 Set rs = CreateObject("ADODB.Recordset")
80 rs.Open "SELECT * FROM Tracking WHERE [Date_Logged] BETWEEN #" & startdt & "# AND #" & stopdt & "# ORDER BY [Date_Logged]", cn, , , adCmdText
' Write the field names
90 For intColIndex = 0 To rs.Fields.Count - 1
100 TargetRange.Offset(1, intColIndex).Value = rs.Fields(intColIndex).Name
110 Next
' Write recordset
120 TargetRange.Offset(1, 0).CopyFromRecordset rs
LetsContinue:
130 Application.ScreenUpdating = True
140 On Error Resume Next
150 rs.Close
160 Set rs = Nothing
170 cn.Close
180 Set cn = Nothing
190 On Error GoTo 0
200 Exit Sub
Whoa:
210 MsgBox "Error Description :" & Err.Description & vbCrLf & _
"Error at line :" & Erl & vbCrLf & _
"Error Number :" & Err.Number
220 Resume LetsContinue
End If
End Sub

Adding records to Access from Excel - recordset is ReadOnly

I'm putting together an Excel front-end with an Access database behind it, so that some of my colleagues can collate data from previous projects (without having to learn to use Access themselves!)
So I have a "Data" sheet in the Excel workbook that occasionally refreshes from the database, then a "Dashboard" sheet that allows filtering of that data.
Where I'm having trouble is in closing the loop - allowing input of new data into the database. The relevant code is below:
Private Sub btnDBImport_Click()
Dim rst As ADODB.Recordset
Dim errorList As String
Dim cn As ADODB.Connection
Set cn = New ADODB.Connection
With cn
.Provider = "Microsoft.ACE.OLEDB.16.0"
.ConnectionString = "\\FileAddress\DB.accdb"
.Open
End With
Set rst = New ADODB.Recordset
rst.CursorLocation = adUseServer
rst.Open Source:="TableName", _
ActiveConnection:=cn, _
CursorType:=adOpenDynamic, _
LockType:=adLockOptimistic, _
Options:=adCmdTableDirect
If validationTest = False Then
errorList = errorList & "Data at row " & i & " was not added to the database due to missing information." & vbCrLf
Else
rst.AddNew
With rst
.Fields(1) = Value1
.Fields(2) = Value2
End With
rst.Update
End If
Next i
If errorList <> "" Then
MsgBox errorList, vbOKOnly, "Data error"
End If
rst.Close
Set rst = Nothing
cn.Close
Set cn = Nothing
End Sub
The above is everything involved in defining, opening and working on a recordset; I get an error at rst.AddNew because "cannot update, database or object is ReadOnly".
Any help appreciated.
So, with thanks to the comment from Jimmy Smith, I've got to the bottom of this, or at least fixed my immediate problem.
The table was being treated as ReadOnly both when accessed through MS Access directly and when loaded as a Recordset using VBA because there was a link to the table in the same Excel workbook.
Now that I've deleted the link to the table and instead update the records in it through code, I'm consistently getting requested levels of access to the relevant table and everything is working as intended. Time to commit to Rubberduck and show it to the userbase.

execute "sp_MSforeachtable" (SQL Server 2008 R2) from Excel 2003 using ADO

I want to analyse my DB and get the results for each table into an MS Excel worksheet by executing "sp_MSforeachtable 'EXECUTE sp_spaceused [?];';" from an ADODB Command or Recordset and then use CopyFromRecordset to output the results into Excel.
Here is the code I am using:
Sub analyseHermesDB()
Dim oConn As Object, oRec As Object, oField As Object, iIndx As Integer
' sp_MSforeachtable 'EXECUTE sp_spaceused [?];';
Set oConn = CreateObject("ADODB.Connection")
Set oRec = CreateObject("ADODB.Recordset")
oConn.Open "FILE NAME=" & ThisWorkbook.Path & "\conn.udl"
With oRec
.activeconnection = oConn
.Source = "sp_MSforeachtable 'EXECUTE sp_spaceused [?];';"
.cursorlocation = 3
.Open
For iIndx = 0 To .fields.Count - 1
ActiveCell.Offset(0, iIndx).Value = .fields(iIndx).Name
Next
Do Until .EOF
ActiveCell.Offset(.absoluteposition, 0).CopyFromRecordset oRec
.movenext
Loop
ActiveCell.Offset(0, 1).Value = .RecordCount
.Close
End With
End Sub
The problem is this: When I execute "sp_MSforeachtable 'EXECUTE sp_spaceused [?];';" in SQL Server 2008 Management Studio I get one resultset for each table.
I am only getting one recordset back from the call using ADO
What am I doing wrong? Should I be trying to get an array of recordsets, or execute sp_MSforeachtable to get the list of tables then execute EXECUTE sp_spaceused [?];' for each one?
sp_MSforeachtable returns multiple result sets and you need to consume them this way
However, you can achieve what you want with some simple SQL instead
sp_spaceused uses similar SQL internally anyway...
Feel free to tweak, this is an exact copy/paste of a script I use
SELECT
o.name,
SUM(ps.reserved_page_count)/128.0 AS ReservedMB,
SUM(ps.used_page_count)/128.0 AS UsedMB
FROM
sys.objects o
JOIN
sys.dm_db_partition_stats ps ON o.object_id = ps.object_id
WHERE
OBJECTPROPERTYEX(o.object_id, 'IsMSShipped') = 0
GROUP BY
o.name
ORDER BY
SUM(ps.reserved_page_count) DESC
I am only getting one recordset back from the call using ADO
That's not true, you're only evaluating one recordset. Use NextRecordset to get the next recordset.
From Visual Basic Concepts:
Running a Stored Procedure That Returns Multiple Resultsets
Private Sub MultipleRSButton_Click()
Dim rs As New ADODB.Recordset
sql = "Select * from Authors Where year_born is not null; " _
& "Select * from Authors where year_born is null"
rs.Open sql, cn
Do
i = MsgBox("Ready for results?", vbYesNoCancel)
If i = vbYes Then
ADOGrid1.ShowData rs
Set rs = rs.NextRecordset
End If
Loop Until rs.State = adStateClosed
End Sub
Note Set rs = rs.NextRecordset.
But for all intents and purposes, gbn's answer is probably better; it doesn't rely on an undocumented stored procedure.

Clearing a table

What I'm trying to do is, while in Excel, use VBA to push data to an existing Access table. I've been able to do this, but am having one small hiccup. Before I push the data to access, I want to clear the current data on the Access table, so when the new data from Excel comes in, it is the only data in the Access table. I really don't know how to write code for Access since the class has been on VBA for Excel. I've tried several different approaches and each time it doesn't work. For example, the code that seemed like it should work is
DoCmd.RunSQL "DELETE tblName.* FROM CoversheetTableFourthAttempt
but I get an error telling me to define an object.
If you could help me with this, I would really appricate it
I've put my code below for reference.
Sub AccessFourthMonth()
Dim cn As ADODB.Connection, rs As ADODB.Recordset, r As Long
' connect to the Access database
Set cn = New ADODB.Connection
cn.Open "Provider=Microsoft.Jet.OLEDB.4.0; " & _
"Data Source=C:\Users\Kent\Documents\MBA\Winter 2009 Semester\MBA 614\Final Project\shilded\testdatabase.mdb"
' open a recordset
Set rs = New ADODB.Recordset
rs.Open "CoversheetTableFourthAttempt", cn, adOpenKeyset, adLockOptimistic, adCmdTable
' all records in a table
r = 2 ' the start row in the worksheet
Do While Len(Range("A" & r).Formula) > 0
' repeat until first empty cell in column A
With rs
.AddNew ' create a new record
' add values to each field in the record
.Fields("Project") = Range("A" & r).Value
.Fields("Description") = Range("B" & r).Value
.Fields("Amount") = Range("C" & r).Value
.Fields("Date") = Range("D" & r).Value
.Update ' stores the new record
End With
r = r + 1 ' next row
Loop
rs.Close
Set rs = Nothing
cn.Close
Set cn = Nothing
End Sub
Try
DoCmd.RunSQL "DELETE * FROM TableName"
This article might be of interest: Executing SQL Statements in VBA Code
Try the following from Excel:
dim cn as adodb.connection
dim cmd as adodb.command
set cn = new adodb.connection
cn.open "put your connection string here"
set cmd = new adodb.command
cmd.commandtype = adcmdtext
cmd.commandtext = "Delete * from myTable"
cmd.activeconnection = cn.connectionstring
cmd.execute
DoCmd is internal to Access application and not recognized by Excel application.
Simple approach to your problem is to fire the delete query from Excel itself.
Add this part after your cn.Open "Provider.. line
cn.Execute "DELETE * FROM CoversheetTableFourthAttempt"
This should clear the table before next part which fills the data runs.
Your DoCmd approach has two problems. You used a quote to start a string, but didn't include a closing quote. But even with proper quoting, your DoCmd won't work because Excel does not know that CoversheetTableFourthAttempt is the name of a table in an Access database.
You showed that you can successfully create an ADO connection to your Access database. So my suggestion is to use the Execute method of the connection object to execute your SQL statment:
cn.Execute "DELETE FROM CoversheetTableFourthAttempt;"
Finally, visit Problem names and reserved words in Access to understand why Date, Description, and Project are not great choices for Access field names.

Resources