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
可以单独创建一个类库项目,专门用于EFCore配置和数据库迁移。
1 2 3 4 5 6 public class Product { public int Id { get ; set ; } public string Name { get ; set ; } public string Category { get ; set ; } public decimal Price { get ; set ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public partial class ProductContext : DbContext { public ProductContext () { } public ProductContext (DbContextOptions<ProductContext> options ) : base (options ) { } public virtual DbSet<Product> Product { get ; set ; } = null !; protected override void OnModelCreating (ModelBuilder modelBuilder ) { base .OnModelCreating(modelBuilder); modelBuilder.Entity<Product>(e=> { e.ToTable("product" ); e.HasKey(p=>p.Id).HasName("id" ); e.Property(p=>p.Name).HasColumnName("name" ).HasMaxLength(50 ); e.Property(p=>p.Category).HasColumnName("category" ).HasMaxLength(50 ); e.Property(p=>p.Price).HasColumnName("price" ); }); } }
实现IDesignTimeDbContextFactory接口,用于数据库迁移
1 2 3 4 5 6 7 8 9 10 public class ProductContextFactory : IDesignTimeDbContextFactory <ProductContext > { public ProductContext CreateDbContext (string [] args ) { var connectionString = "server=localhost;port=3306;uid=root;pwd=123456;database=CodeFirstTest" ; var serverVersion = ServerVersion.AutoDetect(connectionString); var builder = new DbContextOptionsBuilder<ProductContext>() .UseMySql(connectionString, serverVersion); return new ProductContext(builder.Options); } }
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
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
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; }
这种方式只映射子类,字段包括子类字段和其从父类继承来的字段,简单来说,子类只利用父类的字段。
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对类名和属性名进行标注,相当于给表和字段添加说明