1. 程式人生 > 實用技巧 >EntityFrameworkCore資料遷移(一)

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 configuration = BuildConfiguration();

            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:這個是當前實體對映的快照

  生成遷移檔案之後,如果要撤銷此次的遷移檔案,可以使用 Remove-Migration 命令

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

  

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

  執行完Update-Database後,可以看看資料庫:

  

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

  

  四、再次遷移

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

  前面我們雖然建立了表,但是裡面欄位約束等等基本上都放過了,比如欄位長度,外來鍵約束等等,現在我們補上:

  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 -Migration20200727092850_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

  執行後輸出: