EF的CodeFirst模式详解
-
正宗的CodeFirst模式是不含有edmx模型,需要手动创建实体、创建EF上下文,然后生成通过代码来自动映射生成数据库。
-
旨在:忘记SQL、忘记数据库。
-
三类配置:One To One(one-to-zero-or-one)、One To Many、Many To Many。
注意:在该模块使用最简单的模式配置这三种关系,暂时先不考虑DataAnnotation和Fluent API
A. One To One(one-to-zero-or-one)
①:一个学生对应一个或零个地址,一个地址只能对应一个学生
②:实现方式:
a:Student表中添加StudentAddress属性,StudentAddress表中添加Student属性,然后给StudentAddress中的主键加上[ForeignKey("stu")] ,
特别注意stu代表Student属性的名字。
b:编写配置文件
/// <summary> /// 学生表(一个学生只能有一个地址或没有地址) /// </summary> public class Student { public Student() { } public string studentId { get; set; } public string studentName { get; set; } public virtual StudentAddress StudentAddress { get; set; } } /// <summary> /// 学生地址表(一个地址只能对应一个学生) /// </summary> public class StudentAddress { public StudentAddress() { } [ForeignKey("stu")] //特别注意这个地方,stu对应下面的 Student stu; //另外特别注意:studentAddressId,符合默认的Id生成规则,自动映射成主键,否则需要加【key】特性 public string studentAddressId { get; set; } public string addressName { get; set; } public virtual Student stu { get; set; } }
public class dbContext1:DbContext { public dbContext1() : base("name=dbContext1") { } public DbSet<Student> Student { get; set; } public DbSet<StudentAddress> StudentAddress { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
发现的现象
a.已经生成了一次数据库,修改表结构,会报错:数据添加失败支持“dbContext1”上下文的模型已在数据库创建后发生更改。
请考虑使用 Code First 迁移更新数据库(http://go.microsoft.com/fwlink/?LinkId=238269)。
b. 类名+Id结尾属性自动生成主键。
c.生成的数据库默认保存在数据库的安装目录下。
d.在数据库中:StudentAddress中的studentAddressId既是主键,又是外键
B. One To Many
①:一个年级有多个学生,一个学生只能在一个年级
②:实现方式:
a:Student2表中添加Grade1属性,Grade1表中添加ICollection<Student2>属性
b:编写配置文件
/// <summary> /// 年级表(一个年级有多个学生) /// </summary> public class Grade1 { public string grade1Id { get; set; } public string gradeName { get; set; } public virtual ICollection<Student2> student2 { get; set; } } /// <summary> /// 学生表(一个学生只能在一个年级) /// </summary> public class Student2 { public string student2Id { get; set; } public string studentName { get; set; } public virtual Grade1 grade1 { get; set; } }
public class dbContext2 : DbContext { public dbContext2() : base("name=dbContext2") { } public DbSet<Student2> Student2 { get; set; } public DbSet<Grade1> Grade1 { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
发现的现象:
a. 数据库中:Student2表中,增加了一个新的字段grade1_grade1Id作为外键
C. Many To Many
②:实现方式:
a:Student3表中添加ICollection<Course1>属性,Course1表中添加ICollection<Student3>属性
b:编写配置文件
/// <summary> /// 学生表(一个学生有多门课) /// </summary> public class Student3 { public string student3Id { get; set; } public string studentName { get; set; } public virtual ICollection<Course1> course { get; set; } } /// <summary> /// 课程表(一门课会有多个学生上) /// </summary> public class Course1 { public string course1Id { get; set; } public string courseName { get; set; } public virtual ICollection<Student3> student { get; set; } }
public class dbContext3 : DbContext { public dbContext3() : base("name=dbContext3") { } public DbSet<Student3> Student3 { get; set; } public DbSet<Course1> Course1 { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
发现的现象
总结
上面的三种数据库表的对应关系主要都是采用的EF的默认协定,比如:
A: 类名+ID、类名+Id、类名+id自动生成主键(类名忽略大小写),
B: string类型自动映射成nvarchar(max)
但在实际开发中,默认协定显然不灵活,那么我们如何自定义配置呢(自定义主键、设置非空、设置类型、长度等),EF提供两种方式:DataAnnotations和Fluent API,但大多数情况都是两者配合使用,关于二者的详细使用,请见后面
通过DataAnnotations修改默认协定
简介
1. DataAnnotations说明:EF提供以特性的方式添加到 domain classes上,其中包括两类:
A:System.ComponentModel.DataAnnotations命名空间下的特性是表中列的属性的。
包括:Key、Required、MinLength和MaxLength、StringLength、Timestamp、ConcurrencyCheck。
B:System.ComponentModel.DataAnnotations.Schema命名空间下的特性是控制数据库结构的。
包括:Table、Column、ForeignKey、NotMapped。
2. 特性介绍
① Key :声明主键
② Required:非空声明
③ MinLength和MaxLength:设置string类型的最大长度和最小长度,数据库的对应nvarchar
④ StringLength:设置string类型的长度,数据库对应nvarchar
⑤ Timestamp:将byte[]类型设置为timestamp类型
⑥ ConcurrencyCheck:并发检查,执行update操作时,会检查并发性(乐观锁)
⑦ Table: 给代码中的类换一个名来映射数据库中的表名.(还可以设置表的架构名称 [Table("myAddress", Schema = "Admin")] )
⑧ Column: 给代码中类的属性换一个名来映射数据库中表的列名. (还可以设置列的类型、列在表中的显示顺序 [Column("myAddressName2", Order = 1, TypeName = "varchar")])
⑨ ForeignKey:设置外键,特别注意里面的参数填写什么.
⑩ NotMapped: 类中的列名不在数据库表中映射生成. (还可以只设置get属性或者只设置set属性,在数据库中也不映射)
另外还有:Index、InverseProperty、DatabaseGenerated、ComplexType 这四个都不常用,在这里就不多介绍了
(详细可以看:http://www.entityframeworktutorial.net/code-first/dataannotation-in-code-first.aspx)
代码实战
public class Student4 { [Key] //主键声明 public string studentKey { get; set; } [Required] //非空声明 public string stuName { get; set; } [MaxLength(10)] //最大长度 public string stuTxt1 { get; set; } [MaxLength(10), MinLength(2)] //最大长度和最小长度 public string stuTxt2 { get; set; } [Timestamp] //设置为时间戳 public byte[] rowVersion { get; set; } [ConcurrencyCheck] //并发检查 public string stuTxt3 { get; set; } public virtual StudentAddress4 stuAddress4 { get; set; } } [Table("myAddress")] //设置类映射的数据库表名 //[Table("myAddress", Schema = "Admin")] //设置类映射的数据库表名和架构名 public class StudentAddress4 { [ForeignKey("stu")] //设置外键(对应下面声明的 stu) //这里符合 类名+id(忽略大小写)的规则,所以自动生成主键 public string studentAddress4Id { get; set; } [Column("myAddressName")] //设置映射数据库中表的列名 public string stuAddressName { get; set; } [Column("myAddressName2", Order = 1, TypeName = "varchar")] //设置映射数据库中表的列名、顺序、类型 public string stuAddrssName2 { get; set; } [NotMapped]//不映射数据 public string addressNum { get; set; } //不映射数据 public string txt1 { get { return stuAddrssName2;} } //不映射数据 public string _txt2 = "1"; public string txt2 { set { _txt2 = value; } } public virtual Student4 stu { get; set; } }
public class dbContext4 : DbContext { public dbContext4() : base("name=dbContext4") { } public DbSet<Student4> Student4 { get; set; } public DbSet<StudentAddress4> StudentAddress4 { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
通过Fluent API修改默认协定
简介
1. 优先级:Fluent API > data annotations > default conventions.
2. 所有的Fluent API配置都要在 OnModelCreating这个重写方法中进行
3. 常见的配置:
① 获取表对应的配置根: var stu =modelBuilder.Entity<XXX>();
② 设置主键:HasKey<string>(s => s.studentKey);
③ 获取属性:stu.Property(p => p.stuName)
④ 设置可空或非空:IsRequired和IsOptional
⑤ 设置最大值:HasMaxLength
⑥ 修改属性名→修改属性的次序→修改属性对应的数据库类型:HasColumnName→HasColumnOrder→HasColumnType
⑦ 修改表名:ToTable
4. 可以建立多个Fluent API的配置文件,然后通过 modelBuilder.Configurations.Add(new XXX());添加到一起
PS:或者利用这句话 modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly()); 把所有的配置文件一次性全部加进来
代码实战
public class Student5 { //主键声明 public string studentKey { get; set; } //非空声明 public string stuName { get; set; } //最大长度 public string stuTxt1 { get; set; } //最大长度和最小长度 public string stuTxt2 { get; set; } //设置为时间戳 public byte[] rowVersion { get; set; } //并发检查 public string stuTxt3 { get; set; } //public virtual StudentAddress5 stuAddress5 { get; set; } } public class StudentAddress5 { //既是主键、又是外键 public string stuAddressId { get; set; } //设置映射数据库中表的列名 public string stuAddressName { get; set; } //设置映射数据库中表的列名、顺序、类型 public string stuAddrssName2 { get; set; } //不映射数据 public string addressNum { get; set; } //不映射数据 public string txt1 { get { return stuAddrssName2; } } //不映射数据 public string _txt2 = "1"; public string txt2 { set { _txt2 = value; } } //public virtual Student5 stu { get; set; } } /// <summary> /// Game实体,与其它两个没有什么直接关系,单纯的为了演示, Fluent API的配置,可以根据实体进行拆分 /// 文件来配置,方便管理 /// </summary> public class Game { public int GameId { get; set; } public string GameName { get; set; } }
/// <summary> /// Game实体的配置文件 /// </summary> public class GameConfiguration : EntityTypeConfiguration<Game> { public GameConfiguration() { this.HasKey(p => p.GameId); this.Property(p => p.GameName).HasMaxLength(10).IsRequired(); } }
public class dbContext5 : DbContext { public dbContext5() : base("name=dbContext5") { } public DbSet<Student5> Student5 { get; set; } public DbSet<StudentAddress5> StudentAddress5 { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { //所有的FluentAPI均在方法中进行重写 //一. 属性层次上的设置 var stu = modelBuilder.Entity<Student5>(); var stuAddress = modelBuilder.Entity<StudentAddress5>(); //1. 设置主键 stu.HasKey<string>(s => s.studentKey); stuAddress.HasKey<string>(s => s.stuAddressId); //为什么需要动态设置主键,有待研究?? //2. 设置非空 (扩展:IsOptional 设置可空) stu.Property(p => p.stuName).IsRequired(); //3. 设置最大值(不能设置最小值) stu.Property(p => p.stuTxt1).HasMaxLength(10); //4. 修改列的名称、排序、类型 stuAddress.Property(p => p.stuAddrssName2).HasColumnName("myAddress2").HasColumnOrder(1).HasColumnType("varchar"); //5.修改表名 stu.Map<Student5>(c => { c.ToTable("MyStudent"); }); //6.将一个实体映射成多张表,并分别给其命名 //stuAddress.Map<StudentAddress5>(c => //{ // c.Properties(p => new // { // p.stuAddressId, // p.stuAddressName // }); // c.ToTable("MyStuAddress1"); //}).Map<StudentAddress5>(c => //{ // c.Properties(p => new // { // p.stuAddressId, // p.stuAddrssName2 // }); // c.ToTable("MyStuAddress2"); //}); //三. 将Game实体的配置添加进来 modelBuilder.Configurations.Add(new GameConfiguration()); base.OnModelCreating(modelBuilder); } }
数据库的初始化有四种策略及迁移
四种初始化策略
EF的CodeFirst模式下数据库的初始化有四种策略:
1. CreateDatabaseIfNotExists:EF的默认策略,数据库不存在,生成数据库;一旦model发生变化,抛异常,提示走数据迁移
2. DropCreateDatabaseIfModelChanges:一旦model发生变化,删除数据库重新生成
3. DropCreateDatabaseAlways:数据库每次都重新生成
4. 自定义初始化(继承上面的三种策略中任何一种,然后追加自己的业务)
关闭数据库初始化策略:
Database.SetInitializer<dbContext6>(null); (改变实体类,不会报错,不会丢失数据,但无法映射改变数据库结构了)
代码如下:
public class dbContext6 : DbContext { public dbContext6() : base("name=dbContext6") { //在这里可以改变生成数据库的初始化策略 //1. CreateDatabaseIfNotExists (EF的默认策略,数据库不存在,生成数据库;一旦model发生变化,抛异常,提示走数据迁移) //Database.SetInitializer<dbContext6>(new CreateDatabaseIfNotExists<dbContext6>()); //2. DropCreateDatabaseIfModelChanges (一旦model发生变化,删除数据库重新生成) //Database.SetInitializer<dbContext6>(new DropCreateDatabaseIfModelChanges<dbContext6>()); //3.DropCreateDatabaseAlways (数据库每次都重新生成) //Database.SetInitializer<dbContext6>(new DropCreateDatabaseAlways<dbContext6>()); //4. 自定义初始化(继承上面的三种策略中任何一种,然后追加自己的业务) //Database.SetInitializer<dbContext6>(new MySpecialIntializer()); //5. 禁用数据库策略(不会报错,不会丢失数据,但是改变不了数据库的结构了) //Database.SetInitializer<dbContext6>(null); } public DbSet<Animal> Animal { get; set; } public DbSet<AnimalKind> AnimalKind { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } /// <summary> /// 自定义一个初始化策略,每次初始化时追加10条数据 /// </summary> public class MySpecialIntializer : DropCreateDatabaseAlways<dbContext6> { public override void InitializeDatabase(dbContext6 context) { base.InitializeDatabase(context); } //重写seed,追加初始数据 protected override void Seed(dbContext6 context) { for (int i = 0; i < 10; i++) { context.Animal.Add(new Animal() { id = "animal" + i, animalName = "mru" + i, animalKind = "mruru" + i, }); } context.SaveChanges(); base.Seed(context); } } }
总结:以上四种初始化策略,让我们知道了上一个章节中,为什么数据库结构发生变化就会抛异常,但是无论哪种方式,还是没有解决数据丢失的问题,解决数据丢失问题,详见:数据迁移
数据迁移
代码的形式非指令
数据迁移很好的解决了修改表结构,数据丢失的问题,在本章节介绍通过代码的形式处理数据迁移问题,指令控制的形式,在后面章节介绍。
步骤:
①:新建Configuration.cs类,在其构造函数中进行相关配置。
②:改变数据库的初始化策略为,MigrateDatabaseToLatestVersion ,并在Configuration类中配置开启自动迁移:AutomaticMigrationsEnabled = true;
③:显式开启允许修改表结构:AutomaticMigrationDataLossAllowed = true; (默认是关闭的)
进行测试:
① 增加一列:改变Animal类,增加一个字段,刷新数据库,发现数据多了一列 (无需特别的配置),但后面再次增加,还是需要配置AutomaticMigrationDataLossAllowed设置为true
② 删除一列、修改列、改变列的属性:报错,提示需要将AutomaticMigrationDataLossAllowed设置为true, 设置后,修改或删除成功。
③ 增加一张表:增加一个AnimalKind类,运行代码,发现数据库多了一张表。
④ 删除表:注释掉:public DbSet<AnimalKind> AnimalKind { get; set; } ,运行代码,数据库中AnimalKind表被删除.
代码如下:
internal sealed class Configuration : DbMigrationsConfiguration<dbContext6> { public Configuration() { AutomaticMigrationsEnabled = true; //启用自动迁移 AutomaticMigrationDataLossAllowed = true; //更改数据库中结构(增加、删除列、修改列、改变列的属性、增加、删除、修改表),需要显示开启。 } }
public class dbContext6 : DbContext { public dbContext6() : base("name=dbContext6") { //数据库迁移配置 //数据库迁移的初始化方式 Database.SetInitializer(new MigrateDatabaseToLatestVersion<dbContext6, Configuration>("dbContext6")); } public DbSet<Animal> Animal { get; set; } public DbSet<AnimalKind> AnimalKind { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
指令形式
主要分为以下几个内容:
- 启用迁移 :Enable-Migrations
- 添加迁移 :Add-Migration
- 执行迁移 : Update-Database
- 回滚 : update-database -TargetMigration:
Enable-Migrations
启用迁移之后,会在项目中新建 Migrations文件夹,下面有Configuration.class
internal sealed class Configuration : DbMigrationsConfiguration<EFCodeFirst详细问题.DataBaseEntities> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; ContextKey = "EFCodeFirst详细问题.DataBaseEntities"; } protected override void Seed(EFCodeFirst详细问题.DataBaseEntities context) { // This method will be called after migrating to the latest version. // You can use the DbSet<T>.AddOrUpdate() helper extension method // to avoid creating duplicate seed data. } }
可以在构造函数中配置自动迁移,允许数据丢失等内容,重写的Seed方法看注释是在迁移到最后版本之后的执行函数,可以进行初始化数据等操作。
Add-Migration
添加迁移后,在configuration.class同级目录会生成一个升级脚本的C#的表达,其中用两个函数up和down,up是升级操作,down是回滚操作,我们也可以在其中编辑自己的代码,这也是迁移中比较灵活的地方。
public partial class InitialCreate : DbMigration { public override void Up() { CreateTable( "dbo.Schools", c => new { SchoolID = c.Int(nullable: false, identity: true), Name = c.String(), }) .PrimaryKey(t => t.SchoolID); CreateTable( "dbo.Students", c => new { StudentID = c.Int(nullable: false, identity: true), Name = c.String(), Age = c.Int(nullable: false), }) .PrimaryKey(t => t.StudentID); } public override void Down() { DropTable("dbo.Students"); DropTable("dbo.Schools"); } }
Update-Database
这个没什么好说的,就是执行数据迁移--updateToLastVersion,有一个点就是在命令后面加上–verbose,可以看见执行的具体的sql脚本,对于定位错误还是很有用的
update-database -TargetMigration:
付费内容,请联系本人QQ:1002453261
本文来自博客园,作者:明志德道,转载请注明原文链接:https://www.cnblogs.com/for-easy-fast/articles/12528298.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析