什么是ORM
-
要求:数据库,sql基础
关系型数据:表/列组成,表之间的关系由外键进行关联
对象数据库:
-
ORM:Object Relational Mapping,即让开发者用对象操作的形式操作关系型数据库
比如插入:
User u = new User(){Name="James",Password="123"}; orm.Save(u);
比如查询:
Book b = orm.books.Single(b=>b.Id==3||b.Name.Contains(".NET")); string bookName = b.Name; string aName = b.Author.Name;
-
有哪些ORM:EF Core,Dapper,SqlSugar,FreeSql等
EF Core与其他ORM的比较
- EF Core是微软官方的ORM框架。优点:功能强大/官方支持/生产效率高/力求屏蔽底层数据库差异(比如时间,c#只需要写一个DateTime.Now就可以了,插入sqlserver,mysql,postsql等数据库,都可以被EF Core自动转换成对应数据库相应的类型);缺点:复杂/上手难度大/不熟悉EF Core的话可能会踩坑
- Dapper:优点:简单/N分钟即可上手,行为可预期性比较强(因为要写SQL,所以底层发生了什么,都是我们自己写的SQL去执行的,所以可预期);缺点:生产效率低,需要处理底层数据库差异
- EF Core是模型驱动的开发思想(面对的是对象),Dapper是数据库驱动的开发思想(要了解数据库/表等信息)。没有优劣,只有比较
- 性能:Dapper等≠性能高,EF Core≠性能差
- EF Core是官方推荐,推进的框架,尽量屏蔽底层数据库差异,.NET开发者必须数据,根据项目情况再决定使用哪一个
选择
- 对于后台系统/信息系统等和数据库相关开发工作量大的系统,且团队稳定,用EF Core;对于互联网系统等数据库相关工作量不大的系统,或者团队不稳定,用Dapper
- 在项目中可以混用,只要注意EF Core的缓存,Tracking等问题即可
EF Core和EF比较
- EF由DBFirst/ModelFirst/CodeFirst,EF Core不支持模型优先,推荐使用代码有限,遗留系统可以使用Scaffold-DbContext来生成代码实现类似DBFirst的效果,但是推荐用Code First
- EF 会对实体上的标注做校验,EF Core追求轻量化,不校验
- 熟悉EF的话,掌握EF Core会很容易,很多用法都移植过来了,但是EF Core增加了很多的新东西
- EF 中一些类的命名空间以及一些方法的名字在EF Core中稍有不同
- EF不再做新特性的增加
搭建EF Core环境
用什么数据库
- EF Core是对于底层ADO.NET Core的封装,因此ADO.NET Core支持的数据库不一定被EF Core支持
- EF Core支持所有主流的数据库,包括MS SQLSERVER/Oracle/MYSQL/PostgreSQL/SQLite等。可以自己实现Provider支持其他的数据库,国产数据库支持问题
- 对于SQLSERVER支持的最完美,MYSQL,PostgreSQL也不错(有能解决的小坑)。这三者是.NET圈中用的最多的三个,将围绕SQLSERVER将,如果使用其他数据库,只要改一行代码+绕开一些小坑即可,大部分代码用法不变。EF Core已经尽量在频闭底层数据库的差异了
开发环境搭建
-
经典步骤:建实体类;建DbContext;生成数据库;编写调用EF Core的业务代码
-
Books.cs:
public class Book { public long Id { get; set; } public string Title { get; set; } public DateTime PubTime { get; set; } public double Price { get; set; } }
-
Install-package Microsoft.EntityFrameWorkCore.Sqlserver,(注意安装.net6适配的nuget包哦)
-
创建一个实现了IEntityTypeConfiguration接口的实体配置类,配置实体类和数据库表的对应关系:
public class BookEntityConfig : IEntityTypeConfiguration<Book> { public void Configure(EntityTypeBuilder<Book> b) { b.ToTable("T_Books");//配置一个与Book对象对应的表的表名 } }
- 上述对Book映射表只配置了表名,那表里面的各字段的名字是什么?此时就会按照约定大于配置的规则去设置该表的各字段名和字段类型,c#中的各类型都会映射到sqlserver中的一个类型
-
创建继承自DbContext的类:
public class TestDbContext:DbContext { public DbSet<Book> Books { get; set; }//一张表一个DbSet protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connectStr = "Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;Initial Catalog=DBTEST;Data Source=.";//使用下面的链接给的方法获取这个字符串 optionsBuilder.UseSqlServer(connectStr); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); //从当前程序集中拿到所有实现了IEntityTypeConfiguration接口的配置类进行配置的读取 modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); } }
-
为了展示两张表,再新增一个实体,对应配置类,更新DbContext类:
public class Person { public long Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
public class PersonEntityConfig : IEntityTypeConfiguration<Person> { public void Configure(EntityTypeBuilder<Person> builder) { builder.ToTable("T_Persons"); } }
public class TestDbContext:DbContext { public DbSet<Book> Books { get; set; } public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connectStr = "Data Source=.;Initial Catalog=DBTEST;Persist Security Info=True;User ID=sa;Password=y123123!@;Trust Server Certificate=True"; optionsBuilder.UseSqlServer(connectStr); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); } }
如何获取数据库的连接字符串
- 用VS2022的
- 查看链接上的数据库的属性即可查看这个数据库的连接字符串
概念:Migration数据库迁移
-
面向对象的ORM开发中,数据库不是程序员手动创建的,而是由Migration工具生成的。关系数据库只是存放模型数据的一个媒介而已,理想状态下,程序员不用关心数据库的操作。
-
根据对象的定义变化,自动更新数据库中的表以及表结构的操作,叫做Migration(迁移)
-
迁移可以分为多步(项目进化),也可以回滚。
-
为了使用生成数据库的工具,nuget需要安装:Microsoft.EntityFrameworkCore.Tools,否则Add-Migration等命令会报错:
-
安装完上述nuget包之后,再在“程序包管理控制台”中执行如下命令:
Add-Migration InitialCreate
会自动在项目的Migrations文件夹中生成操作数据库的c#代码。InitialCreate是本次操作数据库的代号,或者当次操作的名字
因为每次对数据库的操作都需要先生成对应操作数据库的代码,即执行上述命令,然后执行这些生成的代码,即下面的命令,所以每次操作数据库都需要一个类似Log的名字,就是这个InitialCreate
-
上述生成的代码需要执行之后才能真正的对数据库进行相应的操作。“程序包管理器控制台”中执行:
Update-database
-
查看数据库,表已经建好了
-
注意:因为执行Add-Migration InitialCreate,是编译生成的,所以,我们项目里其他非操作数据库的代码由编译问题,我们执行该命令也不会成功!
修改表结构
-
项目开发中,根据需要,可能会在已有的实体中修改/删除/新增列等
-
想要限制Title的最大长度为50,Title字段设置为“不可为空”,并且想增加一个不可为空且长度最大为20的AutorName属性
-
首先在Book类中增加一个AuthorName属性
-
修改BookEntityConfig:
public class BookEntityConfig : IEntityTypeConfiguration<Book> { public void Configure(EntityTypeBuilder<Book> b) { b.ToTable("T_Books"); b.Property(e => e.Title).HasMaxLength(50).IsRequired();//Title最大长度为50,不允许为空 b.Property(e => e.AuthorName).HasMaxLength(20).IsRequired();//AuthorName最大长度为20,不允许为空 } }
-
分别在程序包管理控制台执行如下命令:
Add-Migration limitTitleLengthAndAddAuthorName update-database
-
检查数据库,确实符合预期
-
总结环境搭建:
- 建实体类
- 为每个实体类建一个配置类,如果不建这个配置类,一切按照约定去进行配置,即实现IEntityTypeConfiguration< T >
- 建总的配置类,即实现DbContext
- 程序包管理控制台执行:Add-migration 本次迁移名字;update-databse
增删改查
插入数据
-
只要操作Books属性,就可以向数据库中增加数据,但是通过c#代码修改Books中的数据只是修改了内存中的数据。对Books做修改之后,需要调用DbContext的异步方法:SaveChangesAsync()把修改保存到数据库中,当然也有同步方法SaveChanges(),EF Core推荐使用异步方法
-
EF Core默认会跟踪(Track)实体类对象以及DbSet的改变
-
比如向T_Books表中插入一条数据:
static async Task Main(string[] args) { //ctx就相当于内存中的数据库 using (TestDbContext ctx = new TestDbContext())//DbContext实现了IDisposable接口,用using可以防止资源泄露 { Book b = new Book() { //Id是主键,不需要赋值,会自增 Title = "Java", PubTime = DateTime.Now, Price = 22.22, AuthorName = "Gosling" }; ctx.Books.Add(b);//修改内存中的数据库 await ctx.SaveChangesAsync();//将内存中的数据库同步到物理数据库 } }
查看数据库,效果符合预期:
查询数据
-
DbSet实现了IEnumerable< T >接口,因此可以对DbSet实施Linq操作来进行数据查询。EF Core会把Linq操作转换成SQL语句。面向对象,而不是面向数据库(SQL)
-
Book b = ctx.Books.Where(b=>b.Price>80).FirstOrDefault(); Books b1 = ctx.Books.Single(b=>b.Title=="零基础学c语言")
-
可以使用OrderBy操作进行数据的排序
IEnumerabe<Book> books = cts.Books.OrderByDescending(b=>b.Price);//按照书的价格降序排序
-
为了便于练习查询操作,准备一些“书”:
static async Task Main(string[] args) { //ctx就相当于内存中的数据库 using (TestDbContext ctx = new TestDbContext())//DbContext实现了IDisposable接口,用using可以防止资源泄露 { Book b1 = new Book() { Title = "Java", PubTime = DateTime.Now, Price = 22.22, AuthorName = "Gosling" }; Book b2 = new Book() { Title = "Python", PubTime = DateTime.Now, Price = 45, AuthorName = "Lebron" }; Book b3 = new Book() { Title = "C++", PubTime = DateTime.Now, Price =32, AuthorName = "James" }; Book b4 = new Book() { Title = "Matlab", PubTime = DateTime.Now, Price = 98, AuthorName = "Rocket" }; Book b5 = new Book() { Title = "Halcon", PubTime = DateTime.Now, Price = 119, AuthorName = "Tom" }; ctx.Books.Add(b1);//修改内存中的数据库 ctx.Books.Add(b2);//修改内存中的数据库 ctx.Books.Add(b3);//修改内存中的数据库 ctx.Books.Add(b4);//修改内存中的数据库 ctx.Books.Add(b5);//修改内存中的数据库 await ctx.SaveChangesAsync();//将内存中的数据库同步到物理数据库 } }
-
比如查询价格大于80的书,把这些书名都打印出来:
await ctx.Books.Where(b=>b.Price>80).ForEachAsync(b => Console.WriteLine(b.Title));
注意,EF Core中的IQueryable类似与Linq中的IEnumerable,用法几乎相近
-
又比如查询书名为C++的书作者叫什么:
Console.WriteLine(ctx.Books.Single(b => b.Title == "C++").AuthorName);
-
按照价格降序输出所有的书名:
await ctx.Books.OrderByDescending(b => b.Price).ForEachAsync(b => Console.WriteLine($"Title:{b.Title},Price:{b.Price}"));
-
大部分的Linq操作都可以运用于EF Core
修改/删除
-
要对数据进行修改,首先需要把要修改的数据查询出来,然后在对查询出来的对象进行修改,然后再执行SaveChangesAsync()保存需改
-
比如:把c++的作者改成Harden:
ctx.Books.Single(b => b.Title == "C++").AuthorName = "James Harden"; await ctx.SaveChangesAsync();
-
删除也是一样的,需要将待删除的Book对象在逻辑的数据库中查询出来,再把这些对象从逻辑数据库中进行删除,然后再把这些更新同步到物理数据库即可,比如删除一本Java的书
var res = await ctx.Books.GroupBy(b => b.Title).Where(g => g.Count() > 1).ToListAsync(); foreach(var group in res) { var tmp = group.OrderBy(b => b.PubTime).FirstOrDefault(); if (tmp != null) { ctx.Books.Remove(tmp); } } await ctx.SaveChangesAsync();
上述代码会报错:
EF Core无法直接将你的LINQ查询翻译成SQL查询,特别是涉及到
GroupBy
和随后的操作时。EF Core对于复杂的查询,尤其是那些包含客户端评估的部分(如你在GroupBy
之后的操作),有特定的要求。为了修复这个问题,你需要在执行任何不能被转换为SQL的逻辑之前,先将数据加载到内存中。你可以通过调用
ToList()
或ToListAsync()
来实现这一点。这里是如何调整你的代码以解决这个问题的方法:var res = await ctx.Books.GroupBy(b => b.Title).Where(g => g.Count() > 1).Select(g => g.ToList()).ToListAsync();//这样可以把 foreach(var group in res) { var tmp = group.OrderBy(b => b.PubTime).FirstOrDefault(); if (tmp != null) { ctx.Books.Remove(tmp); } } await ctx.SaveChangesAsync();
还有这种看起来方便理解的处理方式:
var groups = await ctx.Books .GroupBy(b => b.Title) .Where(g => g.Count() > 1) .Select(g => new { Group = g.Key, Books = g.ToList() }) // 将每个组的数据加载到内存中 .ToListAsync(); foreach(var group in groups) { var earliestBook = group.Books.OrderBy(b => b.PubTime).FirstOrDefault(); if (earliestBook != null) { var booksToRemove = group.Books.Where(b => b.BookId != earliestBook.BookId); foreach (var book in booksToRemove) { ctx.Books.Remove(book); } } } await ctx.SaveChangesAsync();
-
给所有价格低于50的书都涨价5块钱:
ctx.Books.Where(b => b.Price < 50).ToList().ForEach(b=>b.Price+=5); await ctx.SaveChangesAsync();
批量修改/删除
-
目前批量修改/删除多条数据的方法:局限性:性能低,因为需要查出来,然后一条条Update,Delete,而不能像我们写SQL那样一条语句就能处理批量的数据
官方目前还没有支持高效的批量Update,Delete,有在后续版本中增加,但是目前只是前期意见征询阶段
实体的配置
约定配置
主要规则:
- 表名采用DbContext中对应的DbSet的属性名
- 数据表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型最兼容的类型
- 数据表列的可控性取决于对应实体类属性的可空性
- 名字为ID的属性为主键,如果主键为Short,int或者Long类型,则默认采用自增字段,如果主键为Guid类型,则默认采用默认的Guid生成机制去生成主键值。
两种配置方式
Data Annotation
-
把配置以特性(Annotation)的形式标注在实体类中
[Table("T_Books")] public class Book { [Required] [MaxLength(20)] 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; } }
优点:简单;缺点:耦合
-
特性加好之后,还是需要Add-Migration 操作名,update-database才能真正操作到物理数据库(执行编译生成的代码)
Fluent API
就是之前学到的与实体类对应的配置类中对指定表进行配置的形式:
b.ToTable("T_Books");
b.Property(e => e.Title).HasMaxLength(50).IsRequired();//Title最大长度为50,不允许为空
b.Property(e => e.AuthorName).HasMaxLength(20).IsRequired();//AuthorName最大长度为20,不允许为空
把配置写道单独的配置类中
缺点:复杂,优点:解耦
-
大部分功能重叠,可以混用,但是不建议混用。
-
提一下:对于实现了IEntityTypeConfiguration接口的配置类中对一张表进行配置的代码,也可以在实现了DbContext的OnModelCreating方法中进行配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); //这样也行,但是表一多,这个方法就会特别庞大 modelBuilder.Entity<Book>().ToTable("T_Books"); }
-
日常都是一张表一个配置类,然后OnModelCreating指定从当前程序集中加载所有实现了IEntityTypeConfiguration接口的配置类
为什么说Annotation耦合性比较高
-
比如
[Table("T_Books")] public class Book {}
比如mysql里我想把Book映射到Books表里去,SQLSERVER里就用T_Books做表名,这个配置方式,就需要我们改实体类这边的代码
-
而如果我们使用Fluent API的话,我们完全可以实现成:从配置文件读取当前项目想使用哪个数据库,然后再配置类里读取到该信息,然后作出表名的选择:
public void Configure(EntityTypeBuilder<Book> b) { if ("用mysql")//这个判断条件就可以从配置文件中读取 { b.Totable("T_Books") } else { //用sqlserver b.Totable("Books") } }
-
进而可以实现解耦
Fluent API1
-
视图与实体类映射:
modelBuilder.Entity<Blog>().ToView("blogsView");
-
排除属性映射:
modelBuilder.Entity<Blog>().Ignore(b=>b.Name2);
-
配置列名:
modelBuilder.Entity<Blog>().Property(b=>b.BlogId).HasColumnName("blog_id");
-
配置列数据类型;
modelBuilder.Entity<Bolg>().Property(b=>b.Title).HasColumnType("varchar(200)");
-
配置主键:
默认会把实体类中属性名为id或者“实体类型+id”的属性作为主键,可以用HasKey()来配置其他属性作为主键
modelBuilder.Entity<Student>().HasKey(c=>c.Number);
支持复合主键,但是不建议使用。
上述为在实现了DbContext的OnModelCreating方法中进行配置的方式,也可以差不多的在每个实体类的配置类中进行配置,还可以使用Annotation,怎么用,用到啥再查就行。懂就行
配置完还是要执行:Add-migration 操作名 以及update-database
Fluent API2
-
生成列的值
modelBuilder.Entity<Student>().Property(s=>s.Number).ValueGeneratedOnAdd();
-
可以用HasDefaultValue()为属性设定默认值
modelBuidler.Entity<Student>().Property(s=>s.Age).HasDefaultValue(6);
-
索引
modelBuilder.Entity<Blog>().HasIndex(b=>b.Id);
-
复合索引:
//new {p.FirstName,p.LastName}这个匿名对象,既有FirstName属性也有LastName属性 modelBuilder.Entity<Person>().HasIndex(p=>new {p.FirstName,p.LastName});
-
唯一索引:
IsUnique()
-
聚集索引:
IsClustered()
-
用EF Core太多高级特性的时候需要谨慎,尽量不要和业务逻辑混合在一起,以免“不能自拔”。比如Ignore,Shadow,Table Splitting等…
Fluent API其他
-
FluentAPI中有很多的方法都有多个重载方法,比如HasIndex,Property()
-
把Number作为索引下面的方法都可以:
builder.HasIndex("Number"); builder.HasIndex(b=>b.Number);
推荐使用下面那一种,可以利用c#的强类型检查机制
-
所谓的Fluent API:
配置的时候,可以跟一个流一样写一串配置:
b.ToTable("t_Books").Property(b => b.AuthorName).HasColumnType("varchar(20)").HasMaxLength(20).IsRequired();
但并不是所有的配置项都可以在一条语句中全部完成!
自行写出来上述流式配置代码后,看每一个Linq语句的返回值是什么,再怎么能写多长,链式编程,总得符合c#语法要求啊!
主键无小事
- 聚集索引:想象一下你有一本字典,所有的单词都是按照字母顺序排列的。当你想找一个特定的单词时,你可以直接根据字母顺序快速找到它。这个按字母顺序排列的方式就像是聚集索引——数据是按照某个列(比如ID或名字)的实际值排序存储的。因为所有数据行都按照这个顺序存放,所以每个表只能有一个聚集索引,就像字典只能有一种字母顺序一样。
- 非聚集索引:再想象一下你有很多书签,每张书签上写着一本书的名字和这本书在书架上的位置。这些书并没有按照任何顺序排列,但是通过书签你可以很快找到你想找的那本书。这里的书签就像是非聚集索引——它提供了一个指向实际数据位置的指针列表,但数据本身并不按照这个索引排序
自增主键
- EF Core支持多种主键生成策略:自动增长/Guid/HiLo算法等
- 自动增长。优点:简单;缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。Long int等类型主键,默认是自增。因为是数据库生成的值,所以SaveChanges后会自动把主键的值更新到ID属性(可以用于获取插入到数据库中的实体被数据库赋予的Id值)。试验一下。场景:插入帖子后,自动重定向到帖子地址
- 自增字段的代码不能为ID赋值,必须保持默认值0,否则运行的时候会报错。
Guid主键
-
Guid算法(或者UUID算法)生成一个全局唯一的ID,适合与分布式系统,在进行多数据库数据合并的时候很简单。优点:简单,高并发,全局唯一;缺点:磁盘空间占用大
-
Guid值不连续。使用Guid类型做主键的时候,不能把主键设置为聚集索引。因为聚集索引是按照顺序保存主键的,因此用Guid做主键性能差(聚集索引要求索引列顺序存储,如果这个索引列是Guid,那每次插入一条数据,都需要按照指定的顺序将原有数据和新数据进行一次重拍,这样就会导致插入效率极低)。比如Mysq的InnoDB引擎中是强制使用聚集索引的(所以Mysql的主键千万不能使用Guid)。有的数据库支持部分的连续Guid,比如SQLServer中的NewSequentialId(),但也不能解决问题。在SQLServer等中,不要把Guid主键设置为聚集索引,在MYSQL中,插入频繁的表不要用Guid做主键
-
演示Guid用法,既可以让EF Core给赋值,也可以手动赋值(推荐)
-
新建一个实体:
public class Rabbit { public Guid Id { get; set; } public string Name { get; set; } }
-
对应配置类:
public class RabbitEntityConfig:IEntityTypeConfiguration<Rabbit> { public void Configure(EntityTypeBuilder<Rabbit> builder) { builder.ToTable("T_Rabbits"); builder.HasKey(x => x.Id); builder.Property(x => x.Name).IsRequired(); } }
-
DbContext:
public class Dbtext:DbContext { public DbSet<Rabbit> Rabbits { get; set; } override protected void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=DBTEST;Persist Security Info=True;User ID=sa;Password=y123123!@;Trust Server Certificate=True"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(Dbtext).Assembly); } }
-
程序包管理控制台上依次执行:
Add-Migration FirstTry update-database
-
测试代码:
static async Task Main(string[] args) { using (Dbtext db = new Dbtext()) { Rabbit rabbit = new Rabbit { Name = "Rabbit1" }; Console.WriteLine(rabbit.Id); db.Rabbits.Add(rabbit); Console.WriteLine(rabbit.Id); await db.SaveChangesAsync(); Console.WriteLine(rabbit.Id); } }
-
测试结果:
00000000-0000-0000-0000-000000000000 b5b6c3be-33ad-43db-7d1a-08dd6b41472b b5b6c3be-33ad-43db-7d1a-08dd6b41472b
-
结论:
说明插入Guid值的时候,对象被加入到内存中的数据库的之后,Guid值就已经被自动赋值了,而不是真正插入到物理数据库的时候,可以后面验证:自增主键的值在加入内存之后,并不会立刻就自动赋值了,而是在真正插入数据库的时候才会被赋值
也可以手动指定Guid值:(推荐)
using (Dbtext db = new Dbtext()) { Rabbit rabbit = new Rabbit { Name = "Rabbit1" }; rabbit.Id = Guid.NewGuid(); Console.WriteLine(rabbit.Id); db.Rabbits.Add(rabbit); Console.WriteLine(rabbit.Id); await db.SaveChangesAsync(); Console.WriteLine(rabbit.Id); }
-
其他方案
- 混合自动和Guid(非复合主键,实际是有两列)。用自增列做物理的主键,而用Guid列做逻辑上的主键。把自增列设置为表的主键,而在业务上查询数据的时候把Guid当主键用。在和其他表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用的Guid列。不仅保证了性能,而且利用了Guid的优点,而且减轻了主键自增性导致主键值被预测带来的安全问题。
- Hi/Lo算法:EF Core支持Hi/Lo算法来优化自增列。主键值由两部分组成:高位(Hi)和低位(Lo),高位由数据库生成,两个高位之间间隔若干个值,由程序在本地生成低位,低位的值在本地自增生成。不同进程或者集群中不同服务器获取的Hi值不会重复,而本地进程计算的Lo则可以保证在本地高效率的生成主键值。但是HiLo算法不是EF Core的标准。
深入研究Migration
-
使用迁移脚本,可以对当前连接的数据库执行编号更高的迁移,这个操作叫做“向上迁移(UP)”,也可以执行数据库回退到旧版本的迁移,这个操作叫做“向下迁移(Down)”
-
除非有特殊需要,否则不要删除Migration文件夹中的代码
-
进一步分析Migration下的代码。分析Up/Down等方法。查看Migration编号
-
查看数据库的__EFMigrationHistory表:记录当前数据库曾经应用过的迁移脚本,按顺序排列。ProductVersion是EFCore的版本
不要随意动这张表里的东西
数据库迁移的其他命令
- 版本号都写我们定义的就行,比如:
-
把数据库迁移(Up或者Down)到XXX的状态,迁移脚本不动:
Update-Database xxx
-
删除最后一次的迁移脚本:
Remove-migration
-
生成SQL代码。有了Update-Database为什么还是要生成SQL脚本
Script-Migation
- 生产服务器一般必须需要SQL脚本经过DBA检查之后,才能执行数据库操作
- 开发人员一般不能直接操作生产服务器,脚本需要给专门的人操作的
- 对数据的操作需要审计,此时就需要SQL脚本
-
生成从版本D到版本F的SQL脚本
Script-Migration D F
-
生成版本D到最新版本的SQL脚本:
Script-Migration D
反向工程
-
顾名思义:根据数据库表来反向生成实体类
-
:命令
Scaffold-DbContext 'Server=.;Database=demo1;Trusted_Connection=True;MultipleActiveResultSets=true' Microsoft.EntityFrameworkCore.SqlServer
-
要求:还是需要安装两个Nuget包:
-
如果已经有项目下载过这两个nuget包,直接把有包的项目左键单击后:把我说的这一坨:
拷贝到目标项目:
新项目就会在依赖项中的包中,自动添加这两个包,更便捷!新项目能出现这两个nuget包的前提是:已经本地有包的缓存或者联网了!
-
测试:nuget包有了之后,程序包管理控制台(可能要求当前项目是启动项目)执行如下命令:Microsoft.EntityFrameworkCore.SqlServer必须一模一样
scaffold-dbcontext 'Data Source=.;Initial Catalog=DBTEST;Persist Security Info=True;User ID=sa;Password=y123123!@;Trust Server Certificate=True' Microsoft.EntityFrameworkCore.SqlServer
确实生成了好多的实体类!
-
除了很多的实体类被生成,内存中的数据库也生成了:
该类使用了FluentAPI对指定数据库的各表进行了配置代码的生成!并且配置都写在了OnModelCreating方法中,并没有像之前内容提到的那样:一个实体类对应一个配置类哦!
-
注意:
- 生成的实体类可能不满足项目的要求,可能需要手动修改或者增加配置
- 再次运行反向工程工具,对文件所做的任何更改都将丢失
- 不建议把反向工具当成了日常开发工具使用,因为不建议DbFirst
-
如果反向工程操作之后,数据库又多了一张表,那又要进行一次反向操作,此时生成的实体类会被覆盖(修改的内容丢失了!)
EF Core底层如何操作数据库
传统:
介入EFCore:
查看生成的SQL语句
-
SQLSERVER Profiler(收费版的SQLSERVER才有这个)查看SQLServer数据库当前执行的SQL语句
-
例如:
var books = ctx.Books.Where(b=>b.Price>10||b.Title.Contains("张"))
-
EF Core其实就是把C#代码转换成SQL语句的框架
有哪些东西是EFCore做不到的
-
c#语句千变万化,而SQL功能简单。存在合法的C#语句无法被翻译成SQL语句的情况:
var books = cts.Books.where(b=>isOk(b.Title)); private static bool isOk(string title) { return title.Contains("张"); }
-
不同数据库的不同
通过代码查看EF Core生成的SQL
- 前面已经提到了在SQLServer Profiler中可以查看执行了的SQL语句
- 但是SQLServer Profiler查看的是当前服务器所有的数据库活动,操作的人员多了,需要配置过滤器查看指定人员数据库活动,比较麻烦,并且SQLServer Profiler只能查看SQLServer的数据库活动
标准日志
-
如将使用的SQL以日志形式打印到控制台:
-
需要安装的包:
public class Dbtext:DbContext { public static ILoggerFactory LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(b=>b.AddConsole());//重点1 public DbSet<Rabbit> Rabbits { get; set; } override protected void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=DBTEST;Persist Security Info=True;User ID=sa;Password=y123123!@;Trust Server Certificate=True"); optionsBuilder.UseLoggerFactory(LoggerFactory);//重点2 } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(Dbtext).Assembly); } }
简单日志
-
用起来简单,打印出来的东西却很多:
public class Dbtext:DbContext { public DbSet<Rabbit> Rabbits { get; set; } override protected void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=DBTEST;Persist Security Info=True;User ID=sa;Password=y123123!@;Trust Server Certificate=True"); optionsBuilder.LogTo(msg => { Console.WriteLine(msg);//LogTo是一个委托:Action<string>,此处就可以用到Log方法去写到文本即可 }); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(Dbtext).Assembly); } }
-
观察后发现打印sql的操作里总有一个字符串:“CommandExecuting”,所以我们可以过滤消息:
override protected void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=DBTEST;Persist Security Info=True;User ID=sa;Password=y123123!@;Trust Server Certificate=True"); //optionsBuilder.UseLoggerFactory(LoggerFactory); optionsBuilder.LogTo(msg => { if (!msg.Contains("CommandExecuting")) return; Console.WriteLine(msg); }); }
ToQueryString
- 上述两种方式无法直接得到一个操作的SQL语句,而且在操作很多的情况下,容易混乱
- EF Core的Where方法返回的是IQueryable类型,DbSet也实现了IQueryable接口。IQueryable有拓展方法ToQueryString()可以获得SQL
- 优点之一:不需要这的执行查询才获取SQL语句:只能获取查询操作的。
static async Task Main(string[] args)
{
using (Dbtext db = new Dbtext())
{
var rr = db.Rabbits.Where(r => r.Name == "rabbit1");
string sql = rr.ToQueryString();
Console.WriteLine(sql);
}
}
经尝试,这样也行:DbSet直接调用ToQueryString():
var rr = db.Rabbits.ToQueryString();
Console.WriteLine(rr);
//SELECT [t].[Id], [t].[Name]
//FROM [T_Rabbits] AS [t]
用哪个?
- 写测试性代码,用简单日志,正式需要记录SQL给审核人员或排查故障,用标准日志;开发阶段,从繁杂的查询操作中立即查看SQL,用ToQueryString().
同样的LINQ被翻译成不同的SQL语句
不同数据库方言不同
-
SQLServer:
select top(3) * from t
-
MySQL:
select * from t limit 3
-
Oracle:
select * from t where ROWNUM<=3
同样的c#语句在不同数据库中被EF Core翻译成不同的SQL语句。
- 数据库的迁移脚本不可以使用于各类型数据库
- 通过给Add-Migration命令添加“-OutputDir”参数的形式来在同一个项目中为不同的数据库生成不同的迁移脚本。
- 更换数据库的时候,可以把老的Migration目录删除
- 代码层面只需要改逻辑上的数据库所在类的OnConfiguring方法,将数据库指定为MYsql,FluentAPI啥的都不用改
- 实体一样、FluentAPI的配置一样、c#调用DbContext啥的代码都不用变,变了的之后DbContext中的OnConfiguring中指定的数据库变了,以及Migration目录中自动生成的迁移脚本变了。
- 同一份c#代码可能在一个数据库中支持,在另一个数据库中不一定支持!
EF Core一对多的关系配置
什么是实体间关系
- 所谓“关系数据库”:各个表之间存在关联,表之间通过外键进行关联,一起协作存一些更复杂的数据
- 一对一:其中一张表建一个字段指向另一个表即可(记录构成一对一即可)
- 一对多:“多”的那张表建一个字段指向“一”的那张表即可,表明多条数据和同一条数据进行关联,比如“孩子表”里建一个字段,说明当前孩子记录对应的唯一的父亲数据记录
- 多对多:额外建一张表,专门维护两者之间的关系
- 复习:数据库表之间的关系:一对一,一对多,多对多
- EF Core不仅支持单实体操作,更支持多实体的关系操作
- 三部曲:实体类中关系属性;FluentAPI关系配置;使用关系操作
如何配置一对多
- 比如一个帖子和这个帖子下的评论之间的关系就是“一对多”
实体类配置
-
文章类:(有一个List这个List包含很多的评论,表明这个文章拥有的多条评论)
public class Article { public long Id { get; set; } public string Title { get; set; } public string Content { get; set; } public List<Comment> Comments { get; set; }//说明在“一”这边也有对应的属性对应“多” }
-
评论类:(有一个Article属性,表明自己属于哪个文章)
public class Comment { public long Id { get; set; } public Article Article { get; set; }//说明在“多”这边又对应的属性对应“一” public string Message { get; set; } }
关系配置
EF Core中实体之间关系的配置的套路:
HasXXX(…)WithXXX(…):有XXX,反之带有XXX
XXX可选值为One或者Many
比如:
一对多:
HasOne(...)WithMany(...)
一对一:
HasOne(...)WithOne(...)
多对多:
HasMany(...)WithMany(...)
比如给上述文章和评论的FluentAPI进行配置:(在“多”的实体配置类进行FluentAPI配置)
给评论配置多对一,在“多”这一端进行配置
public class CommentEntityConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
//可以翻译为:多个Comment对应一个Article,可以把Comment认作builder,此时HasOne<“一”是谁>
//第二部分就是选择Comment对象中的Article属性对应那个“一”
//第三部分就是对应到Article对象中的Comments属性
//总之:HasOne就是自己可定属于唯一的一篇文章,<T>就是那个唯一,自己将提供哪个属性去做关联
// ,并且对应到那个“一”的哪个属性
builder.HasOne<Article>(c => c.Article).WithMany(a => a.Comments).IsRequired();
}
}
上述实体类,配置类,DbContext类,一对多的关系配置好之后,就可以进行迁移:
- Add-Migration config1
- update-database
就可以去数据库查看这两张表的对应关系了,可以在Comment表中发现EF CORE给我们自动生成了一个列:ArticleId,指向Article表的Id,这就是自动生成的外键,非常的方便;注意,我们是在Comment表里配置的外键,所以自动生成的外键也只会在Comment表中体现
对上述配置关系进行测试:
using (MyDb md = new MyDb())
{
Article a = new Article() { Title = "CH被评为亚洲最酷程序员", Content = "据报道....." };
Comment c1 = new Comment() { Message = "太牛了!" };
Comment c2 = new Comment() { Message = "吹吧!SB" };
a.Comments.Add(c1);
a.Comments.Add(c2);
//下面两行代码不加也可以,因为EF Core够智能,可以把Article关联的两条评论也插入数据库
//md.Comments.Add(c1);
//md.Comments.Add(c2);
md.Articles.Add(a);//这一步千万别忘!其次就是只需要将Article进行存储即可,两个Comment会自动插入到Comments表中!
await md.SaveChangesAsync();
}
总结一点:有关系的表在fluentAPI中配置好关系之后,Dbset对应的其中某张表对应的对象被加入后,与该对象有关系的别的表的数据也会自动加入到有关系的另一张表中,这就是EF Core的“顺杆爬”
**同理:给文章配置一对多:**在“一”这一端进行配置,理解方式和上图如出一辙
public class ArticleEntityConfig : IEntityTypeConfiguration<Article>
{
public void Configure(EntityTypeBuilder<Article> builder)
{
builder.HasMany<Comment>(a => a.Comments).WithOne(c => c.Article);
}
}
可以这么去记这个hasone什么乱七八糟的东西怎么去用:
-
比如文章有多条评论,站在文章的角度看就是:“一对多”,那我们就先建立起这个模板,注意是从后往前“构成一对多”:
public class ArticleEntityConfig : IEntityTypeConfiguration<Article> { public void Configure(EntityTypeBuilder<Article> builder) { builder.HasMany<>().WithOne(); } }
-
然后就是指定其中的泛型类这些东西:
-
具体就会形成这样的写法:
public class ArticleEntityConfig : IEntityTypeConfiguration<Article> { public void Configure(EntityTypeBuilder<Article> builder) { builder.HasMany<Comment>(a=>a.Comments).WithOne(c=>c.Article); } }
-
然后“多对一”的配置写法理解方式和配置步骤一模一样!
EF Core一对多关系数据的获取
-
比如上述的Article对应多条评论Comments,但是你如果这样查询:
using (MyDb md = new MyDb()) { foreach (var item in md.Articles) { Console.WriteLine($"{item.Id}_{item.Title}_{string.Join(",",item.Comments)}"); } }
查询结果:
1_CH被评为亚洲最酷程序员_ 2_CH被评为亚洲最酷程序员_ 3_CH被评为亚洲最酷程序员_
你会发现你并不能查询到当前Article对应的List< Comment > 中所有的Comments,因为所有的Comment只是和某些Article有关联,并不代表就能在Article一张表里就能将所有的数据都查询出来的了
其实查看EF Core给我们生成的SQL语句,就能发现:查询的语句并没有使用join来进行联表查询,那这些comment在另一张表里,而非Article表里,当然查不出什么东西
如何看sql:简单日志也好标准日志也好,都可以查看,比如标准日志:只需要在内存的数据库对象:MyDb:
public class MyDb: DbContext { public DbSet<Model.Book> Books { get; set; } //配置一个与Book对象对应的表 public DbSet<Person> Persons { get; set; } public DbSet<Article> Articles { get; set; } public DbSet<Comment> Comments { get; set; } public static ILoggerFactory LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.AddConsole();//重点1 }); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { string connectStr = "Data Source=.;Initial Catalog=DBTEST;Persist Security Info=True;User ID=sa;Password=y123123!@;Trust Server Certificate=True";//使用下面的链接给的方法获取这个字符串 optionsBuilder.UseSqlServer(connectStr); optionsBuilder.UseLoggerFactory(LoggerFactory);//重点2 //base.OnConfiguring(optionsBuilder); } protected override void OnModelCreating(ModelBuilder modelBuilder) { //在这里配置实体类与数据库表的映射关系 base.OnModelCreating(modelBuilder); //从当前程序集中拿到所有实现了IEntityTypeConfiguration接口的配置类进行配置的读取 modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); } }
-
如何进行关联查询?
foreach (var item in md.Articles.Include(a=>a.Comments)) { Console.WriteLine($"{item.Id}_{item.Title}_{string.Join(",",item.Comments.Select(c=>c.Message))}"); } //打印结果: 1_CH被评为亚洲最酷程序员_太牛了!,吹吧!SB 2_CH被评为亚洲最酷程序员_太牛了!,吹吧!SB 3_CH被评为亚洲最酷程序员_太牛了!,吹吧!SB
重点就是:
我们想在Article表中进行查询,并且想将Article表里与Comments表关联的comments属性填充数据,此时就需要我们使用Include指定Article对象的Comments属性,要求被填充,查询结果正确也可以看到所执行的SQL:(控制台)
info: Microsoft.EntityFrameworkCore.Infrastructure[10403] Entity Framework Core 6.0.36 initialized 'MyDb' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.36' with options: None info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (24ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT [t].[Id], [t].[Content], [t].[Title], [t0].[Id], [t0].[ArticleId], [t0].[Message] FROM [T_Article] AS [t] LEFT JOIN [T_Comment] AS [t0] ON [t].[Id] = [t0].[ArticleId] ORDER BY [t].[Id]
-
那如果查一个Comment对象所属的Article,怎么查:
using (MyDb md = new MyDb()) { Console.WriteLine(md.Comments.Single(c => c.Id == 1).Article.Title); }
执行后,会报Title为Null,同理,观察所执行的SQL:
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (30ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT TOP(2) [t].[Id], [t].[ArticleId], [t].[Message] FROM [T_Comment] AS [t] WHERE [t].[Id] = CAST(1 AS bigint)
并没有查询这个Comment对象所属的Article对象对应的表,所以没法将当前Comment对象所属的Article对象的Title属性值,当然为Null咯
所以我们同样也得使用Include填充Comment对象的Article属性:
using (MyDb md = new MyDb()) { Console.WriteLine(md.Comments.Include(c=>c.Article).Single(c => c.Id == 1).Article.Title); }
观察SQL:
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (28ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT TOP(2) [t].[Id], [t].[ArticleId], [t].[Message], [t0].[Id], [t0].[Content], [t0].[Title] FROM [T_Comment] AS [t] INNER JOIN [T_Article] AS [t0] ON [t].[ArticleId] = [t0].[Id] WHERE [t].[Id] = CAST(1 AS bigint)
总结:联表查询的时候,如果需要查询出当前表关联的另一张表的对应的信息,就需要使用Include关键字填充当前表对象的另一张表对应的属性,这样就会被“填充”
EF Core-额外的外键字段
-
上述Article和Comment实体对象定义的时候,Article有一个List< Comments >属性,Comment对象有一个Article属性,使用迁移指令之后,就会发现Comment表里就会自动生成了ArticleId,作为了外键
-
如果我们直接查询一个Comment对象所属的Article的id:
using (MyDb md = new MyDb()) { Console.WriteLine(md.Comments.Single(c=>c.Id==1).Article.Id); }
会发现数据库里有ArticleId,但是我们点不出来,上述的代码如果不Include,Article.Id也会为null,所以只能Include一下:
using (MyDb md = new MyDb()) { Console.WriteLine(md.Comments.Include(c=>c.Article).Single(c=>c.Id==1).Article.Id); }
同理观察SQL:
info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (25ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT TOP(2) [t].[Id], [t].[ArticleId], [t].[Message], [t0].[Id], [t0].[Content], [t0].[Title] FROM [T_Comment] AS [t] INNER JOIN [T_Article] AS [t0] ON [t].[ArticleId] = [t0].[Id] WHERE [t].[Id] = CAST(1 AS bigint)
可以看出:Comment表里明明有ArticleId,但是我们想把这个id查出来的话,居然还是需要使用Inner Join查询,这就很恶心!
如何查询一个数据的指定列
-
观察上述的查询代码,都是讲一张表的所有的字段都查出来了!我们一般提倡:尽量不要使用select *
-
如何实现:
var res = md.Articles.Select(a => new { Id = a.Id, Title = a.Title }).First(a=>a.Id==1); Console.WriteLine(res.Id+"_"+res.Title); //打印: info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (24ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT TOP(1) [t].[Id], [t].[Title] FROM [T_Article] AS [t] WHERE [t].[Id] = CAST(1 AS bigint) 1_CH被评为亚洲最酷程序员
也就是说:我们需要先使用Select将一个Article对象映射为一个只有Id,Title属性的匿名对象,然后再进行查询,最后First里的Lambda是的a代表的就是这个匿名对象!
尝试使用上述方法,看看能不能不Join查询出外键值
using (MyDb md = new MyDb())
{
var res = md.Comments.Include(c => c.Article).Select(c => new { Id = c.Id, AId = c.Article.Id }).Single(c => c.Id == 1);
Console.WriteLine(res.Id+"_"+res.AId);
}
//打印:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (25ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [t].[Id], [t0].[Id] AS [AId]
FROM [T_Comment] AS [t]
INNER JOIN [T_Article] AS [t0] ON [t].[ArticleId] = [t0].[Id]
WHERE [t].[Id] = CAST(1 AS bigint)
1_1
可以发现,还是用了Inner Join,所以上面的方法并没有解决当前问题!
如何正确处理不用Join查出外键值
-
应当再Comment对象中新增一个属性,并制定这个属性就是一个外键:
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>();//说明在“一”这边也有对应的属性对应“多” }
Comment实体类的FluentAPI中也要指定一下:
public void Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<Comment> builder) { builder.ToTable("T_Comment"); //配置一对多关系 builder.HasOne<Article>(c => c.Article).WithMany(a => a.Comments).HasForeignKey(c=>c.TheArticleId); }
注意上述外键名我故意写成:TheArticleId,此时还没有进行数据库迁移,数据库中这个外键的字段名为:ArticleId(这个是最开始建立起一对多的对应关系之后进行数据库迁移之后,自动生成的一个外键字段,“外键对象名Id”),此时我们修改成TheArticleId,进行完数据库迁移之后,可以发现ArticleId被更名为TheArticleId!
-
经过上述新的配置完之后,执行查询:
var res = md.Comments.Single(c => c.Id == 1); Console.WriteLine(res.TheArticleId); //打印: info: Microsoft.EntityFrameworkCore.Database.Command[20101] Executed DbCommand (26ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] SELECT TOP(2) [t].[Id], [t].[Message], [t].[TheArticleId] FROM [T_Comment] AS [t] WHERE [t].[Id] = CAST(1 AS bigint) 1
-
总结:如果建立了一对多的表关系,在“多”这边有外键,如果我们不手动指定“多”这张表的外键的时候,我们数据库迁移会自动生成一个外键字段,该字段名就是外键指向对象的“类名Id”,此时如果我们仅通过“多”这张表去查询出外键的值,就需要使用Include进行Join查询得到对应的外键值,此时我们正确的做法是手动在“多”这个对象中建立一个额外的外键字段,并在FluentAPI中配置好这个外键,如果我们额外建立的外键字段名和数据库已经一致了,就不用数据库迁移,如果不一致就需要进行一次数据库迁移,此时数据库中原本自动生成的外键字段就会被更名为我们制定的字段名,此时我们就能直接查询出这个外键字段的值具体是什么了,而不需要通过join查询得到!表越多,联表查询的压力会越大,这样做其实属于数据库优化的手段之一
-
除非有必要,一般不需要加这个额外的外键字段
EF Core的单向导航
- 导航属性:前述的一对多的配置关系,我们在两个实体类中都配置了对应关联的另一张表的属性,我们都可以称这两个属性为导航属性,上述两张表都互相导航了,这个叫双向导航
双向导航属性的优缺点
-
优点:像上述文章和其评论建立双向导航是合理的,因为可以很方便的从文章找到其所有的评论,也可以从一条评论方便的找到其所属的文章
-
缺点:
比如上述的User表,所有的“单”都需要导航至其所属的人员记录,也就是说所有的“单”都需要配置一个指向User的属性,但如果我们也想上面一样建立一个反向的导航,那User表字段或者说User对象的属性也太多了吧!
单向导航
-
拿上面请假单的案例:
-
User:
class User { public long Id { get; set; } public string Name { get; set; } }
-
Leave:此处就有Requester,Approver两个属性用于导航至User表
class Leave { public long Id { get; set; } public User Requester { get; set; } public User Approver { get; set; } public string Remark { get; set; } }
-
关键就是配置Leave,从实体类的属性来看,一张请假单只能隶属于一个人,而一个人可以有多个请假单,所以站在请假单的角度构成了“多对一”的情况,按照前面配置的写法,可以先写出这个(“从右往左”):
public void Configure(EntityTypeBuilder<Leave> builder) { builder.ToTable("T_Leave"); builder.HasOne<>().WithMany(); }
-
“从左往右”填充:
public void Configure(EntityTypeBuilder<Leave> builder) { builder.ToTable("T_Leave"); builder.HasOne<User>(l=>l.Requester).WithMany();//由于只是实体类建立的时候就是单向导航的形式去建立的,所以WithMany里填不了东西!但这样已经够了!没错,就是这样写! }
-
同理配置一下Approver:
public void Configure(EntityTypeBuilder<Leave> builder) { builder.ToTable("T_Leave"); builder.HasOne<User>(l=>l.Requester).WithMany().IsRequired(); builder.HasOne<User>(l=>l.Approver).WithMany();//这里不能指定外键,否则产生什么级联删除的效应,不允许 }
-
-
进行一下数据迁移:
Add-Migration Test111 update-database
-
出现了一次update-databse的异常,导致数据库的表生成错误,目前只会暴力解决,就是删除所有的迁移文件,包括shapshot.cs文件,然后删除当前数据库中的所有的表,最后重新执行Add-migration 和update-databse
如何选择单向导航还是双向导航属性
- 对于主从结构的“一对多”表关系,一般是声明双向导航属性,比如:文章和评论,有了文章才有评论,构成主从关系,此时就需要建立双向导航属性,又比如购物车和购物车里面的商品
- 而对于其他“一对多”的表关系,如果表结构属于被很多表引用的基础表,则用单向导航属性
- 如果不满足上述两种条件,则自由选择
关系配置在任何一行都可以
-
之前提到的双向导航属性中,HasOne那些东西是可以配置写在“多”或者"一"这边的配置类中的,都写也可以
-
由于单向导航的存在,因为其进行配置的时候,只能在“多”对应的配置类中配置:
HasOne<>().WithMany()
所以,既然双向导航属性支持:
HasOne<>().WithMany() HasMany<>().WithOne()
那何不统一用单向导航的那种配置方法呢,能统一起来