自定义分页列表将 IQueryable 中的子对象设置为 null

Custom Paginated list setting child objects in IQueryable to null

在我的 ASP.NET 核心 Web 应用程序中,我使用 Entity Framework 核心从我的数据库获取 IQueryable<Task> 到我的控制器中的 result 变量。见最后的数据定义。

如果我检查 resultListTag 会按预期填充,并且其中的每个项目都已填充 User。所以,一切正常。

在将结果传递给视图 (.cshtml) 之前,我想将 IQueryable 转换为 PaginatedList(以便在我的索引页中使用寻呼机)。结果 PaginatedList 仍然填充了子列表 ListTag,但它的 UserNULL

为什么?

注意:在存储库中,我在从数据库中获取数据时同时使用 Include(x => x.ListTag)ThenInclude(u => u.User)

所有代码和数据定义(短):

获取分页列表:

indexModel.ListTasks = await PaginatedList<DetailModel>.CreateAsync(result.AsNoTracking(), pageNumber, pageSize);

PaginatedList class:

public class PaginatedList<T> : List<T>
{
    public int PageIndex { get; private set; }
    public int TotalPages { get; private set; }

    public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
    {
        PageIndex = pageIndex;
        TotalPages = (int)Math.Ceiling(count / (double)pageSize);

        this.AddRange(items);
    }

    public bool HasPreviousPage
    {
        get { return (PageIndex > 1); }
    }

    public bool HasNextPage
    {
        get { return (PageIndex < TotalPages); }
    }

    public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)
    {
        var count = await source.CountAsync();
        var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
        return new PaginatedList<T>(items, count, pageIndex, pageSize);
    }
}

Entity Framework 核心数据模型:

class Task
{
    public int IdTask 
    public string Subject

    public List<Tag> ListTag 
}

class Tag
{
    public int IdTag 
    public string Tag

    [ForeignKey("IdTask")]
    public Task Task{ get; set; }

    public int IdUser 
    [ForeignKey("IdUser")]
    public User User
}

class User
{
    public int IdUser 
    public string Name
}

索引和详细信息模型(用于 cshtml 视图):

class IndexModel
{
    public Helpers.PaginatedList<DetailModel> ListTasks
}

class DetailModel
{
    public int IdTask 
    public string Subject
    public List<Tag> ListTag 
}

编辑 1:已删除(不相关)

编辑 2

我找到了问题和解决方案(我认为)。我所做的是首先从数据库(存储库)中获取数据并在 DetailModel 变量中执行 select:

var result = _tasks.List(searchString);
    .Select(item => new DetailModel
    {
        IdTask = item.IdTask ,
        Subject = item.Subject,
        ListTag = item.ListTag,
    });

并在 PaginatedList 中传递 result 然后到我的视图。显然,用户在“反思”中迷失了方向:ListTag=item.ListTag.

如果我只是改变事物的顺序并首先从数据库中提取到一个临时变量中,将临时变量传递给 PaginatedList,然后 select 传递给本地 DetailModel 变量一切正常。用户已填充。

var temp = _tasks.List(searchString);
var temp2 = await PaginatedList<Task>.CreateAsync(temp.AsNoTracking(), pageNumber, pageSize);

var result = temp2
    .Select(item => new DetailModel
    {
        IdTask = item.IdTask ,
        Subject = item.Subject,
        ListTag = item.ListTag,
    });.ToList();

警告

除非您真的需要,否则不要使用 SkipTake。数据库服务器不知道有多少结果,所以无论如何它都必须创建一个与分页一起工作的执行计划。

根据实际值构造查询很容易,例如:

if (pageSize>1)
{
    source=source.Skip((pageIndex-1)*pageSize);
};
if (pageSize>0)
{
    source=source.Take(pageSize);
}

目前还不清楚需要什么样的分页(通过任务?标签?用户?所有这些?all of them 是什么意思?),但是 Skip().Take() 几乎肯定是不够的。

LINQ 中的

OrderBy().Skip().Take() 或 SQL 中的 ORDER BY .. OFFSET .. FETCH NEXT 适用于 SQL 生成的整个结果集。事实上,EF Core 3+ 生成的代码有点像这样:

SELECT [a_bunch_of_columns] 
  FROM dbo.Tasks LEFT JOIN dbo.Tags on .....
  ORDER BY [some_column_or_columns] 
  OFFSET @p0 ROWS
  FETCH NEXT @p1 ROWS ONLY;

parents 和 children 之间没有区别,因此一页可以包含任务标签的一半,下一页可以包含另一半。

仅分页任务

如果您想分页 任务 ,一种方法是检索 ID 的页面,然后根据 ID 检索完整的 objects。当检索到的列未被索引覆盖时,这可以 improve performance dramatically

您可以编写类似这样的代码来一次检索一页任务 ID,然后加载完整的 objects:

var pageIDs=await dbContext.Tasks
                           .Select(t=>t.IdTask)
                           .OrderBy(t=>t.IdTask)
                           .Skip((pageIndex - 1) * pageSize)
                           .Take(pageSize)
                           .ToListAsync();
var tasks=await dbContext.Tasks
                         .Where(t=>pageIds.Contains(t.IdTask))
                         .OrderBy(t=>t.IdTask)
                         .ToListAsync();

使用显式加载

另一种选择是分页任务,然后使用 Explicit loading 加载相关实体。不过,您必须在每个 Task 实例上调用 Load,这会导致 pagesize 查询:

var tasks=await dbContext.Tasks
                           .OrderBy(t=>t.IdTask)
                           .Skip((pageIndex - 1) * pageSize)
                           .Take(pageSize)
                           .ToListAsync();
foreach(var task in tasks)
{
    dbContext.Entry(tasks)
             .Collection(b => b.Tags)
             .Load();
}

这一段建议您可以使用单个查询,尽管我还没有找到示例:

You can also explicitly load a navigation property by executing a separate query that returns the related entities. If change tracking is enabled, then when query materializes an entity, EF Core will automatically set the navigation properties of the newly loaded entity to refer to any entities already loaded, and set the navigation properties of the already-loaded entities to refer to the newly loaded entity.

假设启用了更改跟踪,您可以使用以下代码加载和附加相关标签。不过我还没有尝试过,我不确定生成的查询的效率如何:

var tasks=await dbContext.Tasks
                           .OrderBy(t=>t.IdTask)
                           .Skip((pageIndex - 1) * pageSize)
                           .Take(pageSize)
                           .ToListAsync();
var ids=tasks.Select(t=>t.IdTask).ToList();
var tags=await dbContext.Tags
                        .Include(t=>t.User)
                        .Where(t=>ids.Contains(t.Task.IdTask))
                        .ToListAsync();