linq C#:我应该对每个相关属性使用延迟加载吗?

j0pj023g  于 2023-09-28  发布在  C#
关注(0)|答案(2)|浏览(131)

我正在研究延迟加载。我不明白延迟加载是如何工作的,我应该做些什么来改变它吗?
以下是我的第一个案例:我有UserUser rolesRoles类。用户与角色具有多对多关系。所以我创建了用户角色类来处理这种关系。用户不包括所有情况下的角色(* 不介意UserRole中的Id *)。

用户

public class User : IUser
{
    public bool IsActive { get; set; }
    public int Id { get; private set; }
    public string Email { get; set; }
    public byte[] PasswordHash { get; set; }
    public byte[] PasswordSalt { get; set; }
    public string? Phone { get; set; }

    public List<UserRole> UserRoles { get; set; }
}

用户角色

public class UserRole : IEntity
{
    public int Id { get; private set; }

    [ForeignKey(nameof(User))]
    public int UserId { get; set; }
    public User User { get; set; }

    [ForeignKey(nameof(Role))]
    public int RoleId { get; set; }
    public Role Role { get; set; }
}

角色

public class Role : IEntity
{
    public int Id { get; private set; }
    public string Name { get; set; }
}

以下是我的第二个案例:我有一个Article类,它与User有关系。在这种情况下,Article使用Creator.EmailUser)或任何情况下的创建者名称,这意味着使用Article的任何查询都包括User

文章

public class Article : IEntity
{
    public Article() => CreatedAt = DateTime.Now;

    public bool IsDeleted { get; set; }
    public int Id { get; private set; }
    public string Title { get; set; } = "Başlık";
    public string Content { get; set; } = "İçerik";
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
    [ForeignKey(nameof(User))] public int CreatorId { get; set; }
    [ForeignKey(nameof(User))] public int? DeletedBy { get; set; }

    public User? Creator { get; set; }
    public List<ArticleCategory> ArticleCategories { get; set; }
}

所以我的问题是我应该做些什么来延迟加载这些相关的属性吗?
如果是-我该怎么办?如果不是-这是如何与EF核心工作?
例如User,它是从数据库返回数据并将其保存在内存中直到被请求,还是直到被请求才进入UserRoles和Roles表?
如果两者都没有,它实际上是如何工作的?
下面是一个例子,我可以从数据库中获取所有数据,在一个非常简单的方式:

var users = context.Users
                    .Include(u => u.UserRoles)
                    .ThenInclude(ur => ur.Role);

在某些情况下,即使我不请求用户的角色,它们会从数据库中获取吗?如果是,它们会保存在内存中吗?

ykejflvf

ykejflvf1#

Lazy<T>是一个框架提供的类,用于支持任何自定义开发人员定义的逻辑/函数的延迟初始化(通常用于多线程环境,但不限于此)。与EF Core无关。
另一方面,EF Core中的Lazy Loading数据是在不使用(至少在面向用户的API/约定中)Lazy的情况下完成的。在EF中有两种主要的延迟加载“风格”--有代理和没有代理。第一种方法依赖于使用特殊的包,并且需要相应的属性是虚拟的:

public class Blog
{
    // ...

    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    // ...

    public virtual Blog Blog { get; set; }
}

这导致生成特殊的代理类,这些代理类将推迟相关数据的加载,除非需要(即以某种方式访问)。举例来说:

var post = _context.Posts.First(); // the Blog will not be loaded

// ...
var postBlog = post.Blog; // here the lazy loading will happen.

这两个概念是相关的,但仍然有点不同。
P.S.

  • 需要注意的一点是- EF DbContext不是线程安全的(即一次只能由一个线程使用,并且不支持通过同一示例对数据库的并行查询),这消除了由Lazy解决的问题的子集。
  • 就我个人而言,我建议谨慎使用延迟加载,因为它可能会导致臭名昭著的N+1问题-检查文档的小心延迟加载部分。
5cnsuln7

5cnsuln72#

EF中的延迟加载会有一个相当糟糕的 Package ,因为当你不了解发生了什么以及EF在幕后做了什么时,你会发现你的代码非常依赖延迟加载,这会导致一个相当严重的性能问题。
在EF中加载相关数据的两个方面是延迟加载和急切加载。Eager loading是用Include完成的,而lazy loading通常是用代理完成的,尽管EF Core提供了第二种选择,您自己的代码可以控制管理lazy请求。每种选择都有潜在的陷阱需要考虑。
延迟加载是延迟查询的一种形式。假设你想加载一个100个帖子的列表,每个帖子可以在类似WPF应用程序的东西中有0到多个评论。您只想列出Posts,因此可以从dbContext.Posts中获取并显示它们,然后当您选择展开其中一个Posts时,您希望显示其Comments。急切加载会预先加载所有帖子的所有评论,这是相当过分的,所以在这种情况下,懒惰加载只会在每个帖子被扩展时获取评论。这完全是由EF在幕后完成的。当您无意中或错误地引入代码“触及”延迟加载的属性时,就会出现这种情况。以加载Posts为例,您决定添加一个列来使用post.Comments.Count显示Comment计数。当渲染代码遍历每个Post时,它会访问Comments,这会为每个Post触发延迟加载。在开发测试数据库时,您可能不会注意到它,但在生产环境中,随着系统的增长,它很快就会引起注意。这可以在DB Profiler中看到,因为您会看到发送到数据库的大量查询。例如,如果你使用给定的日期范围获取了100个帖子,你会看到这样的查询:

SELECT * FROM Posts WHERE PostDate >= @p0 AND PostDate < @p1
SELECT * FROM Comments WHERE PostId = @p0 // Post ID #1
SELECT * FROM Comments WHERE PostId = @p0 // #2
SELECT * FROM Comments WHERE PostId = @p0 // #3
SELECT * FROM Comments WHERE PostId = @p0 // #4
SELECT * FROM Comments WHERE PostId = @p0 // #5
... 100 times

100个SELECT语句来获取每个帖子的评论。这是不好的,并与更多的参考得到“触摸”化合物。这就是令人恐惧的SELECT N+1。
通常,当被这样的延迟加载所刺痛时,推荐的解决方案是使用渴望加载:

var posts = dbContext.Posts
    .Include(x => x.Comments)
    .Where(x => x.PostDate >= startDate && x.PostDate < endDate)
    .ToList();

这将再次在一个SQL SELECT语句中加载这100个帖子及其评论。例如:

SELECT * FROM Posts p 
INNER JOIN Comments c ON c.PostId = p.PostId
WHERE p.PostDate >= @p0 AND p.PostDate < @p1
  • 然而 * 这引入了一个潜在的急切加载性能陷阱,笛卡尔积。现在,在这样一个简单的例子中,没有必要担心,但是如果你有几个Include和嵌套的ThenInclude,事情会很快失控,EF Core会警告你。上面的例子使用了即时加载,将生成一个在Posts和Comments之间执行JOIN的查询。为了在一个查询中加载所有帖子和评论,查询必须返回两个表中的所有列。当只加载Posts时,返回的总数据大小为:
Posts.Columns x Posts.Rows

当按注解联接时,返回的总数据大小为:

(Posts.Columns + Comments.Columns) x Comments.Rows

查询为每个注解返回一行。如果每个帖子平均有5条评论,为了得到100个帖子,我们将返回500行,其中每行都有来自**评论及其帖子的所有列。当你连接实体时,这很容易导致查询中返回更多的数据。
EF Core以AsSplitQuery()的形式提供了一个解决方案,它通过对帖子和评论运行单独的查询来工作,所以你最终会得到这样的结果:

SELECT * FROM Posts WHERE PostDate >= @p0 AND PostDate < @p1
SELECT c.* FROM Comments c
INNER JOIN Posts p ON p.PostId = c.PostId
WHERE p.PostDate >= @p0 AND p.PostDate < @p1

第二个SELECT看起来很像急切加载的SELECT,但它只是返回Comments中的列。这在大多数情况下都可以工作,但是EF仍然需要将所有这些相关的实体缝合在一起,这可能需要一些时间,并且它确实有局限性。在使用拆分查询时,诸如排序、分页和汇总相关数据之类的事情可能会出现问题,因此它是一个可以提供帮助的工具,但不应依赖于它。
回到我们想要一个评论计数的例子……获取所有评论只是为了得到一个可靠的计数是一个很大的浪费。即使我们需要相关实体的一些细节,急于加载整个实体也会浪费时间和内存。这就是投影在数据返回时更具选择性的地方。投影是查看实际需要的数据并使用Select填充对象以满足该需求而不是返回实体的地方。投影的优点是EF可以产生更高效的查询,并避免跟踪缓存因跟踪示例而膨胀。例如,如果我们想显示一个帖子列表,包括标题、发布日期、作者姓名和评论数,我们可以创建一个PostSummaryViewModel,如下所示:

public class PostSummaryViewModel
{
    public int PostId { get; set; } 
    public string Title { get; set; }
    public DateTime PostDate { get; set; }
    public string AuthorName { get; set; }
    public int CommentCount { get; set; }
}

然后读它:

var posts = dbContext.Posts
    .Where(x => x.PostDate >= startDate && x.PostDate < endDate)
    .Select(x => new PostSummaryViewModel
    {
        PostId = x.PostId,
        Title = x.Title,
        PostDate = x.PostDate,
        AuthorName = x.Author.FirstName + " " + x.Author.LastName,
        CommentCount = x.Comments.Count()
    }).ToList();

结果将是EF生成一个查询来获取所请求的数据,并根据需要自动加入表,但只返回所需的列。请注意,我们引用的评论和作者引用从我们的帖子,但不需要Include他们,EF解决了自动通过这些导航属性的引用。这确实意味着,如果我们想实现一些逻辑,比如扩展一个帖子加载它的评论,或者显示更多的信息,我们需要一个单独的调用来从DbContext中获取数据,所以我们不能简单地依赖于懒惰的加载拐杖。但总的来说,好处应该超过方便的损失。
延迟加载、急切加载和投影都是EF中可用的工具,它们在某些情况下都提供了好处,但也需要考虑成本。

相关问题