EntityFrameworkCore 开发实践问题及规范

严重问题

客户端求值

  • 如where条件包含的GetValueOrDefault()不能被翻译成sql语句
  • 不规范代码段例子
            public async Task<List<Person>> GetPersonsAsync()
            {
                var results = await _context.Person
                    .Where(p => p.State.GetValueOrDefault() == 1)
                    .ToListAsync()
                return results;
            }

客户端与服务器评估

作为一般规则,Entity Framework Core 会尝试尽可能全面地评估服务器上的查询。 EF Core 将查询的一部分转换为可在客户端评估的参数。 系统将查询的其余部分(及生成的参数)提供给数据库提供程序,以确定要在服务器上评估的等效数据库查询。 EF Core 支持在顶级投影中进行部分客户端评估(基本上为最后一次调用 Select())。 如果查询中的顶级投影无法转换为服务器,EF Core 将从服务器中提取任何所需的数据,并在客户端上评估查询的其余部分。 如果 EF Core 在顶级投影之外的任何位置检测到不能转换为服务器的表达式,则会引发运行时异常。 请参阅查询工作原理,了解 EF Core 如何确定哪些表达式无法转换为服务器。

在 3.0 版之前,Entity Framework Core 支持在查询中的任何位置进行客户端评估。

顶级投影中的客户端评估
在下面的示例中,一个辅助方法用于标准化从 SQL Server 数据库中返回的博客的 URL。 由于 SQL Server 提供程序不了解此方法的实现方式,因此无法将其转换为 SQL。 查询的所有其余部分是在数据库中评估的,但通过此方法传递返回的 URL 却是在客户端上完成。

var blogs = context.Blogs
    .OrderByDescending(blog => blog.Rating)
    .Select(blog => new
    {
        Id = blog.BlogId,
        Url = StandardizeUrl(blog.Url)
    })
    .ToList();
public static string StandardizeUrl(string url)
{
    url = url.ToLower();

    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }

    return url;
}

不支持的客户端评估

尽管客户端评估非常有用,但有时会减弱性能。 请看以下查询,其中的 where 筛选器现已使用辅助方法。 由于数据库中不能应用筛选器,因此需要将所有数据提取到内存中,以便在客户端上应用筛选器。 根据服务器上的筛选器和数据量,客户端评估可能会减弱性能。 因此 Entity Framework Core 会阻止此类客户端评估,并引发运行时异常。

var blogs = context.Blogs
    .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
    .ToList();

显式客户端评估

在某些情况下,可能需要以显式方式强制进行客户端评估,如下所示
由于数据量小,因此在进行客户端评估时才不会大幅减弱性能。
所用的 LINQ 运算符不会进行任何服务器端转换。
在这种情况下,通过调用 AsEnumerable 或 ToList 等方法(若为异步,则调用 AsAsyncEnumerable 或 ToListAsync),以显式方式选择进行客户端评估。 使用 AsEnumerable 将对结果进行流式传输,但使用 ToList 将通过创建列表来进行缓冲,因此也会占用额外的内存。 但如果枚举多次,则将结果存储到列表中可以带来更大的帮助,因为只有一个对数据库的查询。 根据具体的使用情况,你应该评估哪种方法更适合。

var blogs = context.Blogs
    .AsEnumerable()
    .Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
    .ToList();

客户端评估中潜在的内存泄漏

由于查询转换和编译的开销高昂,因此 EF Core 会缓存已编译的查询计划。 缓存的委托在对顶级投影进行客户端评估时可能会使用客户端代码。 EF Core 为树型结构中客户端评估的部分生成参数,并通过替换参数值重用查询计划。 但表达式树中的某些常数无法转换为参数。 如果缓存的委托包含此类常数,则无法将这些对象垃圾回收,因为它们仍被引用。 如果此类对象包含 DbContext 或其中的其他服务,则会导致应用的内存使用量逐渐增多。 此行为通常是内存泄漏的标志。 只要遇到的常数为不能使用当前数据库提供程序映射的类型,EF Core 就会引发异常。 常见原因及其解决方案如下所示:
使用实例方法:在客户端投影中使用实例方法时,表达式树包含实例的常数。 如果你的方法不使用该实例中的任何数据,请考虑将该方法设为静态方法。 如果需要方法主体中的实例数据,则将特定数据作为实参传递给方法。
将常数实参传递给方法:这种情况通常是由于在客户端方法的实参中使用 this 引起的。 请考虑将实参拆分为多个标量实参,可由数据库提供程序进行映射。
其他常数:如果在任何其他情况下都出现常数,则可以评估在处理过程中是否需要该常数。 如果必须具有常数,或者如果无法使用上述情况中的解决方案,则创建本地变量来存储值,并在查询中使用局部变量。 EF Core 会将局部变量转换为形参。

建议解决

无用追踪

  • 无须追踪的数据没有加AsNoTracking

跟踪与非跟踪查询

跟踪行为决定了 Entity Framework Core 是否将有关实体实例的信息保留在其更改跟踪器中。 如果已跟踪某个实体,则该实体中检测到的任何更改都会在 SaveChanges() 期间永久保存到数据库。 EF Core 还将修复跟踪查询结果中的实体与更改跟踪器中的实体之间的导航属性。

从不跟踪无键实体类型。 无论在何处提到实体类型,它都是指定义了键的实体类型。

跟踪查询

返回实体类型的查询是默认会被跟踪的。 这表示可以更改这些实体实例,然后通过 SaveChanges() 持久化这些更改。 在以下示例中,将检测到对博客评分所做的更改,并在 SaveChanges() 期间将这些更改持久化到数据库中。

var blog = context.Blogs.SingleOrDefault(b => b.BlogId == 1);
blog.Rating = 5;
context.SaveChanges();

非跟踪查询

在只读方案中使用结果时,非跟踪查询十分有用。 可以更快速地执行非跟踪查询,因为无需设置更改跟踪信息。 如果不需要更新从数据库中检索到的实体,则应使用非跟踪查询。 可以将单个查询替换为非跟踪查询。

var blogs = context.Blogs
    .AsNoTracking()
    .ToList();

还可以在上下文实例级别更改默认跟踪行为:

context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

var blogs = context.Blogs.ToList();

标识解析

由于跟踪查询使用更改跟踪器,因此 EF Core 将在跟踪查询中执行标识解析。 当具体化实体时,如果 EF Core 已被跟踪,则会从更改跟踪器返回相同的实体实例。 如果结果中多次包含相同的实体,则每次会返回相同的实例。 非跟踪查询不会使用更改跟踪器,也不会执行标识解析。 因此会返回实体的新实例,即使结果中多次包含相同的实体也是如此。 此行为与 EF Core 3.0 之前的版本中的行为有所不同,请参阅早期版本。
跟踪和自定义投影
即使查询的结果类型不是实体类型,默认情况下 EF Core 也会跟踪结果中包含的实体类型。 在以下返回匿名类型的查询中,结果集中的 Blog 实例会被跟踪。

var blog = context.Blogs
    .Select(b =>
        new
        {
            Blog = b,
            PostCount = b.Posts.Count()
        });

如果结果集包含来自 LINQ 组合的实体类型,EF Core 将跟踪它们。

var blog = context.Blogs
    .Select(b =>
        new
        {
            Blog = b,
            Post = b.Posts.OrderBy(p => p.Rating).LastOrDefault()
        });

如果结果集不包含任何实体类型,则不会执行跟踪。 在以下查询中,我们返回匿名类型(具有实体中的某些值,但没有实际实体类型的实例)。 查询中没有任何被跟踪的实体。

var blog = context.Blogs
    .Select(b =>
        new
        {
            Id = b.BlogId,
            Url = b.Url
        });

EF Core 支持执行顶级投影中的客户端评估。 如果 EF Core 具体化实体实例以进行客户端评估,则会跟踪该实体实例。 此处,由于我们要将 blog 实体传递到客户端方法 StandardizeURL,因此 EF Core 也会跟踪博客实例。

var blogs = context.Blogs
    .OrderByDescending(blog => blog.Rating)
    .Select(blog => new
    {
        Id = blog.BlogId,
        Url = StandardizeUrl(blog)
    })
    .ToList();
public static string StandardizeUrl(Blog blog)
{
    var url = blog.Url.ToLower();

    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }

    return url;
}

EF Core 不会跟踪结果中包含的无键实体实例。 但 EF Core 会根据上述规则跟踪带有键的实体类型的所有其他实例。
在 EF Core 3.0 之前,某些上述规则的工作方式有所不同。 有关详细信息,请参阅早期版本。

没有使用异步方法

  • 没有优先使用异步方法
  • 不规范代码段例子
            public async Task<int> AddPersons(IEnumerable<Person> persons)
            {
                this._context.Person.AddRange(persons);
                return await this._context.SaveChangesAsync();
            }

异步查询

当在数据库中执行查询时,异步查询可避免阻止线程。 异步查询对于在胖客户端应用程序中保持响应式 UI 非常重要。 异步查询还可以增加 Web 应用程序中的吞吐量,即通过释放线程,以处理 Web 应用程序中的其他请求。 有关详细信息,请参阅使用 C# 异步编程。
EF Core 不支持在同一上下文实例上运行多个并行操作。 应始终等待操作完成,然后再开始下一个操作。 这通常是通过在每个异步操作上使用 await 关键字完成的。
Entity Framework Core 提供一组类似于 LINQ 方法的异步扩展方法,用于执行查询并返回结果。 示例包括 ToListAsync()、ToArrayAsync()、SingleAsync()。 某些 LINQ 运算符(如 Where(...) 或 OrderBy(...))没有对应的异步版本,因为这些方法仅用于构建 LINQ 表达式树,而不会导致在数据库中执行查询。

EF Core 异步扩展方法在 Microsoft.EntityFrameworkCore 命名空间中定义 。 必须导入此命名空间才能使这些方法可用。

public async Task<List<Blog>> GetBlogsAsync()
{
    using (var context = new BloggingContext())
    {
        return await context.Blogs.ToListAsync();
    }
}

事务滥用

  • 没必要使用事务的场景使用事务
  • 不规范代码段例子
            public async Task<bool> UpdatePersonInfo(List<Person> persons, List<Address> addresses)
            {
                using (var transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted))
                {
                    try
                    {
                        _dbContext.Person.UpdateRange(persons);
                        await _dbContext.SaveChangesAsync();

                        _dbContext.Address.UpdateRange(addresses);
                        await _dbContext.SaveChangesAsync();
                        
                        transaction.Commit();
                        return true;
                    }
                    catch (Exception ex)
                    {
                        transaction.Rollback();
                        throw new InternalServerErrorException($"更新失败,ErrorMessage:{ex.Message}\r\nInnerException:{ex.InnerException}", ex);
                    }
                }
            }

使用事务

事务允许以原子方式处理多个数据库操作。 如果已提交事务,则所有操作都会成功应用到数据库。 如果已回滚事务,则所有操作都不会应用到数据库。

默认事务行为

默认情况下,如果数据库提供程序支持事务,则会在单次调用 SaveChanges() 时将所有更改都将应用到事务中。 如果其中有任何更改失败,则会回滚事务且所有更改都不会应用到数据库。 这意味着,SaveChanges() 可保证要么完全成功,要么在出现错误时不修改数据库。
对于大多数应用程序,此默认行为已足够。 除非应用程序确有需求,否则不应手动控制事务。

控制事务

可以使用 DbContext.Database API 开始、提交和回滚事务。 以下示例显示了在单个事务中执行的两个 SaveChanges() 操作以及 一个LINQ 查询。
并非所有数据库提供程序都支持事务。 调用事务 API 时,某些提供程序可能会引发异常或不执行任何操作。

规范参考

    数据追踪参考规范: https://docs.microsoft.com/zh-cn/ef/core/querying/tracking

    客户端求值参考规范:https://docs.microsoft.com/zh-cn/ef/core/querying/client-eval

    异步查询参考规范:https://docs.microsoft.com/zh-cn/ef/core/querying/async

    加载相关数据参考规范:https://docs.microsoft.com/zh-cn/ef/core/querying/related-data

    事务使用参考规范:https://docs.microsoft.com/zh-cn/ef/core/saving/transactions
posted @ 2020-07-09 14:37  ddockerman  阅读(901)  评论(1编辑  收藏  举报