ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-07-17 17:02:50  阅读:159  来源: 互联网

标签:Core Book 数据库 实体 Entity book context SaveChanges 随记


Part 1 Getting started

《Entity Framework Core in Action》
-- SECOND EDITION

Author: JON P SMITH

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

3 Changing the database content

3.1 Introducing EF Core’s entity State

介绍 EF Core 的实体属性,称为 State。此属性提供了对 EF Core 工作方式的另一种深入了解,这有助于您理解在添加、更新或删除实体时发生了什么

任何实体类实例都有一个 State,可以通过以下 EF Core 命令访问它:
context.Entry(someEntityInstance).State
当调用 SaveChanges 时,State 告诉 EF Core 如何处理此实例。下面列出了可能的状态以及调用 SaveChanges 时会发生的情况:

  • Added: 需要在数据库中创建实体
  • Unchanged: 该实体存在于数据库中,并且尚未在客户端上进行修改
  • Modified: 该实体存在于数据库中,并已在客户端上进行了修改
  • Deleted: 该实体存在于数据库中,但应该删除
  • Detached: 你提供的实体没有被追踪

调用 SaveChanges 时,它将查看所有被跟踪的实体及其状态,以确定需要对数据库应用哪种类型的数据库更改

被跟踪的实体是通过使用不包含 AsNoTracking 方法的查询从数据库读入的实体实例。或者,在将实体实例用作 EFCore 方法(如 Add、 Update 或 Delete)的参数之后,将对其进行跟踪

3.2 Creating new rows in a table

3.2.1 Creating a single entity on its own

context.Add(itemToAdd);
context.SaveChanges();

3.2.2 Creating a book with a review

var book = new Book
{
    Title = "Test Book",
    PublishedOn = DateTime.Today,
    Reviews = new List<Review>()
    {
        new Review
        {
            NumStars = 5,
            Comment = "Great test book!",
            VoterName = "Mr U Test"
        }
    }
};

context.Add(book);
context.SaveChanges();

WHAT HAPPENS AFTER THE SAVECHANGES RETURNS SUCCESSFULLY?

当 Add 和 SaveChanges 成功完成后,会发生一些事情: 已经插入到数据库中的实体实例现在由 EF Core 跟踪,它们的状态被设置为 Unchanged 。因为我们使用的是一个关系数据库,而且因为两个实体类 Book 和 Review 的主键都是 int 类型,所以默认情况下 EF Core 希望数据库使用 SQL IDENTITY 关键字创建主键。因此,EF Core 创建的 SQL 命令将主键读回到实体类实例中相应的主键中,以确保实体类与数据库匹配

Why you should call SaveChanges only once at the end of your changes

在创建的末尾调用了 SaveChanges 方法,在更新和删除示例中也可以看到相同的模式ーー在创建的末尾调用了 SaveChanges 方法。实际上,即使对于包含创建、更新和删除的复杂数据库更改,您仍然应该在最后只调用一次 SaveChanges 方法。你这样做是因为 EF Core 会保存你所有的更改(创建、更新和删除)并将它们一起应用到数据库中,如果数据库拒绝你的任何更改,你所有的更改都会被拒绝, 通过一个称为事务的数据库特性;

EXAMPLE THAT HAS ONE INSTANCE ALREADY IN THE DATABASE

var foundAuthor = context.Authors.SingleOrDefault(author => author.Name == "Mr. A");
if (foundAuthor == null)
    throw new Exception("Author not found");

var book = new Book
{
    Title = "Test Book",
    PublishedOn = DateTime.Today
};

book.AuthorsLink = new List<BookAuthor>
{
    new BookAuthor
    {
        Book = book,
        Author = foundAuthor
    }
};

context.Add(book);
context.SaveChanges();

3.3 Updating database rows

更新数据库分三个阶段:

  1. 读取数据(数据库行) ,可能有一些关系
  2. 更改一个或多个属性(数据库列)
  3. 将更改写回数据库(更新行)
var book = context.Books
    .SingleOrDefault(p => p.Title == "Quantum Networking");
if (book == null)
    throw new Exception("Book not found");
book.PublishedOn = new DateTime(2058, 1, 1);
context.SaveChanges();

3.3.1 Handling disconnected updates in a web application

正如在上一小节中了解到的,更新是一个分为三个阶段的过程,需要由应用程序的 DbContext 的同一个实例执行一次读取、一次更新和一次 SaveChanges 调用。问题是,对于某些应用程序,如网站和 RESTful API,使用相同的应用程序的 DbContext 实例是不可能的,因为在 Web 应用程序中,每个 HTTP 请求通常是一个新的请求,没有从最后一个 HTTP 请求保留的数据

DISCONNECTED UPDATE, WITH RELOAD

The quickest way to read an entity class using its primary key(s)

当需要更新特定实体并需要使用其主键读取它时,有几个选项。我过去常常使用 Find 命令,但是在深入研究之后,我现在推荐 SingleOrDefault,因为它比 Find 命令快。但是我应该指出关于 Find 方法的两个有用的事情

  • Find 方法检查当前应用程序的 DbContext,以查看所需的实体实例是否已经加载,这可以保存对数据库的访问。但是,如果实体不在应用程序的 DbContext 中,则由于这种额外的检查,加载将会更慢
  • Find 方法输入起来更简单、更快捷,因为它比 SingleOrDefault 版本(比如上下文)更短。context.Find< Book >(key)对比context.SingleOrDefault(p => p.Bookid == key).

使用 SingleOrDefault 方法的好处是,您可以使用 Include 等方法将其添加到查询的末尾,而使用“Find”则无法做到这一点

DISCONNECTED UPDATE, SENDING ALL THE DATA

在某些情况下,所有数据都可能被发回,因此没有理由重新加载原始数据。这可能发生在一些 RESTful API 或 process-to-process 通信中的简单实体类中。这在很大程度上取决于给定的 API 格式与数据库格式的匹配程度以及对其他系统的信任程度

// 模拟前端返回的完整的实体数据
string json;
using (var context = new EfCoreContext(options))
{
    var author = context.Books
        .Where(p => p.Title == "Quantum Networking")
        .Select(p => p.AuthorsLink.First().Author)
        .Single();
    author.Name = "Future Person 2";
    json = JsonConvert.SerializeObject(author);
}

// 处理修改
using (var context = new EfCoreContext(options))
{
    var author = JsonConvert.DeserializeObject<Author>(json);
    context.Authors.Update(author);
    context.SaveChanges();
}

使用 Author 实体实例作为参数调用 EF Core Update 命令,该参数将 Author 实体的所有属性标记为已修改。调用 SaveChanges 命令时,它将更新行中与实体类具有相同主键的所有列

这种方法的优点是数据库更新更快,因为不需要额外读取原始数据。也不必编写代码来复制要更新的特定属性

缺点是可以传输更多的数据,而且除非 API 经过精心设计,否则很难将接收到的数据与数据库中已有的数据进行协调。而且,您相信外部系统能够正确地记住所有数据,尤其是系统的主键

3.4 Handling relationships in updates

3.4.1 Principal and dependent relationships

  • 主体实体ー包含依赖关系通过外键引用的主键
  • 从属实体ーー包含引用主体主键的外键

在数据库中通过处理外键的可空性来处理。如果依赖关系中的外键是非空的,那么依赖关系就不能没有主键而存在

3.4.2 Updating one-to-one relationships: Adding a PriceOffer to a book

public class PriceOffer
{
    public int PriceOfferId { get; set; }
    public decimal NewPrice { get; set; }
    public string PromotionalText { get; set; }
    
    //-----------------------------------------------
    //Relationships
    public int BookId { get; set; }
}

CONNECTED STATE UPDATE

  1. 用任何现有的 PriceOffer 关系加载 Book 实体
  2. 将关系设置为要应用于此书的新 PriceOffer 实体
  3. 调用 SaveChanges 更新数据库
var book = context.Books
    .Include(p => p.Promotion)
    .First(p => p.Promotion == null);

book.Promotion = new PriceOffer
{
    NewPrice = book.Price / 2,
    PromotionalText = "Half price today!"
};

context.SaveChanges();

在这个例子中,这本书没有现有的促销活动,但是如果有现有的促销活动,它也会起作用

现在,如果图书上有一个现有的促销(也就是说,Book 实体类中的“促销”属性不为空) ,会发生什么?这就是为什么装载 Book 实体类的查询中的 Include(p => p.Promotion) 命令如此重要的原因。因为Include方法,EF Core 将知道一个现有的 PriceOffer 分配给这本书,并将删除它之前添加新版本

明确地说,在这种情况下,您必须使用某种形式的关系加载ーーeager, explicit, select, or lazy 加载关系ーー以便 EF Core 在更新之前了解它。如果没有,如果存在关系,EF Core 会对一个重复的外键 BookId 抛出一个异常,EF Core 在该外键上放置了一个唯一的索引,PriceOffers 表中的另一行将具有相同的值

3.4.4 Updating a many-to-many relationship

UPDATING A MANY-TO-MANY RELATIONSHIP VIA A LINKING ENTITY CLASS

var book = context.Books
    .Include(p => p.AuthorsLink)
    .Single(p => p.Title == "Quantum Networking");

var existingAuthor = context.Authors
    .Single(p => p.Name == "Martin Fowler");

book.AuthorsLink.Add(new BookAuthor
{
    Book = book,
    Author = existingAuthor,
    Order = (byte) book.AuthorsLink.Count
});

context.SaveChanges();

需要注意的一点是,在加载 Book 的 AuthorsLink 时,不需要在 Author 实体类中加载相应的 BooksLink。原因是,当您更新 AuthorsLink 集合时,EF Core 知道有一个到图书的链接,在更新过程中,EF Core 将自动填写该链接。下次有人加载 Author 实体类及其 BooksLink 关系时,他们将看到该集合中指向 Quantum Networking 图书的链接

UPDATING A MANY-TO-MANY RELATIONSHIP WITH DIRECT ACCESS TO THE OTHER ENTITY

var book = context.Books
    .Include(p => p.Tags)
    .Single(p => p.Title == "Quantum Networking");

var existingTag = context.Tags
    .Single(p => p.TagId == "Editor's Choice");

book.Tags.Add(existingTag);
context.SaveChanges();

3.4.5 Advanced feature: Updating relationships via foreign keys

到目前为止,我已经向您展示了如何通过使用实体类本身来更新关系。例如,在向图书添加评论时,可以将 Book 实体与其所有的 Reviews 一起加载。这很好,但是在断开连接的状态下,您必须从从浏览器/RESTful API 返回的图书主键加载图书及其所有评论。在许多情况下,您可以删除实体类的加载,改为设置外键

这种技术适用于我目前为止展示的大多数断开连接的更新,但是让我给您一个将评论从一本书移到另一本书的例子。(我知道,这种情况在现实世界中不太可能发生。但这只是一个简单的例子。)下面的清单在用户键入请求后执行更新。该代码假设用户想要更改 Review 的 ReviewId 和他们想要附加 Review 的新 BookId 在一个名为 dto 的变量中返回

var reviewToChange = context
    .Find<Review>(dto.ReviewId);

reviewToChange.BookId = dto.NewBookId;

context.SaveChanges();

这种技术的好处是,您不必加载 Book 实体类或使用 Include 命令来加载与此书相关联的所有 Reviews。在我们的示例 Book App 中,这些实体并不太大,但在实际的应用程序中,主体和依赖实体可能非常大。(例如,一些亚马逊产品有数千条评论。)在disconnected的系统中,我们通常只在disconnect时发送主键,这种方法可以有效地减少对数据库的访问,从而提高性能

当通过外键更新关系时,可能需要访问应用程序的 DbContext 中没有 DbSet < T > 属性的实体,因此如何读取数据?上面使用 Find < T > 方法,但是如果需要更复杂的查询,则可以通过 Set < T > 方法访问任何实体,例如 context.Set< Review >().Where(p => p.NumVotes > 5)

3.5 Deleting entities

3.5.1 Soft-delete approach: Using a global query filter to hide entities

软删除背后的思想是,在现实世界的应用程序中,数据不会停止是数据; 它会转换成另一种状态。以我们的图书为例,一本书可能不再出售,但是这本书存在的事实是毫无疑问的,那么为什么要删除它呢?相反,您可以设置一个标志来表示该实体将隐藏在所有查询和关系中

  • 向 Book 实体类添加名为 SoftDelete 的布尔属性。如果该属性为 true,则 Book 实体实例将被软删除; 在普通查询中不应该找到它
  • 通过 EFCore fluent 配置命令添加一个全局查询过滤器。其效果是对任何对 Books 表的访问应用额外的 Where 筛选器
public class EfCoreContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>().HasQueryFilter(p => !p.SoftDeleted);
    }
}

如果要访问具有 model-level 筛选器的所有实体,请将 IgnoreQueryFilters 方法添加到查询中,例如,context.Books.IgnoreQueryFilters()

3.5.2 Deleting a dependent-only entity with no relationships

var promotion = context.PriceOffers.First();

context.Remove(promotion);
context.SaveChanges();

3.5.3 Deleting a principal entity that has relationships

关系数据库需要保持参照完整性,因此,如果你删除了表中其他行通过外键指向的一行,就必须采取措施防止参照完整性丢失

以下是删除具有相关实体的主体实体时,设置数据库以保持参照完整性的三种方法:

  • 您可以告诉数据库服务器删除依赖于主体实体(称为级联删除)的依赖实体
  • 如果列允许,可以告诉数据库服务器将依赖实体的外键设置为 null
  • 如果这两个规则都未设置,则如果尝试删除具有依赖实体的主体实体,则数据库服务器将引发错误

3.5.4 Deleting a book with its dependent relationships

默认情况下,EFCore 对具有非空外键的依赖关系使用级联删除。从开发人员的角度来看,级联删除使得删除主体更加容易,因为其他两个规则需要额外的代码来处理删除依赖实体的问题。但是在许多业务应用程序中,这种方法可能并不合适

var book = context.Books
    .Include(p => p.Promotion)
    .Include(p => p.Reviews)
    .Include(p => p.AuthorsLink)
    .Include(p => p.Tags)
    .Single(p => p.Title == "Quantum Networking");

context.Books.Remove(book);
context.SaveChanges();

如果你没有在你的代码中加入Include,EF 核心不会知道依赖实体,也不能删除三个依赖实体。在这种情况下,保持参照完整性的问题将落在数据库服务器上,其响应将取决于如何设置外键约束的 DELETE ON 部分。默认情况下,EFCore 为这些实体类创建的数据库将被设置为使用级联删除

链接到 Book 的 Author 和 Tag 不会被删除,因为它们不是 Book 的依赖实体; 只有 BookAuthor 和 BookTag 链接实体会被删除。这种安排是有意义的,因为 Author 和 Tag 可能用于其他图书

4 Using EF Core in business logic

4.7 Adding extra features to your business logic handling

4.7.2 Using transactions to daisy-chain a sequence of business logic code

这种拒绝数据库的方式之所以有效,是因为当你使用 EF Core 创建一个显式的关系数据库事务时,会产生两种效果

  • 在调用事务的 Commit 方法之前,对数据库的任何写操作都对其他数据库用户隐藏
  • 如果您决定不希望数据库写入(比如,因为业务逻辑有错误) ,可以通过调用事务 RollBack 命令放弃在事务中完成的所有数据库写入

public class RunnerTransact2WriteDb<TIn, TPass, TOut> where TOut : class
{
    private readonly IBizAction<TIn, TPass> _actionPart1;
    private readonly IBizAction<TPass, TOut> _actionPart2;
    private readonly EfCoreContext _context;
    public IImmutableList<ValidationResult> Errors { get; private set; }
    public bool HasErrors => Errors.Any();

    public RunnerTransact2WriteDb( EfCoreContext context, IBizAction<TIn, TPass> actionPart1, IBizAction<TPass, TOut> actionPart2)
    {
        _context = context;
        _actionPart1 = actionPart1;
        _actionPart2 = actionPart2;
    }

    public TOut RunAction(TIn dataIn)
    {
        using (var transaction = _context.Database.BeginTransaction())
        {
            var passResult = RunPart(_actionPart1, dataIn);
            if (HasErrors) return null;
            var result = RunPart(_actionPart2, passResult);
            if (!HasErrors)
            {
                transaction.Commit();
            }
            return result;
        }
    }

    private TPartOut RunPart<TPartIn, TPartOut>(IBizAction<TPartIn, TPartOut> bizPart,TPartIn dataIn) where TPartOut : class
    {
        var result = bizPart.Action(dataIn);
        Errors = bizPart.Errors;
        if (!HasErrors)
        {
            _context.SaveChanges();
        }
        return result;
    }
}

如果出现了错误,代码超出 use 子句,这导致了 disposal。由于没有调用事务 Commit,disposal 将导致事务执行其 RollBack 方法,该方法将丢弃数据库对事务的写操作。这些写操作永远不会写入数据库

标签:Core,Book,数据库,实体,Entity,book,context,SaveChanges,随记
来源: https://www.cnblogs.com/huangwenhao1024/p/16487721.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有