EFCore用法总结

Entity Framework Core再一次让开发者领略到框架的强大力量,它不再是一个程序库,而是先进数据库设计手段的典范。

本文简要介绍EFCore的基本用法,更多干货可参考官方文档。

简介

  • 官方文档:Entity Framework Core 概述 - EF Core | Microsoft Learn
  • 是什么?
    • 微软的ORM(Object Relational Mapper,对象关系映射)
    • 原理:在数据库原生访问接口上封装一层,应用开发者将关系数据作为业务模型来进行使用,而不用关心数据库访问的具体操作
  • 优点?
    • 开发效率大大提高
    • 开源且持续更新
    • 访问多种数据库
    • 将程序和数据库进行隔离
    • 灵活,支持三种开发模式
  • 缺点?
    • 抽象封装越多,开发效率越高,但性能可能不及原生SQL语句。
  • EF系统架构
    • 增强版ADO.NET技术
    • C#创建LINQ查询时,EF框架引擎会连接一个provider,将LINQ转换成SQL语句,发往数据库
    • 数据库持久化
    • EDM:Entity Data Model,实体数据模型

  • ORM

    • ORM是允许开发者使用面向对象的编程语言访问RDBMS数据的一系列技术
    • 采用原生SQL语句访问数据库,存在很多问题(效率、维护成本等)
  • EF历史

    • 微软第一款ORM工具:LINQ to SQL
    • 第一版:Database First
    • 第二版(EF4):开始支持Model-First,可根据设计面板创建实体类,生成数据库
    • EF4.1:引入Code First
  • EF的三种开发风格

    • Database First
      • 如果数据库已经存在,花一点时间就可以编写数据访问层,从数据库生成EDM,稍作修改即可
      • 对遗留的数据库进行开发
      • 数据为中心的应用开发时,数据库本身会频繁修改
    • Code First
      • 高度以领域为中心并且领域模型类创建优先的应用程序
      • 数据库更多是一种领域模型的持久化机制,即数据库中没有逻辑
      • 数据库不会手动修改,基于模型类而修
    • Model First
      • 使用EDM可视化设计器设计模型,数据库和类将通过该模型生成

Code First

数据迁移

MySQL

  • 安装相应的NuGet包

  • 在配置文件中添加数据库连接字符串
1
2
3
4
5
{
"ConnectionStrings": {
"MySQLConnection": "server=localhost;port=3306;uid=root;pwd=lizi710;database=CodeFirst"
},
}
  • 建立数据库实体类
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
50
51
52
53
54
55
56
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace CodeFirst.Models {
[Table("类1")]
public class Class1 {
[Key]
public Guid Class1Id { get; set; }
public string Name { get; set; }
}

[Table("类2")]
public class Class2 {
public int Id { get; set; }
public string Name { get; set; }
}

[Table("基类")]
public class BaseClass {
[Key]
public Guid Id { get; set; }
public string BaseClassName { get; set; }
}

public enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}

[Table("product")]
public class Product : BaseClass {
[Key]
[Column("id")]
public int Id { get; set; }

[Column("name")]
[MaxLength(50)]
public string Name { get; set; }

[Column("Category")]
[NotMapped]
public string Category { get; set; }

[Column("price")]
public decimal Price { get; set; }

public Weekday Day { get; set; }
public Class1 C1 { get; set; }
public List<Class2> C2s { get; set; }
}
}
  • 创建上下文类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using Microsoft.EntityFrameworkCore;

namespace CodeFirst.Models {
public partial class ProductContext : DbContext {
public ProductContext() {
}

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

public virtual DbSet<Product> Product { get; set; } = null!;
}
}
  • 在主程序中注册数据库上下文类
1
2
3
4
5
builder.Services.AddDbContext<ProductContext>(opt => {
string connectionString = builder.Configuration.GetConnectionString("MySQLConnection");
var serverVersion = ServerVersion.AutoDetect(connectionString);
opt.UseMySql(connectionString, serverVersion);
});
  • 数据库迁移
1
2
add-migration v1
update-database

Oracle

  • Oracle的NuGet包

  • Oracle数据库连接字符串
1
2
3
4
5
{
"ConnectionStrings": {
"OracleConnection": "Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=ORCLPDB)));Persist Security Info=True;User ID=lihao;Password=lizi710;"
},
}
  • 在主程序中注册数据库上下文类
1
2
3
builder.Services.AddDbContext<MyDbContext>(opt => {
string connectionString = builder.Configuration.GetConnectionString("OracleConnection");
opt.UseOracle(connectionString);

SQLite

  • SQLite的NuGet包

  • SQLite数据库连接字符串
1
2
3
4
5
{
"ConnectionStrings": {
"MySQLConn": "FileName=./demo.db"
},
}
  • 在主程序中注册数据库上下文类
1
2
3
4
5
builder.Services.AddDbContext<MyDbContext>(opt => {
string? connStr = builder.Configuration.GetConnectionString("MySQLConn");
if (connStr == null) return;
opt.UseSqlite(connStr);
});

DB First

  • 方式1:EF Core Power Tools
  • 方式2:通过nuget包管理其实现
    • 安装nuget包,同Code First所需NuGet包
    • 通过nuget包生成(通过OutputDir指定模型生成的文件夹)
1
Scaffold-DbContext "server=localhost;port=3306;user=root;password=lizi710;database=mysqlbzbh" -Provider "Pomelo.EntityFrameworkCore.MySql" -OutputDir Models

常规实体关系

一对一

  • 标准一对一实体关系配置

    在两个实体类中都定义导航属性,这时必须在其中一个实体类中设置外键属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Order {
public long Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public Delivery Delivery { get; set; }
public long DeliveryID { get; set; }
}
public class Delivery {
public long Id { get; set; }
public string CompanyName { get; set; }
public string Number { get; set; }
public Order? Order { get; set; }
}
  • 如果只在其中一个实体类中定义导航属性,那么将在该表中生成外键(这种方式更简洁)
1
2
3
4
5
6
7
8
9
10
11
public class Order {
public long Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public Delivery Delivery { get; set; }
}
public class Delivery {
public long Id { get; set; }
public string CompanyName { get; set; }
public string Number { get; set; }
}
  • 一对一关系可以由导航属性及外键确定,不用手动配置

一对多

  • 标准一对多实体关系配置

    在两个实体类中都定义导航属性,不需要手动指定外键属性,生成表时会自动在多方实体表中添加外键。

    当然,也可以手动指定外键,这样可以直接查询外键,而不需要根据导航属性间接得到,性能更好。

1
2
3
4
5
6
7
8
9
10
11
public class Article {
public long Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<Comment> Comments { get; set; } = new List<Comment>();
}
public class Comment {
public long Id { get; set; }
public Article Article { get; set; }
public string Message { get; set; }
}
  • 可以只在“一方”实体类中定义导航属性,生成表时会自动在“多方”表中设置外键
1
2
3
4
5
6
7
8
9
10
public class Article {
public long Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<Comment> Comments { get; set; } = new List<Comment>();
}
public class Comment {
public long Id { get; set; }
public string Message { get; set; }
}

当然,也可以只在“多方”实体类中定义导航属性,生成表时会自动在“多方”表中设置外键。这时,表结构会与一对一关系一样,但在一对一关系表中外键具有唯一性,定义时应该显示指定。

  • 与一对一关系一样,一对多关系可以由导航属性及外键确定,不用手动配置

多对多

  • 多对多实体关系比较简单,需要在两个实体类中都设置导航属性,缺少其中一个就会被当做一对多关系了
1
2
3
4
5
6
7
8
9
10
11
public class Student {
public long Id { get; set; }
public string Name { get; set; }
public List<Teacher> Teachers { get; set; }
}

public class Teacher {
public long Id { get; set; }
public string Name { get; set; }
public List<Student> Students { get; set; }
}

这时,会生成一张新表,表名为StudentTeacher,其中包含两个外键:StudentsId和TeachersId

  • 可以通过以下语句对新表名进行设置
1
2
3
4
modelBuilder.Entity<Student>()
.HasMany(s => s.Teachers)
.WithMany(t => t.Students)
.UsingEntity("StudentTeacher");

导航属性的查询

可以使用Include查询导航属性,但是这种方法基于连接操作,可能导致“笛卡尔爆炸”。这是建议使用拆分查询方式,性能更好。

1
2
3
4
5
6
7
using (var context = new BloggingContext()) {
var blogs = context.Blogs
.Include(blog => blog.Posts)
// 拆分查询,没有加这句就是普通查询
.AsSplitQuery()
.ToList();
}

复杂实体关系

继承关系

对于以下实体关系:

1
2
3
4
5
6
7
public class Person {
public int Id { get; set; }
public string Name { get; set; }
}
public class Student : Person {
public string Grade { get; set; }
}
  • 方法1:public DbSet<Person> Person { get; set; }

  • 方法2:public DbSet<Student> Student { get; set; }

这种方式只映射子类,字段包括子类字段和其从父类继承来的字段,简单来说,子类只利用父类的字段。

  • 方法3:
1
2
3
4
5
6
7
8
9
10
11
12
public class MyDbContext : DbContext {
public DbSet<Person> Person { get; set; }
public DbSet<Student> Student { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
// 定义两个实体之间的关系
modelBuilder.Entity<Person>()
.HasDiscriminator<string>("PersonType")
.HasValue<Person>("Person")
.HasValue<Student>("Student");
base.OnModelCreating(modelBuilder);
}
}

这种方式以子类和父类的字段来映射成表,自动添加一个表示实例类型的字段Discriminator;可以同时存储子类和父类的实例。

枚举类型

实体类的枚举类型可以在写数据库时转成int类型,可以在读数据库时还原为枚举类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
public long Id { get; set; }
public string Name { get; set; }
public EnumStudentType StudentType { get; set; }
}
public enum EnumStudentType {
Grade1,
Grade2,
Grade3,
Grade4,
Grade5,
Grade6
}

下面定义转换规则:

1
2
3
4
5
6
7
8
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
// 指定转换规则
modelBuilder.Entity<Student>()
.Property(e => e.StudentType)
.HasConversion(p => (int)p, p => (EnumStudentType)p);
}

数据库配置

Fluent API

集中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
internal class MyContext : DbContext {
public DbSet<Blog> Blogs { get; set; }
// 在这里对所有表进行配置
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
}

public class Blog {
public int BlogId { get; set; }
public string Url { get; set; }
}

分组配置

对每张表进行独立配置,需要继承自IEntityTypeConfiguration泛型类:

1
2
3
4
5
public class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog> {
public void Configure(EntityTypeBuilder<Blog> builder) {
builder.Property(b => b.Url).IsRequired();
}
}

这时,需要在DbContext类的OnModelCreating()方法中指定配置:

1
new BlogEntityTypeConfiguration().Configure(modelBuilder.Entity<Blog>());

或者批量指定:

1
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogEntityTypeConfiguration).Assembly);

也可以通过EntityTypeConfigurationAttribute注解来指定:

1
2
3
4
5
[EntityTypeConfiguration(typeof(BlogEntityTypeConfiguration))]
public class Blog {
public int BlogId { get; set; }
public string Url { get; set; }
}

数据注解

有许多数据注解用以配置数据表,以下是简单示例:

1
2
3
4
5
6
7
8
[Table("Blogs")]
[Comment("博客表")]
public class Blog {
[Comment("主键")]
public int BlogId { get; set; }
[Required]
public string Url { get; set; }
}

内置约定

EF Core 包括许多默认启用的模型生成约定,明白这些默认规则能帮助我们简化数据库设计。

数据库文档

  • SmartSQL:可生成各种格式的数据库文档
  • 在实体类中使用Comment对类名和属性名进行标注,相当于给表和字段添加说明

评论