Update changes to related tables in VB.Net - sql-server

I have two related SQL tables, both of which I open with a dataadapter and fill into a dataset. I set the relationship as follows:
DS.Relations.Add("RBT", DS.Tables("Jobs").Columns("ID"), DS.Tables("BillTech").Columns("JobID"), False)
Dim BSBT As New BindingSource
BSBT.DataSource = BS
BSBT.DataMember = "RBT"
The ID column in the Jobs SQL table is an identity seed incremented by 1.
The columns from the Jobs table are displayed on a form with a binding navigator to move from job to job. The related columns from the BillTech table are displayed in a datagridview on the Jobs form.
If the user adds a row to the datagridview (i.e. a new row to the BillTech table) for an existing job, I put the ID from the selected job into the JobID column of the datagridview in the DefaultValuesNeeded event of the datagridview. That all works great.
The problem is when the user adds a new job to the Jobs table and then tries to add a new row in the datagridview for that new job. Since the ID of that new job is unknown (since it hasn't been updated back to the SQL table), I use the dataadapter to update the Jobs table and then retrieve the assigned ID from the updated SQL table using the "GetNewJobID" function. I then try to insert that value in the new row of the datagridview. See the code below:
Private Sub DataGridView2_DefaultValuesNeeded(sender As Object, e As DataGridViewRowEventArgs) Handles DataGridView2.DefaultValuesNeeded
Dim JobID As Integer = 0
With e.Row
Dim DR() As DataRow = DS.Tables("Jobs").Select("Name = '" & Replace(Me.JobName.Text, "'", "''") & "'")
If DR.Length = 1 Then
JobID = DR(0).Item("ID")
Else
Dim Name As String = Me.JobName.Text
BS.EndEdit()
DA.InsertCommand = CB.GetInsertCommand
DA.Update(DS.Tables("Jobs"))
'Because we have already added this job to the database, we keep a list so we can see if the user wants this job added to QB.
If NewJobs.Contains(Name) = False Then
NewJobs.Add(Name)
End If
JobID = GetNewJobID(Name)
End If
.Cells("JobID").Value = JobID
End With
End Sub
I get an error on the next to last line of the above sub ".Cells("JobID").Value = JobID saying that the column "JobID" does not exist. Since this only occurs when adding a BillTech row to a new Job Row, I'm guessing that by updating the Jobs SQL table, this somehow breaks the connection to the datagridview?
How can I update this properly? I've looked at TableAdapterManager, but this seems to only work with typed datasets. Because these SQL tables are on a Windows Server 2003 server, I cannot bring them in with an "Add New Datasource" wizard.

I'll pick on the typical parent/child or master/detail of Customers:Orders. If you were using an access or sql server db with a relationship defined in the db and you made a new dataset, opened it in the designer, right clicked the surface and chose Add TableAdapter, put SELECT * FROM customers and repeated the process (from right click..) for Orders you would end up with a Customers DataTable and TableAdapter, an Orders DataTable and TableAdapter and a DataRelation between the two linking (say) orders.customerid to customers.id
Save the set, then take a look in the data sources window and expand every node.
There is a top level node of Customers and a top level node of Orders, and you'll also notice an Orders node that is a child of Customer
Drag the Customers node out of datasources, onto a new form. A customersdatagridview, dataset, customerstableadapter, customersbindingsource and customersbindingnavigator appear. Delete the navigator (for clarity right now). You will note that the customersdatagridview is bound to the customersbindingsource and the customersbindingsource is bound to the Customers table of the dataset
Drag the top level Orders out of the datasources window onto the form. You'll notice that an ordersdatagridview, ordersbindingsource, orderstableadapter and ordersnavigator appear. Delete the navigator. Note that, like before, the ordersdatagridvew binds to the ordersbindingsource and the ordersbindigsource binds to Orders in the dataset. This is what I think you've done
Delete everything that appeared when you dragged Orders out (dgv/bs/ta for orders). Don't delete the dataset - it's still in use by hings named customers*
Now go back to the datasources window and this time drag the child Orders out that is below the Customers node. The same components appear (ordersdgv/bs/ta/nav). Delete the nav. Look at the ordersbindingsource datasource and datamember properties this time. You'll notice that unlike last time, this ordersbindingsource is not bound to the dataset directly but to the customersbindingsource, specifically to a datamember named after the same name as the datarelation in the set
In this setup, when the parent current row changes in the customersbindingsource, the ordersbindingsource will only see a filtered set of orders rows - the filter passes only rows whose orders.customerid is equal to the current id in th customersbindingsource
Note; it's important that the Orders data has to be in the dataset. Nothing here automagivally downloads database data. If you fill customers but don't fill the orders data then no data will show in the orders, no matter how it is bound.
The simplistic approach takes the default queries in the tableadapters (select * from customers, select * from orders) and dwnloads the ENTIRE db into the dataset. It'll work for demonstration purposes but you really don't want to do that in a production program.
Go back to the dataset and alter the default queries in the table adapters to be SELECT * FROM customers WHERE id = #id and the corollorary for orders. Then add ANOTHER query to the customers TA (right click it and choose add query):
SELECT * FROM customers WHERE lastname = #lastname
Call this query FillByLastName (the wizard will ask you to name it and suggest FillBy. Don't accept the default; give it a sensible name)). This might load 10 customers.
Now go to the ORDERS tableadapter and add another query to that too:
SELECT * FROM orders WHERE customerid = #customerid
Call it FillByCustomerId. This can be used, once we have loaded 10 customers, to retrieve the orders for those 10 customers:
'fill the form dataset with customers named smith:
Me.someDataSet.Clear()
customersTableAdapter.FillByLastName(someDataSet, "Smith") 'it will fill the Customers table with Smiths
ordersTableAdapter.ClearBeforeFill = false 'turn this off or every call to fill will remove existing order from the datatable
For Each cro as CustomersRow in someDataSet.Customers 'for all our smiths
ordersTableAdapter.FillByCUstomerId(someDataSet, cro.Id) 'fill orders for customer with that id
Next cro
So now you have 10 customers, and maybe 50+ orders - you don't have the whole DB in your dataset, just the interesting data. Databinding via the datarelation exposed on the customers bindingsource will take care of showing only the related rows. Click on various rows in the customers datagridview and note that the orders shown in the ordersdatagridview changes to show only the relevant rows
It is intended that you add as many queries to a tableadapter as suits your needs. You could, rather than looping the customers and filling their orders by each customer ID, write a query that returns them all in one hit, on the ORDERS tableadapter:
SELECT orders.*
FROM
orders INNER JOIN customers on orders.customerid = customers.id
WHERE
customers.lastname = #lastName
The query selects only data from orders, but joins through to customers and searches on customer last name, so we should call this FillByCustomerLastName
Our code could then look like:
'fill the form dataset with customers named smith:
Me.someDataSet.Clear()
customersTableAdapter.FillByLastName(someDataSet, "Smith") 'it will fill the Customers table with Smiths
ordersTableAdapter.FillByCustomerLastName(someDataSet, "Smith") 'fill orders for all customers with a last name of smith
Horses for courses which you choose. I tend to pick the first approach and loop because it's more flexible, but where outright performance matters it can be faster to use a pattern like the second

Related

SQL Server - Update All Records, Per Group, With Result of SubQuery

If anyone could even just help me phrase this question better I'd appreciate it.
I have a SQL Server table, let's call it cars, which contains entries representing items and information about their owners including car_id, owner_accountNumber, owner_numCars.
We're using a system that sorts 'importantness of owner' based on number of cars owned, and relies on the owner_numCars column to do so. I'd rather not adjust this, if reasonably possible.
Is there a way I can update owner_numCars per owner_accountNumber using a stored procedure? Maybe some other more efficient way I can accomplish every owner_numCars containing the count of entries per owner_accountNumber?
Right now the only way I can think to do this is to (from the c# application):
SELECT owner_accountNumber, COUNT(*)
FROM mytable
GROUP BY owner_accountNumber;
and then foreach row returned by that query
UPDATE mytable
SET owner_numCars = <count result>
WHERE owner_accountNumber = <accountNumber result>
But this seems wildly inefficient compared to having the server handle the logic and updates.
Edit - Thanks for all the help. I know this isn't really a well set up database, but it's what I have to work with. I appreciate everyone's input and advice.
This solution takes into account that you want to keep the owner_numCars column in the CARs table and that the column should always be accurate in real time.
I'm defining table CARS as a table with attributes about cars including it's current owner. The number of cars owned by the current owner is de-normalized into this table. Say I, LAS, own three cars, then there are three entries in table CARS, as such:
car_id owner_accountNumber owner_numCars
1 LAS1 3
2 LAS1 3
3 LAS1 3
For owner_numCars to be used as an importance factor in a live interface, you'd need to update owner_numCars for every car every time LAS1 sells or buys a car or is removed from or added to a row.
Note you need to update CARS for both the old and new owners. If Sam buys car1, both Sam's and LAS' totals need to be updated.
You can use this procedure to update the rows. This SP is very context sensitive. It needs to be called after rows have been deleted or inserted for the deleted or inserted owner. When an owner is updated, it needs to be called for both the old and new owners.
To update real time as accounts change owners:
create procedure update_car_count
#p_acct nvarchar(50) -- use your actual datatype here
AS
update CARS
set owner_numCars = (select count(*) from CARS where owner_accountNumber = #p_acct)
where owner_accountNumber = #p_acct;
GO
To update all account_owners:
create procedure update_car_count_all
AS
update C
set owner_numCars = (select count(*) from CARS where owner_acctNumber = C.owner_acctNumber)
from CARS C
GO
I think what you need is a View. If you don't know, a View is a virtual table that displays/calculates data from a real table that is continously updated as the table data updates. So if you want to see your table with owner_numCars added you could do:
SELECT a.*, b.owner_numCars
from mytable as a
inner join
(SELECT owner_accountNumber, COUNT(*) as owner_numCars
FROM mytable
GROUP BY owner_accountNumber) as b
on a.owner_accountNumber = b.owner_accountNumber
You'd want to remove the owner_numCars column from the real table since you don't need to actually store that data on each row. If you can't remove it you can replace a.* with an explicit list of all the fields except owner_numCars.
You don't want to run SQL to update this value. What if it doesn't run for a long time? What if someone loads a lot of data and then runs the score and finds a guy that has 100 cars counts as a zero b/c the update didn't run. Data should only live in 1 place, updating has it living in 2. You want a view that pulls this value from the tables as it is needed.
CREATE VIEW vOwnersInfo
AS
SELECT o.*,
ISNULL(c.Cnt,0) AS Cnt
FROM OWNERS o
LEFT JOIN
(SELECT OwnerId,
COUNT(1) AS Cnt
FROM Cars
GROUP BY OwnerId) AS c
ON o.OwnerId = c.OwnerId
There are a lot of ways of doing this. Here is one way using COUNT() OVER window function and an updatable Common Table Expression [CTE]. That you won't have to worry about relating data back, ids etc.
;WITH cteCarCounts AS (
SELECT
owner_accountNumber
,owner_numCars
,NewNumberOfCars = COUNT(*) OVER (PARTITION BY owner_accountNumber)
FROM
MyTable
)
UPDATE cteCarCounts
SET owner_numCars = NewNumberOfCars
However, from a design perspective I would raise the question of whether this value (owner_numCars) should be on this table or on what I assume would be the owner table.
Rominus did make a good point of using a view if you want the data to always reflect the current value. You could also use also do it with a table valued function which could be more performant than a view. But if you are simply showing it then you could simply do something like this:
SELECT
owner_accountNumber
,owner_numCars = COUNT(*) OVER (PARTITION BY owner_accountNumber)
FROM
MyTable
By adding a where clause to either the CTE or the SELECT statement you will effectively limit your dataset and the solution should remain fast. E.g.
WHERE owner_accountNumber = #owner_accountNumber

Insert the value of the foreign key ID and not the ID

I am having a problem with VB.net & databases, which is when inserting data into table, I need to insert the value of the foreign key and not the key itself.
For Example I Have TWO Tables: Employee, Department.
With the following Model
Department (DepID pk, DepName)
Employee (EmpID pk, EmpName, DepID fk)
When Inserting data into Employee using vb winform,
I need to insert (or choose from combo) the DepName in the place of DepID….
NB1: That I could so easily with MS Access Forms using column count & Width.
NB2: I can select the data this way, But I am talking about manipulating data, like Insert, Update, Delete…
What is needed, is to allow the user to choose the DepName when inserting new Employee.. and not the ID
this is precisely the functionality you get from a bound control using ValueMember and DisplayMember:
Dim SQL As String = "Select * FROM Department ORDER BY DeptName"
Using cmd As New OleDbCommand(Sql, dbCon)
myDS = New DataSet("Dept")
Dim da As New OleDbDataAdapter(cmd)
da.Fill(myDS, "Dept")
frmM.cboDept.DisplayMember = "DeptName" ' what to SHOW
frmM.cboDept.ValueMember = "DeptID" ' value to track
frm.cboDept.DataSource = myDS.Tables(0)
End Using
You can add adapters and such to the form to skip the code. Each item in the CBO will be a database row object, but the control will display data from the column indicated by DisplayMember, In this case, "DeptName"
To get the value, that is the DeptID:
Private Sub cboDept_SelectedValueChanged(sender, e ...)
myEmployee.DeptID = Convert.ToInt32(cboDept.SelectedValue)
End Sub
Using the ValueMember property, we get the DeptID from the datarow selected in the cbo using the SelectedValue property. The datatype will match that of the DB Column.
You can do much the same thing using a List(Of Dept) where Dept is a class you build to show the name but allow you to retrieve the DeptID.

Database schema for end user report designer

I'm trying to implement a feature whereby, apart from all the reports that I have in my system, I will allow the end user to create simple reports. (not overly complex reports that involves slicing and dicing across multiple tables with lots of logic)
The user will be able to:
1) Select a base table from a list of allowable tables (e.g., Customers)
2) Select multiple sub tables (e.g., Address table, with AddressId as the field to link Customers to Address)
3) Select the fields from the tables
4) Have basic sorting
Here's the database schema I have current, and I'm quite certain it's far from perfect, so I'm wondering what else I can improve on
AllowableTables table
This table will contain the list of tables that the user can create their custom reports against.
Id Table
----------------------------------
1 Customers
2 Address
3 Orders
4 Products
ReportTemplates table
Id Name MainTable
------------------------------------------------------------------
1 Customer Report #2 Customers
2 Customer Report #3 Customers
ReportTemplateSettings table
Id TemplateId TableName FieldName ColumnHeader ColumnWidth Sequence
-------------------------------------------------------------------------------
1 1 Customer Id Customer S/N 100 1
2 1 Customer Name Full Name 100 2
3 1 Address Address1 Address 1 100 3
I know this isn't complete, but this is what I've come up with so far. Does anyone have any links to a reference design, or have any inputs as to how I can improve this?
This needs a lot of work even though it’s relatively simple task. Here are several other columns you might want to include as well as some other details to take care of.
Store table name along with schema name or store schema name in additional column, add column for sorting and sort order
Create additional table to store child tables that will be used in the report (report id, schema name, table name, column in child table used to join tables, column in parent table used to join tables, join operator (may not be needed if it always =)
Create additional table that will store column names (report id, schema name, table name, column name, display as)
There are probably several more things that will come up after you complete this but hopefully this will get you in the right direction.

SQL views from one table

My colleague asked me a question regarding getting data from a SQL Server database.
I have a sample data set
[ID],[TOWN],[PERSON]
[1],[BELFAST],[JAMES]
[2],[NEWRY],[JOHN]
[3],[BELFAST],[SIMON]
[4],[LARNE],[ALAN]
Now from this I would like to return a SQL Dataset that returns me a different table based upon the view.
Essentially in code I could get a distinct on the town then loop sql filtering on the town. But is there a way I can do this in SQL?
Where I would get (3) views back (2 Belfast, 1 Newry and 1 Larne)
Basicly I it would return
[ID],[Town],[Person]
[1],[Belfast],[James]
[3],[Belfast],[Simon]
Then another view would return for 'Larne' and a Final one for Newry. Basically SQL creating views for each town it finds and then returns the records for each town.
You don't get views back - you have to define them yourself.
E.g. if you need one view for Belfast, a second for Newry and a third for Larne - then you need to create three views that return only those rows that match the relevant city name
CREATE VIEW BelfastView
AS
SELECT ID, Town, Person
FROM dbo.Towns
WHERE Town = 'Belfast'
CREATE VIEW LarneView
AS
SELECT ID, Town, Person
FROM dbo.Towns
WHERE Town = 'Larne'
CREATE VIEW NewryView
AS
SELECT ID, Town, Person
FROM dbo.Towns
WHERE Town = 'Newry'
Now, certain users might only be allowed to select data from the BelfastView and thus would never see any other rows of data from your underlying table.
But views are database objects like tables or stored procedures; you need to create them, maintain them, toss them when no longer needed.
EDIT
Based on your updated question, you simply need to create a view for each town you want to filter:
CREATE VIEW BelfastView AS
SELECT ID,
Town,
Person
FROM YourTable
WHERE Town = 'BELFAST'
Although you've only given us a small sample of your data, what you're asking is almost never a good idea. What happens when you have 50 new towns in your DB? Are you going to create a view for each town? This does not scale well (or at all).
Basically I have decided to Run it as a Stored Procedure to return me each item as a List. So something along the lines of this:
Create Procedure ListTowns
As
declare #towns char(11)
select #towns = (select distinct Town from [Towns])
while #towns is not null <> 0
begin
select * from [YourTable] where Town = #towns
end

One more question regarding triggers PL/SQL

I have run into a problem with a second trigger we are to write regarding the following:
I have just written a stored procedure and stored function that serve to insert a new row into my Orders table. The row update inserts: Ordernum, OrderDate, Customer, Rep, Manufacturer, Product, Qty, and SaleAmount.
I now have to write a trigger that updates my Offices table by adding the amount of the newly added sale. Problem is, not every salesrep has an office assigned to them. I don't understand if I need to have a when clause under 'FOR EACH ROW' which somehow stipulates this, or if I need to stipulate after my SET Sales line. This is my code so far, but unfortunately, it updates all of the offices sales, not just the one that the salesrep belongs to:
CREATE OR REPLACE TRIGGER UpdateOffices
AFTER INSERT ON Orders
FOR EACH ROW
BEGIN
UPDATE Offices
SET Sales = Sales + :NEW.Amount
WHERE Office IN (SELECT RepOffice
FROM Salesreps
WHERE RepOffice IS NOT NULL);
End;
/
SHOW ERRORS
Sales is the name of the column on the Offices table. Amount if the name used in the stored proc.
You've already been advised to add a predicate on Salesreps, e.g. WHERE Salesrep = :NEW.Rep. Why don't you add it to the subquery?
You are going to run into all sorts of problems implementing your solution using this trigger method.
The Office.Sales column value will become stale as soon as amendments to orders are committed to the database. For example, when the price or quantity on an order is changed or deleted.
I would recommend you implement this requirement as a 'Refresh on Commit' Materialized View.

Resources