Adding a new record with VBA - sql

I have a form in which one of the ComboBoxes lists all the documents of a given project. The user should select one and after pressing a button, and if present in Table Dessinsit opens a second form showing that record. If it is not present in that table, I want to add it in.
One of my collegues told me all I had to do was to execute an SQL query with VBA. What I have so far is this:
Dim rsDessin As DAO.Recordset
Dim strContrat As String
Dim strProjet As String
Dim strDessin As String
Dim sqlquery As String
'I think these next 3 lines are unimportant. I set a first query to get information I need from another table
strDessin = Me.Combo_Dessin
strProjet = Me.Combo_Projet
sqlquery = "SELECT [Projet HNA] FROM [Projets] WHERE [Projet AHNS] = '" & strProjet & "'"
Set rsDessin = CurrentDb.OpenRecordset(sqlquery)
If Not rsDessin.RecordCount > 0 Then 'If not present I want to add it
strContrat = rsDessin![Projet HNA]
sqlquery = "INSERT INTO Feuilles ([AHNS], [Contrat], [No Projet]) VALUES (strDessin, strContrat, strDessin)"
'Not sure what to do with this query or how to make sure it worked.
End If
'Checking my variables
Debug.Print strProjet
Debug.Print strContrat
Debug.Print strDessin
'By here I'd like to have inserted my new record.
rsDessin.Close
Set rsDessin = Nothing
I also read online that i could achieve a similar result with something like this:
Set R = CurrentDb.OpenRecordset("SELECT * FROM [Dessins]")
R.AddNew
R![Contrat] = strContrat
R![Projet] = strProjet
R![AHNS] = strDessin
R.Update
R.Close
Set R = Nothing
DoCmd.Close
Is one way better than the other? In the case where my INSERT INTO query is better, what should I do to execute it?

You're asking which is preferable when inserting a record: to use an SQL statement issued to the Database object, or to use the methods of the Recordset object.
For a single record, it doesn't matter. However, you could issue the INSERT statement like this:
CurrentDb.Execute "INSERT INTO Feuilles ([AHNS], [Contrat], [No Projet]) VALUES (" & strDessin & ", " & strContrat & ", " & strDessin & ")", dbFailOnError
(You should use the dbFailOnError option to catch certain errors, as HansUp points out in this answer.)
For inserting multiple records from another table or query, it is generally faster and more efficient to issue an SQL statement like this:
Dim sql = _
"INSERT INTO DestinationTable (Field1, Field2, Field3) " & _
"SELECT Field1, Field2, Field3 " & _
"FROM SourceTable"
CurrentDb.Execute sql
than the equivalent using the Recordset object:
Dim rsSource As DAO.Recordset, rsDestination As DAO.Recordset
Set rsSource = CurrentDb.OpenRecordset("SourceTable")
Set rsDestination = CurrentDb.OpenRecordset("DestinationTable")
Do Until rs.EOF
rsDestination.AddNew
rsDestination!Field1 = rsSource!Field1
rsDestination!Field2 = rsSource!Field2
rsDestination!Field3 = rsSource!Field3
rsDestination.Update
rs.MoveNext
Loop
That said, using an SQL statement has its limitations:
You are limited to SQL syntax and functions.
This is partially mitigated in Access, because SQL statements can use many VBA built-in functions or functions that you define.
SQL statements are designed to work on blocks of rows. Per-row logic is harder to express using only the Iif, Choose, or Switch functions; and logic that depends on the current state (e.g. insert every other record) is harder or impossible using pure SQL. This can be easily done using the Recordset methods approach.
This too can be enabled using a combination of VBA and SQL, if you have functions that persist state in module-level variables. One caveat: you'll need to reset the state each time before issuing the SQL statement. See here for an example.

One part* of your question asked about INSERT vs. Recordset.AddNew to add one row. I suggest this recordset approach:
Dim db As DAO.Database
Dim R As DAO.Recordset
Set db = CurrentDb
Set R = db.OpenRecordset("Dessins", dbOpenTable, dbAppendOnly)
With R
.AddNew
!Contrat = rsDessin![Projet HNA].Value
!Projet = Me.Combo_Projet.Value
!AHNS = Me.Combo_Dessin.Value
.Update
.Close
End With
* You also asked how to execute an INSERT. Use the DAO.Database.Execute method which Zev recommended and include the dbFailOnError option. That will add clarity about certain insert failures. For example, a key violation error could otherwise make your INSERT fail silently. But including dbFailOnError ensures you get notified about the problem immediately. So always include that option ... except in cases where you actually want to allow an INSERT to fail silently. (For me, that's never.)

Related

MS Access Update Query where column updated depends on field in another table

I have a table (InfoTable) that I would like to update based on another table (UpdTable). The UpdTable has three columns, OldEntry, NewEntry, and UpdateField. InfoTable has columns that will match UpdateField. I will have one line per update. So, for example, UpdTable:
OldEntry
NewEntry
UpdateField
Peter
Paul
Customer
123khjiv
3546sfdg
Serial Number
I would like to feed a SQL query or SQL code that takes UpdTable and have it update InfoTable. InfoTable has many columns, but I only want the columns in UpdateField modified.
I am trying to streamline this process since I am currently doing the updates one by one by hand. At this time, I do not have any code written as I have been unable to formulate a working plan.
Thank you.
Consider looping through the records of UpdTable and pass values into a parameterized UPDATE query using DAO Recordset and QueryDef objects:
Dim updRST As DAO.Recordset
Dim qDEF As DAO.QueryDef
' OPEN RECORDSET
Set updRST = CurrentDb.OpenRecordset("UpdTable")
updRST.MoveFirst
' LOOP THROUGH RECORDS
Do While Not updRST.EOF
' PREPARED SQL STATEMENT
sql = "PARAMETERS [new_val] TEXT, [old_val] TEXT; " _
& "UPDATE InfoTable SET [" & updRST!UpdateField & "] = [new_val] " _
& "WHERE [" & updRST!UpdateField & "] = [old_val]"
Set qDEF = CurrentDb.CreateQueryDef("", sql)
qDEF!new_val = updRST!NewEntry ' BIND PARAMS
qDEF!old_val = updRST!OldEntry
qDEF.Execute dbFailOnError ' EXECUTE ACTION
Set qDEF = Nothing
updRST.MoveNext
Loop
updRST.Close
Set updRST = Nothing

SQL statement in VBA

I am trying to run the following SQL statement in ACCESS 2013 VBA but am getting errors due to wrong formatting (in this case I get "Semicolon (;) missing from end of statement"). Could anybody tell me what I am doing wrong in the code below please?
Dim dbs As dao.Database
Set dbs = CurrentDb()
dbs.Execute "INSERT INTO TEMP2 ([Study_Date], [Created_By], [Part_Number],
[Upper_Tolerance], [Lower_Tolerance], [ID21_Number]) VALUES ([Study_Date],
[Created_By], [Part_Number], [Upper_Tolerance], [Lower_Tolerance], [ID21_Number])
FROM RAC_DATA_ENTRY
WHERE [RAC_CAP_VALS] = '" & Me.[RAC_CAP_VALS] & "'"
Don't use VALUES when you're pulling data from one table to INSERT into another. Use SELECT instead.
This example uses just two of your fields. Add in the others you need.
Dim strInsert As String
strInsert = "INSERT INTO TEMP2 ([Study_Date], [Created_By])" & _
" SELECT [Study_Date], [Created_By] FROM RAC_DATA_ENTRY" & _
" WHERE [RAC_CAP_VALS] = '" & Me.[RAC_CAP_VALS].Value & "';"
Debug.Print strInsert '<- view this in Immediate window; Ctrl+g will take you there
dbs.Execute strInsert, dbFailOnError
Notes:
A semicolon at the end of the statement is optional. Access will consider the statement valid with or without it.
Value is not actually required following Me.[RAC_CAP_VALS], since it's the default property. I prefer to make it explicit.
dbFailOnError gives you better information about failed inserts. Without it, a problem such as a primary key violation would fail silently.
Debug.Print strInsert allows you to inspect the statement you built and are asking the db engine to execute. If there is a problem, you can copy the statement text from the Immediate window and paste it into SQL View of a new Access query for testing.

Recordset.Edit or Update sql vba statement fastest way to update?

I recently came across vba update statements and I have been using Recordset.Edit and Recordset.Update to not only edit my existing data but to update it.
I want to know the difference between the two: recordset.update and Update sql Vba statement. I think they all do the same but I can't figure which one is more efficient and why.
Example code below:
'this is with sql update statement
dim someVar as string, anotherVar as String, cn As New ADODB.Connection
someVar = "someVar"
anotherVar = "anotherVar"
sqlS = "Update tableOfRec set columna = " &_
someVar & ", colunmb = " & anotherVar &_
" where columnc = 20";
cn.Execute stSQL
This is for recordset (update and Edit):
dim thisVar as String, someOthVar as String, rs as recordset
thisVar = "thisVar"
someOthVar = "someOtherVar"
set rs = currentDb.openRecordset("select columna, columnb where columnc = 20")
do While not rs.EOF
rs.Edit
rs!columna = thisVar
rs!columnb = someOthvar
rs.update
rs.MoveNext
loop
Assuming WHERE columnc = 20 selects 1000+ rows, as you mentioned in a comment, executing that UPDATE statement should be noticeably faster than looping through a recordset and updating its rows one at a time.
The latter strategy is a RBAR (Row By Agonizing Row) approach. The first strategy, executing a single (valid) UPDATE, is a "set-based" approach. In general, set-based trumps RBAR with respect to performance.
However your 2 examples raise other issues. My first suggestion would be to use DAO instead of ADO to execute your UPDATE:
CurrentDb.Execute stSQL, dbFailonError
Whichever of those strategies you choose, make sure columnc is indexed.
The SQL method is usually the fastest for bulk updates, but syntax is often clumsy.
The VBA method, however, has the distinct advantages, that code is cleaner, and the recordset can be used before or after the update/edit without requering the data. This can make a huge difference if you have to do long-winded calculations between updates. Also, the recordset can be passed ByRef to supporting functions or further processing.
I have found that when I need to update every record in a table in order, such as adding a sequential ID when using Autonumber is not feasible, adding a running total, or any calculation that is incremental based on some value in the recordset, that the DAO method is much faster.
If your data is not in the order you need it processed in, and you instead need to rely on matching values to the data source, then SQL is much more efficient.
Dim rs As DAO.Recordset
Set rs = CurrentDb.OpenRecordset("select invoice_num from dbo_doc_flow_data where barcode = '" & Me.barcode_f & "'")
Do While Not rs.EOF
rs.Edit
rs!invoice_num = Me!invoice_num_f
rs.Update
rs.MoveNext
Loop
rs.Close

count the number of rows in sql query result using access vba

I am trying to count the number of rows in sql query result using access 2007 vba.
What I have is a text box named AGN when a user put value on it check for this value then it bring back MsgBox if the the value is already inserted. What I try to do is :
Dim rs As DAO.Recordset
Dim db As Database
Dim strSQL As String
Set db = CurrentDb
strSQL = "SELECT agencies.[agency no] FROM agencies WHERE agencies.[agency no]= " &Me.AGN.Text
Set rs = db.OpenRecordset(strSQL)
If rs.Fields.Count > 1 Then
MsgBox "this value is already here "
End If
Set rs = Nothing
When I insert any value on the textbox I got run time error 3061 (too few parameters)
The "too few parameters" error message generally means there is something in your SQL statement which Access doesn't recognize as a field, table, function or SQL keyword. In this case, it could happen if [agency no] is text rather than numeric data type. If that is the case, enclose the value of AGN with quotes when you build the SQL statement. (Or you could use a parameter query to avoid the need to quote the text value.)
strSQL = "SELECT a.[agency no] FROM agencies AS a" & vbCrLf & _
"WHERE a.[agency no]= '" & Me.AGN.Value & "'"
Debug.Print strSQL
In case of trouble, go to the Immediate window and copy the output from Debug.Print. Then you can create a new query in the Access query designer, switch to SQL View and paste in the statement text for testing.
Once your SELECT is working, you can check whether or not the recordset is empty. When it is empty both its BOF and EOF properties are true. So to detect when it is not empty, check for Not (BOF And EOF) ...
With rs
If Not (.BOF And .EOF) Then
MsgBox "this value is already here "
End If
End With
However you don't actually need to open a recordset to determine whether a matching row exists. You can check the value returned by a DCount expression.
Dim lngRows As Long
lngRows = DCount("*", "agencies", "[agency no]='" & Me.AGN.Value & "'")
If lngRows > 0 Then
MsgBox "this value is already here "
End If
Notes:
I used AGN.Value instead of AGN.Text because the .Text property is only accessible when the control has focus. But I don't know where you're using that checking code, so unsure which is the proper choice for you.
Notice the similarities between the SELECT query and the DCount options. It's often easy to translate between the two.

How to get id of newly inserted record using Excel VBA?

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.
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.
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")
Re: "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
Here's my solution that does not use ##index or MAX.
Const connectionString = "Provider=SQLOLEDB; Data Source=SomeSource; Initial Catalog=SomeDB; User Id=YouIDHere; Password=YourPassword"
Const RecordsSQL = "SELECT * FROM ThatOneTable"
Private Sub InsertRecordAndGetID()
Set connection = New ADODB.connection
connection.connectionString = connectionString
connection.Open
Set recordset = New ADODB.recordset
recordset.Open SQL, connection, adOpenKeyset, adLockOptimistic
With recordset
.AddNew
!Field1 = Value1
!Field2 = Value2
End With
recordset.MoveLast
ID = recordset.Fields("id")
End Sub
Enjoy!
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
8 years late to the party... The problem you are having is that you are using dbConnectionString to create a new connection. ##identity is specific to the connection you are using.
First, don't close the original connection
'.Close
replace
myRecordset.Open SQL2, dbConnectionString, adOpenStatic, adLockReadOnly
with the connection you previously used for the insert
myRecordset.Open SQL2, databaseConnection, adOpenStatic, adLockReadOnly
and you'd have been all set. In fact, you don't even need to specify the table:
SQL2 = "SELECT ##identity AS NewID"