EF6学习笔记

Posted AI大胜

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了EF6学习笔记相关的知识,希望对你有一定的参考价值。

概述

Entity Framework (EF) Core 是轻量化、可扩展、开源和跨平台版的常用 Entity Framework 数据访问技术。

EF Core 可用作对象关系映射程序 (O/RM),这可以实现以下两点:

  • 使 .NET 开发人员能够使用 .NET 对象处理数据库。
  • 无需再像通常那样编写大部分数据访问代码(SQL)。EF通过使用 LINQ 来查询和保存数据

总的来讲,EF的作用就是:使开发人员专注于写代码(如C#),而不用在与数据库交互的sql语句或者ado.net上花费较多精力。

文档参考:Entity Framework Core 概述 - EF Core | Microsoft Docs

具体数据库的种类和版本,应该用哪个Nuget包,请参考:数据库提供程序 - EF Core | Microsoft Docs

注意:从EF6.3版本开始是跨平台的,可在非Windows平台上得到支持。EF Core 则是始终跨平台的。

默认和约定

默认情况下,EF 将名为 IDclassnameID 的属性解释为主键。

如果EF 中的链接字符串使用的是localDB,那么程序运行后 *.mdf*.ldf 数据库文件位于 C:\\Users\\<username> 文件夹中。

默认情况下,Entity Framework 隐式实现事务。 在对多个行或表进行更改并调用 SaveChanges 的情况下,Entity Framework 自动确保所有更改都成功或全部失败。 如果完成某些更改后发生错误,这些更改会自动回退。

模型

对于 EF Core,使用模型执行数据访问。 模型由实体类和表示数据库会话的上下文对象构成。 上下文对象允许查询并保存数据。EF 支持以下模型开发方法:

  • database frist:从现有数据库生成模型。
  • 对模型手动编码,使其符合数据库。
  • code first:创建模型后,使用 EF 迁移从模型创建数据库。 模型发生变化时,迁移可让数据库不断演进。

DbContext

DbContext 数据库上下文类是为给定数据模型协调 EF 功能的主类。 此类由 Microsoft.EntityFrameworkCore.DbContext 类派生而来。 DbContext 派生类指定数据模型中包含哪些实体。

导航属性

导航属性中包含与此实体相关的其他实体。在1对1和1对n的关系中,导航属性可以分别是某个对象或对象列表。如果某个导航属性可包含多个实体,则其类型必须是可添加、可删除和可更新实体的列表。 可指定 ICollection<T> 或诸如 List<T>HashSet<T> 的类型。 如果指定 ICollection<T>,EF 默认会创建一个 HashSet<T> 集合。

实体状态

数据库上下文跟踪内存中的实体是否与数据库中相应的行同步,并且此信息确定调用 SaveChanges 方法时会发生的情况。 例如,将新实体传递到 Add 方法时,该实体的状态会设置为 Added。 然后调用 SaveChanges 方法时,数据库上下文发出 SQL INSERT 命令。

实体可能处于以下状态之一:

  • Added. 数据库中尚不存在实体。 SaveChanges 方法发出 INSERT 语句。
  • Unchanged. 不需要通过 SaveChanges 方法对此实体执行操作。 从数据库读取实体时,实体将从此状态开始。
  • Modified. 已修改实体的部分或全部属性值。 SaveChanges 方法发出 UPDATE 语句。
  • Deleted. 已标记该实体进行删除。 SaveChanges 方法发出 DELETE 语句。
  • Detached. 数据库上下文未跟踪该实体。

开始使用(code first)

1、安装必要的Nuget包:

比如SQL Server2019: Microsoft.EntityFrameworkCore.SqlServer

使用用于 EF Core 迁移现有数据库中的反向工程(基架)的工具需要安装相应的工具包:

Microsoft.EntityFrameworkCore.ToolsMicrosoft.EntityFrameworkCore.Design

Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet 包提供 EF Core 错误页的 ASP.NET Core 中间件。 此中间件有助于检测和诊断 EF Core 迁移错误。

注意:各个包的版本要保持一致。

2、创建模型

定义构成模型的上下文类和实体类。

3、PM命令行,执行迁移和更新

使用细节

执行原生SQL或存储过程与函数

所需查询不能使用 LINQ 来表示时,可以使用原始 SQL 查询。 如果使用 LINQ 查询导致 SQL 查询效率低下,也可以使用原始 SQL 查询。

在 Web 应用程序中执行 SQL 命令时,请务必采取预防措施来保护站点免受 SQL 注入攻击。 一种方法是使用参数化查询,确保不会将网页提交的字符串视为 SQL 命令。

当前版本(EF Core 6)中

可使用 FromSqlRaw 扩展方法基于原始 SQL 查询开始 LINQ 查询。但是 FromSqlRaw 只能在直接位于 DbSet<> 上的查询根上使用。

所以,要想自由的使用原生SQL语句进行查询或更新,只能最终还是使用 ado.net 来实现。

参考:原生 SQL 查询 - EF Core | Microsoft Docs

旧版本:EF Core 1.0 中的方式

在数据上下文的实例 ( DBContext ) 中有个Database属性,其中有两组方法.ExecuteSqlCommand().SqlQuery()。它们都可以执行SQL语句,只不过.ExecuteSqlCommand()是不返回结果的,只返回受影响的行数,所以更适合执行创建、更新、删除操作。.SqlQuery()则会返回查询到的结果,并将结果保存在数据实体中,所以更适合执行查询操作。

请注意,Code First 不支持映射到存储过程或函数。 但是,可以使用上述两个方法调用存储过程或函数。

性能优化

当数据库上下文检索表行并创建表示它们的实体对象时,默认情况下,它会跟踪内存中的实体是否与数据库中的内容同步。 更新实体时,内存中的数据充当缓存并使用该数据。 在 Web 应用程序中,此缓存通常是不必要的,因为上下文实例通常生存期较短(创建新的实例并用于处理每个请求),并且通常在再次使用该实体之前处理读取实体的上下文。

可以通过调用 AsNoTracking 方法禁用对内存中的实体对象的跟踪。 可能想要执行的典型方案包括以下操作:

  • 在上下文生存期内,不需要更新任何实体,并且不需要 EF 自动加载具有由单独的查询检索的实体的导航属性。 在控制器的 HttpGet 操作方法中经常遇到这些情况。
  • 正在运行检索大量数据的查询,将只更新一小部分返回的数据。 关闭对大型查询的跟踪可能更有效,稍后为少数需要更新的实体运行查询。
  • 想要附加一个实体来更新它,但之前为了其他目的,已检索了相同的实体。 由于数据库上下文已跟踪了该实体,因此无法附加要更改的实体。 处理这种情况的一种方法是在早前的查询上调用 AsNoTracking

有关详细信息,请参阅跟踪与非跟踪

异步代码

异步编程是 ASP.NET Core 和 EF Core 的默认模式。

Web 服务器的可用线程是有限的,而在高负载情况下,可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,异步代码使得服务器更有效地使用资源,并且该服务器可以无延迟地处理更多流量。

异步代码在运行时,会引入的少量开销,在低流量时对性能的影响可以忽略不计,但在针对高流量情况下潜在的性能提升是可观的。

查看生成的SQL 语句

即在配置文件的 Logging中加上一句"Microsoft.EntityFrameworkCore.Database.Command": "Information" 即可,这样程序运行后,相关SQL 语句就会在VS的输出窗口中显示出来。

ASP.NET Core 包含依赖关系注入。 在应用启动过程中通过依赖项注入来注册相关服务,例如 EF 数据库上下文。 需要这些服务的组件(如 MVC 控制器)可以通过构造函数参数来获得这些服务。

查询加载导航属性

DataSet对象的IncludeThenInclude 方法使上下文加载 Student.Enrollments 导航属性,并在每个注册中加载 Enrollment.Course 导航属性。 就是说一个实体对象X有个导航属性A,A里面又有个导航属性B,那么查询X的时候,可以通过上面两个方法,直接加载出A的数据,和A中的B的数据。

其实有三种方式加载实体的导航属性:

预先加载

  • 通过 Lnclude 和 ThenInclude 方法
  • 通过 Load 方法

显示加载

不咋理解。

推迟加载

不咋理解。

其它方法

对指定的表生成实体类(反向工程)

反向工程是基于数据库架构搭建实体类型类和 DbContext 类基架的过程。 可使用 EF Core 包管理器控制台 (PMC) 工具的 Scaffold-DbContext 命令或 .NET 命令行接口 (CLI) 工具的 dotnet ef dbcontext scaffold 命令执行这一过程。

先安装NuGet:Microsoft.EntityFramework.Design

程序包控制台:

Scaffold-DbContext "Server=(localdb)\\mssqllocaldb;Database=Blogging;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Tables "Blog","Post" -ContextDir Context -Context BlogContext -ContextNamespace New.Namespace

OutputDir 后指定了输出模型类的文件夹路径(相对于项目根目录), Tables 后面跟指定的表名 ,还指定了上下文类的文件夹,名称,命名空间。

参考:

反向工程 - EF Core | Microsoft Docs

EF Core 工具参考(包管理器控制台)- EF Core | Microsoft Docs

EF Core 工具参考 (.NET CLI) - EF Core | Microsoft Docs

迁移

如果数据库已经存在,且有了一些数据。那么当实体模型变更时(如新增或添加了一个列),要想保持程序和数据库表结构的一致,且保留原有数据,则需要用到迁移。(而不是删除掉原有的数据库重新建库)。

使用PMC命令或CLI执行迁移,都会生成一个名为 timestamp_InitialCreate.cs 的类文件,里面有个Up方法和Down方法。迁移调用 Up 方法为迁移实现数据模型更改。 输入用于回退更新的命令时,迁移调用 Down 方法。

自定义表字段类型

可以使用指定格式化、验证和数据库映射规则的特性来自定义数据模型,即对c#实体类的属性添加特性。

还可使用特性来控制类和属性映射到数据库的方式。

特性 作用 示例
DataType 用于指定比数据库内部类型更具体的数据类型。 [DataType(DataType.Date)]
DisplayFormat 传达数据在屏幕上的呈现方式。 [DisplayFormat(DataFormatString = "0:yyyy-MM-dd", ApplyFormatInEditMode = true)]
ApplyFormatInEditMode 指定在文本框中显示值以进行编辑时也应用格式。 同上
StringLength 设置数据库中的最大长度. [StringLength(50)]
RegularExpression 输入限制。 [RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
Column 当前属性名对应的数据库表的列名。默认属性名即列名。 [Column("FirstName")]
Required 必填字段 [Required]
Key 主键标识。作为主键的属性名称不符合 IDclassnameID 命名约定时使用。 [Key]

值类型(DateTime、int、double、float 等)等不可为 null 的类型不需要 Required 特性。 系统会将不可为 NULL 的类型自动视为必填字段。Required 特性必须与 MinimumLength 结合使用才能强制执行 MinimumLength

ModelState.IsValid 这个静态属性,用于验证当前模型绑定的对象,是否每个字段值都符合要求。

联合主键

由于外键不可为 null,且它们共同唯一标识表的每一行,因此不需要单独的主键。 InstructorIDCourseID 属性应充当组合主键 。 标识 EF 组合主键的唯一方法是使用 Fluent API(无法借助特性来完成)。

SQL语句中设置联合主键:

create table userOURealation(
  userid int not null,
  nodeid varchar(20) not null,
  primary key(userid,nodeid)
)
--注意,联合主键用到的列,必须为 not null

一个重要的级联删除注意事项

参见:教程:创建复杂数据模型 - ASP.NET MVC 和 EF Core | Microsoft Docs

如何处理多个用户同时更新同一实体时出现的冲突。

两用户同时想要修改同一条数据,即当一用户在上一用户的更改写入数据库之前更新同一实体的数据时,会发生并发冲突。 如果不启用此类冲突的检测,则最后更新数据库的人员将覆盖其他用户的更改。 在许多应用程序中,此风险是可接受的:如果用户很少或更新很少,或者一些更改被覆盖并不重要,则并发编程可能弊大于利。 在此情况下,不必配置应用程序来处理并发冲突。 EF默认在并发时,只保留最后一个的更改

参考:教程:处理并发 - ASP.NET MVC 和 EF Core | Microsoft Docs

检测并发冲突的方案

你可以通过处理 Entity Framework 引发的 DbConcurrencyException 异常来解决冲突。 为了知道何时引发这些异常,Entity Framework 必须能够检测到冲突。 因此,你必须正确配置数据库和数据模型。 启用冲突检测的某些选项包括:

  • 数据库表中包含一个可用于确定某行更改时间的跟踪列。 然后可配置 Entity Framework,将该列包含在 SQL Update 或 Delete 命令的 Where 子句中。

    跟踪列的数据类型通常是 rowversionrowversion 值是一个序列号,该编号随着每次行的更新递增。 在 Update 或 Delete 命令中,Where 子句包含跟踪列的原始值(原始行版本)。 如果正在更新的行已被其他用户更改,则 rowversion 列中的值与原始值不同,这导致 Update 或 Delete 语句由于 Where 子句而找不到要更新的行。 当 Entity Framework 发现 Update 或 Delete 命令没有更新行(即受影响的行数为零)时,便将其解释为并发冲突。

具体实施方案:

现在实体中添加属性:

//下面这个属性用于检测并发冲突.Timestamp 属性指定此列将包含在发送到数据库的 Update 和 Delete 命令的 Where 子句中。
1685073621
public byte[] RowVersion  get; set; 

然后由于实体变更,执行迁移(下面通过.NET CLI):

在项目文件夹中,运行cmd,在cmd中输入以下命令。

dotnet ef migrations add RowVersion

dotnet ef database update


更新于:2023.5.26

EF学习笔记:更新关联数据

学习笔记主目录链接:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理)

上一篇链接:EF学习笔记(七):读取关联数据

本篇原文链接:Updating Related Data

本篇主要考虑对于有关联的数据进行新增、删除、更新操作;比如Course 、Instructor;

对于Course来说,新增时候必须定义属于哪个Department,所以在新增、更新操作的时候,必须要用户选择Department;

MVC5在选择基础控制器及视图框架的时候,如果选择EF的操作框架,则会自动带一部分基础代码,比如Course的Create\\Edit直接就带上了Department的下拉列表选择框;

但是对于一些错误情况处理不够以及没有做显示排序,原文做了些调整优化;

public ActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID")]Course course)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.Courses.Add(course);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    }
    catch (RetryLimitExceededException /* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.)
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Course course = db.Courses.Find(id);
    if (course == null)
    {
        return HttpNotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var courseToUpdate = db.Courses.Find(id);
    if (TryUpdateModel(courseToUpdate, "",
       new string[] { "Title", "Credits", "DepartmentID" }))
    {
        try
        {
            db.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
        }
    }
    PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
    return View(courseToUpdate);
}

private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
    var departmentsQuery = from d in db.Departments
                           orderby d.Name
                           select d;
    ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
}

PopulateDepartmentsDropDownList方法给视图传递了一个用Name排序过的Department列表;

并且处理了新建的时候 选中的Department为空的情况;

Create、Edit的Post请求中都对异常情况做了处理,并同时在下拉框还保持原先的选择;

另外,在Create、Edit的视图中,对于Department的标题也需要从DepartmentID改为Department :

<div class="form-group">
     <label class="control-label col-md-2" for="DepartmentID">Department</label>
     @*@Html.LabelFor(model => model.DepartmentID, "DepartmentID", htmlAttributes: new { @class = "control-label col-md-2" })*@
     <div class="col-md-10">
          @Html.DropDownList("DepartmentID", null, htmlAttributes: new { @class = "form-control" })
          @Html.ValidationMessageFor(model => model.DepartmentID, "", new { @class = "text-danger" })
     </div>
</div>

另外,对于Create视图,EF默认框架建立的视图会根据主关键字段是不是自动列来采用不同的方式;
如果定义为自动增加列,则不需要用户输入,由数据库自增长;

如果定义为非自动增长列,则需要用户手动,则会提供一个输入框;

但对于Edit视图,EF默认框架建立的视图不会明文显示主关键字段,因为它不知道这个字段到底有没有显示意义,它只用一个隐藏字段来包含主关键字段数据;

所以要手动增加显示主关键字段,但是就算自己手动增加显示主关键字段,也不可以删除那个隐藏字段!

@Html.ValidationSummary(true, "", new { @class = "text-danger" })
@Html.HiddenFor(model => model.CourseID)

<div class="form-group">
     @Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
     <div class="col-md-10">
          @Html.DisplayFor(model => model.CourseID)
     </div>
</div>

<div class="form-group">
     @Html.LabelFor(model => model.Title, htmlAttributes: new { @class = "control-label col-md-2" })
     <div class="col-md-10">
          @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } })
          @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" })
     </div>
</div>

同样,在Delete、Detail视图中,需要加入CourseID的显示:

<dt>
    @Html.DisplayNameFor(model => model.Department.Name)
</dt>
<dd> @Html.DisplayFor(model => model.Department.Name) </dd> <dt> @Html.DisplayNameFor(model => model.CourseID) </dt> <dd> @Html.DisplayFor(model => model.CourseID) </dd> <dt> @Html.DisplayNameFor(model => model.Title) </dt> <dd> @Html.DisplayFor(model => model.Title) </dd>

Course 修改效果:
技术分享技术分享

技术分享

 






以上是关于EF6学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

EF6学习笔记

EF6 学习笔记:操练 CRUD 增删改查

EF6 学习笔记:Code First 方式生成数据库及初始化数据库实际操作

EF学习笔记:更新关联数据

EF学习笔记:实施继承

EF学习笔记:读取关联数据