Microsoft Access to SQL Server Transition - Invalid use of null on insert - sql-server

I'm migrating a legacy Microsoft Access application to use SQL Server as the backend. I've noticed since switching to SQL Server all the inserts are now coming up with invalid use of null errors.
From some testing I've found this is due to how the VBA is written. The primary key is referenced before the record is created and not surprisingly it nulls.
Dim recordSet As DAO.Recordset
Dim newID As Long
Set recordSet = dbLocal.OpenRecordset("Select * FROM tblUser", dbOpenDynaset, dbSeeChanges)
With recordSet
.AddNew
newID = !UserID
.Update
End With
recordset.Close
Now this works fine in the old Access to Access application. How is it able to do so and how can i make this work with the new SQL Server back end?
(I am aware that I can change the code to not reference it however this occurs multiple times within the application and as such I would prefer not to).

I'm surprised this ever worked. The correct way to do it is like this:
With myRecordSet
.AddNew
' Set all data columns (not the IDENTITY column)
!Data = someData
' Save new record
.Update
' Move the recordset pointer to the new record to read its ID
.Bookmark = .LastModified
newID = !UserID
' Done
.Close
End With
https://learn.microsoft.com/en-us/office/client-developer/access/desktop-database-reference/recordset-lastmodified-property-dao

Related

ADODB Recordset from Access VBA to a SQL Server fails to update a Boolean field from true to false

I'm maintaining an Access 365 database (32-bit) running on devices using Access 365 Runtime (32-bit) on Windows 10 & 11. The back-end uses Microsoft SQL Server Express (64-bit), version 15.0.4198.2, on AWS RDS. For one feature, the code uses ADODB 2.8 (the VBA reference is Microsoft ActiveX Data Objects 2.8 Library) to open a Recordset, connect to a table, and modify some fields.
The code was working just fine until I included a line to switch a boolean field from true to false. After this change, the code would throw error #-2147217864 with the description Row cannot be located for updating. Some values may have been changed since it was last read.. I isolated the code to a unit test and ensured that no other lines of code changed the recordset, but the code still threw the error.
Here's the unit test with some helper functions shown but not included:
Private Sub TestRelistingDataChangeProcess()
On Error GoTo TestFail
Dim itemSku As String
itemSku = "1234"
Dim verifySql As String
verifySql = StrFormat("SELECT failedImport FROM dbo.myTable WHERE SKU = '{0}'", itemSku)
Dim rsSql As String
rsSql = StrFormat("UPDATE dbo.myTable SET failedImport = 0 WHERE SKU = '{1}'", itemSku)
ExecuteCommandPassThrough rsSql
rsSql = "PARAMETERS SKU Text ( 255 ); SELECT * FROM myTable WHERE SKU=[SKU]"
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
cmd.ActiveConnection = GetCurrentConnection()
cmd.CommandText = rsSql
Dim param As ADODB.Parameter
Set param = cmd.CreateParameter(Name:="[SKU]", Type:=adLongVarChar, Value:=itemSku, Size:=Len(itemSku))
cmd.Parameters.Append param
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
rs.Open cmd, , adOpenDynamic, adLockOptimistic
With rs
Debug.Print "1. Setting field to TRUE."
.Fields("failedImport") = True
.Update
Assert.IsTrue ExecuteScalarAsPassThrough(verifySql)
Debug.Print "2. Setting field to FALSE."
.Fields("failedImport") = False
.Update
Assert.IsFalse ExecuteScalarAsPassThrough(verifySql)
End With
Assert.Succeed
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
Resume TestExit
End Sub
Searching for information on this error led to many possibilities, not all of them in VBA. I was aware of issues with Access and SQL Server tables with nullable boolean fields, so I verified the SQL Server table had a default value for the field. I tried numerous CursorType and LockType combinations when opening the recordset. None worked.
What am I doing wrong that causes this error to be thrown? What can I do to change the code so that it works?
After serious searching and testing, I found this blog post which included this line from the [9 Nov 2009 8:49] Tonci Grgin post:
rsCustomers.Properties("Update Criteria").Value = adCriteriaKey
I didn't recognize the adCriteriaKey enum, so I searched, found, and read this MS documentation page. This enum family "specifies which fields can be used to detect conflicts during an optimistic update of a row of the data source with a Recordset object." Specifically, the adCriteriaKey value "detects conflicts if the key column of the data source row has been changed, which means that the row has been deleted."
Through some testing and debug statements, I learned the recordset I opened used adCriteriaUpdCols by default. This value "detects conflicts if any of the columns of the data source row that correspond to updated fields of the Recordset have been changed." For whatever reason, ADODB was identifying a conflict when there shouldn't be one. I wondered whether the bug had something to do with VBA using -1 as true where SQL Server uses 1, but that doesn't appear to be the case based on this SO post.
I also don't know why the previous version of code worked when changing the boolean field from false to true but not from true to false. Perhaps there is a way to trace into the ADODB code and determine exactly what's going wrong here, but I don't know how to do it yet. I've already spent HOURS on this bug, so I need to move on... :-)
As such, here's the line of code I added to make everything work:
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
rs.Open cmd, , adOpenDynamic, adLockOptimistic
rs.Properties("Update Criteria").Value = adCriteriaKey ' <----- NEW LINE HERE
Note that this line will only work for you if your table includes a primary key and you use it in your Recordset. Also, here's another forum post showing the adCriteriaKey saving the day.
I hope this writeup makes sense to others and helps save someone in the future some time! If nothing else, it was a good exercise for me. :-)

Determining if a SQL Server table has IDENTITY column from MS Access VBA over ODBC

I have an MS Access program that constructs a SQL INSERT statement that inserts rows from an Access table into a linked SQL Server table with the same table structure. The SQL Server table may or may not have an IDENTITY primary key.
The MS Access source table has its corresponding primary key pre-populated, and these values must be inserted into the SQL Server table for referential reasons. SQL Server won't allow this unless I toggle the IDENTITY_INSERT property, but to decide if I need to toggle I need to determine if there is an IDENTITY column in the first place.
So in VBA from MS Access I need to determine if the linked SQL Server table has an IDENTITY column.
This question is covered here but for Transact-SQL.
I actually can get what I need using a pass-thru query, so it is feasible.
Create a pass-thru query to the SQL Server database.
In the SQL view enter
SELECT OBJECTPROPERTY(OBJECT_ID(N'table_name'), 'TableHasIdentity') AS 'HasIdentity'
and execute, it returns a 1-row datasheet with column HasIdentity that has value 1 if the table has an IDENTITY column.
The problem is that in VBA I cannot get at this datasheet. Given the query runs OK from MS Access I think the datasheet must correspond to a recordset and the following code should work.
Public Function metaODBCExecuteSQL(ByVal pstrConn As String) As Recordset
Dim qdf As QueryDef
Dim rst As Recordset
Set qdf = CurrentDb.CreateQueryDef("ODBC Execute")
qdf.ReturnsRecords = True
qdf.Connect = pstrConn
qdf.SQL = "SELECT OBJECTPROPERTY( OBJECT_ID(N'table_name'), 'TableHasIdentity') AS 'HasIdentity'"
Set rst = qdf.OpenRecordset(, dbSeeChanges)
qdf.Close
Set metaODBCExecuteSQL = rst
END Function
Instead the set rst line returns a run-time error 3219 invalid operation (with or without dbSeeChanges, which I believe is necessary for working with tables with IDENTITY).
Can anyone point out how I can retrieve the result set from the SELECT OBJECTPROPERTY query? What is wrong with my code, or is there another way.
After all if the MS Access UI can do it there must be a way.
If I create and save the query, this works great here:
? metaODBCExecuteSQL("")(0)
' Returns 0 or 1.
using this function and adjusting table_name to some valid table name:
Public Function metaODBCExecuteSQL(ByVal pstrConn As String) As Recordset
Dim qdf As DAO.QueryDef
Dim rst As DAO.Recordset
Set qdf = CurrentDb.QueryDefs("ODBC Execute")
qdf.SQL = "SELECT OBJECTPROPERTY( OBJECT_ID(N'table_name'), 'TableHasIdentity') AS 'HasIdentity'"
Set rst = qdf.OpenRecordset()
qdf.Close
Set metaODBCExecuteSQL = rst
End Function

How to change database for linked tables in Access MDB

I have many Access database files that contain linked tables to a SQLServer database. Now the database server has changed and I have to edit the links, possibly without recreating them.
it's possible to do it? I use Access 2013.
Yes it is possible to do with VBA but the way you'll do it really depends on how you linked the tables.
Here are 2 example of connection strings I use for SQL server tables
Direct connection:
Driver=SQL Server Native Client 10.0;Server=server_name;Address=server_address,1433;Network=network_name;Database=database_name;Trusted_Connection=Yes
DSN connection (with an entry in the ODBC control panel)
ODBC;Provider=SQLNCLI10;Server=server_name;DSN=name_of_DSN_entry_in_ODBC_control_panel;Database=database_name;Trusted_Connection=Yes
So the first thing to do is to determine how your tables are linked.
You can use this code (pay attention to the comments):
Public Sub Check_ODBC_tables()
Dim tdef As TableDef
' First loop on all tables to determine the connection strings
For Each tdef In CurrentDb.TableDefs
' only print the constring if the database name is your old database (adapt accordingly)
If InStr(tdef.Connect, "Database=old_database_name") Then
Debug.Print tdef.Connect
End If
Next
End Sub
Run this code (F5) and check the output in the immediate window. You'll find how the table are linked and what are the connection strings.
You should prepare a connection string based on that, and adapt in them the database name to use your new DB.
Once you are ready, you can adapt and run the following code that will delete the old table and recreate them with the new database names (some tweaks might be necessary), so go thought it in debug mode !
Public Sub Change_ODBC_tables()
Dim tDef As TableDef
Dim tDefNew As TableDef
Dim strTable As String
Dim strCOnString As String
' examples of constrings - ADAPT!!!!
strCOnString = "Driver=SQL Server Native Client 10.0;Server=server_name;Address=server_address,1433;Network=network_name;Database=database_name;Trusted_Connection=Yes"
strCOnString = "ODBC;Provider=SQLNCLI10;Server=server_name;DSN=name_of_DSN_entry_in_ODBC_control_panel;Database=database_name;Trusted_Connection=Yes"
For Each tDef In CurrentDb.TableDefs
If InStr(tDef.Connect, "Database=old_database_name") Then
' We find a match, store the table name
strTable = tDef.Name
' delete the linked table
DoCmd.DeleteObject acTable, strTable
' recreate the linked table with new DB name
Set tDef2 = CurrentDB.CreateTableDef(strTable)
tDef2.Connect = strCOnString
tDef2.SourceTableName = strTable
tDef2.Name = strTable
CurrentDb.TableDefs.Append tDef2
End If
Next
End Sub
If you don't fully understand the second piece of code I posted, I urge you to backup your mdb prior to run it because it will delete objects which can cause serious issues.

Inserting into SQL Server using MS Access

I know this will be a common problem, nevertheless I didn't find any solution.
I migrated my access database into SQL Server Express. SELECT and UPDATE work well, actually better that with the Access engine. But I can't get standard insertions running...
This does not work:
Set rc = CurrentDb.openRecordset("SELECT * FROM DBLog WHERE false")
rc.AddNew
rc.update '-->ERROR
rc.close
This does not work either:
DoCmd.RunSQL("INSERT INTO [test2].dbo.DBLog (type) VALUES (6)")
This does work:
sending the above SQL with pass through. So its not the SQL Servers problem.
I have set IDENTITY and PRIMARY in the Database. Access also knows the primary. In design view although there is no "Autonumber", and this may be the problem. But how to resolve that?
As a general rule, after migrating to SQL server for the back end, then a typical and common open reocrdset of
Set rst = CurrentDb.OpenRecordset("Contacts")
Needs to become
Set rst = CurrentDb.OpenRecordset("Contacts", dbOpenDynaset, dbSeeChanges)
So, in your case:
Set rc = CurrentDb.openRecordset("SELECT * FROM DBLog WHERE false" dbOpenDynaset, dbSeeChanges)
Since you have a linked table named DBLog, I would execute an INSERT statement, similar to the one which worked in SQL Server itself, against that linked table.
Dim db As DAO.Database
Dim strInsert As String
strInsert = "INSERT INTO DBLog ([type]) VALUES (6)"
Set db = CurrentDb
db.Execute strInsert, dbFailonerror
I enclosed the field name type in square brackets because it is a reserved word.

Get Connection string for Pass through queries

I have just learned about the pass through queries in MS-ACCESS.
I have a SQL SERVER backend and
if I'm right, for a query access loads all records before to do the where clause... so what would be the point of having a SQL SERVER backend?
That's why I want to try using pass through queries as much as possible but is there a way I can get the connection string from my linked tables for my pass through queries?
I tried CurrentDb.TableDefs("One of my table name").Connect in the ODBC Connect Str property but I got the error saying it's an invalid connection string.
It would be nice because I know I will have to change the connection soon so I wouldn't have to edit the connection string at many places.
Thank you.
I'm not sure what you meant here: "for a query access loads all records before to do the where clause"
If the WHERE clause can be applied at the server, ODBC will translate it to the server's language, and only the matching rows will be sent back to Access:
WHERE date_field >= #2011-01-01# AND date_field < #2012-01-01#
That WHERE clause would limit the rows sent to Access to only those whose date_field values are from 2011.
However, if a WHERE clause includes functions which must be evaluated by Access, ODBC must retrieve all candidate rows and hand them over to the Access db engine so it can perform the evaluation.
WHERE Format(date_field, 'yyyy') = '2011'
But for your actual question ... connection string for pass through queries ... consider the following code example. I have an ODBC link named dbo_foo whose source table in SQL Server is [dbo].[foo]. So I can grab the .Connect property from dbo_foo and use it for the .Connect property of a pass through query based on the same server table.
Public Sub CreatePassThruQuery()
Dim db As DAO.Database
Dim qdf As DAO.QueryDef
Dim strConnect As String
Set db = CurrentDb
strConnect = db.TableDefs("dbo_foo").Connect
Set qdf = db.CreateQueryDef("qryDbo_Foo")
qdf.Connect = strConnect
qdf.SQL = "SELECT * FROM [dbo].[foo];"
qdf.Close
Set qdf = Nothing
Set db = Nothing
End Sub
Still when you change the .Connect property of the table, you will also need to do it for the query. If you have many of them and/or change the connections frequently, it may be worth the effort to create a VBA procedure to update them. Alternatively, you might use a DSN for the .Connect property of the table and matching query. Then revise the DSN as needed. A pitfall with that approach is that, if other people will be using your application, you would need to manage the DSNs on multiple machines.

Resources