使用 LINQ 对 ID、EmailListID、PhoneListID 等进行分组
Using LINQ to group on ID, EmailListID, PhoneListID, etc
- 我正在使用 Dapper 将我的数据库内容映射到
EmployeeModel
个对象。
- 属性的映射工作正常,但最后的分组仍然给我带来麻烦:
EmployeeModel
将 List<PhoneModel>
和 List<EmployeeModel>
作为属性。
- 项目根据
EmployeeID
分组,但是返回了多封电子邮件和 phone 结果,因为我无法找到这样做的语法。
- 我试过在
employeeList
中的 EmployeeIDs
在按 ID
分组之后、在按 ID
分组之前以及在 employeeList
中循环按 ID
. 分组
var sql = @"
SELECT
e.id,
e.FirstName, e.LastName, e.Nickname,
em.id as ID, em.Address, em.Type,
jt.id as ID, jt.Name,
e.id as ID, p.Number, p.Type,
d.id as ID, d.Name,
es.id as ID, es.Name
FROM
dbo.Employees e
LEFT JOIN dbo.Emails em ON em.EmployeeID = e.id
LEFT JOIN dbo.JobTitles jt ON e.JobTitleID = jt.id
LEFT JOIN Phones p ON p.EmployeeID = e.id
LEFT JOIN dbo.Departments d ON e.DepartmentID = d.id
LEFT JOIN dbo.EmployeeStatus es ON e.StatusID = es.id
";
IEnumerable<EmailModel> emailsGrouped = new List<EmailModel>();
var employees = await connection
.QueryAsync<
EmployeeModel,EmailModel,TitleModel,
PhoneModel,DepartmentModel,StatusModel,
EmployeeModel>
(
sql,
( e, em, t, p, d, s ) =>
{
e.EmailList.Add(em);
e.JobTitle = t;
e.PhoneList.Add(p);
e.Department = d;
e.Status = s;
return e;
},
splitOn: "ID, ID, ID, ID, ID"
);
foreach (EmployeeModel emod in employees)
{
emod.EmailList.GroupBy(em => em.ID);
}
var result = employees
.GroupBy(e => e.ID)
.Select(g =>
{
var groupedEmployee = g.First();
groupedEmployee.EmailList = g.Select(e => e.EmailList.Single()).ToList();
groupedEmployee.PhoneList = g.Select(e => e.PhoneList.Single()).ToList();
return groupedEmployee;
});
return result.ToList();
根据要求,这是我的电子邮件定义。它在我的 EmployeeClass 中,所以我已经发布了整个内容。
public class EmployeeModel
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Nickname { get; set; }
public DepartmentModel Department { get; set; }
public TitleModel JobTitle { get; set; }
public DateTime HireDate { get; set; }
public StatusModel Status { get; set; }
public List<EmailModel> EmailList { get; set; } = new List<EmailModel>();
public List<PhoneModel> PhoneList { get; set; } = new List<PhoneModel>();
public List<RestrictionModel> RestrictionsList { get; set; } = new List<RestrictionModel>();
public List<CitationModel> CitationsList { get; set; } = new List<CitationModel>();
public List<CertificationModel> CertificationList { get; set; } = new List<CertificationModel>();
public string ListView
{
get
{
return $"{LastName}, {FirstName}";
}
}
public string ToEmailString()
{
IEnumerable<string> employeeEmailStrings = EmailList.Select(emmod => emmod.ToString());
string employeeEmailString = string.Join($"{Environment.NewLine}", employeeEmailStrings);
return $"{FirstName}, {LastName}: {Environment.NewLine} -{JobTitle.Name}- {Environment.NewLine}";
}
//IEnumerable<string> phoneStrings = PhoneList.Select(plistmod => plistmod.ToString());
//string phoneString = string.Join($"{Environment.NewLine}", phoneStrings);
public string ToCertificationString()
{
IEnumerable<string> certificationStrings = CertificationList.Select(clistmod => clistmod.ToString());
string certificationString = string.Join($"{Environment.NewLine}", certificationStrings);
return certificationString;
}
public class EmailModel
{
public int ID { get; set; }
public string Address { get; set; }
public string Type { get; set; }
public override string ToString()
{
return $"{Address} ({Type})";
}
}
public class PhoneModel
{
public int ID { get; set; }
public string Number { get; set; }
public string Type { get; set; }
public override string ToString()
{
return $"{Number} ({Type})";
}
}
}
}
我现在正在尝试的是遍历 EmployeeModel
中的电子邮件以创建新的电子邮件列表,然后将该新列表设置为 EmployeeModel.List<EmailModel>
。
所以看起来你实际上是在尝试使用 SQL 从数据库中加载一个 object-graph(包含不同类型的节点) - 而你'重新尝试使用单个查询来做到这一点。
那不行。 (天真,单一查询)SQL 不适合 table 查询对象图。这就是 ORM 存在的原因。然而,使用某些特定于 RDBMS 的 SQL 扩展(例如 T-SQL、PL/SQL 等)来执行查询批处理,您 可以 return 来自数据库的对象图。
好消息是 Dapper 通过 QueryMultiple
支持这种情况 - 但据我所知它不会映射集合属性,因此您需要手动执行此操作(请继续阅读!)
(我注意到 Entity Framework,具体来说,将生成单个 SELECT
查询,这些查询 return 表示较低多重性数据的列中的冗余数据 - 这有其权衡但是 一般来说 单独的查询可以通过正确的调整总体上更快地工作(例如使用 table 值变量来保存 KEY
值而不是重新评估批处理中每个查询的相同 WHERE
标准 - 与往常一样,检查您的索引、STATISTICS
对象和执行计划!)。
查询对象图时,您将编写一个 SELECT
查询批处理,其中每个查询 returns all 个相同类型的对象具有 JOIN
与具有 1:1
或 1:0..1
多重性的任何其他实体(如果在同一批次的单独查询中加载它们不是更有效的话)。
在你的情况下,我看到你有:
[Employees]---(1:m)---[Phones]
[Employees]---(1:m)---[Emails]
[JobTitles]---(1:m)---[Employees]
[Departments]---(1:m)---[Employees]
[EmployeeStatus]---(1:m)---[Employees] // is this an enum table? if so, you can probably ditch it
所以试试这个:
- 为了简单起见,
JobTitles
、Departments
和 EmployeeStatus
可以在单个查询中完成。
- 我假设外键列是
NOT NULL
,所以应该使用 INNER JOIN
而不是 LEFT OUTER JOIN
。
const String EMPLOYEES_PHONES_EMAILS_SQL = @"
-- Query 1: Employees, Departments, EmployeeStatuses
SELECT
e.id,
e.FirstName,
e.LastName,
e.Nickname,
t.Name AS JobTitleName, -- This is to disambiguate column names. Never rely on column ordinals!
d.Name AS DepartmentName,
s.Name AS StatusName
FROM
dbo.Employees AS e
INNER JOIN dbo.JobTitles AS t ON e.JobTitleID = t.id
INNER JOIN dbo.Departments AS d ON e.DepartmentId = d.id
INNER JOIN dbo.EmployeeStatus AS s ON e.StatusID = s.id;
-- Query 2: Phones
SELECT
p.EmployeeId,
p.Number,
p.Type
FROM
dbo.Phones AS p;
-- Query 3: Emails
SELECT
m.id,
m.EmployeeId,
m.Address,
m.Type
FROM
dbo.Emails AS m;
";
using( SqlMapper.GridReader rdr = connection.QueryMultiple( EMPLOYEES_PHONES_EMAILS_SQL ) )
{
List<EmployeeModel> employees = ( await rdr.ReadAsync<EmployeeModel>() ).ToList();
var phonesByEmployeeId = ( await rdr.ReadAsync<PhoneModel> () ).GroupBy( p => p.EmployeeId ).Dictionary( grp => grp.Key grp => grp.ToList() );
var emailsByEmployeeId = ( await rdr.ReadAsync<EmailModel> () ).GroupBy( m => m.EmployeeId ).Dictionary( grp => grp.Key, grp => grp.ToList() );
foreach( EmployeeModel emp in employees )
{
if( phonesByEmployeeId.TryGetValue( emp.EmployeeId, out var phones ) )
{
emp.Phones.AddRange( phones );
}
if( emailsByEmployeeId.TryGetValue( emp.EmployeeId, out var emails ) )
{
emp.Emails.AddRange( emails );
}
}
}
我承认我对 Dapper 不是很熟悉 - 上面的代码有问题:它没有指示 Dapper 如何读取包含的Department
、JobTitleModel
和 EmployeeStatus
数据在第一个查询中。我假设 ReadAsync
有一些重载来指定其他包含的数据。
如果您发现自己重复执行这种逻辑,您可以定义自己的扩展方法来处理最糟糕的部分(例如 GroupBy().ToDictionary()
,并从字典中填充集合 属性加载的实体)。
如果您有过滤条件,则需要将结果 EmployeeId
键值存储在 TVV 中,或者在 Employees
上重复该条件作为右侧Phones
和 Emails
.
查询中的 INNER JOIN
例如,如果您想添加按姓名查找所有员工(以及他们的 phone-号码和电子邮件地址)的功能,您可以这样做:
const String EMPLOYEES_PHONES_EMAILS_SQL = @"
-- Query 0: Get EmployeeIds:
DECLARE @empIds TABLE ( EmpId int NOT NULL PRIMARY KEY );
INSERT INTO @empIds ( EmpId )
SELECT
EmployeeId
FROM
dbo.Employees
WHERE
FirstName LIKE @likeFirst
OR
LastName LIKE @likeLast;
-- Query 1: Employees, Departments, EmployeeStatuses
SELECT
e.id,
e.FirstName,
e.LastName,
e.Nickname,
t.Name AS JobTitleName, -- This is to disambiguate column names. Never rely on column ordinals!
d.Name AS DepartmentName,
s.Name AS StatusName
FROM
dbo.Employees AS e
INNER JOIN dbo.JobTitles AS t ON e.JobTitleID = t.id
INNER JOIN dbo.Departments AS d ON e.DepartmentId = d.id
INNER JOIN dbo.EmployeeStatus AS s ON e.StatusID = s.id
INNER JOIN @empIds AS i ON i.EmpId = e.EmployeeId;
-- Query 2: Phones
SELECT
p.EmployeeId,
p.Number,
p.Type
FROM
dbo.Phones AS p
INNER JOIN @empIds AS i ON i.EmpId = p.EmployeeId;
-- Query 3: Emails
SELECT
m.id,
m.EmployeeId,
m.Address,
m.Type
FROM
dbo.Emails AS m
INNER JOIN @empIds AS i ON i.EmpId = m.EmployeeId;
";
using( SqlMapper.GridReader rdr = connection.QueryMultiple( EMPLOYEES_PHONES_EMAILS_SQL, new { likeFirst = "%john%", likeLast = "%smith%" } ) )
{
// same as before
}
- 我正在使用 Dapper 将我的数据库内容映射到
EmployeeModel
个对象。 - 属性的映射工作正常,但最后的分组仍然给我带来麻烦:
EmployeeModel
将List<PhoneModel>
和List<EmployeeModel>
作为属性。- 项目根据
EmployeeID
分组,但是返回了多封电子邮件和 phone 结果,因为我无法找到这样做的语法。 - 我试过在
employeeList
中的EmployeeIDs
在按ID
分组之后、在按ID
分组之前以及在employeeList
中循环按ID
. 分组
var sql = @"
SELECT
e.id,
e.FirstName, e.LastName, e.Nickname,
em.id as ID, em.Address, em.Type,
jt.id as ID, jt.Name,
e.id as ID, p.Number, p.Type,
d.id as ID, d.Name,
es.id as ID, es.Name
FROM
dbo.Employees e
LEFT JOIN dbo.Emails em ON em.EmployeeID = e.id
LEFT JOIN dbo.JobTitles jt ON e.JobTitleID = jt.id
LEFT JOIN Phones p ON p.EmployeeID = e.id
LEFT JOIN dbo.Departments d ON e.DepartmentID = d.id
LEFT JOIN dbo.EmployeeStatus es ON e.StatusID = es.id
";
IEnumerable<EmailModel> emailsGrouped = new List<EmailModel>();
var employees = await connection
.QueryAsync<
EmployeeModel,EmailModel,TitleModel,
PhoneModel,DepartmentModel,StatusModel,
EmployeeModel>
(
sql,
( e, em, t, p, d, s ) =>
{
e.EmailList.Add(em);
e.JobTitle = t;
e.PhoneList.Add(p);
e.Department = d;
e.Status = s;
return e;
},
splitOn: "ID, ID, ID, ID, ID"
);
foreach (EmployeeModel emod in employees)
{
emod.EmailList.GroupBy(em => em.ID);
}
var result = employees
.GroupBy(e => e.ID)
.Select(g =>
{
var groupedEmployee = g.First();
groupedEmployee.EmailList = g.Select(e => e.EmailList.Single()).ToList();
groupedEmployee.PhoneList = g.Select(e => e.PhoneList.Single()).ToList();
return groupedEmployee;
});
return result.ToList();
根据要求,这是我的电子邮件定义。它在我的 EmployeeClass 中,所以我已经发布了整个内容。
public class EmployeeModel
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
public string Nickname { get; set; }
public DepartmentModel Department { get; set; }
public TitleModel JobTitle { get; set; }
public DateTime HireDate { get; set; }
public StatusModel Status { get; set; }
public List<EmailModel> EmailList { get; set; } = new List<EmailModel>();
public List<PhoneModel> PhoneList { get; set; } = new List<PhoneModel>();
public List<RestrictionModel> RestrictionsList { get; set; } = new List<RestrictionModel>();
public List<CitationModel> CitationsList { get; set; } = new List<CitationModel>();
public List<CertificationModel> CertificationList { get; set; } = new List<CertificationModel>();
public string ListView
{
get
{
return $"{LastName}, {FirstName}";
}
}
public string ToEmailString()
{
IEnumerable<string> employeeEmailStrings = EmailList.Select(emmod => emmod.ToString());
string employeeEmailString = string.Join($"{Environment.NewLine}", employeeEmailStrings);
return $"{FirstName}, {LastName}: {Environment.NewLine} -{JobTitle.Name}- {Environment.NewLine}";
}
//IEnumerable<string> phoneStrings = PhoneList.Select(plistmod => plistmod.ToString());
//string phoneString = string.Join($"{Environment.NewLine}", phoneStrings);
public string ToCertificationString()
{
IEnumerable<string> certificationStrings = CertificationList.Select(clistmod => clistmod.ToString());
string certificationString = string.Join($"{Environment.NewLine}", certificationStrings);
return certificationString;
}
public class EmailModel
{
public int ID { get; set; }
public string Address { get; set; }
public string Type { get; set; }
public override string ToString()
{
return $"{Address} ({Type})";
}
}
public class PhoneModel
{
public int ID { get; set; }
public string Number { get; set; }
public string Type { get; set; }
public override string ToString()
{
return $"{Number} ({Type})";
}
}
}
}
我现在正在尝试的是遍历 EmployeeModel
中的电子邮件以创建新的电子邮件列表,然后将该新列表设置为 EmployeeModel.List<EmailModel>
。
所以看起来你实际上是在尝试使用 SQL 从数据库中加载一个 object-graph(包含不同类型的节点) - 而你'重新尝试使用单个查询来做到这一点。
那不行。 (天真,单一查询)SQL 不适合 table 查询对象图。这就是 ORM 存在的原因。然而,使用某些特定于 RDBMS 的 SQL 扩展(例如 T-SQL、PL/SQL 等)来执行查询批处理,您 可以 return 来自数据库的对象图。
好消息是 Dapper 通过 QueryMultiple
支持这种情况 - 但据我所知它不会映射集合属性,因此您需要手动执行此操作(请继续阅读!)
(我注意到 Entity Framework,具体来说,将生成单个 SELECT
查询,这些查询 return 表示较低多重性数据的列中的冗余数据 - 这有其权衡但是 一般来说 单独的查询可以通过正确的调整总体上更快地工作(例如使用 table 值变量来保存 KEY
值而不是重新评估批处理中每个查询的相同 WHERE
标准 - 与往常一样,检查您的索引、STATISTICS
对象和执行计划!)。
查询对象图时,您将编写一个 SELECT
查询批处理,其中每个查询 returns all 个相同类型的对象具有 JOIN
与具有 1:1
或 1:0..1
多重性的任何其他实体(如果在同一批次的单独查询中加载它们不是更有效的话)。
在你的情况下,我看到你有:
[Employees]---(1:m)---[Phones]
[Employees]---(1:m)---[Emails]
[JobTitles]---(1:m)---[Employees]
[Departments]---(1:m)---[Employees]
[EmployeeStatus]---(1:m)---[Employees] // is this an enum table? if so, you can probably ditch it
所以试试这个:
- 为了简单起见,
JobTitles
、Departments
和EmployeeStatus
可以在单个查询中完成。 - 我假设外键列是
NOT NULL
,所以应该使用INNER JOIN
而不是LEFT OUTER JOIN
。
const String EMPLOYEES_PHONES_EMAILS_SQL = @"
-- Query 1: Employees, Departments, EmployeeStatuses
SELECT
e.id,
e.FirstName,
e.LastName,
e.Nickname,
t.Name AS JobTitleName, -- This is to disambiguate column names. Never rely on column ordinals!
d.Name AS DepartmentName,
s.Name AS StatusName
FROM
dbo.Employees AS e
INNER JOIN dbo.JobTitles AS t ON e.JobTitleID = t.id
INNER JOIN dbo.Departments AS d ON e.DepartmentId = d.id
INNER JOIN dbo.EmployeeStatus AS s ON e.StatusID = s.id;
-- Query 2: Phones
SELECT
p.EmployeeId,
p.Number,
p.Type
FROM
dbo.Phones AS p;
-- Query 3: Emails
SELECT
m.id,
m.EmployeeId,
m.Address,
m.Type
FROM
dbo.Emails AS m;
";
using( SqlMapper.GridReader rdr = connection.QueryMultiple( EMPLOYEES_PHONES_EMAILS_SQL ) )
{
List<EmployeeModel> employees = ( await rdr.ReadAsync<EmployeeModel>() ).ToList();
var phonesByEmployeeId = ( await rdr.ReadAsync<PhoneModel> () ).GroupBy( p => p.EmployeeId ).Dictionary( grp => grp.Key grp => grp.ToList() );
var emailsByEmployeeId = ( await rdr.ReadAsync<EmailModel> () ).GroupBy( m => m.EmployeeId ).Dictionary( grp => grp.Key, grp => grp.ToList() );
foreach( EmployeeModel emp in employees )
{
if( phonesByEmployeeId.TryGetValue( emp.EmployeeId, out var phones ) )
{
emp.Phones.AddRange( phones );
}
if( emailsByEmployeeId.TryGetValue( emp.EmployeeId, out var emails ) )
{
emp.Emails.AddRange( emails );
}
}
}
我承认我对 Dapper 不是很熟悉 - 上面的代码有问题:它没有指示 Dapper 如何读取包含的Department
、JobTitleModel
和 EmployeeStatus
数据在第一个查询中。我假设 ReadAsync
有一些重载来指定其他包含的数据。
如果您发现自己重复执行这种逻辑,您可以定义自己的扩展方法来处理最糟糕的部分(例如 GroupBy().ToDictionary()
,并从字典中填充集合 属性加载的实体)。
如果您有过滤条件,则需要将结果 EmployeeId
键值存储在 TVV 中,或者在 Employees
上重复该条件作为右侧Phones
和 Emails
.
INNER JOIN
例如,如果您想添加按姓名查找所有员工(以及他们的 phone-号码和电子邮件地址)的功能,您可以这样做:
const String EMPLOYEES_PHONES_EMAILS_SQL = @"
-- Query 0: Get EmployeeIds:
DECLARE @empIds TABLE ( EmpId int NOT NULL PRIMARY KEY );
INSERT INTO @empIds ( EmpId )
SELECT
EmployeeId
FROM
dbo.Employees
WHERE
FirstName LIKE @likeFirst
OR
LastName LIKE @likeLast;
-- Query 1: Employees, Departments, EmployeeStatuses
SELECT
e.id,
e.FirstName,
e.LastName,
e.Nickname,
t.Name AS JobTitleName, -- This is to disambiguate column names. Never rely on column ordinals!
d.Name AS DepartmentName,
s.Name AS StatusName
FROM
dbo.Employees AS e
INNER JOIN dbo.JobTitles AS t ON e.JobTitleID = t.id
INNER JOIN dbo.Departments AS d ON e.DepartmentId = d.id
INNER JOIN dbo.EmployeeStatus AS s ON e.StatusID = s.id
INNER JOIN @empIds AS i ON i.EmpId = e.EmployeeId;
-- Query 2: Phones
SELECT
p.EmployeeId,
p.Number,
p.Type
FROM
dbo.Phones AS p
INNER JOIN @empIds AS i ON i.EmpId = p.EmployeeId;
-- Query 3: Emails
SELECT
m.id,
m.EmployeeId,
m.Address,
m.Type
FROM
dbo.Emails AS m
INNER JOIN @empIds AS i ON i.EmpId = m.EmployeeId;
";
using( SqlMapper.GridReader rdr = connection.QueryMultiple( EMPLOYEES_PHONES_EMAILS_SQL, new { likeFirst = "%john%", likeLast = "%smith%" } ) )
{
// same as before
}