使用 LINQ 对 ID、EmailListID、PhoneListID 等进行分组

Using LINQ to group on ID, EmailListID, PhoneListID, etc

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:11: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

所以试试这个:

  • 为了简单起见,JobTitlesDepartmentsEmployeeStatus 可以在单个查询中完成。
  • 我假设外键列是 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 如何读取包含的DepartmentJobTitleModelEmployeeStatus 数据在第一个查询中。我假设 ReadAsync 有一些重载来指定其他包含的数据。

如果您发现自己重复执行这种逻辑,您可以定义自己的扩展方法来处理最糟糕的部分(例如 GroupBy().ToDictionary(),并从字典中填充集合 属性加载的实体)。


如果您有过滤条件,则需要将结果 EmployeeId 键值存储在 TVV 中,或者在 Employees 上重复该条件作为右侧PhonesEmails.

查询中的 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
}