Simultaneous OleDbDataAdapter.Fill Calls on Separate Threads? - vb.net

First timer here, so go easy on me. Is it theoretically possible to execute two OleDBDataAdapter.Fill calls on separate threads simultaneously - or is that fundamentally flawed?
Consider a form with 2 buttons and 2 datagridviews. Each button click launches a worker thread using an Async \ Await \ Task.Run pattern that calls a method to return a populated datatable and assigns it to one of the datagridviews. The .Fill in the first thread takes 30 seconds to complete. The .Fill in the second thread takes 1 second to complete. When launched individually, both buttons work as expected.
However, if I launch the first worker thread (30 seconds to Fill), then launch the second thread (1 second Fill), the second DataGridView is not populated until the first .Fill call completes. I would expect the second datagridview to populate in 1 second, and the first datagridview to populate ~30 seconds later.
I have duplicated this issue in my sample code with both the OleDBDataAdapter and the SqlDataAdapter. If I replace the long running query with a simple Thread.Sleep(30000), the second datagridview is populated right away. This leads me to be believe that it is not an issue with my design pattern, rather something specific to issuing the .Fill calls simultaneously.
Private Async Sub UltraButton1_Click(sender As Object, e As EventArgs) Handles UltraButton1.Click
Dim Args As New GetDataArguments
Args.ConnectionString = "some connection string"
Args.Query = "SELECT LongRunningQuery from Table"
Dim DT As DataTable = Await Task.Run(Function() FillDataTable(Args))
If DataGridView1.DataSource Is Nothing Then
DataGridView1.DataSource = DT
Else
CType(DataGridView1.DataSource, DataTable).Merge(DT)
End If
End Sub
Function FillDataTable(Args As GetDataArguments) As DataTable
Dim DS As New DataTable
Using Connection As New OleDbConnection(Args.ConnectionString)
Using DBCommand As New OleDbCommand(Args.Query, Connection)
Using DataAdapter As New OleDbDataAdapter(DBCommand)
DataAdapter.Fill(DS)
End Using
End Using
End Using
Return DS
End Function
Private Async Sub UltraButton2_Click(sender As Object, e As EventArgs) Handles UltraButton2.Click
Dim DS As DataTable = Await Task.Run(Function() LoadSecondDGV("1234"))
DataGridView2.DataSource = DS
End Sub
Function LoadSecondDGV(pnum As String) As DataTable
Dim DX As New DataTable
Using xConn As New OleDbConnection("some connection string")
Using DataAdapter As New OleDbDataAdapter("Select name from products where PNUM = """ & pnum & """", xConn)
DataAdapter.Fill(DX)
End Using
End Using
Return DX
End Function

This depends on what the data source is. Some data sources (like Excel) will only allow one connection at a time. Other data sources (like Access) will allow multiple connections, but actually fulfill the results in serial, such that you don't gain anything. Other data sources, like Sql Server, will allow the true parallel activity that you're looking for.
In this case, you mention that you also tried with an SqlDataAdapter, which indicates to me that you're talking to Sql Server, and this should be possible. What's probably going on here is that your first query is locking some of the data you need for the second query. You can get past this by changing your transaction isolation level or through careful use of the with (nolock) hint (the former option is strongly preferred).
One other thing to keep in mind is that this can only work if you're using a separate connection for each query, or if you've specifically enabled the Multiple Active Result Sets feature. It looks like you're using separate connection objects here, so you should be fine, but it's still something I thought was worth bringing up.
Finally, I need to comment on your FillDataTable() method. This method requires you to provide a completed Sql string, which practically forces you to write code that will be horribly vulnerable to sql injection attacks. Continuing to use the method as shown practically guarantees your app will get hacked, probably sooner rather than later. You need to modify this method so that it encourages you to use parameterized queries.

Related

How to edit a record in an access database - visual basic

I want to edit a specific record in an access database but I keep on getting errors
this is the database I want to edit:
Access database
these are flashcards that the user has created and stored in an access database. What I want is that the user is able to edit the difficulty so it appears more/less often
This is the module:
Module Module1
Public Function runSQL(ByVal query As String) As DataTable
Dim connection As New OleDb.OleDbConnection("provider=microsoft.ACE.OLEDB.12.0;Data Source=flashcard login.accdb") 'Establishes connection to database
Dim dt As New DataTable 'Stores database in table called dt
Dim dataadapter As OleDb.OleDbDataAdapter
connection.Open() 'Opens connection
dataadapter = New OleDb.OleDbDataAdapter(query, connection)
dt.Clear() 'Clears datatable
dataadapter.Fill(dt) 'Fills datatable
connection.Close()
Return dt
End Function
End Module
And here is the button that the user can press to edit the database:
Private Sub btnSubmit_Click(sender As Object, e As EventArgs) Handles btnSubmit.Click
Dim sql As String
sql = "UPDATE flashcards set difficulty = '" & TxtDifficulty.Text
runSQL(sql)
End Sub
The difficulty column in the database should be able to be edited by the user through the value they entered in txtDifficulty.text
Good to hear I found the problem with the apostrophe.
I am going to need a where statement but the problem I have is that the user can create as much flashcards as they want so how would I write the where statement?
An INSERT statement does not have a WHERE clause but an UPDATE does and is usually by a primary key.
Look at how I add a new record ignoring mHasException and specifically using parameters. In this case a List is used but with little effort an array of DataRow can be passed instead.
Here is an example for updating a record with a DataRow.
To get other code samples for ms-access see the following repository.
In closing, in the above repository I don't get into all possibilities yet there should be enough there to get you going. And when reviewing code I flip between Add for adding a parameter and AddWithValue while Add is the recommend way but both are shown to see differences. see also Add vs AddWithValue.

CommandText Property Has Not Been Initialized, retrieving data

Private Sub ButtonSubmitID_Click(sender As Object, e As EventArgs) Handles ButtonSubmitID.Click
Dim comm As New SqlCommand
Dim conn As New SqlConnection
conn.ConnectionString = "Data Source = localhost\SQLEXPRESS; Initial Catalog = test2Forms; Integrated Security = SSPI;"
comm.Connection = conn
Dim ID = TextBoxID.Text
comm.Parameters.AddWithValue("#ID", ID)
Dim adapter As SqlDataAdapter = New SqlDataAdapter(comm.CommandText, comm.Connection)
comm.CommandText = "SELECT * FROM withActivityLog3 WHERE ID = #ID"
Dim records As DataSet = New DataSet
adapter.Fill(records)
DataGridView2.DataSource = records
End Sub
CommandText property has not been initialized is the error I am receiving. I am able to pull all the data from the database into the GridView on the Form Load but when I try to narrow it down to one ID using a WHERE clause on the button trigger, it comes up with the above error. I've used the debugger to trace through one step at a time and the command and connection strings look correct. I've also successfully duplicated the query on my database using the SQL Server command line. I'm searching on a primary key (ID) so the expected results would be one uniquely identified row from the database.
As for the problem you know you have:
' initialize DataAdapter with (EMPTY) commandtext
Dim adapter As SqlDataAdapter = New SqlDataAdapter(comm.CommandText, comm.Connection)
' initialize Command Text
comm.CommandText = "SELECT * FROM withActivityLog3 WHERE ID = #ID"
When you pass the CommandText to the DataAdapter, it is empty because you havent set it yet which results in the error.
There is a fair amount of inefficiency in your code though. Rewritten:
' form level conn string
Private TheConnString As String = "Data Source = localhost\..."
Private Sub ButtonSubmitID_Click(sender ...
Dim dt As New DataTable
Using dbcon As New MySqlConnection(TheConnString)
Using cmd As New MySqlCommand("select * from Sample where Id = #id", dbcon)
cmd.Parameters.Add("#id", MySqlDbType.Int32).Value = Convert.ToInt32(TextBox2.Text)
dbcon.Open()
dt.Load(cmd.ExecuteReader)
dgvA.DataSource = dt
End Using
End Using
End Sub
Note: this uses MySQL but the concepts are the same for Sqlite, Access, SQL Server etc
There is no need to type or paste the connection string and over everywhere it is used. One form level variable will allow DRY (Dont Repeat Yourself) code.
Anything which implements the Dispose() method should be disposed of. That includes nearly all the DB Provider objects. The Using statement allows you to declare and initialize an object and at the End Using it is disposed of. Failing to Dispose of things can cause leaks and even run out of connections or resources to create things like DB Command objects.
There is no need to create a local DbDataAdapter. These are very powerful and useful critters meant to do much more than fill a DataTable. If that is all you are doing, you can use ExecuteReader method on the DbCommand object.
Nor do you need a local DataSet. Contrary to the name, these do not hold data, but DataTables. Since there is only one and it is local (goes out of scope when the method ends), you dont need a DataSet to store it.
The Add method should be used rather than AddWithValue. The code above specifies the datatype for the parameter so there is no guesswork required of the compiler. Of course with that comes the need to convert the text to a number...
...Since this is user input, you should not trust the user, so Integer.Tryparse would be more appropriate: I like pie will not convert to an integer. Data Validation is something you should do before you commence the DB ops.
Dim ID = TextBoxID.Text as used is pointless code. You do not need to move the textbox text into a new variable in order to use it. However, ID might be used to store the integer value

SqlConnection.Open() Establishing a connection EVERY query?

In my Winforms app which uses a remote database, I have the function below. (I also have two other functions which work similarly: One for scalar queries which returns zero upon failure, the other for updates and inserts which returns false upon failure.)
Currently, ALL data manipulation is pumped through these three functions.
It works fine, but overall please advise if I'd be better off establishing the connection upon launching my app, then closing it as the app is killed? Or at another time? (Again, it's a windows forms app, so it has the potential to be sitting stagnant while a user takes a long lunch break.)
So far, I'm not seeing any ill effects as everything seems to happen "in the blink of an eye"... but am I getting data slower, or are there any other potential hazards, such as memory leaks? Please notice I am closing the connection no matter how the function terminates.
Public Function GetData(ByVal Query As String) As DataTable
Dim Con As New SqlConnection(GlobalConnectionString)
Dim da As New SqlDataAdapter
Dim dt As New DataTable
Try
Con.Open()
da = New SqlDataAdapter(Query, Con)
Con.Close()
da.Fill(dt)
Catch ex As Exception
Debug.Print(Query)
MsgBox("UNABLE TO RETRIEVE DATA" & vbCrLf & vbCrLf & ex.Message, MsgBoxStyle.Critical, "Unable to retrieve data.")
End Try
da.Dispose()
Con.Close()
Return dt
End Function
There are exceptions to this, but best practices in .Net do indeed call for creating a brand new connection object for most queries. Really.
To understand why is, first understand actually connecting to a database involves lots of work in terms of protocol negotiations, authentication, and more. It's not cheap. To help with this, ADO.Net provides a built-in connection pooling feature. Most platforms take advantage of this to help keep connections efficient. The actual SqlConnection, MySqlConnection, or similar object used in your code is comparatively light weight. When you try to re-use that object, you're optimizing for the small thing (the wrapper) at the expense of the much larger thing (the actual underlying connection resources).
Aside from the benefits created from connection pooling, using a new connection object makes it easier for your app to scale to multiple threads. Imagine writing an app which tries to rely on a single global connection object. Later you build a process which wants to spawn separate threads to work on a long-running task in the background, only to find your connection is blocked, or is itself blocking other normal access to the database. Worse, imagine trying to do this for a web app, and getting it wrong such that the single connection is shared for your entire Application Domain (all users to the site). This is a real thing I've seen happen.
So this is something that your existing code does right.
However, there are two serious problems with the existing method.
The first is that the author seems not to understand when to open and when to close a connection. Using the .Fill() method complicates this, because this method will open and close your connection all on its own.1 When using this method, there is no good reason to see a single call to .Open(), .Close(), or .Dispose() anywhere in that method. When not using the .Fill() method, connections should always be closed as part of a Finally block: and the easiest way to do that is with Using blocks.
The second is SQL Injection. The method as written allows no way to include parameter data in the query. It only allows a completed SQL command string. This practically forces you to write code that will be horribly vulnerable to SQL Injection attacks. If you don't already know what SQL Injection attacks are, stop whatever else you're doing and go spend some time Googling that phrase.
Let me suggest an alternative method for you to address these problems:
Public Function GetData(ByVal Query As String, ParamArray parameters() As SqlParameter) As DataTable
Dim result As New DataTable()
Using Con As New SqlConnection(GlobalConnectionString), _
Cmd As New SqlCommand(Query, Con),
da As New SqlDataAdpapter(Cmd)
If parameters IsNot Nothing Then Cmd.Parameters.AddRange(parameters)
Try
da.Fill(result)
Catch ex As Exception
Debug.Print(Query)
'Better to allow higher-level method to handle presenting the error to the user
'Just log it here and Rethrow so presentation tier can catch
Throw
End Try
End Using 'guarantees connection is not left hanging open
Return result
End Function
1See the first paragraph of the "Remarks" section.
This isn't a real "answer" to my own question, but I have something to add and I wanted to add some code.
To Joe: Thank you, my code is well on the way to using parameterized queries almost exclusively. Although I knew what SQL injection attacks were, and that they're a pretty big deal, here's my exuse: In the past I had used stored procedures for parameterized queries, and I kinda hate writing those and for the first year my code will be used only within by my small company of 5 employees who are family members... I had planned to switch everything to stored procedures later if I sold the software. This approach is better and I will probably not need stored procedures at all.
I especially like how elegantly parameterized queries handle dates, as I don't have to convert dates to appropriate text. Much easier.
Anopther advantage I'm seeing: Sometimes a "Save button" must execute either Insert or Update, depending on whether the record displayed is new. Using parameters allows me to write two alternate short basic queries, but to use the same parameters for either with less code.
Overall, this means a whole lot less code-intensive construction of the query string.
The part I didn't have, and I learned to do it elsewhere, was assigning the parameter array, calling the procedure, so I'm including an example here hoping others find it useful:
Dim query As String = "Select Phone from Employees where EmpNo = #EmployeeNumber and Age = #Age"
Dim params As SqlParameter() = {
New SqlParameter("#EmployeeNumber", txtEmployeeNumber.Value),
New SqlParameter("#Age", txtAge.Value)
}
Dim Phone as String = GetData(query, params).Rows.Item(0)

Datagridview linked to datatable not getting updated

This is a part of simultaneous url download program that i'm trying to make. It has the url list saved in a datatable named tbl and it is bound to a datagridview named dgvUrls. Evrytime it encounters a dead url, it removes it from the datatable.
I've reproduced the error using the code below. The Button3_Click adds 100 rows to the datatable, makes it as the datasource for datagridview. The q() removes the rows one at a time by removing the 1st row. The prob is that the datagridview don't reflect the changes made in the datatable
Dim tbl = New DataTable
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
'Add 100 urls, for simplicity i'm adding only integers
tbl.Columns.Add("Urls")
For i = 1 To 100
tbl.Rows.Add(i)
Next
'bind to datagridview so that the end user can see the urls being download/removed from the list
dgvUrls.DataSource = tbl
'start multithread download , for simplicited (of this question) we have only one
Dim t As Thread = New Thread(AddressOf Download)
t.Start()
t.Join()
dgvUrls.Refresh()
End Sub
Private Sub download()
'for simplicity, the 1st 80 urls were dead!
For i = 1 To 80
'we remove the dead urls
tbl.Rows.RemoveAt(0)
Next
In general, it is a good thing to Refresh the DataGridView, mainly if you are performing the modifications from another thread; something like this:
Dim t As Thread = New Thread(AddressOf q)
t.Start()
t.Join() 'Waits for the other thread to complete, such that the next line is reached on the right moment
dgvUrls.Refresh()
I deleted Dim ts As ThreadStart = New ThreadStart(AddressOf q) because is not necessary. Also you don't need the Sleep and DoEvents:
Private Sub q()
For i = 1 To 98
tbl.Rows.RemoveAt(0)
Next
End Sub
As a proof of concept (to understand how all this works) is OK; but you should review various ideas in your logic before going further: removing so many rows from the DataSource can provoke problems (you would see that it triggers errors); ideally, (at least, I prefer it) you should modify the DataGridView directly (if possible) to avoid info-synchronisation problems; if you deal with multiple threads you would have to set up a "more proper structure" (the proposed t.Join() should be seen as a temporary fix to make this work).

Retrieving ##IDENTITY in AccessDB

I'm trying to retrieve the ##IDENTITY value from an access database after I add a row to it. However, instead of using hard coded connections, I'm using the DataSet wizard. Unfortunately, due to the Jet engine, I can't do more than one command at a time. I tried to select the ##IDENTITY in a separate command but unfortunately, I guess it counts as a different command as it returns a 0 each time.
My question is, is there a way I can use the GUI/IDE to retrieve the ##IDENTITY or do I have hard code the connection, command, query values and obtain the value that way.
Thanks.
You asked an interesting questions which I did not know the answer to. After some research I found this and it seems promising. However I have never used it and cannot substantiate if and how well it works.
I also don't know the DataSet wizard that well, but vaguely recalls that it generates an OleDbAdapter object, hopefully it exposes a RowUpdated event which you can hook this code to.
I have pasted the interesting MSDN code parts here:
(Link to full documentation)
' Create the INSERT command for the new category.
adapter.InsertCommand = New OleDbCommand( _
"INSERT INTO Categories (CategoryName) Values(?)", connection)
adapter.InsertCommand.CommandType = CommandType.Text
Then hook-up and event listener to RowUpdated
' Include an event to fill in the Autonumber value.
AddHandler adapter.RowUpdated, _
New OleDbRowUpdatedEventHandler(AddressOf OnRowUpdated)
Obtain the ##Identity with the same connection.
Private Shared Sub OnRowUpdated( _
ByVal sender As Object, ByVal e As OleDbRowUpdatedEventArgs)
' Conditionally execute this code block on inserts only.
If e.StatementType = StatementType.Insert Then
' Retrieve the Autonumber and store it in the CategoryID column.
Dim cmdNewID As New OleDbCommand("SELECT ##IDENTITY", _
connection)
e.Row("CategoryID") = CInt(cmdNewID.ExecuteScalar)
e.Status = UpdateStatus.SkipCurrentRow
End If
End Sub
Hopefully this helps you out or least someone will set me straight :)