views:

4339

answers:

4

Seems a common enough problem this, but most solutions refer to concatenating multiple SQL commands, something which I believe can't be done with ADO/VBA (I'll be glad to be shown wrong in this regard however).

I currently insert my new record then run a select query using (I hope) enough fields to guarantee that only the newly inserted record can be returned. My databases are rarely accessed by more than one person at a time (negligible risk of another insert happening between queries) and due to the structure of the tables, identifying the new record is normally pretty easy.

I'm now trying to update a table that does not have much scope for uniqueness, other than in the artificial primary key. This means there is a risk that the new record may not be unique, and I'm loathe to add a field just to force uniqueness.

What's the best way to insert a record into an Access table then query the new primary key from Excel in this situation?

Thanks for the replies. I have tried to get @@IDENTITY working, but this always returns 0 using the code below.

Private Sub getIdentityTest()
    Dim myRecordset As New ADODB.Recordset
    Dim SQL As String, SQL2 As String

    SQL = "INSERT INTO tblTasks (discipline,task,owner,unit,minutes) VALUES (""testDisc3-3"",""testTask"",""testOwner"",""testUnit"",1);"
    SQL2 = "SELECT @@identity AS NewID FROM tblTasks;"

    If databaseConnection Is Nothing Then
        createDBConnection
    End If

    With databaseConnection
        .Open dbConnectionString
        .Execute (SQL)
        .Close
    End With

    myRecordset.Open SQL2, dbConnectionString, adOpenStatic, adLockReadOnly

    Debug.Print myRecordset.Fields("NewID")

    myRecordset.Close

    Set myRecordset = Nothing
End Sub

Anything stand out being responsible?

However, given the caveats helpfully supplied by Renaud (below) there seems nearly as much risk with using @@IDENTITY as with any other method, so I've resorted to using SELECT MAX for now. For future reference though I would be interested to see what is wrong with my attempt above.

+3  A: 

If the artificial key is an autonumber, you can use @@identity.

Note that with both these examples, the transaction is isolated from other events, so the identity returned is the one just inserted. You can test this by pausing the code at Debug.Print db.RecordsAffected or Debug.Print lngRecs and inserting a record manually into Table1, continue the code and note that the identity returned is not that of the record inserted manually, but of the previous record inserted by code.

DAO Example

'Reference: Microsoft DAO 3.6 Object Library '
Dim db As DAO.Database
Dim rs As DAO.Recordset

Set db = CurrentDb

db.Execute ("INSERT INTO table1 (field1, Crdate ) " _
            & "VALUES ( 46, #" & Format(Date, "yyyy/mm/dd") & "#)")
Debug.Print db.RecordsAffected
Set rs = db.OpenRecordset("SELECT @@identity AS NewID FROM table1")
Debug.Print rs.Fields("NewID")

ADO Example

Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset

Set cn = CurrentProject.Connection

cn.Execute ("INSERT INTO table1 (field1, Crdate ) " _
            & "VALUES ( 46, #" & Format(Date, "yyyy/mm/dd") & "#)"), lngRecs
Debug.Print lngRecs
rs.Open "SELECT @@identity AS NewID FROM table1", cn
Debug.Print rs.Fields("NewID")
Remou
Thanks, will give this a try.
Lunatik
With this I get a 3001 error "Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another". I also can't find "OpenRecordset" in the object browser, which might explain things somewhat.I have the ADO 2.6 Library set as a reference, if that's any help.
Lunatik
The code is for DAO, which is usually best for MS Access. I should be able to run up something in ADO, if you need it.
Remou
Note that SELECT @@IDENTITY requires no FROM clause. Indeed, the FROM clause is ignored, as it gets its value from the connection/database object that executed the INSERT and that is not table-specific. In fact, adding a FROM table means you'll get as many rows as there are in the table. I use db.OpenRecordset("SELECT @@IDENTITY")(0) all the time because it returns the first item of the recordset's Fields collection (the default collection of a DAO recordset object).
David-W-Fenton
+3  A: 

About your question:

I'm now trying to update a table that does not have much scope for uniqueness, other than in the artificial primary key. This means there is a risk that the new record may not be unique, and I'm loathe to add a field just to force uniqueness.

If you are using an AutoIncrement for your primary key, then you have uniqueness and you could use SELECT @@Identity; to get the value of the last autogenerated ID (see caveats below).

If you are not using autoincrement, and you are inserting the records from Access but you want to retrieve the last one from Excel:

  • make sure your primary key is sortable, so you can get the last one using a query like either of these:

    SELECT MAX(MyPrimaryField) FROM MyTable;
    SELECT TOP 1 MyPrimaryField FROM MyTable ORDER BY MyPrimaryField DESC;
    
  • or, if sorting your primary field wouldn't give you the last one, you would need to add a DateTime field (say InsertedDate) and save the current date and time every time you create a new record in that table so you could get the last one like this:

    SELECT TOP 1 MyPrimaryField FROM MyTable ORDER BY InsertedDate DESC;
    

In either of these cases, I think you would find adding an AutoIncrement primary key as being a lot easier to deal with:

  • It's not going to cost you much

  • It's going to guarantee you uniqueness of your records without having to think about it

  • It's going to make it easier for you to pick the most recent record, either using @@Identity or through sorting by the primary key or getting the Max().

From Excel

To get the data into Excel, you have a couple of choices:

  • create a data link using a query, so you can use the result directly in a Cell or a range.

  • query from VBA:

    Sub GetLastPrimaryKey(PrimaryField as string, Table as string) as variant
        Dim con As String
        Dim rs As ADODB.Recordset
        Dim sql As String
        con = "Provider=Microsoft.ACE.OLEDB.12.0;" & _
              "Data Source= ; C:\myDatabase.accdb"
        sql = "SELECT MAX([" & PrimaryField & "]) FROM [" & MyTable & "];"
        Set rs = New ADODB.Recordset
        rs.Open sql, con, adOpenStatic, adLockReadOnly
        GetLastPrimaryKey = rs.Fields(0).Value
        rs.Close
        Set rs = Nothing
    End Sub
    

Note about @@Identity

You have to be careful of the caveats when using @@Identity in standard Access databases(*):

  • It only works with AutoIncrement Identity fields.

  • It's only available if you use ADO and run SELECT @@IDENTITY;

  • It returns the latest used counter, but that's for all tables. You can't use it to return the counter for a specific table in MS Access (as far as I know, if you specify a table using FROM mytable, it just gets ignored).
    In short, the value returned may not be at all the one you expect.

  • You must query it straight after an INSERT to minimize the risk of getting a wrong answer.
    That means that if you are inserting your data at one time and need to get the last ID at another time (or another place), it won't work.

  • Last but not least, the variable is set only when records are inserted through programming code.
    This means that is the record was added through the user interface, @@IDENTITY will not be set.

(*): just to be clear, @@IDENTITY behaves differently, and in a more predictive way, if you use ANSI-92 SQL mode for your database.
The issue though is that ANSI 92 has a slightly different syntax than the ANSI 89 flavour supported by Access and is meant to increase compatibility with SQL Server when Access is used as a front end.

Renaud Bompuis
"SELECT MAX" smells real bad. While it will work in circumstances described, as in all cases things change over time, and before you know it this code will fail! Dont make smelly code, think about the next guy
TFD
@TFD: agreed on principle, but there are not that many ways to achieve what the poster wants, at least in MS Access. It's always going to be a trade-off. Max() will work OK for most data types, including strings, although I agree that there are probably cases where you may not get what you want.
Renaud Bompuis
Thanks for the detailed answer, and for the helpful caveats about @@IDENTITY - I'd vote up if I could.
Lunatik
You're welcome Lunatik.
Renaud Bompuis
@@identity used with both DAO and ADO returns the record associated with the connection object or database object you are using in the code, not the current (live) identity. I have made some notes in my post.
Remou
@Remou: could you point to any online source with more info on @@IDENTITY for MS Access that describe the limitations you mention? I think it would be useful here. Thanks.
Renaud Bompuis
"It's only available if you use ADO": not true: if you put the Access UI (Access 2003 and above) into SQL-92 Query Mode then SELECT @@IDENTITY can be executed from a regular Query window in the interface.
onedaywhen
"[if] the record was added through the user interface, @@IDENTITY will not be set": not true: again, when the Access interface is in ANSI-92 Query Mode then SELECT @@IDENTITY can be executed from a regular Query window and return the value generated via the interface.
onedaywhen
The big *if* here is that you must have actively chosen "ANSI-92" compliance in your database options, which changes Access' behaviour.By default, Access behaves as its own documentation says.The bottom line is that @@INDENTITY has many pitfalls and it is inadequately documented in Access.
Renaud Bompuis
Agreed. Indeed, Jet/ACE is inadequately documented in Access IMO (and Jet/ACE is largely undocumented!)
onedaywhen
Latecomer to this party, but ANSI-92 mode is not required SELECT @@IDENTITY to work, even in the Access QBE. It also works in DAO just fine. But it's reliable only when used directly after an INSERT statement, and using the same database object that executed the INSERT. This means it's not terribly helpful excecuted from the Access QBE, but in DAO, you have full control, just as you do in ADO (where you'd use the same connection object). The main caveat is that you can't use CurrentDB to execute the INSERT and then get anything useful from CurrentDB, as the pointer is not the same.
David-W-Fenton
A: 

Try following macro code.First add a command button to the sheet from the control box and paste following codes in the code window

Private Sub CommandButton1_Click()
    MsgBox GetLastPrimaryKey
End Sub

Private Function GetLastPrimaryKey() As String
Dim con As String
Dim cn As ADODB.Connection
Dim rs As ADODB.Recordset
Dim sql As String
con = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\myaccess.mdb;Persist Security Info=False"
sql = "SELECT MAX(id) FROM  tblMyTable"

Set cn = New ADODB.Connection
Set rs = New ADODB.Recordset
cn.Open con
rs.Open sql, cn, 3, 3, 1
If rs.RecordCount <> 0 Then
   GetLastPrimaryKey = rs.Fields(0).Value
End If
rs.Close
cn.Close
Set rs = Nothing
Set cn = Nothing
End Function

Chris

Convert your Excel spreadsheet into an online calculator.
http://www.spreadsheetconverter.com

+2  A: 

I have tried to get @@IDENTITY working, but this always returns 0 using the code below.

Your code sends SQL and SQL2 through different connection objects. I don't think @@identity will return anything other than zero unless you ask from the same connection where you executed your INSERT statement.

Try changing this:

myRecordset.Open SQL2, dbConnectionString, adOpenStatic, adLockReadOnly

to:

myRecordset.Open SQL2, databaseConnection, adOpenStatic, adLockReadOnly
HansUp
D'oh, that's what I get for reusing bits of code from different projects! I'm on another project at the moment, but your suggestion makes perfect sense and I'll try it when I get a chance.
Lunatik