Bootstrap

Entity FrameWork Core教程,从基础应用到原理实战

大家好!我是未来村村长,就是那个“请你跟我这样做,我就跟你这样做!”的村长👨‍🌾!

👩‍🌾“人生苦短,你用Python”,“Java内卷,我用C#”。

​ Entity FrameWork Core(简称EF Core)是.NET Core中的ORM(object relational mapping,对象关系映射)框架,即让程序员以面向对象的方式进行数据库操作。

一、EF Core组建流程

官方文档地址:https://docs.microsoft.com/zh-cn/ef/core/

1、基础配置

(1)NuGet包

​ 我们通常使用MySQL数据库,.NET连接MySQL数据库需要导入两个库:

  • Microsoft.EntityFrameworkCore
  • Pomelo.EntityFrameworkCore.MySql
(2)配置连接字符串
"ConnectionStrings": {
    "MySQL": "Server=localhost; Port=3306; Database=KnowledgeSet; User=root; Password=123456"
}

2、创建实体

namespace Models.Entities
{
    public class Student
    {
        public int StudentId { get; set; }
        public string StudentName { get; set; }

    }
}

对应数据库DDL-SQL

CREATE TABLE `student` (
  `StudentId` int NOT NULL AUTO_INCREMENT,
  `Name` varchar(100) NOT NULL,
  PRIMARY KEY (`StudentId`)
) ENGINE=InnoDB;

3、建立DbContext与服务注册

(1)DbContext建立
namespace Repositories.EFCore
{
    public class LearnTestDbContext : DbContext
    {
        public DbSet<Student> Students { get; set; }

        //配置构造器:用于服务注册
        public LearnTestDbContext(DbContextOptions<LearnTestDbContext> options) : base(options) { }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            //配置简单日志
            optionsBuilder.LogTo(Console.WriteLine);
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }
}
(2)服务注册
public void ConfigureServices(IServiceCollection services)
{        //DbContext
	services.AddDbContext<LearnTestDbContext>(
		options => options.UseMySql(
			Configuration.GetConnectionString("MySQL"),
			MySqlServerVersion.LatestSupportedServerVersion));
}

4、实体配置[Data Annotation/Fluent API]

​ Data Annotation指的是使用.NET提供的Attribute对实体类和属性进行标注的方式实现实体类配置。Fluent API是指通过modelBuilder对象调用相关泛型方法对实体进行配置,一般混合使用[Data Annotation进行基础性配置,Fluent API进行关系配置等复杂配置]。

(1)Data Annotation

​ 类注释:

[Table("数据库表名")]
[Index(nameof(Url))]//索引列
[Index(nameof(FirstName), nameof(LastName))]//复合索引

​ 属性注释:

[Column("对应列名")]//同名则无需配置
[MaxLength(int n)]//最大长度
[Precision(14, 2)]//指精度14,小数位数为2
[Required]//必须属性
[Key]//标注为主键
[ForeignKey("BlogForeignKey")]//外键
(2)Fluent API

​ 后续关键配置中会使用到Fluent API,若有需求可查看官方文档。

二、EF Core关系配置

​ 关系数据库中的关系称为依赖关系,我们将被依赖的实体称为主体实体,对主体实体有依赖的实体称为依赖实体。依赖实体中除主键外,还有外键属性指向主体实体。此处的外键就是在依赖实体中存储了主体实体主键值的属性。

​ 除此,依赖实体或主体实体都应该有对方的属性或属性List,我们称此为导航属性。导航属性可以是双向也可以是单向,一般一对多的一的方,会采用集合导航属性,因为一个主体含有多个依赖;多的一方我们采用主体的实体类型作为引用导航属性。

​ 了解了这些,我们将对EF Core的一对多和一对一关系的配置进行讲解。

1、一对多关系

(1)导航属性

​ 无外键导航属性:

namespace Models.Entities
{
    [Table("student")]
    public class Student
    {
        [Key]
        [Column("StudnetId")]
        public int StudentId { get; set; }
        [Column("Name")]
        public string StudentName { get; set; }
        //集合导航属性
        public List<Book> Books{get;set;} = new List<Book>();

    }
}
namespace Models.Entities
{
    [Table("book")]
    public class Book
    {
        [Key]
        public int BookId { get; set; }
        [Column("Name")]
        public string BookName { get; set; }

        //引用导航属性
        Student Student { get; set; }
    }
}

​ 虽然建议在依赖实体类中定义外键属性,但这不是必需的。在EF Core中的约定中:默认情况下,如果在类型上发现导航属性,将创建关系。 如上所述的Student和List<Book>便为导航属性。

​ 我们在无外键属性中没有配置外键属性,但是根据约定导航属性为Student类型,其对应外键名称为StudentId,在数据库中book表也应该存在名为StudentId的属性,可以不用在数据库中定义其为外键。

Student student = _dbContext.Students.Include(_ => _.Books).Single(_ => _.Id == 1);

对应的查询语句为:

SELECT `t`.`StudentId`, `t`.`Name`, `b`.`BookId`, `b`.`Name`, `b`.`StudentId`
     FROM (
          SELECT `s`.`StudentId`, `s`.`Name`
          FROM `student` AS `s`
          WHERE `s`.`StudentId` = 1
          LIMIT 2
     ) AS `t`
     LEFT JOIN `book` AS `b` ON `t`.`StudentId` = `b`.`StudentId`
     ORDER BY `t`.`StudentId`, `b`.`BookId`

​ 外键导航属性:

​ 即在Book中增加与StudentId同名属性。关系的最常见模式是在关系的两端定义导航属性,并在依赖实体类中定义外键属性。此处有无定义外键属性,对应执行的SQL语句是不变的。一般情况下,我们需要定义该属性,便于数据拿取。

namespace Models.Entities
{
    [Table("book")]
    public class Book
    {
        [Key]
        public int BookId { get; set; }
        [Column("Name")]
        public string BookName { get; set; }
        //外键
        public int StudentId { get; set; }
        //引用导航属性
        Student Student { get; set; }
    }
}
(2)CURD

增加主体与依赖:先将依赖实体对象添加到主体实体对象的集合导航属性中,然后保存主体实体对象,依赖实体对象会自动保存。

Student student = new Student() { StudentName = "章鱼哥1号" };
Book book = new Book() { BookName = "圆圈正义" };
student.Books.Add(book);
_dbContext.Students.Add(student);
_dbContext.SaveChanges();

增加相关实体:先查询到需要的主体实体对象,然后将新建的依赖实体对象保存到主体实体对象的集合导航属性中。

var student = _dbContext.Students.FirstOrDefault(_ => _.Id==1);
Book book = new Book() { BookName = "埃尔登湖" };
student.Books.Add(book);
_dbContext.SaveChanges();

级联删除(删除主体实体):需要通过Include进行联合查询到相应的主体实体对象,此时删除主体实体对象,其相关依赖对象也会被删除。

var student = _dbContext.Students.Include(_ => _.Books).FirstOrDefault(_ => _.Id == 2);
_dbContext.Remove(student);
_dbContext.SaveChanges();

断开关系(删除依赖实体):通过Include进行联合查询到相应的主体实体对象,然后将实体主体对象的集合导航属性中的元素相应属性进行删除。

var student = _dbContext.Students.Include(_ => _.Books).FirstOrDefault(_ => _.Id == 1);
student.Books.First(_ => _.BookName == ".NET").Student = null;
_dbContext.SaveChanges();

​ 如此,只会删除book表中,书名为".NET"的数据,不会删除student对应的主体实体。

更改关系:更改关系同断开关系,都是从主体中获取数据,然后将主体中集合导航属性对应的依赖实体的对应引用导航属性指向新的主体实体。

var student = _dbContext.Students.Include(_ => _.Books).FirstOrDefault(_ => _.Id == 2);
var studentUpdate = _dbContext.Students.First(_ => _.Id == 1);
student.Books.First(_ => _.BookName == "世界观").Student = studentUpdate;
_dbContext.SaveChanges();
(3)一对多关系手动配置

​ 一般我们使用Fluent API进行手动配置实体关系,HasOne/WithOne 用于引用导航属性,HasMany/WithMany 用于集合导航属性。 HasOneHasMany 标识要开始配置的实体类型的导航属性。 然后,将调用链接到 WithOneWithMany 以标识反向导航。

​ 那为什么可以自动配置,还需要手动配置:如果在两个类型之间定义了多个导航属性(即不止一对指向彼此的导航),则由导航属性表示的关系是不明确的。 需要对它们进行手动配置才能解决这种不明确的关系。例如,我们建立的学生表中,可能学生还会与校园卡,选课等产生关联,这是就会产生多个导航属性,此时是不支持自动配置的。

​ 所以建议不使用自动的关系配置,一律采用手动配置,将更加清晰。

​ 如我们刚才的一对多关系可以这样配置:可以翻译为实体Student有多个Book,然后(因为With代表反向,我们使用然后表示转折)一个Book有一个Student。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	base.OnModelCreating(modelBuilder);
	modelBuilder.Entity<Student>().HasMany(s=> s.Books).WithOne(b=> b.Student);
}

​ 同理,我们可以反过来配置:

modelBuilder.Entity<Book>().HasOne(b => b.Student).WithMany(s => s.Books);

2、其它关系

(1)一对一关系与多对多

​ 一对一则是在关系双方皆有指向对方的引用导航属性,多对多则是在关系双方皆有指向对方的集合导航属性。对应的命令分别为HasOne.WithOne和HasMany.WithMany。

​ 在数据库中的多对多关系都是采用联接表的方式进行建立,所以我们也可以通过建立两个一对多关系实现间接多对多,当然也可以使用集合导航属性。

​ 一对一和多对多的相关操作与一对多的类似,不作探讨。

(2)单向导航

​ 有has还有with,我们称为双向。我们知道with是反向配置,如果只有has没有with,我们称为单向导航,单向关系一般建立在多的一方。比如此处:实体Book对应一个Student。我们通过Include获取Book时能获取对应的Student,但是不能在获取Student时通过Include获取属于其的Book。

modelBuilder.Entity<Book>().HasOne(b => b.Student).WithMany();

3、基于关系的复杂查询

​ 建立关系后,我们可以在Where,Select等子句的判断条件中,对集合导航属性进行Linq操作作为判断条件。

var student = _dbContext.Students.Where(s => s.Books.Any(b => b.BookName == ".NET"));

三、EF Core原理实战

1、IQueryable与IEnumerable

​ IQueryable接口继承自IEnumerable,Queryable中定义了长得像LINQ的方法其实是相关的数据库操作,将LINQ与SQL做了相应的转换。

​ IQueryable的延迟执行:可以延迟执行,IQueryable是实际上是”可查询对象“,这意为着IQueryable不会立即执行,Where、OrderBy、Include、Skip、Take都是非立即执行方法,返回的都是IQueryable类型对象,只有当执行ToArray、ToList、First、Max等同类型方法才会成为数据库执行对象。

​ IQueryable构造动态查询条件:IQueryable”可查询对象“是可以复用的,我们可以根据参数的不同,复用同一个”可查询对象“,返回相应的结果,即分步构造IQueryable来动态构造查询条件。

​ IQueryable的底层运行:IQueryable虽然是一个”可查询对象“,但是一个IQueryable对象会占用一个数据库连接。

​ 注意事项:

(1)因为数据库的连接数是有限的,一次性创建多个IQueryable,可能会导致数据库崩溃。

(2)我们要防止在IQueryable中嵌套IQueryable,还要防止在循环中执行查询操作,因为频繁的数据库操作会比较耗时。

(3)”可查询对象“在查询前要做到能过滤则过滤,不能将所有数据都加载到内存中,毕竟内存大小也有所限制。

2、执行原生SQL

​ 非查询语句:

_DbContext.Database.ExecuteSqlInterpolatedAsync(@$"原生非查询的sql语句");

​ 查询语句

_DbContext.Books.FromSqlInterpolated(@$"原生查询的sql语句");

3、表达式树

​ 分步构造IQueryable来动态构造查询条件,但是还不够灵活,我们可以通过动态构建表达式树来灵活创建查询条件。

(1)Expreesion Tree

​ 我们来看Linq中的Where方法:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

​ 再看EF Core中的Where方法:

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);

​ 我们发现传入的参数为Expression而不是Func,Expression对象将运算逻辑保存为AST(abstract syntax tree,抽象语法树)。运算逻辑则是Linq语句转化为Sql的关键逻辑,所以我们可以通过构建Expresssion来实现动态构建查询方法。我们将传入查询方法的Expression类型参数称为表达式树。

(2)代码(API)→表达式树

​ 一般使用时,我们都是传入Lambda表达式,C#编译器会将Lambda表达式转换为表达式树,但是这是硬编码。在国人开发的Freesql中,通过WhereIF来判断查询条件,当一个查询表单的条件非常多时,就会连续写多个WhereIF。在EF Core中,我们可以使用代码来动态地构建表达式树。

​ 我们来看一则实例:

IEnumerable<Book> QueryBooks(string propName,object value){
    //1.通过反射获取所查询的属性类型
    Type type = typeof(Book);
    PropertyInfo propInfo = type.GetProperty(propName);
    Type propType = propInfo.PropertyType;
    //2.创建参数节点
    var b = Parameter(typeof(Book),"b");
    //3.声明表达式树
    Expression<Func<Book,bool>> expressTree;
    //4.根据属性类型的不同生成不同的表达式树
    if(propType.IsPrimitive){//基本数据类型
        expressTree = Lambda<Func<Book,bool>>(Equal(
        	MakeMemberAccess(b,typeof(Book).GetProperty(propName)),
            Constant(value)),b);
        ))
    }else{//string类型
        expressTree = Lambda<Func<Book,bool>>(MakeBinary(ExpressionType.Equal,
        	MakeMemberAccess(b,typeof(Book).GetProperty(propName)),
            Constant(value)),false,propType.GetMethod("op_Equality"),b);
    }
    return _dbContext.Books.Where(expressTree).ToList();
}

​ 由此可见,表达式树比较复杂难懂,还是通过分步构建IQueryable来完成动态构建查询吧。

4、事务

官方文档:https://docs.microsoft.com/zh-cn/ef/core/saving/transactions

​ 当我们使用多个SaveChanges()方法时,就可能产生事务问题,此时我们就要开启显示事务。

using(var transaction = context.Database.BeginTransaction()){
    //数据库操作
    transaction.Commit();
}

​ EF Core还支持回归到指定点:

using var transaction = context.Database.BeginTransaction();//C#8.0新语法
try
{
    //数据库操作【一】
     transaction.CreateSavepoint("RollBackLocation");
    //数据库操作【二】
    transaction.Commit();
}
catch (Exception)
{
    // 回滚到指定点
    transaction.RollbackToSavepoint("RollBackLocation");
}

​ 除此可以使用TransactionScope:

using (TransactionScope scope = new TransactionScope())
{
    using(xxxDbcontext db= new xxDbContext()){
        db.Add(xxx1);
        db.SaveChange();
        db.Add(xxx2);
        db.SaveChange();
        scope.Complete();
    }
}

​ 外部事务:

using var connection = new SqlConnection(connectionString);
connection.Open();
using var transaction = connection.BeginTransaction();
;