How to get data from two SQL tables into .net - c#

Previously, I asked a question about getting data from two tables where I take one row in a table and join it with several rows in another table. Here's the link to that discussion: SQL Select data from two tables (one row -> multiple rows)
Here is my SQL code:
SELECT
customer.fName, customer.lName, phone.phoneNumber
FROM
Customers customer
INNER JOIN phoneNumbers phone ON
customer.customerId = phone.customerId
What I would like to know now is: what is the best way to get this data organized in .net?
Let's suppose I have a C# class as following:
public class CustomerDetails
{
int customerId;
string fname;
string lName;
List<string> phoneNumbers;
}
For the sake of discussion, let's suppose that the above SQL query returns the following result:
fname, lname, phoneNumber
"John", "Smith", "111-111"
"Jane", "Doe", "222-1111"
"Jane", "Doe", "222-2222"
At a glance, I see that I have two customers; one has one phone number and the other has two phone numbers. How can I write code to efficiently parse this in C#?

One option is to use LINQ to create a instance of the CustomerDetails class.
Let me know if you would like an example.
Example 1:
List<CustomerDetails> customers = db.Customers.Select(c => new CustomerDetails(){
customerId = c.customerID,
fname = c.fName,
lName = c.lName,
phoneNumbers = (from p in db.PhoneNumbers where p.customerID == c.customerID select p.phoneNumber1).ToList<String>()});
Example 2:
List<CustomerDetails> custs = (from c in db.Customers
select new CustomerDetails()
{
customerId = c.customerID,
fname = c.fName,
lName = c.lName,
phoneNumbers = (from p in db.PhoneNumbers where p.customerID == c.customerID select p.phoneNumber1).ToList<String>()
}).ToList<CustomerDetails>();

I understand that you are looking for ORM object-relational mapping. In .NET I can recommend IBatis.net or LINQ.

Solution 1:
I assume that the records are sorted by name in the SQL query's results and that you also select the customerID. Appending "ORDER BY customer.fName, customer.lName" to your original query will do the trick.
I'll assume that you get your result in a DataReader, so you can do the following:
// Lets start by declaring a collection for our records
List<CustomerDetails> myRecords = new List<CustomerDetails>();
// Iterate all records from the results and fill our collection
while (yourReader.Read())
{
int customerID = int.Parse(yourReader["customerID"]);
int nrRecords = myRecords.Count;
if (nrRecords > 0 && myRecords[nrRecords - 1].customerId == customerID)
{
myRecords[nrRecords - 1].phoneNumbers.Add(yourReader["phoneNumber"]);
}
else
{
CustomerDetails newCustomerDetails = new CustomerDetails();
newCustomerDetails.customerId = customerID;
newCustomerDetails.fName = yourReader["fName"];
newCustomerDetails.lName = yourReader["lName"];
List<string> phoneNumberList = new List<string>();
phoneNumberList.Add(yourReader["phoneNumber"]);
newCustomerDetails.phoneNumbers = phoneNumberList;
myRecords.Add(newCustomerDetails);
}
}
P.S. If ordering the list is not an option, then you can't just check the latest added records customerid - instead you'll need to iterate through the myRecords list and search for the existance of it. This can be done in many ways including with myRecords.Contains() or with a foreach.
Solution 2:
Do the telephone number grouping directly from SQL. Create a function for selecting a comma separated string with all the telephone numbers of a particular customer:
CREATE FUNCTION [dbo].[GetCommaSeparatedPhoneNumbers]
(
#customerID int
)
RETURNS varchar(max)
AS
BEGIN
declare #output varchar(max)
select #output = COALESCE(#output + ', ', '') + phoneNumbers
from phoneNumbers
where customerId = #customerID
return #output
END
GO
Then you can nicely select the list of all customer you want:
SELECT customerId, dbo.GetCommaSeparatedPhoneNumbers(customerId)
FROM Customers
GROUP BY customerId
This will return:
"John", "Smith", "111-111"
"Jane", "Doe", "222-1111,222-2222"
Now it's all a question of parsing the results with a foreach or while loop, but no checking for existance is needed. Just split the string at ',' and insert the values into the List. If there is a chance, that there will be no phone numbers for some customers, then you can filter that field for null.
PS. Will not work if there is a comma as pointed out by BQ in his comment.

Iterate over the result, for each row check if it is in your list of costumers, if not, add a new one, if yes, add the phone number to the existing one.
Of course, you shouldn't parse the string use appropiate classes to access the database, like ado.net.

Related

Select and group data from 2 datatables with linq and display the result on datagridview

I have 2 DataTables which contain data about employees and orders. I need to get the count of the orders associated with each employee as well as the employee's ID and name.
This SQL shows the data in the format I want it:
select e.EmployeeID as ID,
(e.LName+' '+e.FName+' '+e.MName) as 'Full name',
COUNT(e.EmployeeID) as 'Sales number'
from Employees as e
join Orders as o on e.EmployeeID=o.EmployeeID
group by e.EmployeeID, e.FName, e.LName, e.MName
How can I use LINQ to get the data and display the results in a dataGridView?
This is my code so far:
private void Form1_Load(object sender, EventArgs e)
{
var empSales = from emp in Shop.ShopDB.Tables["Employees"].AsEnumerable()
join order in Shop.ShopDB.Tables["Orders"].AsEnumerable()
on emp.Field<int>("EmployeeID") equals order.Field<int>("EmployeeID")
group emp by emp.Field<int>("EmployeeID");
IEnumerable<DataRow> DataRows = empSales.SelectMany(group => group);
List<object[]> list= DataRows.Select(dr => dr.ItemArray).ToList();
//trying to get the final object but group's key is not available
//because these datarows don't have that field
var empss = DataRows.Select(d => new
{
ID = d.Field<int>("EmployeeID"),
Фамилия = d.Field<int>("Lname"),
Имя = d.Field<int>("Fname"),
Отчество = d.Field<int>("Mname")
});
EmployeesSalesGridView.DataSource = list;
}
Here is a script with some sample data:
create table Orders (OrderID int, CustomerNo int, OrderDate datetime, EmployeeID int)
insert into Orders select 1, 1, '2009-12-28', 2
insert into Orders select 2, 3, '2010-09-01', 4
insert into Orders select 3, 4, '2010-09-18', 4
insert into Orders select 4, 1, '2010-12-10', 2
create table Employee (EmployeeID int, FName varchar(20), LName varchar(20), MName varchar(20))
insert into Employee select 1, 'Alpha', 'Bravo', 'Charlie'
insert into Employee select 2, 'Delta', 'Echo', 'Foxtrot'
insert into Employee select 3, 'Golf', 'Hotel', 'India'
insert into Employee select 4, 'Jacob', 'Kilo', 'Lima'
If I understand the problem correctly, then you want the result to contain the following columns: EmployeeID, FullName, and SalesNumber and to match the result from the SQL query.
In your code you have the comment:
//trying to get the final object but group's key is not available
//because these datarows don't have that field
You almost have the correct Linq query. The problem is that you're flattening the group when you use SelectMany, that's why you can no longer access the group key. There's no need to use SelectMany here.
Select the desired data into an anonymous object, then use it as the DataSource for the GridView:
var empSales = from emp in Shop.ShopDB.Tables["Employees"].AsEnumerable()
join order in Shop.ShopDB.Tables["Orders"].AsEnumerable()
on emp.Field<int>("EmployeeID") equals order.Field<int>("EmployeeID")
group emp by emp.Field<int>("EmployeeID") into g
select new
{
EmployeeID = g.Key,
FullName = g.First().Field<string>("FName") + " " +
g.First().Field<string>("MName") + " " +
g.First().Field<string>("LName"),
SalesNumber = g.Count()
};
EmployeesSalesGridView.DataSource = empSales.ToList();
Which produces the same result as the SQL query for your sample data:

Combining two tables and writing a query from them into a list

I have 2 tables in the database - Customer and Product.
I perform the selection by skipping the first row in the table and then getting the top two rows.
The result I need to get in the List cusPod .
List<Customer> customers = db.Database.SqlQuery<Customer>("SELECT * FROM Customers ORDER BY CustomerId OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY").ToList();
List<Product> products = db.Database.SqlQuery<Product>("SELECT * FROM Products ORDER BY ProductId OFFSET 1 ROWS FETCH NEXT 2 ROWS ONLY").ToList();
var listSas = from p in products
join c in customers on p.ProductId equals c.CustomerId
select new { ProductId = p.ProductId, ProductName = p.ProductName, DateStart = p.DateStart, DateEnd = p.DateEnd, DateRegister = p.DateRegister, PriceCustomer = p.PriceCustomer, CheckPay = p.CheckPay, CustomerId = p.CustomerId, FIO = c.FIO, Email = c.Email, PhoneNumber = c.PhoneNumber };
SidebarController.cusPod = listSas.ToList();
how do I do this right?
From the code it seems that you are using entity framework as your ORM, therefore, you can easily use the Skip and Take methods of EF that are great features instead of writing hard coded sql. for example:
db.Customers.OrderBy(c => c.CustomerId).Skip(1).Take(2);
The rest of your logic seems fine.

LINQ query not showing in DGV

I have been researching this for some time, so if I missed a topic somewhere please point me in the right direction and accept my apologies.
I am performing a LINQ query on an incoming DataTable and setting up new columns based on the filtering done. The information being passed in is a single row containing the columns: CompanyName, CustomerID, LastName, FirstName, ContactTitle.
My question is, am I over-filtering the information to the point where it will return nothing to the DGV, or have I done something else wrong?
What I am trying to do is query another table based on the information passed in from the DataTable. Here is my query:
var query = (from id in IncomingOrderDetails.AsEnumerable()
from o in db.Orders
from c in db.Customers
from r in db.Regions
where (id.Field<int>("OrderID") == o.OrderID)
where (o.CustomerID == c.CustomerID)
where (c.Region == r.RegionDescription)
select new
{
CustomerID = c.CustomerID,
CompanyName = c.CompanyName,
ContactName = c.ContactName,
RegionDescription = r.RegionDescription,
Country = c.Country,
Phone = c.Phone
}).ToList();
custInfoDGV.DataSource = query;
This line:
where (c.Region == r.RegionDescription)
...looks dubious. If c.Region = r.RegionDescription, why do you need to bring in r at all? All you use it for is retrieving the RegionDescription.
I don't know your tables, but joining two fields that don't match would certainly produce 0 records returned. :)

Correct use of multimapping in Dapper

I'm trying to use the multimapping feature of Dapper to return a list of ProductItems and associated Customers.
[Table("Product")]
public class ProductItem
{
public decimal ProductID { get; set; }
public string ProductName { get; set; }
public string AccountOpened { get; set; }
public Customer Customer { get; set; }
}
public class Customer
{
public decimal CustomerId { get; set; }
public string CustomerName { get; set; }
}
My Dapper code:
var sql = #"select * from Product p
inner join Customer c on p.CustomerId = c.CustomerId
order by p.ProductName";
var data = con.Query<ProductItem, Customer, ProductItem>(
sql,
(productItem, customer) => {
productItem.Customer = customer;
return productItem;
},
splitOn: "CustomerId,CustomerName"
);
This works fine, but I seem to have to add the complete column list to the "splitOn" parameter to return all the customers' properties. If I don't add "CustomerName", it returns null. Am I misunderstanding the core functionality of the multimapping feature? I don't want to have to add a complete list of column names each time.
I just ran a test that works fine:
var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(1 as decimal) CustomerId, 'name' CustomerName";
var item = connection.Query<ProductItem, Customer, ProductItem>(sql,
(p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();
item.Customer.CustomerId.IsEqualTo(1);
The splitOn param needs to be specified as the split point, it defaults to Id. If there are multiple split points, you will need to add them in a comma delimited list.
Say your recordset looks like this:
ProductID | ProductName | AccountOpened | CustomerId | CustomerName
--------------------------------------- -------------------------
Dapper needs to know how to split the columns in this order into 2 objects. A cursory look shows that the Customer starts at the column CustomerId, hence splitOn: CustomerId.
There is a big caveat here, if the column ordering in the underlying table is flipped for some reason:
ProductID | ProductName | AccountOpened | CustomerName | CustomerId
--------------------------------------- -------------------------
splitOn: CustomerId will result in a null customer name.
If you specify CustomerId,CustomerName as split points, dapper assumes you are trying to split up the result set into 3 objects. First starts at the beginning, second starts at CustomerId, third at CustomerName.
Our tables are named similarly to yours, where something like "CustomerID" might be returned twice using a 'select *' operation. Therefore, Dapper is doing its job but just splitting too early (possibly), because the columns would be:
(select * might return):
ProductID,
ProductName,
CustomerID, --first CustomerID
AccountOpened,
CustomerID, --second CustomerID,
CustomerName.
This makes the splitOn: parameter not so useful, especially when you're not sure what order the columns are returned in. Of course you could manually specify columns... but it's 2017 and we just rarely do that anymore for basic object gets.
What we do, and it's worked great for thousands of queries for many many years, is simply use an alias for Id, and never specify splitOn (using Dapper's default 'Id').
select
p.*,
c.CustomerID AS Id,
c.*
...voila! Dapper will only split on Id by default, and that Id occurs before all the Customer columns. Of course it will add an extra column to your return resultset, but that is extremely minimal overhead for the added utility of knowing exactly which columns belong to what object. And you can easily expand this. Need address and country information?
select
p.*,
c.CustomerID AS Id,
c.*,
address.AddressID AS Id,
address.*,
country.CountryID AS Id,
country.*
Best of all, you're clearly showing in a minimal amount of SQL which columns are associated with which object. Dapper does the rest.
Assuming the following structure where '|' is the point of splitting and Ts are the entities to which the mapping should be applied.
TFirst TSecond TThird TFourth
------------------+-------------+-------------------+------------
col_1 col_2 col_3 | col_n col_m | col_A col_B col_C | col_9 col_8
------------------+-------------+-------------------+------------
Following is the Dapper query that you will have to write.
Query<TFirst, TSecond, TThird, TFourth, TResut> (
sql : query,
map: Func<TFirst, TSecond, TThird, TFourth, TResut> func,
parma: optional,
splitOn: "col_3, col_n, col_A, col_9")
So we want for TFirst to map to col_1 col_2 col_3, for TSecond to col_n col_m ...
The splitOn expression translates to:
Start mapping of all columns into TFirst till you find a column named or aliased as 'col_3', and also include 'col_3' into the mapping result.
Then start mapping into TSecond all columns starting from 'col_n' and continue mapping till new separator is found, which in this case is 'col_A', and mark the start of TThird mapping and so on.
The columns of the SQL query and the props of the mapping object are in a 1:1 relation (meaning that they should be named the same). If the column names resulting from the SQL query are different, you can alias them using the 'AS [Some_Alias_Name]' expression.
If you need to map a large entity write each field must be a hard task.
I tried #BlackjacketMack answer, but one of my tables has an Id Column other ones not (I know it's a DB design problem, but ...) then this insert an extra split on dapper, that's why
select
p.*,
c.CustomerID AS Id,
c.*,
address.AddressID AS Id,
address.*,
country.CountryID AS Id,
country.*
Doesn't work for me. Then I ended with a little change to this, just insert an split point with a name that doesn't match with any field on tables, In may case changed as Id by as _SplitPoint_, the final sql script looks like this:
select
p.*,
c.CustomerID AS _SplitPoint_,
c.*,
address.AddressID AS _SplitPoint_,
address.*,
country.CountryID AS _SplitPoint_,
country.*
Then in dapper add just one splitOn as this
cmd =
"SELECT Materials.*, " +
" Product.ItemtId as _SplitPoint_," +
" Product.*, " +
" MeasureUnit.IntIdUM as _SplitPoint_, " +
" MeasureUnit.* " +
"FROM Materials INNER JOIN " +
" Product ON Materials.ItemtId = Product.ItemtId INNER JOIN " +
" MeasureUnit ON Materials.IntIdUM = MeasureUnit.IntIdUM " +
List < Materials> fTecnica3 = (await dpCx.QueryAsync<Materials>(
cmd,
new[] { typeof(Materials), typeof(Product), typeof(MeasureUnit) },
(objects) =>
{
Materials mat = (Materials)objects[0];
mat.Product = (Product)objects[1];
mat.MeasureUnit = (MeasureUnit)objects[2];
return mat;
},
splitOn: "_SplitPoint_"
)).ToList();
There is one more caveat. If CustomerId field is null (typically in queries with left join) Dapper creates ProductItem with Customer = null. In the example above:
var sql = "select cast(1 as decimal) ProductId, 'a' ProductName, 'x' AccountOpened, cast(null as decimal) CustomerId, 'n' CustomerName";
var item = connection.Query<ProductItem, Customer, ProductItem>(sql, (p, c) => { p.Customer = c; return p; }, splitOn: "CustomerId").First();
Debug.Assert(item.Customer == null);
And even one more caveat/trap. If you don't map the field specified in splitOn and that field contains null Dapper creates and fills the related object (Customer in this case). To demonstrate use this class with previous sql:
public class Customer
{
//public decimal CustomerId { get; set; }
public string CustomerName { get; set; }
}
...
Debug.Assert(item.Customer != null);
Debug.Assert(item.Customer.CustomerName == "n");
I do this generically in my repo, works good for my use case. I thought I'd share. Maybe someone will extend this further.
Some drawbacks are:
This assumes your foreign key properties are the name of your child object + "Id", e.g. UnitId.
I have it only mapping 1 child object to the parent.
The code:
public IEnumerable<TParent> GetParentChild<TParent, TChild>()
{
var sql = string.Format(#"select * from {0} p
inner join {1} c on p.{1}Id = c.Id",
typeof(TParent).Name, typeof(TChild).Name);
Debug.WriteLine(sql);
var data = _con.Query<TParent, TChild, TParent>(
sql,
(p, c) =>
{
p.GetType().GetProperty(typeof (TChild).Name).SetValue(p, c);
return p;
},
splitOn: typeof(TChild).Name + "Id");
return data;
}
I would like to note a very important aspect: the property name within the Entity must match the select statement. Another aspect of splitOn is how it looks for Id by default, so you don't have to specify it unless your naming is something like CustomerId, instead of Id. Let's look at these 2 approaches:
Approach 1
Entity Customer : Id Name
Your query should be something like:
SELECT c.Id as nameof{Customer.Id}, c.Foo As nameof{Customer.Name}.
Then your mapping understands the relationship between the Entity and the table.
Approach 2
Entity Customer: CustomerId, FancyName
Select c.Id as nameof{Customer.CustomerId}, c.WeirdAssName As nameof{Customer.FancyName}
and at the end of the mapping, you have to specify that the Id is the CustomerId by using the SplitOn.
I had an issue where I was not getting my values even though the mapping was correct technically because of a mismatch with the SQL statement.

getting two column values from same table depending on the condition

I have two tables:
table product1 with columns:
product_id
prodcut_name
category_id
another table categories
category_id
category_name
category_description
I am populating product details using DataGridView and it's working fine.
I am trying to get the two column values from same table depending on the same condition that I have got the code below:
string desc = Convert.ToString(selectedRow.Cells["productdescr"].Value);
string productname = Convert.ToString(selectedRow.Cells["productnam"].Value);
string productprices = Convert.ToString(selectedRow.Cells["productprice"].Value);
int productids = Convert.ToInt32(selectedRow.Cells["productid"].Value);
condition 1:
int categoryids = (from cats in tsg.product1
where cats.product_Name.Equals(productname)
select cats.category_Id).SingleOrDefault();
condition 2:
var catogynames = (from categorytypes in tsg.categories
where categorytypes.category_Id.Equals(categoryids)
select categorytypes.category_Name
).SingleOrDefault();
condition 3:
var categoprydecription = (from categorytable in tsg.categories
where categorytable.category_Id.Equals(categoryids)
select categorytable.category_Description
).SingleOrDefault();
I want to get the categorytypes.category_description also along with this categorytypes.category_Name from the condition 2, is it possible to combine the two conditions? (condition 2 and condition 3)
I think you can do this in this fashion
(from categorytable in tsg.categories
where categorytable.category_Id == categoryids
select new {Name=categorytable.category_Name,
Description=categorytable.category_Description}).SingleOrDefault();
This will be an anonymous class holding both name and description of the categories you want.

Categories