Compare and synchronize DataTables - vb.net

I need to Compare Two dataTables.
dataTable A contains current set of Data on clients machine.
dataTable B contains future updates to dataTable A.
dataTable A structure
ID | firstname | lastName
1 | "test" | "last"
2 | "whatever" | "someone"
3 | "hi" | "hello
dataTable B Structure
ID | firstname | lastName
1 | "updated" | "yes"
2 ->deleted
3 | "hi" | hello" ->unchanged
4 | "new" |record " ->row added
When I go dataTableA.merge(datatableB)
I basically just get dataTableA with dataTableB added rows
so for example
ID | firstname | lastName
1 | "test" | "last"
2 | "whatever" | "someone"
3 | "hi" | "hello
1 | "updated" | "yes"
3 | "hi" | hello" ->unchanged
4 | "new" |record " ->row added
It doesn't match on the IDs and get updated or deleted. I just want to compare two tables, update table A that should look exactly like table B. I'm not quite sure how to accomplish this properly.
Basically there is a SQL table in the clients machine that needs to get completed updated and sync exact to a datatable B that is being passed in. In theory i just want to take table B and basically update table A. So after I need to update the SQL table. I tried something like this.
Dim adapter = New SqlDataAdapter("select * from test_table2", connection)
Using (New SqlCommandBuilder(adapter))
adapter.Fill(dTable)
connection.Open()
adapter.Update(dTable)
End Using
Doesn't seem to work.

If the datatables are supposed to end up identical, instead of using
dataTableA.merge(datatableB)
Try
dataTableA=datatableB.Copy
Got this info from here
https://msdn.microsoft.com/en-us/library/system.data.datatable.copy(v=vs.110).aspx

Your both datatables are being created separately, independent of each other. In this context you can use following DataTable and LINQ operations such as
Intersect to find matching rows in both DataTables,
Except to find unique rows only in one table
ImportRow to copy specific rows of one DataTable to another.
Copy to copy an entire datatable into another
It could be like this
Dim dtA As DataTable 'Your original Table A
Dim dtB As DataTable 'Your modified Table B
Dim dtC As DataTable 'Unchanged Rows
Dim dtD As DataTable 'Final synchronized Table
Dim dtE As DataTable 'Deleted Rows
'Populate your Table A and Table B into dtA and dtB
'get unchanged rows into dtC (all rows - changed - deleted)
dtC = dtA.AsEnumerable.Intersect(dtB.AsEnumerable, DataRowComparer.Default).CopyToDataTable
'Copy all unchanged rows to final table
dtD = dtC.Copy
'Copy the structure to blank datatable
dtE = dtC.Clone
'process modified rows (changed + deleted) i.e. (all rows - unchanged rows)
For Each cdRow As DataRow In dtA.AsEnumerable.Except(dtC.AsEnumerable, DataRowComparer.Default).CopyToDataTable.Rows
'check if this row exists in modified rows
If dtB.Select("cid = " & cdRow.Item("cid").ToString).Count > 0 Then
'This is a modified row copy it to the final table or process it to database
dtD.ImportRow(dtB.Select("cid = " & cdRow.Item("cid").ToString).CopyToDataTable.Rows(0))
Else
'This is a deleted row copy it to the deleted records table or process it directly to the database
dtE.ImportRow(cdRow)
End If
Next
'Now dtE contains your deleted rows
'Finally dtD is your synchronized datatable
Assumptions and other details:
All DataTables should have same table structure
This code assumes that ID column is unique to identify new and old rows.
You will still have to process deleted rows (dtE) from original database.
DataTable dtD, dtE can be avoided, but just for the purpose of simplicity and better understanding of the concept these are included.
Instead of later processing all records in dtD and dtE in a separate loop you can directly update them to the database as mentioned in code above.
There may be memory, resources, processing/timing related issues in case you are processing large DataTables

Related

How to filter an Access datasheet form based on fields of datasheet and subdatasheet

I have an Access DB, along with tables and forms.
In one table I have customers and in a second table as a subdatasheet on the first table I have tags.
I have created a form which shows the customer list along with the subdatasheet which displays all the tags that a customer has.
I would like to filter based on a field of the main datasheet, and on the subdatasheet for a tag.
E.g. where customer_name = "Peter" and tag ="Neighbor"
I saw this code but when trying to use it fails.
Filter on Subdatasheet
When trying Set mainDS = Me.Controls(dataSheetName).Form where do I find the datasheetName?
I tried entering Form Name, Table Name, I searched in form properties but didn't manage to find a solution.
Based on your question it sounds like you are new to Access and you should review table normalization.
Everything starts with a normalized table structure. Unfortunately you are starting with the relatively difficult Many to Many Relationship. Each Customer can have many tags which is a One to Many Relationship, but there wouldn't be much point to comparing customers unless they could have the same tags, which means each tag can have many customers as well. For example, create your tables with the corresponding primary and foreign keys and tell access about the relationhip by hitting the ribbon and selecting the relationships tool:
The raw data in the CustomersTags table is what you are interested in, but it is not userfriendly
----------------------------------------------------------------
| CustomerTagID | CustomerID | TagID |
----------------------------------------------------------------
| 1 | 1 | 1 |
----------------------------------------------------------------
| 2 | 3 | 1 |
----------------------------------------------------------------
| 3 | 2 | 3 |
----------------------------------------------------------------
| 4 | 2 | 4 |
----------------------------------------------------------------
| 5 | 2 | 5 |
----------------------------------------------------------------
| 6 | 3 | 1 |
----------------------------------------------------------------
| 7 | 2 | 1 |
----------------------------------------------------------------
| 8 | 3 | 5 |
----------------------------------------------------------------
| 9 | 2 | 2 |
This is why we use forms for entering and viewing the data. For speeds sake select CustomersTags and hit create table:
This is not user friendly so we don't show primary keys and replace (right-click and select change to) all the i'ds with user friendly combo-boxes. I also change the forms format to Data sheet.
I continue making the form prettier and add two unbound combo boxes to the header to filter the form with. You can do this with listboxes or checkboxes, or etc as well. For instance a multi-select list box so you can select multiple tags. In all cases you just set the forms filter
Option Compare Database
Option Explicit
'select the combobox afterupdate property to get these events
'you can use macro's if you want
'the exact code is very dependent on things like whether you have default values. I just show the most necessary case
Private Sub cmbCustomer_AfterUpdate()
If (IsNull(Me.cmbCustomer) Or IsNull(Me.cmbTag)) Then
'do nothing
Else
'filter the forms record source
Me.filter = "(CustomerID = " & Me.cmbCustomer & ") AND TagID = " & Me.cmbTag
Me.FilterOn = True
End If
End Sub
Private Sub cmbTag_AfterUpdate()
If (IsNull(Me.cmbCustomer) Or IsNull(Me.cmbTag)) Then
'do nothing
Else
'the filter is just the WHERE PART of an sql statement without the where
Me.filter = "(CustomerID = " & Me.cmbCustomer & ") AND TagID = " & Me.cmbTag
Me.FilterOn = True
End If
End Sub
Next, I switch the forms format to continuous forms and continue prettyifying because to my surprise the datasheet format was blocking setting the filter string.
Here is some guidance for making comboboxes, comboboxes are used for displaying userfriendly values instead of IDs. For instance instead of displaying the raw CustomerID we display the Customer_name but the value in the combobox is the CustomerID. The unbound combobox's value is the CustomerID corresponding to the user friendly Customer_Name the user selects. Whether the combobox is bound or unbound you set them up in the same way. Select a bunch of columns from some table or query then declare how many columns you are showing and their width. To hide a column set it's width to 0. The order of the columns determines the order of the numbers. I bound to the tables and both tables go ID then user friendly description so column-widths is 0,1
This example allows selecting impossible combinations of Customer and tag which will cause an error. One way to avoid this would be to set the rowsource of the alternate combobox when you choose a value on the first. For instance, when you pick a customer you restrict the tags which can be selected in the next combobox. This is cascading comboboxes:
Option Compare Database
Option Explicit
Private Sub cmbCustomer_AfterUpdate()
'it helps to use the query designer to get the sql right
Me.cmbTag.RowSource = "SELECT Tags.TagID, Tags.FriendlyDescription" & _
" FROM Tags INNER JOIN (Customers INNER JOIN CustomersTags ON Customers.CustomerID = CustomersTags.CustomerID)" & _
" ON Tags.TagID = CustomersTags.TagID WHERE (((CustomersTags.CustomerID)= " & Me.cmbCustomer & "))"
Me.cmbTag.Visible = True
End Sub
Private Sub cmbTag_AfterUpdate()
Me.filter = "(CustomerID = " & Me.cmbCustomer & ") AND TagID = " & Me.cmbTag
Me.FilterOn = True
Me.cmbCustomer.SetFocus
Me.cmbTag.Visible = False 'quick and dirty reset 'can't invis a focused control
Me.Refresh
End Sub

Lookup value in Access table without a unique identifier using VBA

I have a table like this:
+-----------+------+
| TableName | Flag |
+-----------+------+
| TableG | 0 |
| TableE | 0 |
| TableR | 1 |
| TableL | 1 |
| TableN | 0 |
What I'm trying to do is have a function that looks up every TableName where Flag=1 so that I can assign each table where Flag=1 to a button in a form (e.g. assign the first instance to Button1, etc.). The tricky part is that this table cannot be edited, so no unique ID/count can be assigned to easily grab items.
Here's what I've tried so far:
DLookup does not handle having multiple lookup matches (e.g. DLookup("TableName", "MyTable", "Flag=1") would not allow me to access all the values where Flag=1), so I don't think I can use that to store them in an array
I've tried using a RecordSet (see below), but I could not figure out how to have the SQL string automatically add a count so that each table name could be referenced (e.g. TableNameRS(1) to pull the name of the first table with Flag=1)
Public Function TableNameRS(TableID As Integer)
Dim RS As DAO.Recordset
Dim SQL As String
Dim Name As Variant
SQL = "select TableName, COUNT(TableName) from MyTable where Flag=1"
Set RS = CurrentDb.OpenRecordset(SQL)
Name = RS("TableName")
TableNameRS = Name
RS.Close
End Function
I am out of ideas - what are my options here?
If the number of records and buttons are fixed, say 10, and buttons have names like btn1, btn2, etc., consider a procedure like:
Public Sub TableNameRS(TableID As Integer)
Dim RS As DAO.Recordset
Set RS = CurrentDb.OpenRecordset("SELECT TableName FROM MyTable WHERE Flag=1 ORDER BY TableName")
For x = 1 to 10
Me("btn" & x).Caption = RS("TableName")
RS.MoveNext
Next
RS.Close
End Function
Procedure can be called in form Open event. Then code in button Click event can pass its caption as an argument to whatever other procedure needs to take action with TableName value.
Calculating a sequence identifier in Access query is a common topic MS Access Restart Number Sequence.
Assuming TableName values are unique:
SELECT TableName, DCount("*", "table", "TableName<'" & [TableName] & "' AND Flag=1")+1 AS Seq,
FROM table
WHERE Flag=1
ORDER BY TableName;
Can use that query as RecordSource for form or report or execute DLookup() on it.
Be aware any change in filter will impact sequence calculation.

Access Query or VBA - Split data into line level

How do I get the following
Project | Code 1 | Code 2 | Code 3
1 | a | b | c
2 | a | d
translate into getting code on line level by project?
Project | Code
1 | a
1 | b
1 | c
2 | a
2 | d
I am currently doing this manually but the problem is I have 30 codes. My current process is do a select query and then do a union for all 30 codes. Can someone help me out with this? I know a vba script will do the job but weren't sure how to do this. Thanks!
You can support any number of project codes by creating a new table, let's call it ProjectCode.
So you'd have one table for Projects, that doesn't have anything to do with codes.
If codes have extra data attached, you can create a Code table and add that information.
ProjectCode helps the database to see how they are related by using a JOIN. the structure would be
ProjectID | CodeID
What I've done for cases like this, is to use the main form for projects, and have a continuous form in datasheet view as a subform, linking to the ProjectCode-like table, and link the ID from Projects to the ProjectID of the subform. (Link Master Fields & Link Child Fields properties of the subform object), note you'll want to hide ProjectID on the subform.
To get a list of codes, you can just query ProjectCode with the project ID. if you need additional information from projects, use a join:
SELECT * FROM Projects INNER JOIN ProjectCode ON ProjectCode . ProjectID = Projects.ID
WHERE ID = 'some id'
Note that the information from Projects will be repeated for each code.
With this function you can read data from a table with N codes fields named (code1, code2, ..., codeN) and write them to a table with only two fields:
Function Verticalize(numberofcodes As Integer)
Dim sql As String
Dim rst As New ADBDb.Recordset
sql = "SELECT * FROM tname"
rst.Open sql, CurrentProject.AccessConnection
While Not rst.EOF
For i = 1 To numberofcodes
DoCmd.RunSQL "insert into tdest values (" & rst(0) & "," & rst("code") & Str(i) & ")"
Next i
rst.MoveNext
Wend
End Function

How do I limit multi-select options for one field based on data entered into another field when they both reference the IDs of another table?

I have two tables, we'll call them T1 and T2. T1 currently has nearly 600 records in it, one of which contains the ID number and another of which contains a title, so ID and TITLE:
T1
ID | TITLE
-----|----------
1 | Title ABC
... | ...
201 | Title XYZ
... | ...
411 | Title 123
T2 has an ID field, a Titles field, an Accepted Titles field, and a Rejected Titles field, so ID, TITLES, ACCEPTED TITLES, and REJECTED TITLES. The access form uses a multiple select ListBox to select one or more TITLES from T1, however many are required, but usually no more than ten. Once entries are made into the TITLES field of T2, which is Numeric for the record IDs corresponding to the titles selected from T1, I want a combo box for each of the ACCEPTED TITLES and REJECTED TITLES in T2 to be limited to showing only those titles that correspond to IDs entered into the TITLES field. So, if I have in the TITLES field of T2,
T2
ID | TITLES | ACCEPTED TITLES | REJECTED TITLES
---|---------------|------------------|----------------
1 | 1, 201, 411 | |
I want the dropdown for the ComboBox to show only the titles corresponding to those IDs entered into the TITLES field. So, taking the ACCEPTED TITLES field, it might look like this:
T2
ID | TITLES | ACCEPTED TITLES | REJECTED TITLES
---|---------------|--------------------|---------------
1 | 1, 201, 411 | | [ ] Title ABC \/|
| [ ] Title XYZ |
| [ ] Title 123 |
I'm thinking I should be able to build a SELECT WHERE IN (...) statement that I can use in the "Row Source" properties of ACCEPTED TITLES and REJECTED TITLES. Then the list would be as short as the items selected for TITLES rather than 600+ records long. This also completely eliminates the potential for erroneous input under ACCEPTED TITLES or REJECTED TITLES since those titles can only be selected from those entered under TITLES. But, I don't yet know how to build such a SELECT statement.
Any assistance will be appreciated. Thanks for your time.
I propose a slightly different design for T2 (TitleStatus).
Create Table [TitleStatus] ([TitleID] Number, [StatusID] Number);
Create Index TitleIDindex ON [TitleStatus] (TitleID) WITH PRIMARY;
Then another table to house the statuses. Something like
Create Table [Statuses] ([StatusID] Number, [StatusText] String);
Create Index StatusIDindex ON [Statuses] (StatusID) WITH PRIMARY;
(this table could be organized with one column, but either way you want to prevent statuses like 'Accepted' and 'Accepted Title' from creeping in. Then you'd have two records which are the same thing but you don't naturally know to look for both)
Then T2 (TitleStatus) will look like
TitleStatus
TITLEID | STATUSID
----------|----------
1 | 1
... | ...
201 | 1
... | ...
411 | 1
500 | 2
Where Statuses Looks like
StatusID | StatusText
1 | Accepted
2 | Rejected
You're inserts should be fairly straight forward form there.
You can get all the accepted titles like this
Select T1.Title, Status.StatusText
From T1
Inner join TitleStatus TS on TS.TitleID = T1.ID
Inner Join Statuses S on S.StatusID = TS.StatusID
Where S.StatusText = 'Accepted'
I found the solution. It's actually much simpler than I realized:
Public Function GetTitleIDs() As String
Dim IDData As ADODB.Recordset
Dim SubLookup As Variant
Dim SelectSubmissions As String
' Should pull the same records and in the same order as '
' those found in originating Listbox '
Set IDData = CurrentProject.Connection.Execute("SELECT [Poems].[ID], [Poems].[Title] FROM Poems ORDER BY [Title];")
SubLookup = IDData.GetRows
' Submissions is the name of my originating Listbox. The rest remains unaltered. '
Dim listrow As Integer
For listrow = 0 To Me.Submissions.ListCount - 1
If Me.Submissions.Selected(listrow) = True Then
SelectSubmissions = ", " & SubLookup(0, listrow) & SelectSubmissions
End If
Next
If Len(SelectSubmissions) > 0 Then
SelectSubmissions = Right(SelectSubmissions, Len(SelectSubmissions) - 1)
End If
GetTitleIDs = SelectSubmissions
End Function
Private Sub Form_Current()
' Needed to update existing Listbox entry or entries when record is loaded '
' in form. If record is new, Listbox(es) will simply contain no records. This '
' can source all records for the fields you want in order to show existing '
' Listbox entries without fail. '
Me.Declined.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems ORDER BY [Title];"
Me.Accepted.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems ORDER BY [Title];"
End Sub
Private Sub Declined_GotFocus()
' Presumably your subset selection list gets focus after '
' checking list entries in originating Listbox. '
Me.Declined.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems WHERE [ID] IN (" & GetTitleIDs & ") ORDER BY [Title];"
End Sub
Private Sub Accepted_GotFocus()
Me.Accepted.RowSource = "SELECT [Poems].[ID], [Poems].[Title], [Poems].[Year Completed], [Poems].[Blog Location] FROM Poems WHERE [ID] IN (" & GetTitleIDs & ") ORDER BY [Title];"
End Sub
This will show only options in Declined and Accepted that are checked under Submissions, limiting selection options to only those values of interest for these fields. It's perfect.
So long as the table and ordering is the same for both the records assigned to IDData and the ListBox you're checking against, the IDs will always line up correctly.

Returning SQL rows with data concated per row in access

In access I have a table that is like
ID| ROOMS | NOTES|
78| 234| |
3 | 231| key |
78| 195| |
3 | 164| |
I want to a sql query that will take the ID and combine them into one row each so it is like
78 -> 234,195
3->231, 164 -> key
i just want to combine the rows only in the query no into a new table
Unfortunately you'll have to build a function to do this, but thankfully access supports the use of VBA functions inside of your SQL query.
For example function to concatenate the rim's together based on a given ID would be as follows:
Public Function MyRooms(lngID As Variant) As Variant
Dim rstRooms As DAO.Recordset
If IsNull(lngID) Then Exit Function
Set rstRooms = "select Rooms from tblBookings where id = " & lngID
Do While rstRooms.EOF
If MyRooms <> "" Then MyRooms = MyRooms & "->"
MyRooms = MyRooms & rstRooms!Rooms
rstRooms.MoveNext
Loop
rstRooms.Close
End Function
Now your query can look like this:
Select id, MyRooms([id]) as Rooms, Notes from some table.
You can also make an identical function much the same for the notes column, and again simply place that in the above query.