Reducing round-trips to database - asp.net

I'm using ASP.NET Web Forms and trying to load data from SQL server. Here's the pseudo-code on how I do it:
connect1 = connect("database")
categories = connect.query("select * from category")
loop categories as category
print category
connect2 = connect("database")
subCategories = connect2.query("select * from subCategory where category = #0", category)
loop subCategories as subCategory
print subCategory
connect3 = connect("database")
items = connect3.query("select * from item where subCategory = #0", subCategory)
loop items as item
print item
end loop 'items
connect3.close
end loop 'subcategories
connect2.close
end loop 'categories
connect1.close
As you can see, there are lots of round-trips going on in my script, this is fine when I only have few records but when dealing with hundreds or more, this takes forever to display the data.
What can I do to reduce the number of round-trips? I thought of getting all the data at once from the database then categorize them in the application side but is that possible?

Why don't you get all data you need by one query with joins and then filter in on a client-side; Or other way you can do it (it there's not too much data) is getting data as xml, deserialize it to ienumerable an iterate through.
As i see it you do
categories = connect.query("select * from category");
so all you need is:
whole_data = connect.query("select * from category c inner join subCategory sc on c.id = sc.id inner join item i on i.id = si.id") /*or some kind of*/
/*let me think that whole_data is a list of objects, not a dataset*/
categories = whole_data.Distinct(d => d.category);
subCategories = whole_data.Distinct(d => d.subCategories);
/*and then do your loops*/
c# code for manual mapping might be like that:
using (var connection = new SqlConnection(connString))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "select * from ...";
var reader = command.ExecuteReader();
while (reader.Read())
{
var a = reader.GetInt32(0);
var b = reader.GetString(1);
/*so basically you read all fields that you get from DB and build object here, then you add it to List or any other kind of collection*/
}
}

Depending upon the latency to your database, making the connections, even if pooled, can take a long time. To avoid that over head, make all your connections outside of the loops. That is, don't nest the connections. Instead, structure it like this:
Connect1 = connect("database")
Connect2 = connect("database")
Connect3 = connect("database")
sql 1 nest
sql 2 nest
sql 3 nest
end nest
end nest
end nest
close connections.
If you have 10 entries per loop, and a connection takes 10 mSec, you will spend 10 x 10 x 10 = 1000 mS just doing connections. Take them outside the nest, then you spend only 30 mS making connections. Close your datareader at the completion of each nest so the connection can be reused.
Of course, for the example you showed, doing a single query is the best solution.
But, if your query is selective and you need to perform some business logic that cannot be combined in the query, then always move your connections outside of the loop.

Related

ASP.NET returns incorrect data from oData Controller

I have an ASP.NET Web API app using Oracle's Entity Framework driver. I have an entity defined for a view as follows:
CREATE OR REPLACE FORCE VIEW "PHASE_TWO"."EDIPRODUCT" ("ID", "STK_NUM", "TITLE", "ISBN", "UPC", "ITEMNO", "LONGFORMAT", "ABRIDGED", "WEB_TITLES_ID", "OCLC", "GENRE", "RELYEAR", "ORIG_REL", "LANG", "ORIG_STKNUM", "PUBLISHER", "PEOPLELIST", "SALES_ORG", "NOT_AVAIL") AS
SELECT sap_product.id,
sap_product.stk_num,
sap_product.longdesc AS title,
sap_product.isbn,
sap_product.upc,
sap_product.itemno,
sap_product.longformat,
sap_product.abridged,
mwt_product.web_titles_id,
mwt_product.oclc,
mwt_product.genre,
mwt_product.RELYEAR,
sap_product.orig_rel,
sap_product.lang,
sap_product.orig_stknum,
UPPER (publisher.name) publisher,
(SELECT LISTAGG (p.FULLNAME, ', ') WITHIN GROUP (
ORDER BY pp.rank) AS People
FROM people p
JOIN product_people pp
ON p.id = pp.peopleid
WHERE pp.stk_num = sap_product.stk_num
GROUP BY pp.STK_NUM
) PeopleList,
sppg.PRICING_TYPE as sales_org,
sap_product.not_avail
FROM sap_product
JOIN mwt_product ON sap_product.stk_num = mwt_product.stk_num
JOIN publisher ON mwt_product.publisherid = publisher.id
JOIN SAP_PRODUCT_PRICING_GROUP sppg on sppg.STK_NUM = mwt_product.stk_num and sppg.MARKED_FOR_DELETION = 0
WHERE mwt_product.WEB_PRODUCTS_ID > 0;
This view works as expected in SQL Developer. My getEDIPRODUCT function (yes, it's VB.NET) in my controller is as follows:
' GET: odata/EDIPRODUCTs
<EnableQuery>
Function GetEDIPRODUCT() As IQueryable(Of EDIPRODUCT)
Dim results As IQueryable
results = db.EDIPRODUCT
For Each _product In results
Console.Write(_product)
Next
Return results
End Function
I just added the for loop in order to inspect the results. What I see when I inspect the results is the same product record is returned for each row. The value for the ID is duplicate and the only other field that should have variant values (sppg.PRICING_TYPE as sales_org) also just repeats.
I have other views where this does not occur. The correct number of records are always returned, but the first record retrieved is always just repeated in each row of the result set. Any idea what could be going on here?
I never actually resolved this issue and am still interested in why this fails, but I rewrote the portion of the app that uses this view to use OData's $expand to retrieve the related data.

Update 2000 records with one query

I have a Database table:
Item
ID (uniqueidentifier)
Index (int)
I have a list of 2000 key-value pairs items where the key is ID and value is Index, which i need to update it. How can i update all the 2000 items from database using one single sql query?
Right now i have something like this:
// this dictionary has 2000 values
Dictionary<Guid, int> values = new Dictionary<Guid,int>();
foreach(KeyValuePair<Guid, int> item in values)
{
_db.Database.ExecuteSqlCommand("UPDATE [Item] SET [Index] = #p0 WHERE [Id] = #p1", item.Value, item.Key);
}
However, i am making too many requests to the SQL Server, and i want to improve this.
Use table value parameters to send those values to SQL Server and update Items table in one shot:
CREATE TYPE KeyValueType AS TABLE
(
[Key] GUID,
[Value] INT
);
CREATE PROCEDURE dbo.usp_UpdateItems
#pairs KeyValueType READONLY
AS
BEGIN
UPDATE I
SET [Index] = P.Value
FROM
[Item] I
INNER JOIN #pairs P ON P.Id = I.Id
END;
GO
If you really need to update in that manner and have no other alternative - the main way around it could be this rather "ugly" technique (and therefore rarely used, but still works pretty well);
Make all 2000 statements in one string, and execute that one string. That makes one call to the database with the 2000 updates in it.
So basically something like this (code not made to actually run, it's an example so t
Dictionary<Guid, int> values = new Dictionary<Guid, int>();
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach (KeyValuePair<Guid, int> item in values)
{
sb.Append(String.Format("UPDATE [Item] SET [Index] = {0} WHERE [Id] = '{1}';", item.Value, item.Key));
}
_db.Database.ExecuteSqlCommand(sb.ToString);

Comparing Two Queries and Synchronizing Results

I have a table (Rooms) which contains a number of rows. Each row represents a room and each room needs to exist twice (once for fall, once for spring semester). Sometimes when folks add a room they only add it for one semester. I'm working on a process that will synchronize the rooms between semesters.
First I've pulled two queries, one that gets all of the rooms with fall in their semester column and one that gets all of the rooms with spring in their semester column, like so:
Dim getFallRooms = (From p In dbContext.Rooms _
Where p.semester = "Fall" _
Select p)
Dim getSpringRooms = (From p In dbContext.Rooms _
Where p.semester = "Spring" _
Select p)
The results will each contain multiple rows with the following columns: id, building, room, occupant, and semester.
What I want to do is something like this (pseudo):
For Each row in getFallRooms
If row.building and row.room not in getSpringRooms Then
' Code to add new row with fall as the semester.
End If
Next
Any suggestions on how I can make this into actual, workable code?
For Each row in getFallRooms
If (From p in getSpringRooms
Where p.building = row.building
AndAlso p.room = row.room).Count = 0 Then
' Code to add new row with fall as the semester.
End If
Next
You could do something quite similar to what's being done at this link.
I don't know precisely how to do it in VB.NET, but here's how I would do it in C#, using the LINQ extension methods:
var fallRooms = dbContext.Rooms.Where(room => room.semester.Equals("Fall")).Select(r => r.Name);
var nonSpringRooms = dbContext.Rooms.Where(room => room.semester.Equals("Spring"))
.Select(r => r.Name)
.AsQueryable()
.Except(fallRooms);
Afterwards, you could then do a For Each loop on nonSpringRooms to do whatever it is that you'd want to do.
On a side note, someone should correct me if I'm wrong, but I believe that the above code would only go to the database once. Hence, you'd get that benefit as well.
EDIT: I realized that since they'd be on the same table that the records would have the same primary key. I've subsequently changed the query above, assuming that you have a name field that would be the same in both records.

LINQ - listview - 2 levels of data in one table

I have been trying to solve this problem and I can't seem to figure it out. I'm not sure if it's because of my db design and LINQ, but I'm hoping for some direction here.
My db table:
Id         Name         ParentId
1          Data1        null
2          Data2        null
3          Data3        null
4          Data4        1
5          Data5        1
6          Data6        2
7          Data7        2
Basically Data1 and Data2 are the top levels that I want to use for headings and their children will be related based on their ParentID.
I am trying to use a listview to present the data like the following:
Data1
-----
Data4
Data5
Data2
-----
Data6
Data7
I am trying to use a combination of LINQ and listview to accomplish this.
The following is the code for the linq query:
var query = from data in mydb.datatable
where data.ParentId == null
select data;
But this only gives the heading level... and unfortunately listview only takes in 1 datasource.
While it's possible with some databases (like SQL Server post 2005) to write recursive queries, I don't believe those get generated by LINQ. On the other hand, if the number of records is sufficiently small, you could materialize the data (to a list) and write a LINQ query that uses a recursive function to generate your list.
This is from memory, but it would look something like this:
Func<int?,IEnumerable<data>> f = null;
f = parentId => {
IEnumerable<data> result = from data in mydb.datatable
where data.ParentId = parentId
select data;
return result.ToList().SelectMany(d=>f(d.Id));
};
That should get you the hierarchy.
If your hierarchy has only two levels you can use a group join and anonymous objects:
var query = from data in mydb.datatable.Where(x => x.ParentId == null)
join child in mydb.datatable.Where(x => x.ParentId != null)
on data.Id equals child.ParentId into children
select new { data, children };
Edit: You will have to convert the data to a collection that can be bound to a ListView. One hack would be to have a list that is only one level deep with spacing in front of the subitems:
var listViewItems = (from item in query.AsEnumerable()
let dataName = item.data.Name
let childNames = item.children.Select(c => " " + c.Name)
from name in dataName.Concat(childNames)
select new ListViewItem(name)).ToArray();
You could also try to find a control that fits better, like a TreeView. You might want to ask a separate question about this issue.
I just wrote up a blog post describing a solution to build a graph from a self-referencing table with a single LINQ query to the database which might be of use. See http://www.thinqlinq.com/Post.aspx/Title/Hierarchical-Trees-from-Flat-Tables-using-LINQ.

Using a Repeater with a dynamically generated table, ie, so unknown field names

I'm trying to produce a repeater showing amounts of money taken by various payment types into a table.
Payment types available come from a global settings file as an array, I am creating a dataTable by looping this list and extracting sales reports (there might be a more efficient way than this loop, but this is not my concern at the minute).
My question: How do I bind this to a repeater and display it when I dont necessarily know the table column names?
I've tried various methods to give the table a header row and give the columns numerical names from a for > next loop, but am either getting no results, or
System.Data.DataRowView' does not contain a property with the name '7'. < or whatever number
This is where I currently am:
EDIT: JUST REALISED MY CODE WAS AWFUL, SO UPDATED:
Dim paymentTable As New DataTable("paymentTable")
For j = 0 To UBound(paymentTypes)
Dim Type = Trim(paymentTypes(j))
Dim headers As DataColumn = New DataColumn(j.ToString)
paymentTable.Columns.Add(headers)
Next
Dim titleRow As DataRow = paymentTable.NewRow()
For k = 0 To UBound(paymentTypes)
Dim Type = Trim(paymentTypes(k))
titleRow.Item(k) = Type
Next
paymentTable.Rows.Add(titleRow)
Dim newRow As DataRow = paymentTable.NewRow()
For i = 0 To UBound(paymentTypes)
Dim Type = Trim(paymentTypes(i))
Try
newRow.Item(i) = '' GO OFF AND GET STUFF FROM DB
Catch
newRow.Item(i) = "0 "
End Try
Next
paymentTable.Rows.Add(newRow)
THIS EDITED CODE WORKS BUT I ONLY GET ONE ITEM
What I was hoping for would look something like:
card | cash | paypal ... etc (headings row)
£250 | £54 | £78 ... etc (values row)
Obviously there're a million ways this can be done, but this makes sense for my application, which has to be expandable and contractable depending on payment types available and this whole table needs to be repeated for multiple locations (also variable depending on who's viewing, and the number of locations in the system)
No, dont give up but just dont name columns by absolute number with no string before try
Dim headers As DataColumn = New DataColumn("col"+ j.ToString)

Resources