I am trying to update access table using simple VBA code, however it finished with an error. I have tried various ways to solve it but without success.
Could you please help? The code is as follow:
strSQL = "UPDATE Projects " & _
"SET Projects.id_status = '" & Me.T_project_s.Value & "' " & _
"WHERE Projects.id_project = '" & Me.curr_open.Value & "';"
I have also tried:
strSQL = "UPDATE Projects " & _
"SET Projects.id_status = [" & Me.T_project_s.Value & "] " & _
"WHERE Projects.id_project = [" & Me.curr_open.Value & "];"
or
strSQL = "UPDATE [Projects] " & _
"SET [Projects].[id_status] = '" & Me.T_project_s.Value & "' " & _
"WHERE [Projects].[id_project] = '" & Me.curr_open.Value & "';"
But it asks for a data which is available in those fields.
Your suggestion helped. I started with only a text then I have changed particular variables I wanted to be read. So in the Where statement there is no need to have beside "" also '' :).
strSQL = "UPDATE [Projects] " & _
"SET [Projects].[id_status] = '" & Me.T_project_s.Value & "' " & _
"WHERE [Projects].[id_project] = " & Me.curr_open.Value & ";"
Thanks.
Once again, here is an example where parameterization (an industry best practice in SQL programming) helps beyond avoiding SQL injection. With querydef parameters you:
avoid the need of quote enclosure;
avoid string interpolation of variables;
abstract data (i.e., VBA variables) from code (i.e., SQL statement) for cleaner scripts;
(plus as OP found out with mixed types) explicitly define the data types of values to be binded;
execute the query via DAO for smoother user interface than DoCmd.RunSQL that raises warnings to users.
Temp Query
Dim qdef As QueryDef
' PREPARED STATEMENT, DEFINING PLACEHOLDERS (NO DATA)
strSQL = "PARAMETERS [project_s_param] Text(255), [curr_open_param] Long;" & _
" UPDATE [Projects]" & _
" SET [Projects].[id_status] = [project_s_param]" & _
" WHERE [Projects].[id_project] = [curr_open_param];"
' CREATE UNNAMED TEMP QUERYDEF, ASSIGNING PREPARED STATEMENT
Set qdef = CurrentDb.CreateQueryDef("", strSQL)
' BIND VBA VALUES TO PARAMETER PLACEHOLDERS
qdef![project_s_param] = Me.T_project_s.Value
qdef![curr_open_param] = Me.curr_open.Value
' EXECUTE ACTION
qdef.Execute dbFailOnError
Set qdef = Nothing
Saved Query
Even better, save entire prepared statement as a stored Access query and avoid any SQL in VBA.
SQL (save as any regular query object whose name is referenced in VBA)
PARAMETERS [project_s_param] Text(255), [curr_open_param] Long;
UPDATE [Projects]
SET [Projects].[id_status] = [project_s_param]
WHERE [Projects].[id_project] = [curr_open_param]
VBA
Dim qdef As QueryDef
' REFERENCE EXISTING QUERYDEF, ASSIGNING PREPARED STATEMENT
Set qdef = CurrentDb.QueryDefs("mySavedQuery")
' BIND VBA VALUES TO PARAMETER PLACEHOLDERS
qdef![project_s_param] = Me.T_project_s.Value
qdef![curr_open_param] = Me.curr_open.Value
' EXECUTE ACTION
qdef.Execute dbFailOnError
Set qdef = Nothing
Related
I'm using this code and very similar others that are working perfectly but I'm wondering if there is a more elegant way to do it:
(Extract)
Erase ArrUserMetRecCentros
Cnn.Open
CnnQry = _
"SELECT" & _
" APP_METERS_Q_Profiles.ProfileId, " & _
" Main_Profile_Info.FieldValue, " & _
" APP_METERS_T_Access.[Read], " & _
" APP_METERS_T_Access.Write " & _
"FROM (APP_METERS_Q_Profiles " & _
" INNER JOIN Main_Profile_Info ON APP_METERS_Q_Profiles.ProfileId = Main_Profile_Info.ProfileId) " & _
" INNER JOIN APP_METERS_T_Access ON APP_METERS_Q_Profiles.ProfileId = APP_METERS_T_Access.APPMETERSQProfileId " & _
"WHERE " & _
" (((Main_Profile_Info.FieldId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSQUserId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSTDataTypeId)=1) AND " & _
" ((APP_METERS_T_Access.[Read])=-1));"
Set rst = Cnn.Execute(CnnQry)
With rst
x = x + 1
Do Until rst.EOF
For i = 1 To 4
ArrUserMetRecCentros(x, i) = rst.Fields(i - 1).Value
Next i
.MoveNext
x = x + 1
Loop
ArrUserMetRecCentros(0, 0) = x - 1
End With
Cnn.Close
This connects to a MS SQL EXPRESS server and stores the requested data in an array that I'm going to use in EXCEL.
I'm not looking for you to rewrite my code (it's my job ;)) just few tips if there's any. Things like "Not to use 'Set rst= blablabla' better option is to do 'this'. Or "define all your queries in different public string variables to keep code cleaner", etc.
Thank you very much for any help received.
Gustavo.
CnnQry = _
"SELECT" & _
" APP_METERS_Q_Profiles.ProfileId, " & _
" Main_Profile_Info.FieldValue, " & _
" APP_METERS_T_Access.[Read], " & _
" APP_METERS_T_Access.Write " & _
"FROM (APP_METERS_Q_Profiles " & _
" INNER JOIN Main_Profile_Info ON APP_METERS_Q_Profiles.ProfileId = Main_Profile_Info.ProfileId) " & _
" INNER JOIN APP_METERS_T_Access ON APP_METERS_Q_Profiles.ProfileId = APP_METERS_T_Access.APPMETERSQProfileId " & _
"WHERE " & _
" (((Main_Profile_Info.FieldId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSQUserId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSTDataTypeId)=1) AND " & _
" ((APP_METERS_T_Access.[Read])=-1));"
That doesn't belong in code. It's a gigantic string literal: you get no syntax highlighting, no validation, no intellisense; you need to track parentheses manually, and beyond 20 or so line continuations, it stops compiling and you start doing things like CnnQry = CnnQty & "...some more inline SQL..."
If it needed parameter values, they'd probably be just concatenated in - and then you'd need to care for single quotes, and poof you have a SQL Injection vulnerability that's not only a serious security issue, but also a bug, no less.
Inline SQL is an abomination, regardless of what language it's done in. The less you have, the better.
For what it's worth I make a typo every single time I try to type CnnQry: vowels are permitted, and there's nothing wrong with using sql either.
You want SQL queries on the SQL Server side, not in your code.
CREATE PROCEDURE dbo.GiveMeAGoodName
AS BEGIN
SELECT
...
FROM ...
INNER JOIN ...
INNER JOIN ...
WHERE ...
END
And now your inline SQL in VBA becomes:
Const sql As String = "dbo.GiveMeAGoodName"
And the VBA code should use a Command to get the Recordset:
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
cmd.ActiveConnection = Cnn
cmd.CommandText = sql
cmd.CommantType = adCmdStoredProc
'add parameters if needed:
'cmd.Parameters.Append cmd.CreateParameter(...)
Dim results As ADODB.Recordset
Set results = cmd.Execute
Note that with proper parameterization as above, you're letting SQL Server do its job and deal with quoting, and Little Bobby Tables can't do any harm.
Use Range.CopyFromRecordset to dump a recordset onto a Range without looping.
I can't comment due to lack of rep.
Rather than loop your recordset at the 'With' Statement; I'd just add the recordset as a whole to the spreadsheet.
Set rst = Cnn.Execute(CnnQry)
If rst.EOF Then
'Catching empty recordsets
Else
Worksheets("Today").Range("A2").CopyFromRecordset rst
End If
Cnn.Close
To advise. We use an ADODB Connection and Recordset objects. Your code looks like you do this also.
You could look at just using the array from .GetRows method of the recordset object
see
https://learn.microsoft.com/en-us/sql/ado/reference/ado-api/getrows-method-ado?view=sql-server-ver15
You can add the wheres to your join also, like customers join orders on (customers.id=orders.id AND order.value>100)
I want some sort of label that is counting every record that is imported in the database.
Do I need to use a for each loop?
Can someone explain me how to do it, or set me on the right way to do it?
With cmd
.CommandText = "INSERT INTO Workflow ([Import], [DossierPad]) VALUES ('" + Import + "','" + Pad + "')"
.ExecuteNonQuery()
.CommandText = "INSERT INTO Data ([DossierPad], [Tif], [Grootte]) VALUES ('" + Pad + "','" + stukjes(0) + "','" + stukjes(2) + "')"
.ExecuteNonQuery()
If Tifcheck(Tif) = False Then
cmd.CommandText = "Update Data Set Tif = '" & Tif & "' WHERE Tif="
ElseIf Tifcheck(Tif) = True Then
End If
If stukjes(2) < 20000 Then
.CommandText = "UPDATE Data SET Blanco = '" & blanco & "' WHERE DossierPad = '" & Pad & "'"
.ExecuteNonQuery()
Else
.CommandText = "UPDATE Data SET Blanco = '" & blanco1 & "' WHERE DossierPad = '" & Pad & "'"
.ExecuteNonQuery()
End If
End With
This is the part of code where I insert records in my database.
Now my question is how can I get a label to count every record in the database while it is intering.
As specified in the MSDN docs, the method ExecuteNonQuery() returns the number of rows affected by your query. This is often used to check that your insert query ended up correctly.
So what you can do is declare an Integer somewhere in your code, and increment this integer with the result of the ExecuteNonQuery() method call.
At the end, you can update a Label with the value of your integer.
Some code:
'At the beginning of your update function
Dim myCounter As Integer
'...
'Whenever you run ExecuteNonQuery() on an INSERT statement
myCounter += .ExecuteNonQuery()
'...
'Finally, on the label that should display the result
myLabel.Text = "Updated " & myCounter.toString() & " rows."
I have been working on passing store procedures in different scenarios. Currently, I am trying to call a SP via pass-through query inside the do-while loop. I have a SP named usp_MovePending and a pass-through query qryPending. I have a code with do-while loop in which I have to call the SP.
Public Sub Import()
On Error Resume Next
Dim DB As Database
Dim rs As Recordset
Dim UserStr As String
Dim ROG As Date
Dim qdf As QueryDef
Dim sql As String
UserStr = sqlfixup(CurrentUser())
Set DB = CodeDb
Set rs = DB.OpenRecordset("select * from tblQty where materialid <> 0")
DoCmd.Hourglass True
Set qdf = DB.CreateQueryDef("")
qdf.Connect = SQLConnectString
qdf.ReturnsRecords = True
Do While rs.EOF = False
With MatManagement.Transactions
.BeginTransaction
.TransactionClassID = 1
.MaterialID = rs!MaterialID
.TransactionCreateUser = CurrentUser()
.TransactionDate = Date
.CommitTransaction
If .Failure <> True Or .TransactionID <> 0 Then
CodeDb.Execute "update tblMMTransactions set transNote = '" & sqlfixup(rs.Fields("note")) & "' where transactionid = " & .TransactionID, dbSeeChanges
End If
With CurrentDb.QueryDefs("qryPending")
.sql = "exec usp_MovePending #materialid=" & rs!MaterialID & ",#SysUser=" & " '" & UserStr & "'" & "," & " #ROG=getdate()" & ",#force= 1"
.Execute
End With
End With
rs.MoveNext
Loop
rs.Close
qdf.Close
DB.Close
DoCmd.Hourglass False
End Sub
In above code, I had added few lines of code in the old code which is shown as below:
With CurrentDb.QueryDefs("qryPending")
.sql = "exec usp_MovePending #materialid=" & rs!MaterialID & ",#SysUser=" & " '" & UserStr & "'" & "," & " #ROG=getdate()" & ",#force= 1"
.Execute
End With
When I debug my code, it retrieves the SP's parameter to pass-through query. But, I think my code is not calling SP (when I test SP by itself, it works just fine). I think SQL is waiting for some return values but Access is not returning anything. I tried different approaches but while researching many developers recommend to use this approach. Can anyone point out what I am missing out here?
Have you tried referencing the SP in VB rather than SQL?
With CurrentDb.QueryDefs("qryPending")
.sql = "exec " & usp_MovePending & " #materialid=" & rs!MaterialID & ",#SysUser=" & " '" & UserStr & "'" & "," & " #ROG=getdate()" & ",#force= 1"
.Execute
End With
What result does this give you?
Is the SP a global variable that the SQL can see? can you easily call it within the access query builder into a field? Or is it only seen by the VB code within a procedure?
In the below code, my second query will not insert into the SQL database, but the first one will update. I can copy the query (from the msgbox i added for testing) and paste it in SQL Server Management Studio, and it will execute fine. I also do not get any error messages back from SQL, though i'm not sure if that code is correct (it was copied + pasted from another source). Also, can i simplify the code to pass both queries at the same time?
Dim Conn As New System.Data.SqlClient.SqlConnection 'sql server datastream connection
Dim Cmd As New System.Data.SqlClient.SqlCommand 'sql command vars
Dim SqlQuery As String 'string var used to hold various SQL queries
Dim data As System.Data.SqlClient.SqlDataReader 'datareader object variable
Dim MVDataset As New DataSet
Dim MVDatatable As DataTable
Dim MVDatarow As DataRow
Private Sub MVUpdateButton_Click(sender As Object, e As EventArgs) Handles MVUpdateButton.Click
vbyn = MsgBox("Are you sure you want to update Tally Sheet Master Variables?" & vbCrLf & vbCrLf & "Changes to these variables will change the functionality of the Tally Sheet!", vbYesNo, )
Try
Select Case vbyn
Case vbNo
GoTo MVTableUpdateBypass
Case vbYes
'get new data from textboxes
Vers = TextBox1.Text
If TextBox2.Text = True Then
Testing = 1
Else
Testing = 0
End If
FlatFeeCharge = TextBox3.Text
PrepricingCharge = TextBox4.Text
SendMailAcct = TextBox5.Text
SendMailPW = TextBox6.Text
TestingEmail = TextBox7.Text
PrePricingEmail = TextBox8.Text
ImperataEmail = TextBox9.Text
'update existing active row to mark inactive
SqlQuery = "Update MasterVars set Active = 0 where PKEY = " & PKEY & ";"
MsgBox(SqlQuery)
If Conn.State = ConnectionState.Closed Then
Conn.ConnectionString = "Data Source=SQL01;Initial Catalog=TallySheet;Integrated Security=SSPI;"
End If
Conn.Open()
Dim MVDataAdapter As New SqlDataAdapter(SqlQuery, Conn)
Dim MVUpdateCommand As SqlCommand
MVUpdateCommand = New SqlCommand(SqlQuery)
MVDataAdapter.UpdateCommand = MVUpdateCommand
'insert new active row
SqlQuery = "Insert into MasterVars (Vers, Testing, FlatFeeCharge, PrePricingCharge, SendMailAcct, SendMailPW, TestingEmail, PrePricingEmail, ImperataEmail, DTS, UserName, Active) Values (" & "'" & Vers & "', " & Testing & ", '" & FlatFeeCharge & "'" & ", '" & PrepricingCharge & "'" & ", '" & SendMailAcct & "'" & ", '" & SendMailPW & "'" & ", '" & TestingEmail & "'" & ", '" & PrePricingEmail & "'" & ", '" & ImperataEmail & "'" & ", '" & Date.Now & "'," & "'QGDOMAIN\" & Environment.UserName & "'," & 1 & ");"
MsgBox(SqlQuery)
Dim MVInsertCommand As SqlCommand
MVInsertCommand = New SqlCommand(SqlQuery)
MVDataAdapter.InsertCommand = MVInsertCommand
MVDataAdapter.Fill(MVDataset, "MasterVars")
End Select
Catch ex As SqlException
Dim i As Integer
Dim errormessages As String
errormessages = ""
For i = 0 To ex.Errors.Count - 1
errormessages = errormessages & " " & ("Index #" & i.ToString() & ControlChars.NewLine _
& "Message: " & ex.Errors(i).Message & ControlChars.NewLine _
& "LineNumber: " & ex.Errors(i).LineNumber & ControlChars.NewLine _
& "Source: " & ex.Errors(i).Source & ControlChars.NewLine _
& "Procedure: " & ex.Errors(i).Procedure & ControlChars.NewLine)
Next i
Console.WriteLine(errorMessages.ToString())
End Try
'reload form with updated variables
Conn.Close()
Conn.Dispose()
MVTableUpdateBypass:
End Sub
The Fill method of the SqlDataAdapter executes the SelectCommand not the UpdateCommand or the InsertCommand. In any case these two commands (and the DeleteCommand) are executed when you call the Update method of the adapter.
Moreover the Update method runs the commands looking for rows changed/added/deleted in the DataTable/DataSet retrieved by the SelectCommand and works only for those rows.
But you don't need an SqlDataAdapter to execute your two queries. You should simply construct an SqlCommand with both texts separated by a semicolon and call ExecuteNonQuery
SqlQuery = "Update MasterVars set Active = 0 where PKEY = #key;" & _
"Insert into MasterVars (Vers, Testing, .....) VALUES (#p1, #o2, ....)"
Using Conn = New SqlConnection("Data Source=SQL01;......")
Using cmd = New SqlCommand(SqlQuery, Conn)
Conn.Open()
cmd.Parameters.Add("#key", SqlDbType.Int).Value = PKEY
cmd.Parameters.Add("#p1", SqlDbType.NVarChar).Value = vers
cmd.Parameters.Add("#p2", SqlDbType.Int).Value = testing
... and so on with other parameters ....
cmd.ExecuteNonQuery()
End Using
End Using
In this incomplete example (too many parameters to write down) I have concatenated the two sql texts in a single string and prepared it with parameter placeholders. Then I build the parameter collection with the exact datatypes required by your table and finally call ExecuteNonQuery to run everything on the database side.
Notice that is not needed to keep global objects like the connection or the command. It is always better to create a local variable, use and destroy it when done. In particular disposable objects like the connection and the command should always created in a Using block
I have a table in MS Access 2003 in which I want to archive all old data.
The criteria is that the creation data should be less than a specific date.
I can write a SQL statement to select them, but I don't know how to move them to another database/table? Assuming that the archive database/table is already created and data structure matches current table.
Also how I can make sure that all data which is moved to archive table is removed from current table?
I want to write VBA code to run the command check that data is archived correctly.
You want to 1) move data meeting certain criteria from one table to another, existing table with the same format. 2) You want to "make sure that all data which is moved to archive table is removed from current table." And 3) you "want to write VBA code to run the command check that data is archived correctly."
Contrary to popular opinion, Access does support transactions (the claim that Access SQL does not support transactions is true, but we can still use transactions in VBA code). So modifying code in this post to use transactions in a workspace, I believe this would do the trick (tested in Access 2010 using DAO).
The code to lock, get counts and unlock is not really necessary, and may increase the difficulty of implementing the archive, since it will require that no one be writing to the table while you're updating it. And if it did find a problem, Access does not support transaction logging, so you would have a very short list of options as to how to fix it. But it sounded like you wanted to be absolutely sure the counts were correct, so this adds another level, arguably unnecessary, of checking.
Option Compare Database
Option Explicit
Sub ArchiveOldRecords()
Dim nSourceCount As Long, nMoveCount As Long, nDestCount As Long
Dim strSQL As String, sMsg As String
Dim rsLock As DAO.Recordset
Dim rsBefore As DAO.Recordset, rsAfter As DAO.Recordset
Dim wrk As Workspace, db As DAO.Database
Const strcTableSource As String = "t_TestWithDate" ' Move records FROM table
Const strcTableArch As String = "t_ArchiveTestWithDate" ' Move records TO table
Const strcWHERE As String = " WHERE field2 < " _
& "DATEADD(""yyyy"", -1, Date())" ' Select date field and DATEADD params
Const strcCount As String = "SELECT COUNT(*) As "
On Error GoTo TrapError
Set db = CurrentDb
Set wrk = DBEngine.Workspaces(0)
' Lock table - so no one can add/delete records until count is verified
Set rsLock = db.OpenRecordset(strcTableSource, dbOpenTable, dbDenyWrite)
' Get initial table counts
Set rsBefore = db.OpenRecordset( _
strcCount & "SourceCount, " _
& "(SELECT COUNT(*) FROM " & strcTableSource _
& strcWHERE & ") As MoveCount, " _
& "(SELECT COUNT(*) FROM " & strcTableArch & ") As DestCount " _
& "FROM " & strcTableSource & ";", dbOpenForwardOnly)
nSourceCount = rsBefore!SourceCount
nMoveCount = rsBefore!MoveCount
nDestCount = rsBefore!DestCount
rsBefore.Close
wrk.BeginTrans
' Copy records
strSQL = "INSERT INTO " & strcTableArch _
& " SELECT * FROM " & strcTableSource & " " & strcWHERE & ";"
db.Execute strSQL, dbFailOnError
' Unlock table - only needed for counts
rsLock.Close
Set rsLock = Nothing
' Delete copied records
strSQL = "DELETE * FROM " & strcTableSource & " " & strcWHERE & ";"
db.Execute strSQL, dbDenyWrite + dbFailOnError
' Lock table - only needed for counts
Set rsLock = db.OpenRecordset(strcTableSource, dbOpenTable, dbDenyWrite)
wrk.CommitTrans
' Get final table counts
Set rsAfter = db.OpenRecordset( _
strcCount & "SourceCount, " _
& "(SELECT COUNT(*) FROM " & strcTableSource _
& strcWHERE & ") As MoveCount, " _
& "(SELECT COUNT(*) FROM " & strcTableArch & ") As DestCount " _
& "FROM " & strcTableSource & ";", dbOpenForwardOnly)
' Double-check counts
If (rsAfter!SourceCount <> nSourceCount - nMoveCount) _
Or (rsAfter!DestCount <> nDestCount + nMoveCount) _
Or (rsAfter!MoveCount > 0) Then
sMsg = vbNewLine
sMsg = sMsg & "Records in " & strcTableSource & " before: "
sMsg = sMsg & nSourceCount
sMsg = sMsg & vbTab & "after: "
sMsg = sMsg & rsAfter!SourceCount
sMsg = sMsg & vbNewLine
sMsg = sMsg & "Records to archive from " & strcTableSource & ": "
sMsg = sMsg & nMoveCount
sMsg = sMsg & vbTab & "after: "
sMsg = sMsg & rsAfter!MoveCount
sMsg = sMsg & vbNewLine
sMsg = sMsg & "Records in " & strcTableArch & " before: "
sMsg = sMsg & nDestCount
sMsg = sMsg & vbTab & "after: "
sMsg = sMsg & rsAfter!DestCount
MsgBox "Count double-check failed" & sMsg
End If
Exit_Sub:
On Error Resume Next
' Unlock table and close recordsets
rsLock.Close
rsBefore.Close
rsAfter.Close
Set rsBefore = Nothing
Set rsAfter = Nothing
Set rsLock = Nothing
Set db = Nothing
Set wrk = Nothing
Exit Sub
TrapError:
MsgBox "Failed: " & Err.Description
wrk.Rollback
Err.Clear
Resume Exit_Sub
End Sub
There is no MOVE command but you can copy the records across to the target and then use a similar query to remove them from the source when you are sure you have no Paste Errors.
INSERT INTO MyArchive (fld1, fld2, fld3, fld4) SELECT fld1, fld2, fld3, fld4 FROM MyTable WHERE fld4 < DATEADD("y", -5, Date())
That copies across everything older than 5 years. After confirming the transfer,
DELETE * FROM MyTable WHERE fld4 < DATEADD("y", -5, Date())
That's off the top of my head and I transition between T-SQL and MS Access a fair bit but I think that is pretty solid Access query code. Your own field lists will vary accordingly.