《Entity Framework Core in Action》--- 读书随记(3)

Part 1 Getting started

《Entity Framework Core in Action》
-- SECOND EDITION

Author: JON P SMITH

如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的

5 Using EF Core in ASP.NET Core web applications

5.3 Understanding dependency injection

5.3.3 The lifetime of a service created by DI

6 Tips and techniques for reading and writing with EF Core

本章分为两个部分: 从数据库读取和写入数据库。每个部分都涵盖了您可能遇到的某些读/写问题,但同时解释了 EF Core 如何实现解决方案

6.1 Reading from the database

6.1.1 Exploring the relational fixup stage in a query

当您使用 EF Core 查询数据库时,会运行一个称为关系修复的阶段来填充查询中包含的其他实体类的导航属性。其中 Book 实体链接到了它的 Author。到目前为止,您看到的所有查询都只链接当前查询读入的实体类。但实际上,正常读写查询的关系修复可以在单个查询之外链接到任何被跟踪的实体

正如这个简单的例子所示,当一个查询完成后,关系修复程序会根据数据库关键字的约束来填充所有的导航链接,它非常强大。例如,如果你在四个不同的查询中加载所有的 Books、 Reviews、 BookAuthor 和 Authors,EF Core 会正确地链接所有的导航属性

6.1.2 Understanding what AsNoTracking and its variant do

  • AsNoTracking: 产生更快的查询时间,但并不总是代表确切的数据库关系
  • AsNoTrackingWithIdentityResolution: 通常比普通查询快,但比 AsNoTrace 的相同查询慢。改进之处在于正确地表示了数据库关系,数据库中的每一行都有一个实体类实例

为了提供最佳性能,AsNoTrace 方法不执行称为标识解析的特性,该特性确保数据库中每行只有一个实体实例。不将标识解析特性应用于查询意味着您可能会获得实体类的额外实例

两本书有同一个作者,当使用两种方法的时候,返回的结果有所不同

在大多数只读的情况下,比如显示每本书的作者名,有四个 Author 类实例并不重要,因为重复的类包含相同的数据。在这些类型的只读查询中,应该使用 AsNoTrack 方法,因为它生成的查询最快

但是,如果您以某种方式使用这些关系,例如创建一个图书报告,该报告链接到同一作者的其他图书,AsNoTrace 方法可能会导致问题。在类似的情况下,应该使用 AsNoTrackingWithIdentityResolution 方法

AsNoTrace 和 AsNoTrackingWithIdentityResoly 方法的另一个特性是关系修复阶段仅在查询中工作。因此,即使第一个查询加载了相同的数据,使用 AsNoTrace 或 AsNoTrackingWithIdentityResolm 的两个查询也将创建每个实体的新实例。对于普通查询,两个单独的查询将返回相同的实体类实例,因为关系修复阶段可以跨所有被跟踪的实体工作

6.1.3 Reading in hierarchical data efficiently

我曾经为一个客户工作,这个客户有很多层次数据——数据有一系列具有不确定深度的链接实体类。问题在于,我必须解析整个层次结构,然后才能显示它。我最初是通过对前两个级别进行eager加载来实现这一点的; 然后对更深的级别使用explicit加载。这种技术是有效的,但是性能很慢,而且数据库因为大量单个数据库访问而超载

你可以用 .Include(x => x.WorksForMe).ThenInclude(x => x.WorksForMe) 等等,但是只有一个.Include(x => x.WorksForMe) 就足够了,因为关系修复程序可以解决其余的问题

6.1.4 Understanding how the Include method works

随着 EF Core 3.0的出现,包含方法转换为 SQL 的方式发生了变化。EF Core 3.0在许多情况下提供了性能改进,但是对于一些复杂的查询,它对性能有负面影响。以 BookApp 数据库中的一个示例为例,查看如何加载带有评论和作者的 Book。下面的代码片段显示了查询:

var query = context.Books
    .Include(x => x.Reviews)
    .Include(x => x.AuthorsLink)
    .ThenInclude(x => x.Author);

EF Core 3.0处理加载集合关系的方式的好处在于性能,在许多情况下性能更快

但是在一些特定的情况下,它可以非常缓慢,正如我接下来要讲的

如果有多个要包含在查询中的集合关系,并且其中一些关系在集合中有大量条目,则会发生性能问题。通过查看上图最右侧的两个计算,您可以看到这个问题。这个图显示了在3.0之前通过 EF Core 版本读取的行数是通过添加这些行计算出来的。但是在 EF Core 3.0及更高版本中,读取的行数是通过行数相乘来计算的。假设您正在加载3个关系,每个关系有100行。前3.0版本的 EF Core 可以读取100 + 100 + 100 = 300行,但是 EF Core 3.0以及更高版本将使用100 * 100 * 100 = 100万行

如果您发现使用多个Include的查询速度较慢,可能是因为两个或多个Include的集合包含大量条目。在这种情况下,在“Include”之前添加 AsSplitQuery 方法,以便切换到每个包含集合的单独负载

var result = context.ManyTops
    .AsSplitQuery()
    .Include(x => x.Collection1)
    .Include(x => x.Collection2)
    .Include(x => x.Collection3)
    .Single(x => x.Id == id)

6.1.5 Making loading navigational collections fail-safe

我总是尝试使任何代码具有故障安全性,我的意思是,如果我在代码中犯了错误,我宁愿它因为异常而失败,也不愿意默默地做错事。我担心的一个问题是,在加载具有关系的实体时,忘记添加正确的 include 集。似乎我永远不会忘记这样做,但是在具有许多关系的应用程序中,这很容易发生。事实上,我已经做了很多次,包括在我客户的应用程序中,这就是为什么我使用自动防故障方法。让我解释一下这个问题,然后再给出我的解决方案

对于使用集合的任何导航属性,我经常看到开发人员将一个空集合分配给集合导航属性,或者在构造函数中,或者通过对该属性的赋值

开发人员这样做是为了更容易地在新创建的实体类实例上向导航集合添加条目。缺点是,如果忘记使用 Include 来加载导航属性集合,则当数据库可能具有应该填充该集合的数据时,将得到一个空集合

如果要替换整个集合,则还有另一个问题。如果没有“包含”,数据库中的旧条目就不会被删除,因此会得到新实体和旧实体的组合,这是错误的答案

var book = context.Books
//missing .Include(x => x.Reviews)
    .Single(p => p.BookId == twoReviewBookId);
book.Reviews = new List<Review>{ new Review{ NumStars = 1}};
context.SaveChanges();

不将空集合分配给集合的另一个好理由是性能。例如,如果您需要使用集合的显式加载,并且您知道它已经加载了,因为它不是 null,那么您可以跳过(多余的)显式加载

因此在我的代码中,我不会用集合预加载任何导航属性。当代码访问导航收集属性时,不会在忽略 Include 方法时无声无息地失败,而是会得到 NullReferenceException。在我看来,这个结果比得到错误的数据要好得多

6.1.6 Using Global Query Filters in real-world situations

USING QUERY FILTERS TO CREATE MULTITENANT SYSTEMS

多租户系统是指不同的用户或用户组拥有只能由特定用户访问的数据的系统

在查询筛选器的软删除使用中,您使用了一个布尔值作为筛选器,但是对于多租户系统,您需要一个更复杂的键,我将其称为 DataKey

每个租户都有一个惟一的 DataKey。租户可能是一个单独的用户,或者更可能是一组用户

public class EfCoreContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().HasQueryFilter(x => x.CustomerName == _userId);
    }
}

6.1.7 Considering LINQ commands that need special attention

  • 一些 LINQ 命令需要额外的代码来适应数据库的工作方式,例如 LINQ 的 Average、 Sum、 Max 和其他处理 null 返回所需的聚合命令。只有 Count 这个聚合不会返回 null
  • 一些 LINQ 命令可以与数据库一起工作,但只能在严格的边界内工作,因为数据库不支持命令的所有可能性。一个例子是 GroupBy LINQ 命令,数据库只能有一个简单的键,并且对于 iGrouping 部分有很大的限制
  • 一些 LINQ 命令与数据库特性很匹配,但是对数据库返回的内容有一些限制。例如 Join 和 GroupJoin

6.1.8 Using AutoMapper to automate building Select queries


  • Same type and same name mapping: 属性通过具有相同的类型和相同的名称从实体类映射到 DTO 属性
  • Trimming properties: 通过从 DTO 中省略实体类中的属性,Select 查询将不会加载这些列
  • Flattening relationships: DTO 中的名称是导航属性名称和导航属性类型中的属性的组合。例如,Book实体的 Promotion.NewPrice 映射到 DTO 的 PromotionNewPrice 属性
  • Nested DTOs: 此配置允许您将集合从实体类映射到 DTO 类,因此可以在导航集合属性中从实体类复制特定属性

FOR SIMPLE MAPPINGS, USE THE [AUTOMAP] ATTRIBUTE

[AutoMap(typeof(Book))]
public class ChangePubDateDtoAm
{
    public int BookId { get; set; }
    public string Title { get; set; }
    public DateTime PublishedOn { get; set; }
}

通过 AutoMap 属性映射的类使用 AutoMapper 的约定配置,只有一些参数和属性允许进行一些调整

COMPLEX MAPPINGS NEED A PROFILE CLASS

public class BookListDtoProfile : Profile
{
    public BookListDtoProfile()
    {
        CreateMap<Book, BookListDto>()
        .ForMember(p => p.ActualPrice,
            m => m.MapFrom(s => s.Promotion == null 
                ? s.Price : s.Promotion.NewPrice))
        .ForMember(p => p.AuthorsOrdered, 
            m => m.MapFrom(s => string.Join(", ",
                s.AuthorsLink.Select(x => x.Author.Name))))
        .ForMember(p => p.ReviewsAverageVotes,
            m => m.MapFrom(s =>s.Reviews.Select(y =>
                    (double?)y.NumStars).Average()));
    }
}

REGISTER AUTOMAPPER CONFIGURATIONS

需要使用包 AutoMapper.Extensions.Microsoft.DependencyInjection

当使用这个包的方法 AddAutoMapper 之后,它会自动扫描你提供的assemblies,然后注册一个服务接口 IMapper,然后我们就可以使用这个接口。您可以使用 IMapper 接口为具有[ AutoMap ]属性的所有类以及继承 AutoMapper 的 Profile 类的所有类注入配置

6.1.9 Evaluating how EF Core creates an entity class when reading data in

到目前为止,这本书中的实体类还没有用户定义的构造函数,所以如果你读这个实体类,EF Core 使用默认的无参数构造函数,然后直接更新属性和备份字段。但是有时候,使用带参数的构造函数是有用的,因为它使创建实例变得更容易,或者因为您希望确保以正确的方式创建类

我本可以在 ReviewGood 类中添加一个构造函数来设置所有的非导航属性,但是我想指出的是 EF Core 可以使用一个构造函数来创建实体实例,然后填充任何不在构造函数参数中的属性。现在,我们已经看到了一个可以工作的构造函数,让我们看看 EF Core 不能或不会使用的构造函数,以及如何处理每个问题

CONSTRUCTORS THAT CAN CAUSE YOU PROBLEMS WITH EF CORE

EF Core 不能使用的第一种构造函数类型是带有类型或名称不匹配的参数的构造函数类型

如果只有这个构造函数,那么在第一次使用应用程序的 DbContext 时,EFCore 将抛出异常

EFCore 不能使用的构造函数的另一个例子是带有设置导航属性的参数的构造函数

如果您的构造函数不匹配 EF Core 的按照约定模式,那么您需要提供一个 EF Core 可以使用的构造函数。标准的解决方案是添加一个私有的无参数构造函数,EF Core 可以使用该构造函数创建类实例并使用其常规的参数/字段设置

另外,如果在将参数数据赋给匹配属性时更改参数数据,则会发生更微妙的问题。下面的代码片段会引起问题,因为读入的数据会在赋值中被更改

public ReviewBad(string voterName)
{
    VoterName = "Name: "+voterName; //alter the parameter before assign to
    property
    //... other code left out
}

6.2 Writing to the database with EF Core

6.2.1 Evaluating how EF Core writes entities/relationships to the database

当您创建一个具有新关系的新实体时,导航属性是您的朋友,因为 EF Core 会为您解决填充外键的问题

var book = new Book
{
    Title = "Test",
    Reviews = new List<Review>()
};
book.Reviews.Add(
    new Review { NumStars = 1 });
context.Add(book);
context.SaveChanges();

6.2.2 Evaluating how DbContext handles writing out entities/relationships

即使您没有尝试像 SaveChanges 问题之前/之后那样复杂的事情,了解 EF Core 是如何工作的也是有益的

//STAGE1
var author = context.Authors.First();
var bookAuthor = new BookAuthor { Author = author };
var book = new Book
{
    Title = "Test Book",
    AuthorsLink = new List<BookAuthor> { bookAuthor }
};

//STAGE2
context.Add(book);

//STAGE3
context.SaveChanges();

STAGE1:

STAGE2:

Book 和 BookAuthor 这两个新实体的状态已更改为 Add。同时,Add 方法尝试设置外键: 它知道 Author 的主键,因此可以在 BookAuthor 实体中设置 AuthorId。它不知道 Book 的主键(BookId) ,所以它在隐藏的跟踪值中放置一个唯一的负数,充当伪键。Add 还有一个关系修复阶段,用于填充任何其他导航属性

STAGE3:

6.2.4 A quick way to delete an entity

var book = new Book
{
    BookId = bookId
};
context.Remove(book);
context.SaveChanges();
posted @   huang1993  阅读(127)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示