EF

值转换

值转换器可在从数据库读取或向其中写入属性值时转换属性值。

值转换器目前只能执行值与一个数据库列之间的转换。 此限制意味着对象的所有属性值都必须被编码为一个列值。 这通常是通过在对象进入数据库时对其进行序列化,然后在出路时再次对其进行反序列化来处理的。

modelBuilder.Entity<Post>()
    .Property(e => e.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null),
        new ValueComparer<ICollection<string>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => (ICollection<string>)c.ToList()));

使用表达式树的目的是使它们可被编译到数据库访问委托中,以便进行高效转换。 

public readonly struct AnnualFinance
{
    [JsonConstructor]
    public AnnualFinance(int year, Money income, Money expenses)
    {
        Year = year;
        Income = income;
        Expenses = expenses;
    }

    public int Year { get; }
    public Money Income { get; }
    public Money Expenses { get; }
    public Money Revenue => new Money(Income.Amount - Expenses.Amount, Income.Currency);
}

值对象作为只读结构实现。 也就是说,EF Core 可以在不出问题的情况下截取快照和比较值

 

标识解析

由于跟踪查询使用更改跟踪器,因此 EF Core 将在跟踪查询中执行标识解析。 当具体化实体时,如果 EF Core 已被跟踪,则会从更改跟踪器返回相同的实体实例。 如果结果中多次包含相同的实体,则每次会返回相同的实例。

非跟踪查询不会使用更改跟踪器,也不会执行标识解析。 因此会返回实体的新实例,即使结果中多次包含相同的实体也是如此。

可以在同一查询中合并上述两种行为。 也就是说,可以使用非跟踪查询并对结果执行标识解析。 我们添加了另一个运算符 AsNoTrackingWithIdentityResolution(),就像添加 AsNoTracking() 可查询运算符一样。 QueryTrackingBehavior 枚举中也添加了一个关联项。

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

 

如果将查询配置为使用标识解析和非跟踪行为,生成查询结果时我们将在后台使用独立的更改追踪器,以便仅将每个实例具体化一次。 此更改追踪器不同于上下文中的更改追踪器,因此上下文不会追踪这些结果。 完成枚举查询后,该更改追踪器将超出范围,并根据需要对其进行垃圾回收。

配置默认跟踪行为

如果你发现自己更改了许多查询的跟踪行为,则建议改为更改默认值:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying.Tracking;Trusted_Connection=True")
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

默认情况下,这会使所有查询都不被跟踪。 仍可添加 AsTracking() 来进行特定查询跟踪。

 

加载数据

还可以使用单个 Include 方法加载多个导航。 
using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Owner.AuthoredPosts)
        .ThenInclude(post => post.Blog.Owner.Photo)
        .ToList();
}

 

延迟加载

使用延迟加载的最简单方式是通过安装 Microsoft.EntityFrameworkCore.Proxies 包,并通过调用 UseLazyLoadingProxies 来启用该包
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseLazyLoadingProxies()
        .UseSqlServer(myConnectionString);

EF Core 接着会为可重写的任何导航属性(即,必须是 virtual 且在可被继承的类上)启用延迟加载。

延迟加载可能会导致发生不必要的额外数据库往返(即 N+1 问题),应注意避免此问题

 

另一种方法是,使用代理进行延迟加载的工作方式是将 ILazyLoader 注入到实体中,

此方法不要求实体类型为可继承的类型,也不要求导航属性必须是虚拟的,且允许通过 new 创建的实体实例在附加到上下文后可进行延迟加载。 但它需要对 Microsoft.EntityFrameworkCore.Abstractions 包中定义的 ILazyLoader 服务的引用。

 

删除关系

默认情况下,对于必选关系,将配置级联删除行为,并将从数据库中删除子实体/依赖实体。 对于可选关系,默认情况下不会配置级联删除,但会将外键属性设置为 null。
public class Post
    {       
        public string Title { get; set; }
        public string Content { get; set; }

        public int BlogId { get; set; }
        public Blog Blog { get; set; }
    }

如果不加 BlogId ,默认删除blog时不会删除post

 
 

编写高性能查询

  • 比较不可为 null 的列比比较可为 null 的列更简单且更快。 如果可能,请考虑将列标记为不可为 null。

  • 检查相等性 (==) 比检查不相等 (!=) 更简单且更快,因为查询无需区分 null 和 false 结果。 尽可能使用相等性比较。 不过,只是否定 == 比较这一点实际上与 != 等效,因此不会提高性能。

  • 在某些情况下,可通过从列中显式筛选出 null 值来简化复杂比较,例如,当不存在 null 值或这些值在结果中不相关时。 请看下面的示例:

var query1 = context.Entities.Where(e => e.String1 != e.String2 || e.String1.Length == e.String2.Length);
var query2 = context.Entities.Where(
    e => e.String1 != null && e.String2 != null && (e.String1 != e.String2 || e.String1.Length == e.String2.Length));

在第二个查询中,null 结果从 String1 列中显式筛选出来。 在比较过程中,EF Core 可安全地将 String1 列视为不可为 null 的列,从而生成更简单的查询。

 

客户端异步 LINQ 运算符

var groupedHighlyRatedBlogs = await context.Blogs
    .AsQueryable()
    .Where(b => b.Rating > 3) // server-evaluated
    .AsAsyncEnumerable()
    .GroupBy(b => b.Rating) // client-evaluated
    .ToListAsync();

 

Logging

EF Core 会自动与 ASP.NET Core 的日志记录机制集成 AddDbContext AddDbContextPool 

 

DbContextPool

services.AddDbContextPool<BloggingContext>(
    options => options.UseSqlServer(connectionString));

 上下文池的工作方式是跨请求重复使用同一上下文实例。 这意味着,它可以有效地根据实例本身注册为 单一 实例,以便能够持久保存。

对于需要 范围内 的服务或需要更改配置的情况,请不要使用 pooling。 除高度优化的方案外,池的性能提升通常可以忽略不计。

 

连接复原

连接复原会自动重试失败的数据库命令。 此功能可用于任何数据库,方法是提供 "执行策略",它封装了检测失败和重试命令所需的逻辑。

 

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True;ConnectRetryCount=0",
            options => options.EnableRetryOnFailure());
}
or
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<PicnicContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));
}

如果发生暂时性故障,每个查询和每个调用 SaveChanges() 都将作为一个单元重试。但是,如果您的代码使用 BeginTransaction() 您定义自己的一组操作,这些操作需要被视为一个单元,则需要播放该事务中的所有内容时,都将发生故障。 

解决方案是使用代表需要执行的所有内容的委托手动调用执行策略。

using (var db = new BloggingContext())
{
    var strategy = db.Database.CreateExecutionStrategy();

    strategy.Execute(() =>
    {
        using (var context = new BloggingContext())
        {
            using (var transaction = context.Database.BeginTransaction())
            {
                context.Blogs.Add(new Blog {Url = "http://blogs.msdn.com/dotnet"});
                context.SaveChanges();

                context.Blogs.Add(new Blog {Url = "http://blogs.msdn.com/visualstudio"});
                context.SaveChanges();

                transaction.Commit();
            }
        }
    });
}

 

事务提交失败

对于大多数更改数据库状态的操作,可以添加代码来检查它是否成功。 EF 提供扩展方法来简化此过程 IExecutionStrategy.ExecuteInTransaction 。

此方法会启动并提交事务,还会接受参数中的函数,该 verifySucceeded 参数在事务提交期间发生暂时性错误时被调用。

using (var db = new BloggingContext())
{
    var strategy = db.Database.CreateExecutionStrategy();

    var blogToAdd = new Blog {Url = "http://blogs.msdn.com/dotnet"};
    db.Blogs.Add(blogToAdd);

    strategy.ExecuteInTransaction(db,
        operation: context =>
        {
            context.SaveChanges(acceptAllChangesOnSuccess: false);
        },
        verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));

    db.ChangeTracker.AcceptAllChanges();
}

在调用时,将 SaveChanges acceptAllChangesOnSuccess 设置为 false ,以避免在成功时将实体的状态更改 为 Unchanged 。 如果提交失败并且事务已回滚,则允许重试相同的操作。

 

如果需要使用存储生成的密钥,或者需要一种常规方法来处理不依赖于执行的操作的提交失败,则可以为其分配一个在提交失败时检查的 ID。

  1. 向数据库添加一个用于跟踪事务状态的表Transactions。
  2. 在每个事务开始时向表中插入一行。
  3. 如果在提交期间连接失败,请检查数据库中是否存在相应的行。
  4. 如果提交成功,则删除相应的行以避免表增长。
using (var db = new BloggingContext())
{
    var strategy = db.Database.CreateExecutionStrategy();

    db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });

    var transaction = new TransactionRow {Id = Guid.NewGuid()};
    db.Transactions.Add(transaction);

    strategy.ExecuteInTransaction(db,
        operation: context =>
        {
            context.SaveChanges(acceptAllChangesOnSuccess: false);
        },
        verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));

    db.ChangeTracker.AcceptAllChanges();
    db.Transactions.Remove(transaction);
    db.SaveChanges();
}

 

避免 DbContext 线程问题

不支持在同一实例上运行多个并行操作 DbContext 。 这包括异步查询的并行执行以及从多个线程进行的任何显式并发使用。 因此,应 await 立即异步调用,或 DbContext 对并行执行的操作使用单独的实例。

AddDbContext DbContext 默认情况下,扩展方法使用范围生存期注册类型。任何并行执行多个线程的代码都应确保 DbContext 不会同时访问实例。

 

posted @ 2020-10-09 15:13  yetsen  阅读(227)  评论(0编辑  收藏  举报