Invoking a SQL Procedure in the database via VBA code - sql

I have an SQL query which takes up a parameter stored in the db. I want to invoke the same from VBA code which is running on the same db. I am using MS Access for my work.
So for example consider, I have an SQL query 'Q' which takes a parameter 'p', I intend to invoke this SQL query from my VBA code 'C' , which also naturally involves passing this parameter 'p' to the query.
Help much appreciated
Soham

There are a few possibilities here.
Let us say it is a SELECT query built on a form that holds the parameters to be used and that the input is safe:
s = "SELECT * FROM MyTable WHERE AText ='" & Me.MyText & "'"
This can be used like so:
Forms!SomeForm.RecordSource = s
Or
Set qdf = CurrentDb.CreateQueryDef("NewQuery", s)
However, the above can be done in other, better ways.
Let us say it is a ACTION query run from a form that holds the parameters to be used and that the input is safe:
s = "UPDATE MyTable Set AText ='" & Me.MyText & "'"
Then
Set db = CurrentDB
db.Execute s, dbFailOnError
Or you can use a temporary query, which can be safer:
'Temporary query
s = "UPDATE MyTable Set AText = MyRext"
Set qdf = db.CreateQueryDef("", s)
qdf.Parameters!MyText = Me.MyText
qdf.ReturnsRecords = False
qdf.Execute dbFailOnError
Something similar to the above would also be suitable for an existing query.
You can also pass the parameter to a procedure, in which case Me.MyText becomes a variable, or you can use Inputbox, which is rarely a good idea.
After that, there is the whole world of ADO.

Related

How can I combine these two SQL queries (Access/VBA)

I am using two SQL queries in VBA that i believe they could be done in one, but I cant get it to work. I Want to turn the VBA portion into a Query outside of VBA, the VBA keeps breaking my file due to the amount of data it processes. (By break i mean it gives a message that says "this file is not a valid database" rendering the file corrupted). I search for that error but all i found was not related to breaking because of VBA code.
Anyways, here are the two queries ran with VBA.
SELECT ET.VerintEID AS EID, Sum(ET.ExceptMin)/60 AS Exeptions
FROM Tbl_VExceptTime AS ET
INNER JOIN Tbl_VCodes ON ET.Exception = Tbl_VCodes.Exception
WHERE (ET.ExceptDate Between #" & sDate & "# And #" & eDate & "#)
GROUP BY ET.VerintEID, Tbl_VCodes.IsApd
HAVING Tbl_VCodes.IsApd = ""OFF"";
I loop these results to update a table.
Do While Not .EOF
SQL = "UPDATE Tbl_AttendanceByAgent SET EXC = " & recSet.Fields(1).Value & _
" WHERE VerintID = '" & recSet.Fields(0).Value & "'"
CurrentDb.Execute SQL
.MoveNext
Loop
I know that i can save the results from the first query into a table and without looping I can update the main table with another SQL query, but I believe it can be done on a single SQL. I have tried using an UPDATE with a SELECT of the first query but it just errors out on me with an invalid syntax.
Yes this could be achieved in one single query as shown below
UPDATE Tbl_AttendanceByAgent
SET Tbl_AttendanceByAgent.EXC = t2.Exeptions
from Tbl_AttendanceByAgent t1
inner join (
SELECT ET.VerintEID AS EID, Sum(ET.ExceptMin)/60 AS Exeptions
FROM Tbl_VExceptTime AS ET
INNER JOIN Tbl_VCodes as TV ON ET.Exception = TV.Exception
WHERE (ET.ExceptDate Between #" & sDate & "# And #" & eDate & "#)
GROUP BY ET.VerintEID, TV.IsApd
HAVING Tbl_VCodes.IsApd = 'OFF'
) AS t2 on t2.EID = t1.VerintID
Note: I suppose you will replace sDate, eDate with values within your code
This question is an answer to the described errors and the given code, although it technically does not answer the request for a single SQL statement. I started adding a comment, but that's just too tedious when this answer box allows everything to be expressed efficiently at once.
First of all, referring to CurrentDb is actually NOT a basic reference to a single object instance. Rather it is more like a function call that generates a new, unique "clone" of the underlying database object. Calling it over and over again is known to produce memory leaks, and at the least is very inefficient. See MS docs for details.
Although the given code is short, it's not sweet. Not only is it repeatedly creating new database objects, it is repeatedly executing an SQL statement to update what I assume is a single row each time. That also entails regenerating the SQL string each time.
Even if executing the SQL statement repeatedly was an efficient option, there are better ways to do that, like creating a temporary (in-memory) QueryDef object with parameters. Each loop iteration then just resets the parameters and executes the same prepared SQL statement.
But in this case, it may actually be more efficient to load the table being updated into a DAO.Recordset, then use the in-memory Recordset to search for a match, then use the recordset to update the row.
I suspect that addressing a couple of those issues would make your VBA code viable.
Dim db as Database
Set db = CurrentDb 'Get just a single instance and reuse
Dim qry as QueryDef
SQL = "PARAMETERS pEXC Text ( 255 ), pID Long; " & _
" UPDATE Tbl_AttendanceByAgent SET EXC = pEXC " & _
" WHERE VerintID = pID"
set qry = db.CreateQueryDef("", SQL)
'With recSet '???
Do While Not .EOF
qry.Parameters("pEXC") = recSet.Fields(1).Value
qry.Parameters("pID") = recSet.Fields(0).Value
qry.Execute
.MoveNext
Loop
'End With recSet '???
'OR an alternative
Dim recUpdate As DAO.Recordset2
Set recUpdate = db.OpenRecordset("Tbl_AttendanceByAgent", DB_OPEN_TABLE)
Do While Not .EOF
recUpdate.FindFirst "VerintID = " & recSet.Fields(0).Value
If Not recUpdate.NoMatch Then
recUpdate.Edit
recUpdate.Fields("EXC") = recSet.Fields(1).Value
recUpdate.Update
End If
.MoveNext
Loop
I realized in commenting on Gro's answer, that the original query's aggregate clauses will produce unique values on EID, but it then becomes obvious that there is no need to group on (and sum) values which do not have Tbl_VCodes.IsApd = 'OFF'. The query would be more efficient like
SELECT ET.VerintEID AS EID, Sum(ET.ExceptMin)/60 AS Exeptions
FROM Tbl_VExceptTime AS ET
INNER JOIN Tbl_VCodes ON ET.Exception = Tbl_VCodes.Exception
WHERE (ET.ExceptDate Between #" & sDate & "# And #" & eDate & "#)
AND Tbl_VCodes.IsApd = 'OFF'
GROUP BY ET.VerintEID;
BTW, you could consider implementing the same temporary QueryDef pattern as I showed above, then you'd change the first WHERE expression to something like
PARAMETERS PsDate DateTime, PeDate DateTime;
...
WHERE (ET.ExceptDate Between [PsDate] And [PeDate])
...

Running Update Qry in VBA Based on a Query Value

I am trying to run an Update Query in VBA and am at a lost as to what I'm supposed to write for the code. I'm running a query to find the most recent date from a table. That query works fine. Now I want to run an update query to update another table's date field to equal to the date that was queried. Here is what I have:
Dim Date1 As Date
Dim newdate1
'selects datadate 1
Date1 = CurrentDb.OpenRecordset("Select Max(Date1_Event) from TBL_Event WHERE ID = '" & [Forms]![FRM_Main]![ID] & "'")(0)
'update datadate 1
newdate1 = CurrentDb.OpenRecordset("Update Tbl_Name set CollectionDate = DataDate1 WHERE PID = '" & [Forms]![FRM_Main]![ID] & "'")(0)
Is there a way to run an update query like this? Thank you.
Action queries (DELETE, UPDATE, INSERT INTO) are to be executed (CurrentDb.Execute) while SELECT queries are to be opened as recordsets (CurrentDb.OpenRecordset).
Additionally, consider using parameterization to avoid any need of quote enclosure or string concatenation in query. And here the max date is calculated with domain aggregate, DMax(), instead of opening another query.
Dim strSQL As String
Dim qdef As Querydef
' PREPARE SQL STATEMENT
strSQL = "PARAMETERS [MaxDateParam] Date, [FormIDParam] Long;" _
& "UPDATE Tbl_Name SET CollectionDate = [MaxDateParam]" _
& " WHERE PID = [FormIDParam];"
' BUILD TEMP QUERY
Set qdef = CurrentDb.CreateQueryDef("", strSQL)
' BIND PARAMETERS
qdef!MaxDateParam = DMax("Date1_Event", "TBL_Event", "ID=" & [Forms]![FRM_Main]![ID])
qdef!FormIDParam = [Forms]![FRM_Main]![ID]
' EXECUTE ACTION
qdef.Execute dbFailOnError
Set qdef = Nothing
Though above may look unusual and slightly more lines. Don't be intimidated and run for the easy 1-2 lines. Parameterization is a programming industry best practice not just in VBA but across all general purpose languages that run dynamic SQL queries using values from user input.

MS ACCESS VBA: type mismatch error in recordset name into SQL statement string

How should I write the name of the recordset correctly? I have a type mismatch error on & rsg & at "sq3=..." line. Thanks in advance.
Dim rsG As DAO.Recordset
Dim sq2, sq3 As String
sq2 = "SELECT * from GeneralTable "
sq2 = sq2 & "where gsede='LION' and (gExclu is null) and (gAda is null) "
sq2 = sq2 & "order by gnomb,gnif;"
Set rsG = CurrentDb.OpenRecordset(sq2)
sq3 = "UPDATE " & rsG & " SET gsede ='AA' WHERE gsede='LION'"
DoCmd.RunSQL (sq3)
You are mixing up totally different ways to update data - UPDATE SQL and VBA recordset.
If you want to update a recordset row by row, you do something like
Do While Not rsG.EOF
If rsG!foo = "bar" Then
rsG.Edit
rsG!gsede = "AA"
rsG.Update
End If
rsG.MoveNext
Loop
Do the update using a single query:
update generaltable
set gsede = 'AA'
where gsede ='LION' and (gExclu is null) and (gAda is null);
If you do the update off of rsG, then you'll likely do a separate update for each row, and that is inefficient.
You can't mix and match SQL and recordset objects. You can either build a full update statement that includes the logic of the first select (as other answer suggests), or you can open a dynamic recordset and loop through the records programmatically to make the updates.
That being said, looping through large recordsets programmatically to make updates is usually better handled by a bulk SQL statement, unless it is essential to consider each row individually inside the code - which in your basic example would not be the case.
MS has a good article on the DAO.Recordset object. There is a dynamic-type Recordset example about halfway down.

Adding a new record with VBA

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.)

querydef doesnt work in exportxml

I have a query that i wan't to use in Application.ExportXML. But the query expects a parameter, this parameter has to be a value of textbox in my form. Right now i have something like this.
sql = "SELECT * FROM Reports WHERE Reports.Ref_Q ='" & txtRef & "';"
Set qd = CurrentDb.QueryDefs("getReports")
qd.sql = sql
Application.ExportXML acExportQuery, getReports, strPath
When I run this code i get the following error:
The command or action '|' isn't avaible now
I als tried doing it like this
Set qd = CurrentDb.QueryDefs("getReports")
qd.Parameters("prm") = txtRef.Value
But then the prompt screen for the parameter keeps comming on the screen.
Set qd = CurrentDb.QueryDefs("getReports") is expecting to find a saved query called getReports, which may not exist.
You would need something like:
sql = "SELECT * FROM Reports WHERE Reports.Ref_Q ='" & txtRef & "';"
Set qd = CurrentDb.CreateQueryDef("getReports", sql)
DB.QueryDefs.Refresh
Application.ExportXML acExportQuery, getReports, strPath
Or:
sql = "SELECT * FROM Reports WHERE Reports.Ref_Q = [prm];"
Set qd = CurrentDb.CreateQueryDef("getReports", sql)
qd.Parameters("[prm]") = txtRef.Value
DB.QueryDefs.Refresh
Application.ExportXML acExportQuery, getReports, strPath
DB.QueryDefs.Refresh is usually necessary, as without it, Access won't find your newly created query.
Using parameters is preferable when you need to reuse the query lots of times with different parameters, as creating a query has more overhead than just calling it with a different parameter.
You might want to add a DB.QueryDefs.Delete "getReports" if you don't want your query hanging around.