Entity Framework 核心包括然后取 2

Entity Framework Core Include then Take 2

出于学习目的,我使用 EFCore 3.1 制作了一个控制台应用程序

我有这样的关系:

Person belongs To one Type

Person have many Queries

Query belongs to one Person

Query belongs to a Location

此控制台应用程序已连接到一个 Mysql 数据库脚手架到 实体 与 Pomelo。

现在我需要从数据库中获取每个 Persons 然后是他的相关 Type 和 Last Queries 按日期排序,每个人都有数百个查询,这个数据库上有数千人,所以如果我尝试

using (var ctx = new DBContext) 
{
    var persons = ctx.Persons
                  .Include(p => p.Type)
                  .Include(p => p.Queries).ThenInclude(q => q.Location)
}

它会超时,因为他正在尝试获取每个人的每个查询,就像它应该的那样,因为我在另一个答案的某处读到,包含它是全有或全无。

在我的数据库中,我有一个用于此目的的双链接视图,一个用于为每个 person_id 获取 max_query_id (person_maxquery),一个用于连接这 4 个表

SELECT p.*, ty.*, pq.*, loc.* FROM persons p
LEFT JOIN person_maxquery max ON p.id = max.person_id
LEFT JOIN person_queries pq ON pq.id = max.query_id
LEFT JOIN locations loc ON loc.id = pq.location_id
LEFT JOIN types ty ON ty.id = p.type_id

我想在 EF 中实现相同的功能,但我不知道这是否可行,我可以过滤从 DB 获取的数据吗?我应该 "make" 我的对象使用视图手动将每个值分配给相关模型值吗?

提取数据集的查询很少需要适用实体的全部详细信息。您可以通过将实体投影到反映您真正需要的数据的视图模型结构来缓解这种情况并过滤数据。

using (var ctx = new DBContext) 
{
    var persons = ctx.Persons
        .Select(x => new PersonViewModel
        {
           PersonId = x.PersonId,
           Name = x.Name,
           TypeName = x.Type.Name,
           RecentQueries = x.Queries
               .OrderByDecending(q => q.QueryDate)
               .Select(q => new QueryViewModel
               {
                  QueryId = q.QueryId,
                  Name = q.Name,
                  LocationName = q.Location.Name
                  // ... etc.
               }).Take(2).ToList()
        }).ToList();
    //...
}

这种方法有助于仅使用您需要的数据构建更高效的查询。拉取完整的实体图更像是一个按项目的请求,例如在对一个人及其相关实体应用更新时,而不是试图拉回所有人及其相关实体的全部或一部分。

编辑:服务器和视图之间某些数据的生命周期示例:

第 1 部分...列出一群人。这些是摘要,足以满足摘要列表视图的数据。

public IEnumerable<PersonSummaryViewModel> GetPeople(/*criteria*/)
{
    using (var ctx = new DBContext) 
    {
        var people = ctx.Persons
            .Where( /* based on criteria */ )
            .Select(x => new PersonSummaryViewModel
            {
               PersonId = x.PersonId,
               Name = x.Name,
               TypeName = x.Type.Name,
               RecentQueries = x.Queries
                   .OrderByDecending(q => q.QueryDate)
                   .Select(q => new QuerySummaryViewModel
                   {
                      QueryId = q.QueryId,
                      Name = q.Name,
                      LocationName = q.Location.Name
                      // ... etc.
                   }).Take(2).ToList()
            }).ToList();
        //...
        return people;
    }
}

第 2 部分:获取人物详细模型。一旦用户 select 成为一个人,我们就会想要提取更多数据。这个视图模型可以有更多的字段和关系来满足视图。但即使在这里,我们也可能希望排除不常查看的数据(例如在可扩展区域或选项卡中),这些数据可以根据用户需要 Ajax 调用 if/when 按需拉回。

public PersonDetailViewModel> GetPerson(int personId)
{
    using (var ctx = new DBContext) 
    {
        var person = ctx.Persons
            .Select(x => new PersonDetailViewModel
            {
               PersonId = x.PersonId,
               Name = x.Name,
               TypeName = x.Type.Name,
               // ... Can load all visible properties and initial, important related data...
            }).Single(x => x.PersonId == personId);
        //...
        return person;
    }
}

第 3 部分:示例,更新人物

public void UpdatePerson(UpdatePersonViewModel updatePersonModel)
{
    if (updatePersonModel == null)
       throw new ArgumentNullException("updatePersionModel");

    // TODO: Validate updateModel, ensure everything is Ok, not tampered/invalid.
    using (var ctx = new DBContext) 
    {
        var person = ctx.Persons
            // can .Include() any related entities that can be updated here...
            .Where( x => x.PersonId == updatePersonModel.PersonId )
            .Single();
        person.Name = updatePersonModel.Name;
        // etc.
        ctx.SaveChanges();
    }
}

第 4 部分:示例,添加查询。

public void AddQuery(int personId, AddQueryViewModel queryModel)
{
    if (queryModel == null)
       throw new ArgumentNullException("queryModel");

    // TODO: Validate queryModel, ensure everything is Ok, not tampered/invalid.
    using (var ctx = new DBContext) 
    {
        var person = ctx.Persons
            .Include( x => x.Queries )
            .Where( x => x.PersonId == personId )
            .Single();

        // TODO: Maybe check for duplicate query already on Person, etc.
        var query = new Query
        {
            // copy data from view model
        };
        person.Queries.Add(query);
        ctx.SaveChanges();
    }
}

视图模型(或 DTO)仅涉及在服务器和客户端之间传输数据。当我们将数据返回到服务器并想要更新实体时,我们加载这些实体。通过使用这些视图模型,我们减少了客户端和服务器之间发送的数据量(只是需要的字段,而不是整个实体图),这意味着更快的代码和更少的数据传输。我们不会暴露比客户需要知道的更多的实体结构,我们不会冒险简单地用 Attach + EntityState.Modified + SaveChanges 之类的东西覆盖返回的数据,其中数据可能是陈旧的(自从该副本被获取后,其他人进行了修改)或者返回的数据可能被篡改了。视图模型是单一用途的。我们可以在 UpdatePerson 类型操作中使用 PersonDetailViewModel,但我们的更新可能只适用于 select 少数属性,因此存在将所有内容发送回服务器的开销,并且没有暗示限制什么应该,什么不应该允许更新。 (特别是如果您使用 Automapper 等来帮助在实体和视图模型之间来回复制字段)

在我们只想与一些实体数据交互而不将其发送回客户端的情况下,我们不需要视图 models/DTOs,我们可以 select 主动检索字段使用匿名类型的整个实体图。例如,如果我只想检查一个人是否具有特定的匹配条件并总结匹配的查询名称:

var queryNames = ctx.Persons
    .Where(x => x.PersonId == personId)
    .SelectMany(x => Queries.Select( q => new  
    {
       q.QueryId,
       q.Name
    }).ToList();
var message = string.Join(", ", 
    queryNames.Select(x => string.Format("{0} ({1})", x.Name, x.QueryId)));

就像一个简单的例子,匿名类型 return 一个可以被更多代码使用的结构,而不需要视图模型/DTO。我们不会传回实体数据,而可能只是检查值以确定操作过程,或者编写类似消息字符串的内容。