Comparing datatables / identify identical datatables - mysql

I am working with VB.net and mysql, I have a function which fills a datatable with mysql content then posts it (as appropriate) to a listview. This function is currently on a timer which activates every 5 seconds which unfortunately does horrible destruction to the drag-and-drop features of my software.
The solution: I have decided after filling the content to the listview to copy the content from the active datatable to another datatable for comparison, every time the data is taken from the mysql db have it save to a datatable and compare the two datatables - if they are not identical the software should run the function however if they are identical there is no reason to update the listview with the same exact data.
Public pendrun As New DataTable
Public postrun As New DataTable
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
con.ConnectionString = "server=localhost;" _
& "user id=user;" _
& "password=password;" _
& "database=DMT"
adptr = New MySqlDataAdapter("Select * from data", con)
Try
adptr.Fill(pendrun)
Catch err As Exception
Dim strError As String = "Exception: & err.ToString()"
End Try
if pendrun <> postrun then
ListView2.Items.Clear()
pendrun.Clear()
' commands to add
' pendrun datatable information
' to listview
postrun = pendrun 'to transfer data to the holding datatable
end if
the problem is
if pendrun <> postrun then
is not a valid way to compare datatables.
How do I identify if the datatables are identical (all rows and columns identical)

There's no built-in understanding of "identical" for types you've defined. You'll need to write that logic.
Essentially you need to build a function which accepts two DataTables as input, compares them in accordance with whatever business logic you define, and returns a Boolean indicating whether or not they pass that logic. Something like this:
Function TablesAreEqual(ByVal firstTable As DataTable, ByVal secondTable As DataTable) As Boolean
' Compare your tables, probably by looping through rows/values
Return areEqual
End Function
Then you'd use that function in your comparison:
If TablesAreEqual(pendrun, postrun) Then

Here is the resulting code I created to deal with this problem - thanks to David for the direction
Function TablesAreEqual(ByVal firstTable As DataTable, ByVal secondTable As DataTable) As Boolean
dim db1 As String = ""
dim db2 As String = ""
For rown As Integer = 0 To firstTable.Rows.Count - 1
For cown As Integer = 0 To firstTable.Columns.Count - 1
db1 = db1 & firstTable.Rows(rown).Item(cown)
Next
Next
For rown As Integer = 0 To secondTable.Rows.Count - 1
For cown As Integer = 0 To secondTable.Columns.Count - 1
db2 = db2 & secondTable.Rows(rown).Item(cown)
Next
Next
Return db1 = db2
End Function

If you want to use your pendrun and postrun datatables
try to serialize "writeXml" datatables and compare them.
Anyway i suggest you to think about a different approach instead of using a polling timer.

Better yet why not put in logic to not get new data if it has not changed.
Don't know about MySQL but MSSQL has Notification and TimeStamp
The columns should only change with a schema change.
First compare count and if not the same for sure there was a change.
If counts are the same then loop through the columns and compare.
As soon as one does not compare then done immediately return false.
On rows same thing. First test row count.
After that need to compare row by row and column by column.
Remember to sort the select statement so you would catch rows out of order.
And again as soon as one does not compare stop and return false.
Right with less code than you have in your answer
My inserts are C#
Function TablesAreEqual(ByVal firstTable As DataTable, ByVal secondTable As DataTable) As Boolean
if(firstTable.Rows.Count != secondTable.Rows.Count) return false;
if(firstTable.Columns.Count != secondTable.Columns.Count) return false;
For rown As Integer = 0 To firstTable.Rows.Count - 1
For cown As Integer = 0 To firstTable.Columns.Count - 1
if (firstTable.Rows(rown).Item(cown) != secondTable.Rows(rown).Item(cown)) return false;
Next
Next
return true;
End Function

Related

Delete command using ID from DataGridView

I have data shown in DataGridView. I made a Button when selecting IDs in the grid the code below runs but I keep getting an error.
Dim cnx As New MySqlConnection("datasource=localhost;database=bdgeststock;username=root;password=")
Dim cmd As MySqlCommand = cnx.CreateCommand
Dim resultat As Integer
If ConnectionState.Open Then
cnx.Close()
End If
cnx.Open()
If grid.SelectedCells.Count = 0 Then
MessageBox.Show("please select the ids that u want to delete")
Else
cmd.CommandText = "delete from utilisateur where idu= #P1"
cmd.Parameters.AddWithValue("#P1", grid.SelectedCells)
resultat = cmd.ExecuteNonQuery
If (resultat = 0) Then
MessageBox.Show("error")
Else
MessageBox.Show("success")
End If
End If
cnx.Close()
cmd.Dispose()
How does this make sense?
cmd.Parameters.AddWithValue("#P1", grid.SelectedCells)
As you tagged this question WinForms, you are presumably using a DataGridView rather than a DataGrid (names matter so use the right ones). In that case, the SelectedCells property is type DataGridViewSelectedCellCollection. How does it make sense to set your parameter value to that? How is that going to get compared to an ID in the database?
If you expect to use the values in those cells then you have to actually get those values out. You also need to decide whether you're going to use a single value or multiple. You are using = in your SQL query so that means only a single value is supported. If you want to use multiple values then you would need to use IN and provide a list, but that also means using multiple parameters. I wrote an example of this type of thing using a ListBox some time ago. You can find that here. You could adapt that code to your scenario like so:
Dim connection As New SqlConnection("connection string here")
Dim command As New SqlCommand
Dim query As New StringBuilder("DELETE FROM utilisateur")
Select Case grid.SelectedCells.Count
Case 1
query.Append(" WHERE idu = #idu")
command.Parameters.AddWithValue("#idu", grid.SelectedCells(0).Value)
Case Is > 1
query.Append(" WHERE idu IN (")
Dim paramName As String
For index As Integer = 0 To grid.SelectedCells.Count - 1 Step 1
paramName = "#idu" & index
If index > 0 Then
query.Append(", ")
End If
query.Append(paramName)
command.Parameters.AddWithValue(paramName, grid.SelectedCells(index).Value)
Next index
query.Append(")")
End Select
command.CommandText = query.ToString()
command.Connection = connection
SelectedCells is a collection of cells
so it never can be only one id, so you have to guess which was you want or only allow one row to be selected
grid.SelectedCells(0).Value.ToString()
Or you have to program a loop to delete all selected rows

Populating ListBox More Quickly

Is there a way to make populating ListBox fast, because the UI is freezing on form load upon populating the ListBox?
This is my form load code:
Dim abc As String = itemCount()
Dim output = Account_Get(a)
For Each s In output
ListBox1.Items.Add(s)
count1 += 1
If count1 = abc Then
ListBox1.Visible = True
End If
Next
This is the query in module:
Public Function Account_Get(ByVal chk As String) As List(Of String)
Dim result = New List(Of String)()
Try
cn.Open()
sql = "select column_name as str from table where status = 'New' order by rand()"
cmd = New MySqlCommand(sql, cn)
dr = cmd.ExecuteReader
While dr.Read
result.Add(dr("str").ToString())
End While
Return result
Catch ex As Exception
MsgErr(ex.Message, "Error Encounter")
Return Nothing
Finally
cn.Close()
End Try
End Function
this is working fine. but the fact that it loads too many datas. the ui is freezing on load. hoping someone could help me with this. thanks!
Since you are incrementing count1 I assume it is some sort of number. However, you are then comparing it to a string in the If statement. Please use Option Strict.
Changed the Function to return an Array of String. Took the random sort form the sql statement and moved it to a little linq at the end or the function.
You could add a Stopwatch to the data retrieval and the display sections to see where your bottleneck is. BeginUpdate and EndUpdate on the listbox prevents repainting on every addition.
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim output = Account_Get()
ListBox2.BeginUpdate()
ListBox2.Items.AddRange(output)
ListBox2.EndUpdate()
End Sub
Private Rand As New Random
Public Function Account_Get() As String()
Dim dt As New DataTable
Dim result As String()
Using cn As New MySqlConnection("Your connection string")
Dim Sql = "select column_name as str from table where status = 'New'" 'order by rand()"
Using cmd = New MySqlCommand(Sql, cn)
Try
cn.Open()
dt.Load(cmd.ExecuteReader)
Catch ex As Exception
MessageBox.Show(ex.Message, "Error Encounter")
Return Nothing
End Try
End Using
End Using
result = (From dRow In dt.AsEnumerable()
Let field = dRow("str").ToString
Order By Rand.Next
Select field).ToArray
Return result
End Function
The query you are using contains a random order. Ordering records randomly can be a huge performance issue within MySQL as it has to go through all records in the table and then sort them randomly. The more records in the table, the bigger the performance penalty. There is also no limitation on the number of records in your query. So if there are thousands of items in your table the listbox will also be thousands of items in size, which could also take a long time.
If you really require the random ordering you could do something about it in your code. I'm now assuming here that you are: 1) using identifiers in your table, 2) you actually wish to limit the number of items in your listbox and not display all of them.
Get a grasp of the total number of records in the table by a query
Pick a random number from the range of items in your table
Fetch the nearest record
Hope this helps you to get going to find a solution

MySQL String Not Converting to .NET Time

I am trying to convert a MySQL time to a string using VB.NET.
Dim adpt As New MySqlDataAdapter(dbcmdstring, connection)
Dim myDataTable As New DataTable()
adpt.Fill(myDataTable)
DataGridView1.DataSource = myDataTable
DataGridView1.Columns.Remove("ActivityID")
DataGridView1.Columns.Remove("ActivityDate")
DataGridView1.Columns.Remove("UserID")
DataGridView1.Columns(0).HeaderCell.Value = "Name"
DataGridView1.Columns(1).HeaderCell.Value = "Start Time"
DataGridView1.Columns(2).HeaderCell.Value = "End Time"
DataGridView1.Columns.Add("Duration", "Duration")
DataGridView1.RowHeadersVisible = False
Dim duration As New TimeSpan
Dim durationStr As String = ""
Dim i As Integer = 0
For Each row As DataGridViewRow In DataGridView1.Rows
duration = Date.Parse(row.Cells(2).Value.ToString).Subtract(Date.Parse(row.Cells(1).Value.ToString))
durationStr = Math.Round(duration.TotalMinutes).ToString & ":" & Math.Round(duration.TotalSeconds).ToString
row.Cells(3).Value = durationStr
Next
When the date is parsed during the construction of the duration variable, it throws an error:
An unhandled exception of type 'System.NullReferenceException'
occurred in WindowsApplicationSQL.exe
Additional information: Object reference not set to an instance of an
object.
I can successfully parse the date and show it in a messagebox, but not convert it to a useable string. I have also tried using just the .value of the time as well.
Any help?
There is a much easier way to get a Duration, and a much, much easier way.
Part of the problem is this: I am trying to convert a MySQL time to a string using VB.NET. There is no need to convert to string. If the column is in fact, a Time() column in MySQL, it's has a NET counterpart: TimeSpan and it can be easily used to calculate a difference using subtraction.
Much Easier Method
dtLog.Columns.Add(New DataColumn("Duration", GetType(TimeSpan)))
For Each r As DataRow In dtLog.Rows
r("Duration") = r.Field(Of TimeSpan)("EndTime") - r.Field(Of TimeSpan)("StartTime")
Next
If you are using a DataSource, it is rarely a good idea to manipulate the data thru the DataGridView. Note: no strings were needed.
Much, MUCH Easier Method
Perform the operation in SQL:
Dim sql = "SELECT ... StartTime, EndTime, TIMEDIFF(EndTime, StartTime) As Duration FROM ActivityLog"
This will create a Duration column in the DataTable containing the result. Similarly, if you dont want certain columns, you can omit them from the SQL to start rather than removing DGV Columns.
Object reference not set to an instance of an object.
Finally, this error probably has nothing to do with the conversion of MySQL to VB, strings or TimeSpans. By default, the DGV has that extra row at the bottom - the NewRow for the user to start adding data - but all the cells are Nothing. So:
For Each row As DataGridViewRow In DataGridView1.Rows
This will try to process the NewRow and poking around it's cells will result in an NRE. When you switched to the datatable, it went away because they dont have the extra row. You still don't need all those gyrations though.
I tried, instead, doing it via the DataTable rather than the DataGridView and it worked!
myDataTable.Columns.Add("Duration", GetType(String))
myDataTable.Columns.Remove("ActivityID")
myDataTable.Columns.Remove("ActivityDate")
myDataTable.Columns.Remove("UserID")
DataGridView1.DataSource = myDataTable
DataGridView1.RowHeadersVisible = False
DataGridView1.Columns(0).HeaderCell.Value = "Name"
DataGridView1.Columns(1).HeaderCell.Value = "Start Time"
DataGridView1.Columns(2).HeaderCell.Value = "End Time"
Dim duration As New TimeSpan
Dim durationStr As String = ""
Dim i As Integer = 0
For Each row As DataRow In myDataTable.Rows
duration = Date.Parse(row.Item("EndTime").ToString).Subtract(Date.Parse(row.Item("StartTime").ToString))
durationStr = Math.Round(duration.Minutes).ToString & ":" & Math.Round(duration.Seconds).ToString
row.Item("Duration") = durationStr
Next

Create query in QueryEditor with VBA function call for specifying the WHERE IN clause

I have written a couple of VBA functions which in the end return a Collection of Integers:
Public Function ValidIDs() As Collection
Now I want to run create a query in the QueryEditor with the following condition: WHERE TableID IN ValidIDs(). That does not work since access for some reason does not even find my function as long as it returns a Collection. Therefore I wrote a wrapper around it, which joins the Collection:
Public Function joinCollectionForIn(Coll As Collection) As String
Now a third function which calls ValidIDs(), passes the result to joinCollectionForIn and returns that result. Lets call it GetIDCollectionAsString().
As a result I can now change my query to WHERE TableID IN (GetIDCollectionAsString()). Note the added parenthesis since the IN needs them in any case, they can not just be at the end and the beginning of the String returned by GetID....
Running that query however results in
Data type mismatch in criteria expression.
I guess that results from the fact that I return a String, therefore access automatically wraps that string in ' for the SQL and the IN-clause no longer works because I would check if a number is IN a collection of 1 string.
Therefore my question is:
Is there a way to prevent access from wrapping the returned string for the SQL
or (would be a whole lot better):
Is there an already existing way to pass a collection or array to the WHERE IN-clause?
P.S.: I am currently using a workaround by writing a placeholder in the parenthesis following the IN (e.g. IN (1,2,3,4,5)) and replacing that placeholder in Form_Load with the result of GetIDCollectionAsString() - that works but it is not pretty...
Edit: The final query should look like SELECT * FROM TestTable t WHERE t.ID IN (1,2,3,4,5,6,7). That actually works using above method, but not in a nice way.
Well this required more work than it seems.... i couldn't find a straight solution so here is a workaround
Public Function ListforIn(inputString As String) As String
Dim qdf As QueryDef
Dim valCriteria As String
Dim strsql As String
Dim splitcounter As Byte
Dim valcounter As Byte
Set qdf = CurrentDb.QueryDefs(**TheNameOfQueryYouWantToModify**)
strsql = qdf.sql
strsql = Replace(strsql, ";", "") 'To get rid of ";"
splitcounter = Len(inputString) - Len(Replace(inputString, ",", ""))
For valcounter = 0 To splitcounter
valCriteria = valCriteria & ValParseText(inputString, valcounter, ",")
Next
strsql = strsql & " WHERE TableId IN (" & Left(valCriteria, Len(valCriteria) - 1) & ")"
qdf.sql = strsql
End Function
Public Function ValParseText(TextIn As String, X As Byte, Optional MyDelim As String) As Variant
On Error Resume Next
If Len(MyDelim) > 0 Then
ValParseText = "Val(" & (Split(TextIn, MyDelim)(X)) & "),"
Else
ValParseText = Split(TextIn, " ")(X)
End If
End Function

grab linq to sql record by primarykey without knowing its type

how can i grab a record (and eventually delete it) using linq2sql without knowing the type at compile time?
so far i've got
Sub Delete(ByVal RecordType As String, ByVal ID As Integer)
Dim dummy = Activator.CreateInstance(MyAssembly, RecordType).Unwrap
Dim tbl = GetTable(dummy.GetType)
tbl.DeleteOnSubmit(dummy)
End Sub
but of course the dummy is not an actual record, its just a dummy
i don't want to use direct sql (or executecommand) as there's business logic going on at deletion in the datacontext partial class
can this be done somehow?
thank you very much!
EDIT
in response to striplinwarior, i edited my code to:
Sub Delete(ByVal RecordType As ObjectType, ByVal ID As Integer)
Dim dummy = Activator.CreateInstance(ObjectType.Account.GetType.Assembly.FullName, RecordType.ToString).Unwrap
SetObjProperty(dummy, PrimaryKeyField(RecordType), ID)
Dim tbl = GetTable(dummy.GetType)
tbl.Attach(dummy)
tbl.DeleteOnSubmit(dummy)
SubmitChanges()
End Sub
this does fire off the deletion code correclty, but also seems to try to add the record first to the db, as i get a sqlexception that some "not null" fields are empty, which i guess is true about the dummy record, as the only thing this has is the primarykey, else is all empty. so i tried the other code u posted (something i anyways always wanted to have) and that works excellent!
hers my current code:
Function LoadRecord(ByVal RecordType As String, ByVal RecordID As Integer) As Object
Dim dummy = Activator.CreateInstance(AssemblyName, RecordType).Unwrap
Dim rowType = dummy.GetType
Dim eParam = Expression.Parameter(rowType, "e")
Dim idm = rowType.GetProperty(PrimaryKeyField(RecordType))
Dim lambda = Expression.Lambda(Expression.Equal(Expression.MakeMemberAccess(eParam, idm), Expression.Constant(RecordID)), eParam)
Dim firstMethod = GetType(Queryable).GetMethods().[Single](Function(m) m.Name = "Single" AndAlso m.GetParameters().Count() = 2).MakeGenericMethod(rowType)
Dim tbl = GetTable(rowType)
Dim obj = firstMethod.Invoke(Nothing, New Object() {tbl, lambda})
Return obj
End Function
Sub Delete(ByVal RecordType As String, ByVal RecordID As Integer)
Dim obj = LoadRecord(RecordType, RecordID)
Dim tbl = GetTable(obj.GetType)
tbl.DeleteOnSubmit(obj)
SubmitChanges()
End Sub
Thank You
The only way I can think of is to use the model information from your database mapping to figure out which member represents the primary key:
Dim primaryKey = (From t In db.Mapping.GetTables() _
Where t.RowType.Type = tableType _
Let keyMember = (From dm In t.RowType.DataMembers where dm.IsPrimaryKey).FirstOrDefault() _
Select keyMember.Member.Name).First()
(I'm using LinqPad here: I assume typical LINQ to SQL models have this mapping information available.)
Then use reflection to set the value of that key member on the dummy item you've created. After that, you need to attach the dummy to the table before trying to delete it, passing false as a second parameter to tell LINQ to SQL that you don't actually want to update the object using its current values, but that it should track changes from here on.
tbl.Attach(dummy, false)
tbl.DeleteOnSubmit(dummy)
db.SubmitChanges()
Does that make sense?
Edit
When you're only deleting an object, you don't necessarily have to get the record from the database. If you set the ID value of the object and then attach it to the context (as shown above), LINQ to SQL will treat it as if it were retrieved from the database. At that point, calling DeleteOnSubmit should tell the context to construct a DELETE statement in SQL based on that object's primary key value.
However, if you need to retrieve the object for some purpose other than deletion, you'll need to construct an expression to represent the query for that object. So, for example, if you were writing the query manually, you would say something like:
Dim obj = tbl.First(Function(e) e.Id = ID)
So to dynamically build the lambda expression inside the parentheses, you might do something like this:
Dim eParam = Expression.Parameter(rowType, "e")
Dim lambda = Expression.Lambda(Expression.Equal(Expression.MakeMemberAccess(eParam, idMember), Expression.Constant(ID)), eParam)
Then you would need to use reflection to invoke the generic First method:
Dim firstMethod = GetType(Queryable).GetMethods().[Single](Function(m) m.Name = "Single" AndAlso m.GetParameters().Count() = 2).MakeGenericMethod(rowType)
Dim obj = firstMethod.Invoke(Nothing, New Object() {tbl, lambda})