How to retrieve multiple values matching a single SQL id - c#

I'm trying to gather multiple "Class ID's" for a single "Teacher" using an SQL query inside a C# Web app. I've been able to successfully link the tables in the query but I'm only receiving one class taught per teacher, even when there are multiple classes taught by one teacher in the database.
Here's my code to generate the SQL query and post the information:
public Teacher FindTeacher(int TeacherId)
{
MySqlConnection Conn = School.AccessDatabase();
Conn.Open();
MySqlCommand cmd = Conn.CreateCommand();
cmd.CommandText = "Select * from Teachers, Classes where teachers.teacherid = " + TeacherId;
MySqlDataReader ResultSet = cmd.ExecuteReader();
Teacher SelectedTeacher = new Teacher();
while (ResultSet.Read())
{
int Id = Convert.ToInt32(ResultSet["teacherid"]);
string TeacherFName = ResultSet["teacherfname"].ToString();
string TeacherLName = ResultSet["teacherlname"].ToString();
string TaughtClassName = ResultSet["classname"].ToString();
string TaughtClassCode = ResultSet["classcode"].ToString();
SelectedTeacher.TeacherId = Id;
SelectedTeacher.TeacherFName = TeacherFName;
SelectedTeacher.TeacherLName = TeacherLName;
SelectedTeacher.TaughtClassCode = TaughtClassCode;
SelectedTeacher.TaughtClassName = TaughtClassName;
}
EDIT:
Thank you for the help so far, I'm quite new to this So I appreciate the assistance.
I've changed the syntax to an INNER JOIN but I'm still not getting the desired output; I want the output to be like this: "Mr Smith teaches Class A and Class B" where "Class A" and "Class B" are both fetched from the database.
Here's my updated code:
//Set up and define query for DB
MySqlCommand cmd = Conn.CreateCommand();
cmd.CommandText = "Select * from Teachers Join Classes on teachers.teacherid = classes.teacherid where teachers.teacherid=" + TeacherId;
//Collect query result in a variable
MySqlDataReader ResultSet = cmd.ExecuteReader();
//Create a variable in which to store the current teacher
Teacher SelectedTeacher = new Teacher();
//go through each row of the query result
while (ResultSet.Read())
{
int Id = Convert.ToInt32(ResultSet["teacherid"]);
string TeacherFName = ResultSet["teacherfname"].ToString();
string TeacherLName = ResultSet["teacherlname"].ToString();
string TaughtClassName = ResultSet["classname"].ToString();
string TaughtClassCode = ResultSet["classcode"].ToString();
SelectedTeacher.TeacherId = Id;
SelectedTeacher.TeacherFName = TeacherFName;
SelectedTeacher.TeacherLName = TeacherLName;
SelectedTeacher.TaughtClassCode = TaughtClassCode;
SelectedTeacher.TaughtClassName = TaughtClassName;
}

Let us suppose you have two teachers:
1, John
2, Jane
Let us suppose you have 3 classes. John teaches Math+English, Jane teaches French
ClassId, ClassName, TeacherId
101, Math, 1 <-- john
102, English, 1 <-- john
103, French, 2 <-- jane
Your query here:
Select * from Teachers, Classes where teachers.teacherid = 1
Conceptually does this first:
1, John, 101, Math, 1
1, John, 102, English, 1
1, John, 103, French, 2
2, Jane, 101, Math, 1
2, Jane, 102, English, 1
2, Jane, 103, French, 2
Then it filters according to what you've asked for, teacher id 1 in the teachers table:
1, John, 101, Math, 1
1, John, 102, English, 1
1, John, 103, French, 2
^
filter operated on this column
Doing FROM a,b establishes no relation between the data sets at all.. it just combines everything from A with everything from B such that you get AxB number of rows out (6, in the case of 2 teachers and 3 classes)
You need to establish correlation between the datasets. That looks like this:
SELECT *
FROM
teachers t
INNER JOIN classes c ON t.teacherid = c.teacherid
when the classes table has a TeacherId column that defines which teacher teaches the class. There are other kinds of joins that allow for e.g. teachers that don't teach any class, but start simple for now, and always follow this pattern when writing an SQL (a INNER JOIN b ON column_from_a = column_from_b) - never again write FROM a, b; it has been unnecessary for about 30 years. If you're reading a tutorial that instructs you to write this way, throw it out and get a better one
The result from this query is:
1, John, 101, Math, 1
1, John, 102, English, 1
Now that your join is fixed, let's examine the logic of what you're doing in your C#:
while (ResultSet.Read())
{
int Id = Convert.ToInt32(ResultSet["teacherid"]);
string TeacherFName = ResultSet["teacherfname"].ToString();
string TeacherLName = ResultSet["teacherlname"].ToString();
string TaughtClassName = ResultSet["classname"].ToString();
string TaughtClassCode = ResultSet["classcode"].ToString();
SelectedTeacher.TeacherId = Id;
SelectedTeacher.TeacherFName = TeacherFName;
SelectedTeacher.TeacherLName = TeacherLName;
SelectedTeacher.TaughtClassCode = TaughtClassCode;
SelectedTeacher.TaughtClassName = TaughtClassName;
}
SelectedTeacher is just a single object. There's no way it can hold more than one value. Every time the loop goes round you just overwite the existing teacher data with the next teacher. You need a collection of teachers as your results are going to have repeated teacher data in them:
List<Teacher> selectedTeachers = new List<Teacher>();
while (ResultSet.Read())
{
int id = Convert.ToInt32(ResultSet["teacherid"]);
string teacherFName = ResultSet["teacherfname"].ToString();
string teacherLName = ResultSet["teacherlname"].ToString();
string taughtClassName = ResultSet["classname"].ToString();
string taughtClassCode = ResultSet["classcode"].ToString();
Teacher selectedTeacher = new Teacher();
selectedTeacher.TeacherId = id;
selectedTeacher.TeacherFName = teacherFName;
selectedTeacher.TeacherLName = teacherLName;
selectedTeacher.TaughtClassCode = taughtClassCode;
selectedTeacher.TaughtClassName = taughtClassName;
selectedTeachers.Add(selectedTeacher);
}
But wait.. We're still not done..
Because you specified a teacherID = 1, the teacher in the results (look at the example data I put for John above) is actually the same teacher repeated over and over, with a different class associated, so building this code like this isn't that useful perhaps..
There are multiple ways to fix this. One would be to run two separate queries. The other would be to use some fast lookup device, like a Dictionary, to keep track of if we've seen a particular Teacher before, and retrieve the one we already have rather than adding another copy of the same teacher details
I don't know how in-scope either of thse things are, but I'll leave it for you to ponder on with some discussion below.
Perhaps your classes should look like:
class Teacher{
string Name {get; set;}
List<TaughtClass> Classes { get; set; } = new();
}
class TaughtClass{
string Name {get; set;}
}
And your code could first select * from teachers where id = 1 and then it could later select * from classes where teacherid = 1, and fill up the TaughtClasses list on the one Teacher..
Even that approach is a bit awkward, when someone says "what if we have two teachers.. e.g. select * from teachers where name like 'j%' - it will return both john and jane..
That you can handle by pulling the results into a list of teacher - so you have 2 Teacher objects in your list, and you can then do a loop over the list to look up their Classes, something like (pseudocode):
foreach(var t in selectedTeachers){
cmd.CommandText = "SELECT * FROM TaughtClasses WHERE teacherId = #tid";
cmd.Parameters.Clear();
cmd.Parameters.AddWithValue("#tid", t.TeacherId); //NB: addwithvalue is safe to use in mysql
var reader = cmd.ExecuteQuery();
//now do a loop here that loops over the reader, makes TaughtClass objects and fills up the t.Classes collection
}
You might regard that "run a query for the classes per teacher id" as inefficient (it's not actually that bad) and want to get all the relevant TaughtClass data in one hit. That's doable too; consider:
SELECT c.*
FROM
teachers t
INNER JOIN classes c ON t.teacherid = c.teacherid
WHERE
t.name like `j%'
After running a query for all teachers with a name like J% we can rerun the query above to get all of John's and Jane's classes in one hit. This will use a join between teachers and classes (because we're giving a teacher name but we want classes data out), and select only c.* which is classes data.
You'll need to use the arriving Teacher Id in the classes data, to sort out which local Teacher object gets which TaughtClasses assigned to their list. That will basically involve searching your local list of Teachers you've already made, and finding the right one, then adding a TaughtClass to their list
If this two queries idea is something disallowed (academic exercise?) take a look at a Dictionary<int, Teacher>
var d = new Dictionary<int, Teacher>();
As you're looping over the teacher-classes joined query you can pull the teacher id and see if you've seen it before:
int id = Convert.ToInt32(ResultSet["teacherid"]);
if(!d.TryGetValue(id, out Teacher t)){
//teacher is not found locally in the dictionary, create a new teacher and add them
string f = ResultSet["teacherfname"].ToString();
string l = ResultSet["teacherlname"].ToString();
d[id] = t = new Teacher(){ Id = id, FName = f, LName l };
}
//at this point t is either the new teacher, or one you added previously
//you can now make a new TaughtClass and add it to t.Classes
Which is a better approach? Wel.. Tough question, no good answer there. The multiple queries approach might return unrelated data (what if someone added another teacher called Jim in between the time you downloaded John and Jane, and then started selecting their classes, based on that LIKE 'j%' that fetched the classes? If you go for one big block of data query then you don't get that problem, but you do download loads of repeated data. If you use a database transaction to make your queries consistent you use a lot of server resources..
..there just isn't one silver bullet, you pick the one that is the best of a so-so bunch. They'll all be fine in this exercise I think :D

Related

c# LINQ to object Autoincrement in subquery

Good Afternoon! Could anyone suggest how I can implement autoincrement in linq to object subquery which must be started from 1 on every group of main linq query? I have query
from student in students
group student by student
into keyStudents
select new StudentClass
{
Student = keyStudent.Key,
Subjects = from key in keyStudent
select new SubjectClass
{
Subject = key.Subject,
SubjectId = (stuck here)
}
}
My result must be like this:
Student
John
1. Math
2. Bio
Fred
1. Math
2. Physics
3. Bio
My query returns accurate data without SubjectId, but I need to show SubjectId as well. So the SubjectId is just index number started with one in every group.
You'd have to use method syntax so you can use the override of Select that includes an index
from student in students
group student by student
into keyStudents
select new StudentClass
{
Student = keyStudent.Key,
Subjects = keyStudent.Select((x, i) =>
new SubjectClass
{
Subject = x.Subject,
SubjectId = i + 1,
}),
}

Get the value of a field if it exists in linq c# asp.net

I've a Many to Many relation between a student (etudiant) and a course (cours) table which is called studentCours in SQL but not use as a model in Entity Framework (i've taken the habit of it now, even if i think its weird).
I need to show the result of the student (which is on another table etuResult) but the student may not have any result (nullable int) yet as the year is still in progress !
I'm able to get the value of studentCours and show them and also of studentResult, but i'm not able to show the concatenation of both (and leave the result empty or with other value than a number between 1 and 20).
On the following picture you can see :
On the left : the student with the id 3 has 2 cours with the id 1 and 10 (i dont know why the course_id 1 appears twice...) => These are the course he's following.
On the right : The student with the id 3 has one result for the course_id 10 and has the result 10/20
Here are my two queries to get the data :
var etuCours = from c in bdd.cours
where c.etudiant.Any(e => e.etudiant_id == id)
select new resultCours { cours_id = c.cours_id, libelle = c.libelle, code = c.code, annee = c.annee };
IEnumerable<resultCours> etuResult = from er in bdd.etuResult
where er.etudiant_id == id
select new resultCours { cours_id = er.cours_id, cote = er.cote };

How to write SQL query where one column returns multiple results C#

I have two tables:
users:
ID Name
----------------
1 Bob
2 Dave
3 Mike
Books:
ID UserId BookName
------------------------
1 1 Cat in Hat
2 1 Happy Birthday
3 1 One Fish
4 2 Goldilocks
5 2 Three Crows
6 3 Hitchhikers
UserId is a key showing which books each user owns
In my html I want to create output that looks like this:
Name Books Owned
-------------------------
Bob Cat in Hat, Happy Birthday, One Fish
Dave Goldilocks, Three Crows
Mike Hitchhikers
Currently I am doing this with a loop and nested query:
var queryusers = db.Query("SELECT * FROM users");
foreach (var user in queryusers) {
<span>#user.name</span>
var querybooks = db.Query("SELECT * FROM books WHERE userid = #0", user.id);
foreach (book in querybooks ) {
<span>#book.bookname</span>
}
}
I know that's not good, but I don't know how to do this with JOIN. If I try:
var queryusersandbooks = db.Query("SELECT * FROM users INNER JOIN books ON users.id = books.userid");
My query returns 6 rows, but I only want 3. Is there a way to do this without having a SQL query in the loop?
Thanks
You are looking for GROUP_CONCAT. Try something like this:
SELECT name AS Name, GROUP_CONCAT(books.bookname SEPARATOR ', ') AS Books
FROM books INNER JOIN users ON users.id=books.userid
GROUP BY books.userid;
Keep in mind that GROUP-CONCAT has a 1024 character limit, so if you need more space, set a bigger threshold before executing the SQL, like this:
SET SESSION group_concat_max_len = 1000000;
SELECT name AS Name, GROUP_CONCAT(books.bookname SEPARATOR ', ') AS Books
FROM books INNER JOIN users ON users.id=books.userid
GROUP BY books.userid;
Here is a working fiddle.
Try something like this
SELECT u.name,CONCAT_WS(',',b.bookname) AS user_books FROM users u INNER JOIN books b ON u.id = b.userid GROUP BY u.id
If I were going to do something like this in mysql, I'd group by user and use group_concat to combine the results:
select users.id, users.name, group_concat(bookname) as books
from users inner join books on books.userid = user.id
group by users.id, users.name
You can order and specify the separator if you like, read about group_concat for more details. This breaks down quickly if there might be a lot of books, or if you want each book to be its own column or anything like that, but it's fine in simple cases.
You are on the right track. In your original code the inner foreach would have executed 6 times as well, once for each of the books.
Using the code below (basing off your own code), the query would still return 6 rows, as you have found, but only prints the user name once.
var queryusersandbooks = db.Query("SELECT * FROM users INNER JOIN books ON users.id = books.userid ORDER BY users.name");
int curruser = 0;
foreach (var userandbook in queryusersandbooks)
{
if (userandbook.userid != curruser)
{
<span>#userandbook.name</span>
curruser = userandbook.userid;
}
<span>#userandbook.bookname</span>
}

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.

How to get data from two SQL tables into .net

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.

Categories