Entity Framework Core

EF 6.x 和 EF Core有一些不同,详细区别参见官方文档比较 EF Core 和 EF6

开发方式

Entity Framework Core支持两种开发方式

  • Code-First
  • Database-First

EF Core主要针对代码优先的方法,对数据库优先的方法提供的支持很少,因为在EF Core 2.0中不支持DB模型的可视化设计器或向导。
1

EF Core支持特性有:

  • DbContext & DbSet
  • Data Model
  • Querying using Linq-to-Entities
  • Change Tracking
  • SaveChanges
  • Migrations

安装EF

安装 EF Core DB Provider

EF Core允许我们通过提供者模型访问数据库。对于不同的数据库,有不同的EF核心DB提供程序可用。这些提供程序可以作为NuGet包使用。
此处以Ms SQL Server Database为例,需要在NuGet中安装名为Microsoft.EntityFrameworkcore.SqlServer的NuGet安装包

1.右键单击解决方案中的项目,选择选择NuGet程序包
图片01
2.搜索Microsoft.EntityFrameworkcore.SqlServer,点击安装
图片02
3.接受许可证
图片03
4.安装完成
图片4

同时,也可以通过 工具->NuGet包管理器->程序包管理器控制台 输入以下命令安装

1
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer

安装 EF Core Tools

安装EF Core Tools以使用EF core命令。同上,打开NuGet UI,安装名为Microsoft.EntityFramworkCore.Tools的NuGet安装包
05
至此,成功安装EF Core到解决方案中

使用DB-First方法

在使用DB-First方法之前,需要先建立完整的数据库,然后在PMC(程序包管理控制台)中使用以下命令:

1
PM> Scaffold-DbContext "Data Source=ServerName;Database=SchoolDB;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

第一个参数为连接字符串,其中Data Source为服务器名称,Database为数据库名称;第二个参数为provider名称,此处使用Microsoft.EntityFrameworkCore.SqlServer;第三个参数为EF Core创建的POCO类输出的所在文件夹。

EF Core会自动创建POCO类
图片6

可参考命令:

Scaffold-DbContext [-Connection] [-Provider] [-OutputDir] [-Context] [-Schemas>] [-Tables>][-DataAnnotations][-Force] [-Project] [-StartupProject]

可以使用以下命令获取帮助

get-help scaffold-dbcontext -detailed

DbContext

DbContext的实例表示与数据库的会话,可以使用该会话查询实体的实例并将其保存到数据库中。DbContext是工作单元和存储库模式的组合。

DbContext在EF Core中执行以下任务:

  • 管理数据库连接
  • 配置模型和关系
  • 查询数据库
  • 保存数据到数据库
  • 配置变化追踪
  • 缓存
  • 事务管理

首先需要创建派生自DBContext的类,cntext类中为每个实体创建DbSet属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public partial class SchoolDBContext : DbContext
{
public SchoolDBContext()
{
}

public SchoolDBContext(DbContextOptions<SchoolDBContext> options)
: base(options)
{
}

public virtual DbSet<Course> Course { get; set; }
public virtual DbSet<Standard> Standard { get; set; }
public virtual DbSet<Student> Student { get; set; }
public virtual DbSet<StudentAddress> StudentAddress { get; set; }
public virtual DbSet<Teacher> Teacher { get; set; }
}

我们需要创建context类来连接数据库,并其中的学生或课程等数据进行CRUD

DbContext方法:

  • Add
    • 使用Add的状态向DbContext添加一个新实体,并开始跟踪它。当调用SaveChanges()时,这个新的实体数据将被插入到数据库中。
  • AddAsync
    • 异步方法,用于将具有Add状态的新实体添加到DbContext并开始跟踪它。当调用SaveChangesAsync()时,这个新的实体数据将被插入到数据库中。
  • AddRange
    • 使用Add的状态向DbContext添加一组新实体,并开始跟踪它。当调用SaveChanges()时,这个新的实体数据将被插入到数据库中。
  • AddRangeAsync
    • 用于Add将保存在SaveChangesAsync()上的新实体集合的异步方法。
  • Attach
    • 将新实体或现有实体以未更改的状态附加到DbContext并开始跟踪它。
  • AttachAsync
    • 一组新实体或现有实体以未更改的状态附加到DbContext,并开始跟踪它。
  • Entry
    • 获取给定实体的EntityEntry。该条目提供对实体更改跟踪信息和操作的访问。
  • Find
    • 查找具有给定主键值的实体。
  • FindAsync
    • 用于查找具有给定主键值的实体的异步方法。
  • Remove
    • 将指定实体设置为删除属性,该实体将在调用SaveChanges()时删除数据。
  • RemoveRange
    • 将一组实体设置为删除状态,当调用SaveChanges()时,这些实体将在单个DB往返过程中删除数据。
  • SaveChanges
    • 为添加、修改或删除状态的实体向数据库执行INSERT、UPDATE或DELETE命令。
  • SaveChangesAsync
    • SaveChanges的异步方法
  • Set
    • 创建一个DbSet,可用于查询和保存TEntity实例。
  • Update
    • 将断开连接的实体连接到修改后的状态,并开始跟踪它。调用SaveChagnes()时将保存数据。
  • UpdateRange
    • 用修改后的状态附加一组断开连接的实体,并开始跟踪它。调用SaveChagnes()时将保存数据。
  • OnConfiguring
    • 重写此方法来配置用于此上下文的数据库(和其他选项)。对于创建的上下文的每个实例都调用此方法。
  • OnModelCreating
    • 重写此方法,以进一步配置根据约定从派生上下文中DbSet属性中公开的实体类型发现的模型。

DbContext属性

  • ChangeTracker
    • 提供对此上下文跟踪的实体实例的信息和操作的访问
  • Database
    • 提供对此上下文的数据库相关信息和操作的访问
  • Model
    • 返回关于实体的形状、它们之间的关系以及它们如何映射到数据库的元数据。

EF Core控制台程序演示

创建一个新的控制台应用程序。 文件->新建-》项目->控制台应用(.net Core),使用PMC安装SQL Server Provider安装包:

Install-package Microsoft.EntityFrameworkCore.SqlServer

安装详情可查看前文 安装EF

创建模型

EF Core需要一个实体数据模型来与数据库进行交互。它会根据领域类来创建模型。EF Core基于您使用的提供程序构建存储模型和映射。EF将此模型用于底层数据库的CRUD(创建、读取、更新、删除)操作。
创建实体类:

1
2
3
4
5
6
7
8
9
10
11
12
//课程类
public class Course
{
public int CourseId { get; set; }
public String CourseName { get; set; }
}
//学生类
public class Student
{
public int StudentId { get; set; }
public String Name { get; set; }
}

创建context类:

1
2
3
4
5
6
7
8
9
10
class SchoolContext: Microsoft.EntityFrameworkCore.DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=ServerName;Database=DatabaseName;Trusted_Connection=True;");
}
}

在重写的OnConfiguring方法中,使用UseSqlServer指定要连接的数据库,连接字符串中Server为服务器名称,Database为数据库名称。
在添加完实体和context之后,通过迁移来添加一个数据库。

迁移

此时假定还没有数据库,我们需要创建一个SchoolDB,在PMC中使用以下命令:

PM> add-migration CreateSchoolDB

此时就会在项目中船舰一个名为Migrations的新文件夹,如下图所示:
图片07

迁移之后,在PMC中使用以下命令创建数据库:

PM> update-database -verbose

此时数据库也成功创建
图片8

查询

函数查询

在EF Core中允许使用函数查询,这在EF 6中是不允许的。

1
2
3
4
5
6
7
8
9
pravite static void Main(String[] args){
var context=new SchoolContext();
var StudentWithSameName=context.Students
.Where(S=>s.FristName==GetName())
.ToList();
}
public static String GetName(){
return "zhangsan";
}

立即加载

使用include()方法来实现立即加载,与EF6不同的是EF Core支持lambda表达式设定导航参数。如下所示:

1
2
3
4
5
var context=new SchoolContext();
vat studentWithGrade=context.Students
.Where(s=>s.FirstName=="Bill")
.Include(s=>s.Grade)
.FirstOrDefault();

当然,也可以在Include()中指定字符串为”Grade”,同EF 6一样,但是不建议使用该操作,容易引发异常。
EF Core 2.0版本中,不能在Find()方法后使用Include()方法,例如,不能采用 context.Students.Find(1).Include()。

EF Core中引入了新的ThenInclude属性,在使用时,先调用Include()方法,之后再调用ThenInclude()方法。如下所示:

1
2
3
4
5
var context = new SchoolContext();
var student = context.Students.Where(s=>s.FirstName=="Bill")
.Include(s=>s.Grade)
.ThenInclude(g=>g.Teachers)
.FirstOrDefault();

投影查询

我们也可以使用投影加载来代替Include()和ThenInclude()方法。如下所示:

1
2
3
4
5
6
7
8
9
var context=new SchoolContext();
var student=context.Students.Where(s=>s.FirstName=="Bill")
.Select(s=> new
{
Student=s,
Grade=s.Grade,
GradeTeachers=s.Grade.Teachers
})
.FirstOrDefualt();

显式加载和延迟加载

EF Core2.0中,显示加载同EF 6,但不在支持延迟加载(Lazy Loading)。

保存数据

保存数据分为在连接的场景中保存数据和在非连接的场景中保存数据,此处为在连接的场景中保存数据。
EF Core会根据EntityState的状态进行CRUD操作,在连接的场景中,DbContext的一个实例跟踪所有实体,因此每当创建、修改或删除实体时,它都会自动设置每个实体的适当EntityState

下图演示了CUD(增删改)操作过程:
CUD

插入数据

使用DbSet.Add()DbContext.Add将实体状态设置为Added,并在调用SaveChanges()后将数据提交到数据库。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
using(var context=new SchoolContext())
{
var student=new Student()
{
FirstName="Bill",
LastName="Gates"
};
context.Students.Add(student);
//也可以使用以下形式
//context.Add<Student>(student);

context.SaveChages();
}

更新数据

在此场景(连接的)中,TrakingChanges自动跟踪实体状态,当对数据作出修改时,会将EntityState设置为Modified,并在调用SaveChangs()后将数据提交到数据库。如下所示:

1
2
3
4
5
6
7
using(var context=new SchoolContext)
{
var student=context.Students.First<Student>(1);
std.FirstName="Steve";

context.SaveChanges();
}

示例中,我们只修改了FirstName属性,并未对其他属性作出修改,所以保存到数据库中时也只修改该属性。

删除数据

使用DbSet.Remove()DbContext.Remove()方法删除数据。TrackingChanges将EntityState设置为Deleted,在调用SaveChanges()之后将请求发送到数据库。如下所示:

1
2
3
4
5
6
7
8
using(var context=new SchoolContext())
{
var student=context.Students.First<Student>();
context.Students.Remove(student);
//或采用以下方式
//context.Remove<Student>(student);
context.SaveChanges();
}

配置

约定优先于配置(Convention over Configuration)。有两种方法配置域类:

  • 注解
  • Fluent API

注解

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Table("StudentInfo")]  //创建表
public class Student
{
public Student() { }

[Key] //设置主键
public int SID { get; set; }

[Column("Name", TypeName="ntext")] //设置列
[MaxLength(20)] //设置长度
public string StudentName { get; set; }

[NotMapped] //不映射
public int? Age { get; set; }


public int StdId { get; set; }

[ForeignKey("StdId")] //设置外键
public virtual Standard Standard { get; set; }
}

Fluent API

EF Core使用Fluent API 来重写域类的约定。可以通过ModelBuilder类来配置比注解更多的东西。
EF Core的Fluent API有以下三个方面来配置模型:

  • 模型配置
    • HasDbFluent():在针对关系数据库时配置数据库函数。
    • HasDefaultSchema():指定数据库模式
    • HasAnnotation():向实体添加或修改注解属性
    • HasSequence():配置关系型数据库序列
  • 实体配置
    • HasAlternateKey():在EF模型中为实体配置一个备用键
    • HasIndex():配置指定属性的索引
    • HasKey():将属性或属性列表配置为主键
    • HasMany:配置一对多多对多关系中的部分
    • HasOne():配置一对多一对一关系中的部分
    • Ignore():配置类或属性不应映射到表或列
    • OwnsOne():
    • ToTable():配置实体映射到的数据库表
  • 属性配置
    • HasCloumnName():在数据库中为属性配置相应的列名
    • HasColumnType():在数据库中为属性配置相应的数据类型
    • HasComputedColumnSql():在关系型数据库中,配置要映射到数据库中计算列的属性
    • HasDefaultValue():配置关系型数据库中属性映射到列的默认值
    • HasDefaultValueSql():配置关系型数据库中属性映射到列的默认值的表达式
    • HasField():指定要与属性一起使用的支持字段
    • HasMaxLength():配置可存储在属性中的数据最大长度
    • IsConcurrencyToken():配置要用作乐观并发令牌的属性。
    • IsRequired():配置是否需要属性的有效值或null是否为有效值。
    • IsRowVersion():配置要用于乐观并发检测的属性。
    • IsUnicode():配置是否包含unicode字符的字符串属性
    • ValueGeneratedNever():配置在保存实体时不能生成值的属性
    • VakueGeneratedOnAdd():配置属性在保存新实体时具有生成的值。
    • ValueGeneratedOnAddOrUpdate():配置属性在保存新实体或现有实体时具有生成的值
    • ValueGeneratedOnUpdate():配置属性在保存现有实体时具有生成的值。

Fluent API 配置

重写OnModelCreating放啊,并使用ModelBuilder类型的modelBuilder的类来配置域类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SchoolDbContext:DbContext
{
public DbSet<Studnet> Students{get;set;}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//Write Fluent API configurations here
//Property Configurations
modelBuilder.Entity<Student>()
.Property(s=>s.StudentId)
.HasColumnName("Id")
.HasDefaultValue(0)
.IsRequired();
{
}

在EF Core中使用Fluent API配置一对多关系

EF Core中已经包含了足够的约定来自动配置一对多的关系,但也可以用Fluent API来配置它。

EF Core使Fluent API配置关系变得很容易。参考以下学生和年级类,其中年级实体包含许多学生实体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Student
{
public int Id { get; set; }
public string Name { get; set; }

public int CurrentGradeId { get; set; }
public Grade Grade { get; set; }
}

public class Grade
{
public int GradeId { get; set; }
public string GradeName { get; set; }
public string Section { get; set; }

public ICollection<Student> Students { get; set; }
}

使用Fluent API 来重写OnModelCreating()方法,从而配置以上实体间多对多的关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SchoolContext:DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=ServerName;Database=DatabaseName;Trusted_Connection=True");
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasOne<Grade>(s=>s.Grade)
.WithMany(g=>g.Studnets)
.HasForeignKey(s=>s.CurrentGradeId);
}

public Dbset<Grade> Grades{get;set;}
public Dbset<Student> Students{get;set;}
}

流程
1.首先我们需要配置一个实体类,要么学生类或者年级类,所以modelBuilder.Entity<Student>()Student类开始。
2..HasOne<Grade>(s=>s.Grade)指定学生实体包含一个名为Grade``的年级类型属性。 3..WithMany(g=>g.Studnets)指定年级实体包含许多Student实体。WithMany推断集合导航属性。 4..HasForeignKey`指定外键名称。只有当依赖项中有外键Id属性时才能使用。
如图所示:
流程图

在EF Core中使用Fluent API 配置级联删除

级联删除在删除相关父行时自动删除子行。例如,如果删除了一个年级,那么该年级的所有学生也应该自动从数据库中删除。演示如下:

1
2
3
4
5
modelBuilder.Entity<Grade>()
.HasMany<Student>(g => g.Students)
.WithOne(s => s.Grade)
.HasForeignKey(s => s.CurrentGradeId)
.OnDelete(DeleteBehavior.Cascade);

OnDelete方法来级联删除使用了DeleteBehavior参数,我们可以根据需求选择以下DeleteBehaivor的值:

  • Cascade: 主体实体被删除事,依赖实体也会被删
  • ClientSetNull:将依赖实体中的外键属性设置为Null
  • Restrict:防止级联删除
  • SetNull:依赖实体中的外键属性值将设置为null

在EF Core中使用Fluent API配置一对一关系

通常,不需要手动配置一对一关系,因为EF Core包含一对一关系的约定。但是,如果键或外键属性不遵循约定,则可以使用注解或Fluent API来配置两个实体之间的一对一关系。
StudentStudentAddress为例,他们不遵循外键约定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Student
{
public int Id { get; set; }
public string Name { get; set; }

public StudentAddress Address { get; set; }
}

public class StudentAddress
{
public int StudentAddressId { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Country { get; set; }

public int AddressOfStudentId { get; set; }
public Student Student { get; set; }
}

在EF Core中使用Fluent API配置一对一关系,可以使用HasOneWithOneHasForenginKey方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SchoolContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=.\\SQLEXPRESS;Database=EFCore-SchoolDB;Trusted_Connection=True");
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasOne<StudentAddress>(s => s.Address)
.WithOne(ad => ad.Student)
.HasForeignKey<StudentAddress>(ad => ad.AddressOfStudentId);
}

public DbSet<Student> Students { get; set; }
public DbSet<StudentAddress> StudentAddresses { get; set; }
}

1.modelBuilder.ENtity<Student>()开始配置学生实体。
2..HasOne<StudentAddress>方法指定Student实体包含一个使用lambda表达式的Student address引用属性。
3..WithOne配置关系的另一端StudentAddress实体。它指定Student address实体包含Student类型的引用导航属性
4.HasForeignKey<StudentAddress>(ad=ad.AddressOfStudentId)指定外键属性名称。
如下图所示:
流程图

在EF Core中使用Fluent API配置多对多关系

让我们实现以下学生和课程实体之间的多对多关系,其中一个学生可以注册多个课程,同样,一个课程可以由多个学生加入。

1
2
3
4
5
6
7
8
9
10
11
12
public class Student
{
public int StudentId { get; set; }
public string Name { get; set; }
}

public class Course
{
public int CourseId { get; set; }
public string CourseName { get; set; }
public string Description { get; set; }
}

数据库中的多对多关系由一个包含两个表的外键的连接表表示。此外,这些外键是组合主键。
示意图
在EF Core中没有可用的自动配置多对多关系的约定,所以需要使用Fluent API手动配置。

我们必须为连接表创建连接实体类。上述学生和课程实体的连接实体应包括一个外键属性和每个实体的一个参考导航属性。配置多对多关系的步骤如下:

  • 定义一个新的连接实体类,其中包含每个实体的外键属性和引用导航属性
  • 在实体的两侧(在本例中是Student和Course)包含一个集合导航属性来定义两个实体和新实体间的一对多关系
  • 使用Fluent API将连接实体中的两个外键配置为组合键
1
2
3
4
5
6
7
8
9
	//首先定义新实体 StudentCourse
public class StudentCourse
{
public int StudentId{get;set;}
public Student Student{get;set;}

public in CourseId{get;set;}
public Course Course{get;set;}
}

上述新实体StudentCourse中包含了引用导航属性StudentCourse,同时也分别包含了外键属性StudentIdCourseId(外键遵循约定)。
现在我们需要分别实现Student---StudentCourseCourse---StudentCourse的一对多关系,方式与前面上述一对多关系实现方法相同。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Student
{
public int StudentId{get;set;}
public String Name{get;set;}

public IList<StudentCourse> StudentCourse{get;set;}
}

public class Course
{
public int CourseId{get;set;}
public String Course{get;set;}
public String Description{get;set;}

public Ilist<StudentCourse> StudentCourse{get;set;}
}

如上,StudentCourse实体中现在都包含有StudentCourse类型的集合导航属性。而在StudentCourse中又包含有两个实体的外键属性和导航属性。这让Student---StudentCoureseCourse---StudentCourse成为完全的一对多关系。现在,只能使用Flunet API来将新实体中的外键转换成组合主键。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SchoolContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=.\\SQLEXPRESS;Database=EFCore-SchoolDB;Trusted_Connection=True");
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StudentCourse>().HasKey(sc => new { sc.StudentId, sc.CourseId });
}

public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
public DbSet<StudentCourse> StudentCourses { get; set; }
}

上述过程中,modelBuilder.Entity<StudentCourse>().HasKey(sc => new { sc.StudentId, sc.CourseId });StudentIdCourseId转换为组合主键。

如果实体遵循连接实体的一对多关系的约定,则可以通过这种方式配置多对多关系。假设外键属性名称不遵循约定(例如SID代替StudentId, CID代替CourseId),那么可以使用Fluent API配置它,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
modelBuilder.Entity<StudentCourse>().HasKey(sc => new { sc.SId, sc.CId }); //设置SId和CId为主键

modelBuilder.Entity<StudentCourse>() //配置Student和StudentCourse的一对多关系
.HasOne<Student>(sc => sc.Student)
.WithMany(s => s.StudentCourses)
.HasForeignKey(sc => sc.SId);


modelBuilder.Entity<StudentCourse>()//配置Course和StudentCourse的一对多关系
.HasOne<Course>(sc => sc.Course)
.WithMany(s => s.StudentCourses)
.HasForeignKey(sc => sc.CId);

在断开连接的场景中操作数据

插入数据

在断开连接的场景中,无法使用TrackingChange来自动跟踪实体状态,Context不知道实体是否添加或者修改,因此需要将实体附加到DbContext的同时设定实体状态。如下图所示:
示意图

示例中,需要使用EntityState设置实体状态属性,DbContext将在调用SaveChanges()方法后,依照设定的实体状态对数据库进行操作。例如设定EntityStateAdded,在调用SaveChanges()方法后,数据库会执行Insert操作。

断开场景中执行操作可分为以下步骤:

  • 使用适当的EntityState将实体附加到DbContext中。
  • 调用SaveChanges()方法。

下面演示插入操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//在断开连接的场景中的插入操作演示
var std = new Student(){ Name = "Bill" };

using (var context = new SchoolContext())
{
//1. 将"Added"状态的实体附加到context中
context.Add<Student>(std);

//或者采用以下方法
// context.Students.Add(std);
// context.Entry<Student>(std).State = EntityState.Added;
// context.Attach<Student>(std);

//2. 调用SaveChanges()方法
context.SaveChanges();
}
DbContext方法 DbSet方法 描述
DbContext.Attach DbSet.Attach 将一个实体附加到DbContext中,为键属性有值的实体设置Unchaged属性,为键属性为空或默认值的实体添加Added属性
DbContext.Add DbSet.Add 将一个实体附加到DbContex中,并设为Added状态
DbContext.AddRange DbSet.AddRange 将一组实体附加到DbContex中,并设为Added状态
DbContext.Entry - 获取提供对更改跟踪信息和访问操作的实体的EntityEntry
DbContext.AddAsync DbSet.AddAsync Add方法的异步方法
DbContext.AddRange DbSet.AddRange AddRange方法的异步方法

插入关联数据
使用DbContextDbSet将实体添加到数据库中。Add方法将实体附加到context中,并将添加的状态设置为 id 属性为空,null,或者默认值。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var stdAddress = new StudentAddress()
{
City = "SFO",
State = "CA",
Country = "USA"
};

var std = new Student()
{
Name = "Steve",
Address = stdAddress
};
using (var context = new SchoolContext())
{
// Attach an entity to DbContext with Added state
context.Add<Student>(std);

// Calling SaveChanges to insert a new record into Students table
context.SaveChanges();
}

插入多条记录

AddRange方法 描述
void AddRange(IEnumerable entities) 将相同或不同类型的实体集合添加到具有Added属性的DbContext中
void AddRange(param object[] entities) 将相同或不同类型的实体数组添加到具有Added属性的DbContext中
void AddRangeAsync(IEnumerable, CancellationToken) 异步方法将相同或不同类型的实体集合添加到具有添加状态的DbContex

如下所示:

1
2
3
4
5
6
7
8
9
10
var studentList = new List<Student>() {
new Student(){ Name = "Bill" },
new Student(){ Name = "Steve" }
};

using (var context = new SchoolContext())
{
context.AddRange(studentList);
context.SaveChanges();
}

同样,也可以添加不同类型的实体集合。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var std1 = new Student(){ Name = "Bill" };

var std2 = new Student(){ Name = "Steve" };

var computer = new Course() { CourseName = "Computer Science" };

var entityList = new List<Object>() {
std1,
std2,
computer
};

using (var context = new SchoolContext())
{
context.AddRange(entityList);

// or
// context.AddRange(std1, std2, computer);

context.SaveChanges();
}

更新数据

在断开连接场景中修改数据,我们不知道哪些实体经过修改,所以我们需要将修改后的实体的EntityState设置为Modified,并附加到DbContext中。

DbContext方法 DbSet方法 描述
DbContext.Update DbSet.Update 将一个实体以Modified状态添加到DbContext中
DbContext.UpdateRange DbSet.UpdateRange 将一组实体以Modified状态添加到DbContext中

下面的示例将演示这些方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//断开连接的学生实体
var stud = new Student(){ StudentId = 1, Name = "Bill" };

stud.Name = "Steve";

using (var context = new SchoolContext())
{
context.Update<Student>(stud);

// 也可以采用以下形式
// context.Students.Update(stud);
// context.Attach<Student>(stud).State = EntityState.Modified;
// context.Entry<Student>(stud).State = EntityState.Modified;

context.SaveChanges();
}

更新多个数据

使用DbContext.UpdateRangeDbSet.UpdateRange方法更新多个数据。如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var modifiedStudent1 = new Student()
{
StudentId = 1,
Name = "Bill"
};

var modifiedStudent2 = new Student()
{
StudentId = 3,
Name = "Steve"
};

var modifiedStudent3 = new Student()
{
StudentId = 3,
Name = "James"
};

IList<Student> modifiedStudents = new List<Student>()
{
modifiedStudent1,
modifiedStudent2,
modifiedStudent3
};

using (var context = new SchoolContext())
{
context.UpdateRange(modifiedStudents);

// or the followings are also valid
//context.UpdateRange(modifiedStudent1, modifiedStudent2, modifiedStudent3);
//context.Students.UpdateRange(modifiedStudents);
//context.Students.UpdateRange(modifiedStudent1, modifiedStudent2, modifiedStudent3);

context.SaveChanges();
}

在EF Core中,使用了Update方法能够更新数据,但如果更新前根实体或者子实体的键(Key)属性,则会被判定为是一个新的实体,EntityState属性将会被设置为Added状态。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static void Main()
{
var newStudent = new Student()
{
Name = "Bill"
};

var modifiedStudent = new Student()
{
StudentId = 1,
Name = "Steve"
};

using (var context = new SchoolContext())
{
context.Update<Student>(newStudent);
context.Update<Student>(modifiedStudent);

DisplayStates(context.ChangeTracker.Entries());
}
}
private static void DisplayStates(IEnumerable<EntityEntry> entries)
{
foreach (var entry in entries)
{
Console.WriteLine($"Entity: {entry.Entity.GetType().Name},
State: {entry.State.ToString()} ");
}
}

输出结果:
Entity:Student , State:Added
Entity:Student , State:Modified

上述例子中newStudent没有键属性(StudentID),因此Update方法将他设定为Added,而modifiedStudent有键属性,所以被设置为Modified

删除数据

EF Core中删除数据没有连接场景和断开连接场景的区别。

DbContext方法 DbSet方法 描述
DbContext.Remove DbSet.Remove 将指定的一个实体附加到具有已删除状态的DbContext,并开始跟踪它。
DbContext.RemoveRange DbSet.RemoveRange 将指定的一组实体附加到具有已删除状态的DbContext,并开始跟踪它。

异常:
如果Remeove()RemoveRange()中指定的实体中的键值在数据库中不存在,那么将会抛出异常。

1
2
3
4
5
6
7
8
9
10
var student = new Student() {
StudentId = 50 //假设StudentId=50的实体不存在
};

using (var context = new SchoolContext()) {

context.Remove<Student>(student);

context.SaveChanges();
}

上述示例会抛出以下异常:

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.

所以在删除数据前最好能先确保数据库中有存在的目标数据。也可以try...catch...一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var student = new Student() {
StudentId = 50
};

using (var context = new SchoolContext())
{
try
{
context.Remove<Student>(deleteStudent);
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
throw new Exception("Record does not exist in the database");
}
catch (Exception ex)
{
throw;
}
}

删除多条记录
可以通过DbContext.RemoveRange或`DbSet.RemoveRange来一次删除多条记录。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IList<Student> students = new List<Student>() {
new Student(){ StudentId = 1 },
new Student(){ StudentId = 2 },
new Student(){ StudentId = 3 },
new Student(){ StudentId = 4 }
};

using (var context = new SchoolContext())
{
context.RemoveRange(students);

// or
// context.Students.RemoveRange(students);

context.SaveChanges();
}

如果一个实体与其他实体存在关系(如一对一或一对多),则根实体被删除时相关数据是否删除取决于关系的配置方式。
例如,在Student和Grade实体中存在这一对多的关系,许多学生被划分为一个年级。当我们想删除包含有学生实体的年级时就会抛出异常。要解决这个问题可以通过配置Fluent API来解决。例如,可以配置关系为级联删除,如下所示:

1
2
3
4
5
modelBuilder.Entity<Student>()
.HasOne<Grade>(s => s.Grade)
.WithMany(g => g.Students)
.HasForeignKey(s => s.GradeId)
.OnDelete(DeleteBehavior.Cascade);

变更追踪器

EF Core中的变更追踪器(ChangeTracker)负责跟踪使用相同Context实例检索的每个实体的状态。能够标记的状态如下所示:

  • Added
  • Modified
  • Deleted
  • Unchanged
  • Detached

Unchanged状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void Main()
{
using (var context = new SchoolContext())
{
// retrieve entity
var student = context.Students.First();
DisplayStates(context.ChangeTracker.Entries());
}
}

private static void DisplayStates(IEnumerable<EntityEntry> entries)
{
foreach (var entry in entries)
{
Console.WriteLine($"Entity: {entry.Entity.GetType().Name},
State: {entry.State.ToString()} ");
}
}

//输出结果
Entity:Student,State:Unchanged

Added状态

在使用Add()Update()方法时,如果所添加的实体没有键属性,那么会被标记为Added状态。如下所示:

1
2
3
4
5
6
using (var context = new SchoolContext())
{
context.Add(new Student() { FirstName = "Bill", LastName = "Gates" });

DisplayStates(context.ChangeTracker.Entries());
}

//输出结果
Entity:Student,State:Added

Modified状态

如果在DbContext中的实体的任意属性被更改,那么会被标记为Modified状态。如下所示:

1
2
3
4
5
6
using (var context = new SchoolContext())
{
var student = context.Students.First();
student.LastName = "LastName changed";

DisplayStates(context.ChangeTracker.Entries());

//输出结果
Entity:Student,State:Modified

Deleted状态

使用Remove()RemoveRange()方法,将会被标记为Deleted状态

1
2
3
4
5
6
7
using (var context = new SchoolContext())
{
var student = context.Students.First();
context.Students.Remove(student);

DisplayStates(context.ChangeTracker.Entries());
}

//输出结果
Entity:Student,State:Deleted

Detached状态

断开连接的实体图

EF Core提供几种不仅将实体附加到context中,还可以更改断开连接实体图中每个实体的EntityState:

  • Attach()
  • Entry()
  • Add()
  • Update()
  • Remove()

Attach()

DbContext.Attach()DbSet.Attach()方法指定添加一个断开连接的实体图并开始追踪它。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void Main()
{
var stud = new Student() { //Root entity (empty key)
Name = "Bill",
Address = new StudentAddress() //Child entity (with key value)
{
StudentAddressId = 1,
City = "Seattle",
Country = "USA"
},
StudentCourses = new List<StudentCourse>() {
new StudentCourse(){ Course = new Course(){ CourseName = "Machine Language" } },//Child entity (empty key)
new StudentCourse(){ Course = new Course(){ CourseId = 2 } } //Child entity (with key value)
}
};

var context = new SchoolContext();
context.Attach(stud).State = EntityState.Added;

DisplayStates(context.ChangeTracker.Entries());
}

private static void DisplayStates(IEnumerable<EntityEntry> entries)
{
foreach (var entry in entries)
{
Console.WriteLine($"Entity: {entry.Entity.GetType().Name},
State: {entry.State.ToString()} ");
}
}

//输出结果
Entity: Student, State: Added
Entity: StudentAddress, State: Unchanged
Entity: StudentCourse, State: Added
Entity: Course, State: Added
Entity: StudentCourse, State: Added
Entity: Course, State: Unchanged

Entry()

DbContext.Entry()方法在EF Core和EF 6中有所不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var student = new Student() { //Root entity (empty key)
Name = "Bill",
Address = new StudentAddress() //Child entity (with key value)
{
StudentAddressId = 1,
City = "Seattle",
Country = "USA"
},
StudentCourses = new List<StudentCourse>() {
new StudentCourse(){ Course = new Course(){ CourseName="Machine Language" } },//Child entity (empty key)
new StudentCourse(){ Course = new Course(){ CourseId=2 } } //Child entity (with key value)
}
};

var context = new SchoolContext();
context.Entry(student).State = EntityState.Modified;

DisplayStates(context.ChangeTracker.Entries());

上述例子中,context.Entry(student).State = EntityState.Modified将实体附加到context中,不管实体是否有键属性,都将其设置为Modified状态。但不对子实体进行操作。

Add()

DbContext.Add()DbSet.Add()方法将实体图附加到context中,不论子实体或根实体中是否有键属性,将子实体或根实体的状态都设置为Added

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var student = new Student() { //Root entity (with key value)
StudentId = 1,
Name = "Bill",
Address = new StudentAddress() //Child entity (with key value)
{
StudentAddressId = 1,
City = "Seattle",
Country = "USA"
},
StudentCourses = new List<StudentCourse>() {
new StudentCourse(){ Course = new Course(){ CourseName="Machine Language" } },//Child entity (empty key)
new StudentCourse(){ Course = new Course(){ CourseId=2 } } //Child entity (with key value)
}
};

var context = new SchoolContext();
context.Students.Add(student);

DisplayStates(context.ChangeTracker.Entries());

//输出结果
Entity: Student, State: Added
Entity: StudentAddress, State: Added
Entity: StudentCourse, State: Added
Entity: Course, State: Added
Entity: StudentCourse, State: Added
Entity: Course, State: Added

Update()

DbContext.Update()DbSet.Context()根据根实体和子实体中是否有键值来设定状态,不论根实体或子实体中,有键属性则设置为Modified,没有键属性设置为Added。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var student = new Student() { //Root entity (with key value)
StudentId = 1,
Name = "Bill",
Address = new StudentAddress() //Child entity (with key value)
{
StudentAddressId = 1,
City = "Seattle",
Country = "USA"
},
StudentCourses = new List<StudentCourse>() {
new StudentCourse(){ Course = new Course(){ CourseName="Machine Language" } },//Child entity (empty key)
new StudentCourse(){ Course = new Course(){ CourseId=2 } } //Child entity (with key value)
}
};

var context = new SchoolContext();
context.Update(student);

DisplayStates(context.ChangeTracker.Entries());

Entity: Student, State: Modified
Entity: StudentAddress, State: Modified
Entity: StudentCourse, State: Added
Entity: Course, State: Added
Entity: StudentCourse, State: Added
Entity: Course, State: Modified

Remove()

DbContextDbSet方法将根实体设置为Deleted状态。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var student = new Student() { //Root entity (with key value)
StudentId = 1,
Name = "Bill",
Address = new StudentAddress() //Child entity (with key value)
{
StudentAddressId = 1,
City = "Seattle",
Country = "USA"
},
StudentCourses = new List<StudentCourse>() {
new StudentCourse(){ Course = new Course(){ CourseName="Machine Language" } },//Child entity (empty key)
new StudentCourse(){ Course = new Course(){ CourseId=2 } } //Child entity (with key value)
}
};

var context = new SchoolContext();
context.Remove(student);

DisplayStates(context.ChangeTracker.Entries());

Entity:Student,State:Deleted
Entity: StudentAddress, State: Unchanged
Entity: StudentCourse, State: Added
Entity: Course, State: Added
Entity: StudentCourse, State: Added
Entity: Course, State: Unchanged

跟踪实体图

EF Core2.0引入,为每个发现的实体调用指定的回调,并且必须为每个实体设置适当的EntityState。回调函数允许我们实现自定义逻辑来设置适当的状态。如果没有设置状态,则实体将保持未跟踪状态。

public virtual void TrackGraph(Object rootEntity,Action callback)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var student = new Student() { //Root entity (with key value)
StudentId = 1,
Name = "Bill",
Address = new StudentAddress() //Child entity (with key value)
{
StudentAddressId = 1,
City = "Seattle",
Country = "USA"
},
StudentCourses = new List<StudentCourse>() {
new StudentCourse(){ Course = new Course(){ CourseName="Machine Language" } },//Child entity (empty key)
new StudentCourse(){ Course = new Course(){ CourseId=2 } } //Child entity (with key value)
}
};

var context = new SchoolContext();

context.ChangeTracker.TrackGraph(student, e => {
if (e.Entry.IsKeySet)
{
e.Entry.State = EntityState.Unchanged;
}
else
{
e.Entry.State = EntityState.Added;
}
});

foreach (var entry in context.ChangeTracker.Entries())
{
Console.WriteLine($"Entity: {entry.Entity.GetType().Name},
State: {entry.State.ToString()} ");
}

//输出结果
Entity: Student, State: Added
Entity: StudentAddress, State: Unchanged
Entity: StudentCourse, State: Added
Entity: Course, State: Added
Entity: StudentCourse, State: Added
Entity: Course, State: Unchanged

上述示例中,context.ChangeTracker.TrackGraph为每个学生实体图设定状态,第一个是参数时学生实体图,第二个参数时设置的实体状态,使用lambda表达式为有键属性的实体设置状态为Unchanged,为键属性为空的实体图设置状态为Added。其中,IsKeySet()方法判定是否有键属性,有则为true,反之则为false。

原始Sql查询

EF Core中执行原始查询

EF Core也支持原始数据查询,使用DbSet.FormSql()方法来实现,如下所示:

1
2
3
4
5
var context=new SchoolContext();

var students=context.Students
.FromSql("Select * from Students where Name = 'Bill'")
.ToList();

参数化查询

如下两个示例:

1
2
3
4
5
6
string name = "Bill";

var context = new SchoolContext();
var students = context.Students
.FromSql($"Select * from Students where Name = '{name}'")
.ToList();
1
2
3
4
5
string name = "Bill";

var context = new SchoolContext();
var students = context.Students
.FromSql("Select * from Students where Name = '{0}'", name)

Linq 操作

1
2
3
4
5
6
7
string name = "Bill";

var context = new SchoolContext();
var students = context.Students
.FromSql("Select * from Students where Name = '{0}'", name)
.OrderBy(s => s.StudentId)
.ToList();