将Linq查询结果映射到DTO类

9
我希望使用EF从数据库中获取记录并将值分配给DTO类。考虑以下用于Linq查询的表。

TableA,TableB,TableC

对于每个TableA记录,在TableB中有多个记录。对于每个TableB记录,在TableC中有多个记录。 现在我的DTOs看起来像这样:

public class TableA_DTO
{
    public int tableA_rowid { get; set; }
    //remaining tableA field definitions

    public List<TableB_DTO> TableB_records { get; set; }
}

public class TableB_DTO
{
    public int tableB_rowid { get; set; }
    //remaining tableB  field definitions

    public List<TableC_DTO> TableC_records { get; set; }
}

public class TableC_DTO
{
    public int tableC_rowid { get; set; }
    //remaining tableC field definitions
}

我的LINQ查询大致如下

var qry = from ent in TableA
          select ent;

在我的映射类中,我会通过以下方式循环遍历查询结果中的项目:

    foreach (var dataitem in query)
    {
        TableA_DTO dto = new TableA_DTO();
        dto.tableA_rowid =  dataitem.ID;
        //remaining field definitions here
    }

现在这个方法适用于TableA表格中的所有字段,它从数据库中输出一个记录,并为TableA表格中的每个字段设置所需的属性。我还想在名为TableB_records的TableA属性字段中填充TableB中的所有匹配记录,以及在TableC中与TableB_DTO的属性名称为TableC_records的所有匹配记录。
这可以实现吗?我需要更改什么?是linq查询还是我的映射方式?
感谢您的时间...

3
你是否有任何理由不能使用实体框架POCO(也称为DbContext,有时被错误地称为Code First)?基本上,你能否消除需要使用DTO并改用EF POCO? - JMarsch
1
你是否考虑过使用AutoMapper?根据你的DTO有多不同,这可能只需要两三行代码就可以完成映射。 - Robaticus
@jMarsch:数据库已经存在,所以我们采用了edmx的方式。 - user20358
@Robaticus:我是否仍需要更改linq查询以首先获取数据,以便automapper可以使用它?从我的概述中看不像automapper可以做到这一点...如果您说它可以...谢谢 :) - user20358
@user20358 但是你使用了POCO吗?我说“先编码”,因为很多人都这么叫它,但你也可以选择edmx路线,仍然使用poco/dbcontext而不是对象上下文(只需将不同的T4钩接到模型图中)。这样就可以得到轻量级的POCO,但仍然是以模型或数据库为先。 - JMarsch
显示剩余2条评论
4个回答

6

我建议将您的DTO从List更改为IEnumerable,然后在LINQ查询中完成所有操作。

var query = 
    from ent in TableA
    select new TableA_DTO
    {
        TableAProperty = a.Property,
        TableB_records = 
            from b in TableB
            where ent.Key == b.Key
            select new TableB_DTO
            {
                TableBProperty = b.Property,
                TableC_records =
                    from c in TableC
                    where b.Key == c.Key
                    select new TableC_DTO
                    {
                        TableCProperty = c.Property
                    }
            }
    };

然而,这种方法的问题在于它会触发比 N + 1 更多的查询;它会触发 M * (N + 1) + 1 次查询,几乎肯定会导致非常差的性能。 - Steven
是的,直到您开始迭代TableB_recordsTableV_records属性。仔细查看使用SQL分析器执行的单个查询。您会注意到它缺少有关TableBTableC的所有信息。 - Steven
3
经过对 EF 4.0 进行一些测试,我得出结论你是对的。抱歉。这很不错,因为根据我的经验,与 LINQ to SQL 相比,EF 4 仍然很糟糕。但这是 LINQ to SQL 明显做不到的(它会执行 N + 1 次查询)。 - Steven

4

首先,我需要问一下您是否能够使用Entity Framework 4.1和POCO(DbContext),从而完全避免使用DTO?

假设答案是否定的,那一定是因为您没有检索所有字段,或者您以某种方式修改了数据的“形状”。

在这种情况下,您可以将LINQ查询更改为以下内容:

from t in table
where ...
select new DTOA()
{
  TheDtoProperty = theTableProperty,
  AndSoOn = AndSoOn
};

这种方式的好处在于:如果您打开SQL Profiler,您应该会看到只有您请求的列才会进入实际的SQL查询。如果您首先查询所有内容,然后再提取值,那么所有列都将通过网络传输。

0
我会创建一个工厂方法,例如:TableA_DTO CreateDTO(TableAItem item); 使用这个方法,你可以将查询重写为:
IEnumerable<TableA_DTO> = TableA.AsEnumerable().Select(CreateDTO);

这将直接为您提供“DTO”对象的集合。

话虽如此,如果您正在使用Entity Framework,则最近版本中添加的EF Code First可能更有用。


CreateDTO是什么?它是一个类吗?它的定义是什么? - user20358
@Reed:在 IQueryable<T> 上调用 AsEnumerable() 将确保您从数据库中拉取所有行。除非表格少于一千行(以及它们的所有数据),或者除非您想要获取所有记录,否则这对性能来说将是非常糟糕的。 - Steven
@Steven 要将它们转换为本地类(即DTO),必须先将它们拉下来。如果需要过滤,则应在AsEnumerable()调用之前进行,但是转换为DTO需要首先将其设为本地。 - Reed Copsey
@Reed:呵呵呵……你在这里试图太聪明了,Reed;)。我们的工作是给予OP好的建议。你的建议会直接将他踢进失败的深渊 :-)。 - Steven
@Steven 我不同意你的看法。在这种情况下,OP正在询问如何构建DTO - 根据定义,DTO只应用于通过网络传输的项目。在这里,问题暗示这些项目将被请求。 - Reed Copsey
显示剩余2条评论

0

更新

正如其他人指出的那样,当使用Entity Framework 4.0时,不需要将结果展平(如下所示),因为它可以将LINQ查询转换为高效的展平结果。因此,只有在使用LINQ to SQL(或可能是其他LINQ提供程序)时才需要以下代码。请注意,我仅在SQL Server上使用EF进行了测试,而没有在Oracle上进行测试,因为这种行为可能是LINQ提供程序特定的,这意味着Oracle提供程序(仍处于beta版)或用于Oracle的商业Devart提供程序仍然可能会执行N + 1。


你想要做的是获取一组像树形结构一样的对象。如果没有特殊处理,你将会触发许多数据库查询。如果有一层嵌套,你将会触发N + 1个查询,但由于你的嵌套深度为两层,你将会触发M x (N + 1) + 1个查询,这几乎肯定会对性能产生非常不利的影响(无论数据集的大小如何)。你需要确保只发送一个查询到数据库。为了确保这一点,你必须创建一个中间查询来展开结果,就像在早期的SQL时代检索树形数据一样 :-)请看下面的例子:

var records =
    from record in db.TableC
    where ... // any filtering can be done here
    select record;

// important to call ToArray. This ensures that the flatterned result
// is pulled in one single SQL query.
var results = (
    from c in records
    select new
    {
        tableA_rowid = c.B.A.Id,
        tableA_Prop1 = c.B.A.Property1,
        tableA_Prop2 = c.B.A.Property2,
        tableA_PropN = c.B.A.PropertyN,
        tableB_rowid = c.B.Id,
        tableB_Property1 = c.B.Property1,
        tableB_Property2 = c.B.Property2,
        tableB_PropertyN = c.B.PropertyN,
        tableC_rowid = c.Id,
        tableC_Property1 = c.Property1,
        tableC_Property2 = c.Property2,
        tableC_PropertyN = c.PropertyN,
    })
    .ToArray();

下一步是将使用匿名类型的内存数据结构转换为DTO对象的树形结构:
// translate the results to DTO tree structure
TableA_DTO[] dtos = (
    from aresult in results
    group aresult by aresult.tableA_rowid into group_a
    let a = group_a.First()
    select new TableA_DTO
    {
        tableA_rowid = a.tableA_rowid,
        tableA_Prop1 = a.tableA_Prop1,
        tableA_Prop2 = a.tableA_Prop2,
        TableB_records = (
            from bresult in group_a
            group bresult by bresult.tableB_rowid into group_b
            let b = group_b.First()
            select new TableB_DTO
            {
                tableB_rowid = b.tableB_rowid,
                tableB_Prop1 = b.tableB_Prop1,
                tableB_Prop2 = b.tableB_Prop2,
                TableC_records = (
                    from c in group_b
                    select new TableC_DTO
                    {
                        tableC_rowid = c.tableC_rowid,
                        tableC_Prop1 = c.tableC_Prop1,
                        tableC_Prop2 = c.tableC_Prop2,
                    }).ToList(),
            }).ToList()
     })
    .ToArray();

正如您所看到的,解决方案的第一部分实际上是“旧”的方法,在那个时候我们仍然会手写SQL查询。但好的是,一旦我们获得了这个内存数据集,我们可以再次利用LINQ(对对象)将这些数据获取到我们想要的结构中。

请注意,这也允许您进行分页和排序。这可能会有点棘手,但肯定不是不可能的。


1
整个“展平”步骤是完全不必要的,因为Entity Framework会为您执行此操作。使用@Aducci的策略将导致单个数据库查询从SQL返回扁平行的结果,然后它将自动将这些值组合成分层结构。 - StriplingWarrior
@StriplingWarrior:经过一些测试,似乎你说得完全正确。实体框架在这里让我眼前一亮 :-) 终于找到了它比LINQ to SQL更出色的地方,因为LINQ to SQL确实会执行N + 1查询。这确实很酷。 - Steven
是的,看起来LINQ to SQL可以处理嵌套模式,但仅限于一级深度(TableA和TableB),如果超过这个深度,每个TableC中的项目都将需要单独的往返。 - StriplingWarrior

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接