EF Core入门

不同的EF Core数据库提供的质量参次不齐,除了微软官方的Microsoft SQL Server的EF Core数据库提供程序之外,还存在着很多第三方的EF Core数据库提供程序,它们对于EF Core的支持大部分是一致的,但是会有细微的差别。

EF Core环境搭建

无论是在控制台项目中还是在ASP.NET Core项目中,EF Core的用法都是一样的。
EF Core用于将对象和数据库中的表进行映射,因此在进行EF Core开发的时候,需要创建C#类(也叫作实体类)和数据库表两项内容。
在经典的EF Core使用场景下,由开发人员编写实体类,然后EF Core可以根据实体类数据库表。

创建一个.NET Core控制台项目,然后再项目中创建Book实体类。

1
2
3
4
5
6
7
class Book
{
public long Id { get; set; }//主键
public string Title { get; set; }//标题
public DateTime PubTime { get; set; }//发布日期
public double Price { get; set; }//单价
}

为项目安装NuGet包

1
Microsoft.EntityFrameworkCore.SqlServer

我们先创建一个实现了IEntityTypeConfiguration接口的实现类的配置类BookEntityConfig,它用于配置实体类和数据库表的对应关系。

1
2
3
4
5
6
7
class BookConfig:IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("T_Books");
}
}

我们通过接口类型IEntityTypeConfiguration<T>的泛型参数类指定这个类要对哪个实体类进行配置,然后再Configure方法中对实体类和数据库表的关系做详细的配。
其中builder.ToTable(“T_Books”)表示这个实体类对应数据库中的名字为T_Books的表。
这里没有配置各个属性在数据库中的列名和数据库类型,EF Core将会默认把属性的名字作为列名,并且根据属性的类型来推断数据库表中各列的数据库表中各列的数据库类型。

创建一个继承自DbContext类的MyDbContext类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace EF_Core
{
class MyDbContext:DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Person> Persons { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//连接数据库
base.OnConfiguring(optionsBuilder);
string connstr = "Server=localhost;Database=student;Trusted_Connection=True;MultipleActiveResultSets=true;Encrypt=false;";
optionsBuilder.UseSqlServer(connstr);

}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//从当前程序集中加载所有的IEntityTypeConfiguration
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}

}
}

MyDbContext中的Books属性对应数据库中的T_Books表,对Books的操作会反应到数据库的T_Books表中。这样继承自DbContext的类叫作“上下文”。

开始创建程序对应的数据库和数据库表

EF Core这种根据实体类生成数据库表的操作也被叫作“迁移”(migration)。
为了使用EF Core生成数据库的工具,通过NuGet为项目安装之后

1
Microsoft.EntityFrameworkCore.Tools

再【程序包管理器控制台】中执行如下命令:

1
Add-Migration 合理的变量名

Add-Migration会自动再项目的Migrations文件夹中生成

打开

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
37
38
39
40
41
42
43
44
45
46
47
48
49
namespace EF_Core.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "T_Books",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Title = table.Column<string>(type: "nvarchar(max)", nullable: false),
PubTime = table.Column<DateTime>(type: "datetime2", nullable: false),
Price = table.Column<double>(type: "float", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_T_Books", x => x.Id);
});

migrationBuilder.CreateTable(
name: "T_Persons",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Age = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_T_Persons", x => x.Id);
});
}

/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "T_Books");

migrationBuilder.DropTable(
name: "T_Persons");
}
}
}

可以看到,这个文件中包含用来创建数据库表的表名、列名、列数据类型、主键等的代码。
上面的代码还没有执行,他们需要被执行后才会应用到数据库,因此我们接着在【程序包管理器控制台】中执行Update-database执行数据库迁移代码。

1
Update-database

SQLServer数据库

查看SQLServer数据库,我们可以发现数据库student及数据库表T_Books,T_PersonT_Books,T_Persons已经创建成功,数据库表T_Books的结构也和实体类中配置的一致。

可以根据需要修改实体类的配置,进而修改数据库中的表。
生成的T_Books表中Title字段的类型为nvarchar(MAX),想把它修改为nvarchar(50),把Title字段设则为“不可为空”,并且想增加一个不为空且最大长度为20的AuthorName字符串类型的属性,可以修改Book实体类,为其增加一个AuthorName属性,为其添加一个AuthorName属性,然后修改BookEntityConfig的Configure方法。

添加AuthorName属性

1
2
3
4
5
6
7
8
public class Book
{
public long Id { get; set; }//主键
public string Title { get; set; }//标题
public DateTime PubTime { get; set; }//发布日期
public double Price { get; set; }//单价
public string AuthorName { get; set; }//作者
}
1
2
3
4
5
6
7
8
9
class BookConfig:IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("T_Books");
builder.Property(e=>e.Title).HasMaxLength(50).IsRequired();
builder.Property(e=>e.AuthorName).HasMaxLength(20).IsRequired();
}
}

其中HasMaxLength(50)用来配置属性的最大长度为50,IsRequired用来配置属性的值为“不可为空”。

完成修改后,再执行Add-Migration AddAuthorName_ModifyTitle
由于我们把现在的Title列的字段长度从MAX修改为了50,因此可能会造成数据库中旧数据的丢失,Add-Migration命令给出了“An operation was scaffolded that may result in the loss of data.”这个警告。
上面的命令执行完后,在项目文件夹下又生成了一个新的.cs文件,这个文件包含了修改T_Books表的Title列的长度、新增AuthorName列等的代码。

插入数据

TestDbContext类中的Books属性对应数据库中的T_Books表,Books属性是DbSet<Book>类型的。
因此我们只要操纵Books属性,就可以向数据库中增加数据,但是通过C#代码修改Books属性中的数据只是修改了内存中的数据,对Books属性做修改后,还需要调用异步方法SaveChangesAsync把修改保存到数据库。其实DbContext中也有同步的保存方法SaveChanges,但是采用异步方法通常能提升系统的并发处理能力,因此我们推荐使用异步方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using EF_Core;
//Console.WriteLine("Hello World!");
//ctx = 逻辑上的数据库
using (MyDbContext ctx = new MyDbContext())
{
Dog d = new Dog();
d.Name = "Trump";
ctx.Dogs.Add(d);//把d对象加入到Dogs这个逻辑上的列表中

ctx.Dogs.Add(d);//把d对象加入到Dogs这个逻辑上的列表中
await ctx.SaveChangesAsync();//把逻辑上的数据库的变化应用到物理数据库中

Dog d2 = new Dog();
d2.Name = "Biden";
ctx.Dogs.Add(d2);

Book book = new Book();
book.Title = "C#";
book.AuthorName = "Bill";
book.Price = 100;
book.PubTime = DateTime.Now;
ctx.Books.Add(book);
await ctx.SaveChangesAsync();
}

这样我们就实现了不用编程写SQL语句,而是通过创建对象和为对象赋值的方式完成对数据库的操作。

查询数据

Books属性和数据库中的T_Books表对应,Books属性是DbSet类型的,而DbSet实现了IEnumerable<T>接口,因此我们可以使用LINQ操作对DbSet进行数据查询。

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
37
38
39
 //var b1 = new Book { AuthorName = "YOUXIANYU", Title = "零基础学c#", Price = 100, PubTime = new DateTime(2019, 3, 1) };
//var b2 = new Book { AuthorName ="Almngao", Title = "零基础学c++", Price = 90, PubTime = new DateTime(2018, 3, 1) };
//var b3 = new Book { AuthorName = "you", Title = "零基础学c", Price = 50, PubTime = new DateTime(2016, 3, 1) };
//var b4 = new Book { AuthorName = "yu", Title = "零基础学java", Price = 80, PubTime = new DateTime(2015, 3, 1) };
//var b5 = new Book { AuthorName = "xi", Title = "零基础学python", Price = 60, PubTime = new DateTime(2014, 3, 1) };

//ctx.Books.AddRange(b1);
//ctx.Books.AddRange(b2);
//ctx.Books.AddRange(b3);
//ctx.Books.AddRange(b4);
//ctx.Books.AddRange(b5);
//await ctx.SaveChangesAsync();


var books = ctx.Books.Where(b => b.Price > 80);
foreach (var b in books)
{
Console.WriteLine(b.Title);
}
var book=ctx.Books.Single(b=>b.Title == "零基础学c#");
Console.WriteLine(book.AuthorName);

var book1 = ctx.Books.OrderBy(b => b.Price);
foreach (var b in book1)
{
Console.WriteLine(b.Title+","+b.Price);
}

var book2 = ctx.Books.GroupBy(b => b.AuthorName).Select(g => new
{
Name = g.Key,
BooksCount = g.Count(),
MaxPrice = g.Max(b => b.Price)
});
foreach (var e in book2)
{
Console.WriteLine(e.Name + "," + e.BooksCount + "," + e.MaxPrice);
}

修改和删除数据

使用EF Core,还可以对已有的数据进行修改、删除操作。常规来讲,如果要对数据进行修改,我们首先需要把要修改的数据查询初来,然后对查询出来的数据进行修改,再执行SaveChangesAsync保存修改即可。

同样,要对数据进行删除,我们要先把待删除的数据查询出来,然后调用DbSet或者DbContextd的Remove方法把数据删除,再执行SaveChangesAsync方法保存结果到数据库。

1
2
3
4
5
6
var b = ctx.Books.Single(b => b.Title == "零基础学java");
b.AuthorName = "youxianyu";

var d=ctx.Dogs.Single(b => b.Id == 3);
ctx.Dogs.Remove(d);
await ctx.SaveChangesAsync();

值得注意的是,无论是上面的修改数据的代码还是删除数据的代码,都是要先执行数据的查询操作,把数据查询出来,再执行修改或删除操作。这样在EF Core的底层其实发生了先执行Select的SQL语句,然后执行Update或者Delete的SQL语句。

批量删除

1
2
3
4
5
6
7
8
9
10
//删除给定的数据
await ctx.Books.Where(static x=>x.Id==1).ExecuteDeleteAsync();

//删除所有价格大于10的数据
await ctx.Books.Where(b => b.Price > 10).ExecuteDeleteAsync();
//批量更新
await ctx.Books
.Where(b => b.Title == "零基础学")
.ExecuteUpdateAsync(e => e.SetProperty(d => d.Price, d => d.Price + 10)
.SetProperty(d => d.Title, d => d.Title + "C#"));