EntityFrameworkCore數據遷移(一)


  .net core出來已經有很長一段時間了,而EentityFrameworkCore(后面簡稱EFCore)是.net framework的EntityFramework在.net core中的實現,至於EntityFramework是什么,這里就不介紹了。

  本文主要介紹EFCore的CodeFirst方式下的數據遷移。

  

  一、創建項目

  首先創建項目結構如下:

  

   說明:

  EFCoreDemo.EntityFrameworkCore:這個是一個標准類庫,主要一些EFCore的一些ORM實體與配置。

  EFCoreDemo.ConsoleApp:這個是一個控制台項目,主要用於使用EFCore的使用,因為一般都是使用WebApi或者MVC,然后后使用三層架構,或者一些其他的架構諸如ABP等,使用倉儲等模式使用EFCore,但這里為了簡單,直接使用一個控制台程序模擬。

  EFCoreDemo.EntityFrameworkCore.Host:這個也是一個控制台程序,主要用於EFCore的數據遷移。

  於是乎,我們的項目引用關系是:

  EFCoreDemo.ConsoleApp 引用 EFCoreDemo.EntityFrameworkCore

    EFCoreDemo.EntityFrameworkCore.Host 引用 EFCoreDemo.EntityFrameworkCore

   

  二、創建實體及上下文

  對EFCoreDemo.EntityFrameworkCore項目使用nuget安裝EFCore所需的包:

   # 如果使用MySql,安裝 
  Microsoft.EntityFrameworkCore Pomelo.EntityFrameworkCore.MySql

  # 如果使用SqlServer,安裝
  Microsoft.EntityFrameworkCore  
  Microsoft.EntityFrameworkCore.SqlServer

  這里使用的是MySql

  現在假設我們要創建3張表,用戶表(Account),活動表(Activity),活動記錄表(ActivityRecord)

  於是我們分別創建3個實體與它對應  

  
    /// <summary>
    /// 用戶表
    /// </summary>
    public class Account : BaseEntity
    {
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 手機號碼
        /// </summary>
        public string Phone { get; set; }
        /// <summary>
        /// 年齡
        /// </summary>
        public int Age { get; set; }
        /// <summary>
        /// 創建時間
        /// </summary>
        public DateTime CreationTime { get; set; }
    }
用戶表
  
    /// <summary>
    /// 活動表
    /// </summary>
    public class Activity : BaseEntity
    {
        /// <summary>
        /// 活動名稱
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 開始時間
        /// </summary>
        public DateTime StartTime { get; set; }
        /// <summary>
        /// 結束時間
        /// </summary>
        public DateTime EndTime { get; set; }
        /// <summary>
        /// 活動狀態
        /// </summary>
        public int Status { get; set; }
        /// <summary>
        /// 創建時間
        /// </summary>
        public DateTime CreationTime { get; set; }
    }
活動表
  
    /// <summary>
    /// 活動記錄表
    /// </summary>
    public class ActivityRecord : BaseEntity
    {
        /// <summary>
        /// 活動Id
        /// </summary>
        public int ActivityId { get; set; }
        /// <summary>
        /// 用戶Id
        /// </summary>
        public int AccountId { get; set; }
        /// <summary>
        /// 創建時間
        /// </summary>
        public DateTime CreationTime { get; set; }
    }
活動記錄表

  其中BaseEntity是一個抽象類,主要用來記錄主鍵Id的,推薦每個表都有單列主鍵,盡量不要使用復合主鍵,如果有其他唯一標識,可以使用唯一值索引  

    public abstract class BaseEntity
    {
        /// <summary>
        /// Primary Key
        /// </summary>
        public int Id { get; set; }
    }

  這里設置主鍵類型是int,當然也可以使用其他數據類型,只需將BaseEntity寫成泛型類就可以了,如:  

    public abstract class BaseEntity<T>
    {
        /// <summary>
        /// Primary Key
        /// </summary>
        public T Id { get; set; }
    }

  接下來,創建實體映射配置類,同樣的,這里也創建一個基類,基類主要做一些通用的配置:    

    public abstract class BaseEntityTypeConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class
    {
        /// <summary>
        /// 配置實體類型
        /// </summary>
        /// <param name="builder"></param>
        public virtual void Configure(EntityTypeBuilder<TEntity> builder)
        {
            //映射表名
            builder.ToTable("demo_" + typeof(TEntity).Name.ToLower());

            //映射主鍵
            if (typeof(BaseEntity).IsAssignableFrom(typeof(TEntity)))
            {
                builder.HasKey(nameof(BaseEntity.Id));
                builder.Property(nameof(BaseEntity.Id)).ValueGeneratedOnAdd();//自增
            }

            //種子數據
            var seeds = this.GetSeeds();
            if (seeds != null && seeds.Length > 0)
            {
                builder.HasData(seeds);
            }
        }

        /// <summary>
        /// 種子數據
        /// </summary>
        /// <returns></returns>
        public virtual TEntity[] GetSeeds()
        {
            return new TEntity[0];
        }
    }

  然后分別對用戶表(Account),活動表(Activity),活動記錄表(ActivityRecord)三個實體類創建映射配置類  

  
    public class AccountEntityTypeConfiguration : BaseEntityTypeConfiguration<Account>
    {
        /// <summary>
        /// 配置實體類型
        /// </summary>
        /// <param name="builder"></param>
        public override void Configure(EntityTypeBuilder<Account> builder)
        {
            base.Configure(builder);
        }

        /// <summary>
        /// 種子數據
        /// </summary>
        /// <returns></returns>
        public override Account[] GetSeeds()
        {
            return new Account[] {
                new Account(){
                    Id=1,
                    Name="admin",
                    Phone="110",
                    Age=100,
                    CreationTime=new DateTime(2020,02,02)//注意,種子數據不要使用DateTime.Now之類的,避免每次都會遷移數據
                }
            };
        }
    }
AccountEntityTypeConfiguration
  
    public class ActivityEntityTypeConfiguration : BaseEntityTypeConfiguration<Activity>
    {
        /// <summary>
        /// 配置實體類型
        /// </summary>
        /// <param name="builder"></param>
        public override void Configure(EntityTypeBuilder<Activity> builder)
        {
            base.Configure(builder);
        }
    }
ActivityEntityTypeConfiguration
  
    public class ActivityRecordEntityTypeConfiguration : BaseEntityTypeConfiguration<ActivityRecord>
    {
        /// <summary>
        /// 配置實體類型
        /// </summary>
        /// <param name="builder"></param>
        public override void Configure(EntityTypeBuilder<ActivityRecord> builder)
        {
            base.Configure(builder);
        }
    }
ActivityRecordEntityTypeConfiguration

  到這里,有些朋友可能會覺着這樣做很麻煩,倒不如直接使用特性標識實體類來的簡單,但筆者認為,使用特性標識確實簡單,但它的局限性太大:

  1、使用特性讓實體看起來不干凈,而且當描述實體關系時不是很方便

  2、特性功能有限,很多復雜的關系實現不了,而初始化數據也不方便實現

  3、當特性不能滿足我們的需求是,我們可能需要將配置移到其他地方去,如DbContext的OnModelCreating方法,這樣就造成了配置的分散

  總之,當使用CodeFirst方式開發時,推薦使用單獨的映射配置類去實現,盡可能不要使用特性

  接着就可以創建DbContext上下文了:  

    public class DemoDbContext : DbContext
    {
        public DemoDbContext(DbContextOptions options) : base(options)
        {

        }

        #region Method
        /// <summary>
        /// 配置
        /// </summary>
        /// <param name="optionsBuilder"></param>
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.EnableSensitiveDataLogging();
            base.OnConfiguring(optionsBuilder);
        }
        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="modelBuilder"></param>
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyFromAssembly(typeof(DemoDbContext));
        }
        #endregion
    }

  這里的DbContext沒有創建那些DbSet屬性,不表示不能創建,只是為了說明EFCore的遷移不依賴那些而已。

  其中modelBuilder.ApplyFromAssembly是一個拓展方法:  

    public static class EntityFrameworkCoreExtensions
    {
        /// <summary>
        /// 從程序集加載配置
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="modelBuilder"></param>
        /// <returns></returns>
        public static ModelBuilder ApplyFromAssembly<T>(this ModelBuilder modelBuilder)
            => modelBuilder.ApplyFromAssembly(typeof(T));
        /// <summary>
        /// 從程序集加載配置
        /// </summary>
        /// <param name="modelBuilder"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static ModelBuilder ApplyFromAssembly(this ModelBuilder modelBuilder, Type type)
            => modelBuilder.ApplyFromAssembly(type.Assembly);
        /// <summary>
        /// 從程序集加載配置
        /// </summary>
        /// <param name="modelBuilder"></param>
        /// <param name="assembly"></param>
        /// <returns></returns>
        public static ModelBuilder ApplyFromAssembly(this ModelBuilder modelBuilder, Assembly assembly)
        {
            return modelBuilder.ApplyConfigurationsFromAssembly(assembly, t => !t.IsAbstract);
        }
    }

  到這里,EFCoreDemo.EntityFrameworkCore項目就基本上完成了,項目結構如下:

  

 

  三、開始遷移

  注意,這里說明一下,其實數據遷移也是可以在EFCoreDemo.EntityFrameworkCore中完成的,但是數據遷移不是我們業務上下文的的一部分,他只是我們開發過程中用到的,所以沒有必要將它們放在一起,最好將它獨立出來,比如這里,將它獨立到一個單獨的控制台程序中,這個就是EFCoreDemo.EntityFrameworkCore.Host項目:

  首先,對EFCoreDemo.EntityFrameworkCore.Host使用nuget安裝:  

    Microsoft.EntityFrameworkCore.Design
    Microsoft.EntityFrameworkCore.Tools

  這兩個包是遷移所需的工具包。

  創建一個遷移上下文:  

    public class MigrationDbContext : DemoDbContext
    {
        public MigrationDbContext(DbContextOptions options) : base(options)
        {
        }
    }

  可以看到,這個遷移數據上下文其實是繼承了DemoDbContext類,然后其他的什么都沒做,那為什么不直接使用DemoDbContext

  當然,使用DemoDbContext也是可以的,甚至可以將MigrationDbContext繼承DbContext,然后將DemoDbContext中的內容復制一份到MigrationDbContext也是可以的。

  而這里單獨創建一個MigrationDbContext主要是為了遷移的方便,因為EFCore生成遷移時默認是從啟動項目中去查找DbContext類的,如果DbContext類在其他類庫中,那么就需要自己指定-Context參數。

  其次,單獨的MigrationDbContext上下文,也能保證生成的遷移文件在當前項目,而不會跑到其他項目中去!

  接着創建一個遷移時使用的上下文工廠類,這個類需要實現IDesignTimeDbContextFactory<DbContext>接口:  

    public class DemoMigrationsDbContextFactory : IDesignTimeDbContextFactory<MigrationDbContext>
    {
        public MigrationDbContext CreateDbContext(string[] args)
        {var builder = new DbContextOptionsBuilder<MigrationDbContext>()
                .UseMySql("Server=192.168.209.128;Port=3306;Database=demodb;Uid=root;Pwd=123456");

            return new MigrationDbContext(builder.Options);
        }
    }

   到這里,EFCoreDemo.EntityFrameworkCore.Host項目就完成了,項目結構如下:

  

   到這里,就可以生成遷移文件了。

  通過【導航欄=》工具=》NuGet包管理器=》程序包管理器控制台】打開控制台:

  

   然后輸入 Add-Migration init_20200727

  

   這里使用的 Add-Migration 命令生成遷移文件,其中 init_20200727 是遷移名稱。

   Add-Migration命令常用參數如下:  

    -o|--output-dir <PATH>    存放遷移文件的相對路徑,默認是Migrations
    -c|--context <DBCONTEXT>    遷移使用的上下文
    -a|--assembly <PATH>    遷移使用的程序集
    -s|--startup-assembly <PATH>    遷移時的啟動程序集
    --project-dir <PATH>    項目路徑
    --language <LANGUAGE>    語言,默認是C#    

  注意,要將 EFCoreDemo.EntityFrameworkCore.Host 設置成啟動項目,然后將控制台的默認項目也設置成 EFCoreDemo.EntityFrameworkCore.Host ,否則生成遷移文件的是否會報錯

  Your startup project 'EFCoreDemo.EntityFrameworkCore' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is correct, install the package, and try again.

  

   More than one DbContext was found. Specify which one to use. Use the '-Context' parameter for PowerShell commands and the '--context' parameter for dotnet commands.

   

    Add-Migration 命令執行成功后,會生成一個Migrations目錄,目錄下會有三個文件:

  

   20200727075220_init_20200727.cs:這個是我們的遷移文件,其中兩個方法,Up方法時往數據庫遷移數據時執行,Down方法時撤銷遷移時執行

   20200727075220_init_20200727.Designer.cs:這個設計文件,記錄的是當前遷移之后實體映射的一個快照

     MigrationDbContextModelSnapshot.cs:這個是當前實體映射的快照

  注意,上面的文件名中,20200727075220是時間戳(UTC時間),后面的init_20200727是遷移名稱,EFCore的遷移執行順序是按照時間戳的順序執行的。

  如果需要修改遷移的執行順序,可以自行改變這個時間戳,但是不是改變文件名,而是對應的Designer.cs文件中的Migration特性標識的那個時間戳!

  生成遷移文件之后,如果要撤銷此次的遷移文件,可以使用 Remove-Migration 命令

    接着我們可以使用 Update-Database 命令開始執行遷移:

  

    注意:Update-Database命令會執行所有的數據遷移,可以指定一個遷移名稱去執行單個數據遷移

   執行完Update-Database后,可以看看數據庫:

  

  其中除了我們業務需要的表之外,還有一張名稱為__EFMigrationsHistory的表,這個表其實就是EFCore的遷移記錄表

  注:這里說的是使用Nuget命令來生成遷移和執行遷移,但是如果要上線,總不能使用nuget連接線上數據庫操作吧?這里有兩種解決辦法:

  1、在nuget中使用Script-Migration命令將遷移生成腳本,然后到線上去執行

  2、使用程序去執行遷移,比如控制台程序,或者在項目啟動時執行,具體可參考下一篇:EntityFrameworkCore數據遷移(二)

  

  四、再次遷移

   上面的遷移其實是生成了數據庫和其中的一些表,但是我們的需求是不斷變化的。

  前面我們雖然創建了表,但是里面字段約束等等基本上都放過了,比如字段長度,外鍵約束等等,現在我們補上:

  1、Account中的姓名和手機號碼長度不超過100,而且姓名為必須(非空);

  2、Activity中名稱長度也不超過100,且必須(非空);

  3、Activity中增加備注列,長度不超過1000

    4、ActivityRecord對ActivityId和AccountId增加外鍵約束

  首先,Activity增加列,只需要在實體中添加對應的屬性就可以了:  

  
    /// <summary>
    /// 活動表
    /// </summary>
    public class Activity : BaseEntity
    {
        /// <summary>
        /// 活動名稱
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 開始時間
        /// </summary>
        public DateTime StartTime { get; set; }
        /// <summary>
        /// 結束時間
        /// </summary>
        public DateTime EndTime { get; set; }
        /// <summary>
        /// 活動狀態
        /// </summary>
        public int Status { get; set; }
        /// <summary>
        /// 創建時間
        /// </summary>
        public DateTime CreationTime { get; set; }
        /// <summary>
        /// 備注
        /// </summary>
        public string Remark { get; set; }
    }
Activity

  ActivityRecord要增加外鍵約束,首先在ActivityRecord中新增Activity和Account的類型引用:  

  
    /// <summary>
    /// 活動記錄表
    /// </summary>
    public class ActivityRecord : BaseEntity
    {
        /// <summary>
        /// 活動Id
        /// </summary>
        public int ActivityId { get; set; }
        /// <summary>
        /// 用戶Id
        /// </summary>
        public int AccountId { get; set; }
        /// <summary>
        /// 創建時間
        /// </summary>
        public DateTime CreationTime { get; set; }

        public Activity Activity { get; set; }
        public Account Account { get; set; }
    }
ActivityRecord

  其他的,為實現這幾項約束,我不使用特性,只需要在上面的實體映射配置中用代碼實現就可以了:  

  
    public class AccountEntityTypeConfiguration : BaseEntityTypeConfiguration<Account>
    {
        /// <summary>
        /// 配置實體類型
        /// </summary>
        /// <param name="builder"></param>
        public override void Configure(EntityTypeBuilder<Account> builder)
        {
            base.Configure(builder);

            builder.Property(p => p.Name).HasMaxLength(100).IsRequired(true);
            builder.Property(p => p.Phone).HasMaxLength(100);
        }

        /// <summary>
        /// 種子數據
        /// </summary>
        /// <returns></returns>
        public override Account[] GetSeeds()
        {
            return new Account[] {
                new Account(){
                    Id=1,
                    Name="admin",
                    Phone="110",
                    Age=100,
                    CreationTime=new DateTime(2020,02,02)//注意,種子數據不要使用DateTime.Now之類的,避免每次都會遷移數據
                }
            };
        }
    }
AccountEntityTypeConfiguration
  
    public class ActivityEntityTypeConfiguration : BaseEntityTypeConfiguration<Activity>
    {
        /// <summary>
        /// 配置實體類型
        /// </summary>
        /// <param name="builder"></param>
        public override void Configure(EntityTypeBuilder<Activity> builder)
        {
            base.Configure(builder);

            builder.Property(p => p.Name).HasMaxLength(100).IsRequired(true);
        }
    }
ActivityEntityTypeConfiguration
  
    public class ActivityRecordEntityTypeConfiguration : BaseEntityTypeConfiguration<ActivityRecord>
    {
        /// <summary>
        /// 配置實體類型
        /// </summary>
        /// <param name="builder"></param>
        public override void Configure(EntityTypeBuilder<ActivityRecord> builder)
        {
            base.Configure(builder);

            builder.HasOne(p => p.Activity).WithMany().HasForeignKey(p => p.ActivityId);
            builder.HasOne(p => p.Account).WithMany().HasForeignKey(p => p.AccountId);
        }
    }
ActivityRecordEntityTypeConfiguration

  然后,接着在程序包管理器控制台輸入命令生成遷移文件:

  

    注意設置項目啟動項和默認程序

   執行成功后,在Migrations目錄下又會多兩個遷移文件。

  另外,說明一下,上面的遷移執行拋出了警告,這個是因為我的遷移中修改了數據列的長度,可能會導致數據丟失所致。

  接着執行Update-Database命令執行遷移,將其更新至數據庫:

  

    最后,忘了說明了,前面說了,如果想撤銷遷移,可以使用Remove-Migration命令,但是如果已經執行Update-Database將遷移更新至數據庫后,Remove-Migration命令將會拋出異常:

  The migration '20200727094748_alter_20200727' has already been applied to the database. Revert it and try again. If the migration has been applied to other databases, consider reverting its changes using a new migration.

  

    此時只需要先執行 Update-Database -Migration <migration> ,這個命令后面攜帶的參數是遷移名稱(可以去數據庫中__EFMigrationsHistory表查看),執行這個命令表示將數據庫遷移執行到指定名稱的遷移處,后續全部的遷移都會被撤銷!

  比如,如果我們想撤銷20200727094748_alter_20200727這個遷移,可以先執行 Update-Database -Migration 20200727092850_init_20200727 ,注意,這里的遷移名稱是要撤銷的遷移的上一個!也就是20200727092850_init_20200727

  執行完上面命令后再執行 Remove-Migration 就可以了

  

    

  五、應用使用

    實體模型創建好了,數據遷移也完成了,可以到應用了吧。

  上面可以看到,我們將實體模型與數據遷移分成了兩個項目,這樣EFCoreDemo.ConsoleApp只需要引用EFCoreDemo.EntityFrameworkCore去使用實體模型就夠了,因為它跟數據遷移完全沒關系!

  修改Program:  

  
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new DbContextOptionsBuilder<DemoDbContext>()
                .UseMySql("Server=192.168.209.128;Port=3306;Database=demodb;Uid=root;Pwd=123456");

            using (var db = new DemoDbContext(builder.Options))
            {
                //查詢管理員信息
                var admin = db.Set<Account>().Find(1);

                //新建活動
                var activity = new Activity()
                {
                    Name = "活動1",
                    StartTime = DateTime.Now,
                    EndTime = DateTime.Now.AddMonths(1),
                    Status = 1,
                    Remark = "備注",
                    CreationTime = DateTime.Now
                };
                db.Set<Activity>().Add(activity);

                //新增活動記錄
                var record = new ActivityRecord()
                {
                    Account = admin,
                    Activity = activity,
                    CreationTime = DateTime.Now
                };
                db.Set<ActivityRecord>().Add(record);

                db.SaveChanges();
            }

            using (var db = new DemoDbContext(builder.Options))
            {
                var records = db.Set<ActivityRecord>().Include(f => f.Activity).Include(f => f.Account).ToArray();
                foreach (var record in records)
                {
                    Console.WriteLine($"{record.Account.Name}參加了{record.Activity.Name}");
                }
            }

            Console.ReadKey();
        }
    }
Program

   執行后輸出:

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM