EF的CodeFirst模式详解

默认约定

  1. 正宗的CodeFirst模式是不含有edmx模型,需要手动创建实体、创建EF上下文,然后生成通过代码来自动映射生成数据库。

  1. 旨在:忘记SQL、忘记数据库。

  1. 三类配置: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);
        }
    }
复制代码

发现的现象

a. 数据库中多了一张表:Student3Course1,里面有两个外键

总结

  上面的三种数据库表的对应关系主要都是采用的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);
        }

    }
复制代码

指令形式

主要分为以下几个内容:

  1. 启用迁移 :Enable-Migrations
  2. 添加迁移 :Add-Migration
  3. 执行迁移 : Update-Database
  4. 回滚 : 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:

 

以上内容来自:https://www.cnblogs.com/yaopengfei

posted @   明志德道  阅读(1248)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示