在开发涉及到数据库的程序时,常会遇到一开始设计的结构不能满足需求需要再添加新字段或新表的情况,这时就需要进行数据库迁移。
实现数据库迁移有很多种办法,从手动管理各个版本的ddl脚本,到实现自己的migrator,或是使用Entity Framework提供的Code First迁移功能。
Entity Framework提供的迁移功能可以满足大部分人的需求,但仍会存在难以分项目管理迁移代码和容易出现"context has changed"错误的问题。

这里我将介绍ZKWeb网页框架在Fluent NHibernate和Entity Framework Core上使用的办法。
可以做到添加实体字段后,只需刷新网页就可以把变更应用到数据库。

实现全自动迁移的思路

数据库迁移需要指定变更的部分,例如添加表和添加字段。
而实现全自动迁移需要自动生成这个变更的部分,具体来说需要

  • 获取数据库现有的结构

  • 获取代码中现有的结构

  • 对比结构之间的差异并生成迁移

这正是Entity Framework的Add-Migration(或dotnet ef migrations add)命令所做的事情,
接下来我们将看如何不使用这类的命令,在NHibernate, Entity Framework和Entity Framework Core中实现全自动的处理。

Fluent NHibernate的全自动迁移

ZKWeb框架使用的完整代码可以查看这里

首先Fluent NHibernate需要添加所有实体的映射类型,以下是生成配置和添加实体映射类型的例子。
配置类的结构可以查看这里

var db = MsSqlConfiguration.MsSql2008.ConnectionString("连接字符串");var configuration = Fluently.Configure();
configuration.Database(db);
configuration.Mappings(m => {m.FluentMappings.Add(typeof(FooEntityMap));m.FluentMappings.Add(typeof(BarEntityMap));...
});

接下来是把所有实体的结构添加或更新到数据库。
NHibernate提供了SchemaUpdate,这个类可以自动检测数据库中是否已经有表或字段,没有时自动添加。
使用办法非常简单,以下是使用的例子

configuration.ExposeConfiguration(c => {    // 第一个参数 false: 不把语句输出到控制台// 第二个参数 true: 实际在数据库中执行语句new SchemaUpdate(c).Execute(false, true);
});

到这一步就已经实现了全自动迁移,但我们还有改进的余地。
因为SchemaUpdate不保存状态,每次都要检测数据库中的整个结构,所以执行起来EF的迁移要缓慢很多,
ZKWeb框架为了减少每次启动网站的时间,在执行更新之前还会检测是否需要更新。

var scriptBuilder = new StringBuilder();
scriptBuilder.AppendLine("/* this file is for database migration checking, don't execute it */");new SchemaExport(c).Create(s => scriptBuilder.AppendLine(s), false);var script = scriptBuilder.ToString();if (!File.Exists(ddlPath) || script != File.ReadAllText(ddlPath)) {    new SchemaUpdate(c).Execute(false, true);onBuildFactorySuccess = () => File.WriteAllText(ddlPath, script);
}

这段代码使用了SchemaExport来生成所有表的DDL脚本,生成后和上次的生成结果对比,不一致时才调用SchemaUpdate更新。

NHibernate提供的自动迁移有以下的特征,使用时应该注意

  • 字段只会添加,不会删除,如果你重命名了字段原来的字段也会保留在数据库中

  • 字段类型如果改变,数据库不会跟着改变

  • 关联的外键如果改变,迁移时有可能会出错

总结NHibernate的自动迁移只会添加表和字段,基本不会修改原有的结构,有一定的限制但是比较安全。

Entity Framework的全自动迁移

ZKWeb框架没有支持Entity Framework 6,但实现比较简单我就直接上代码了。
例子

// 调用静态函数,放到程序启动时即可// Database是System.Data.Entity.DatabaseDatabase.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, MyConfiguration>());public class MyConfiguration : DbMigrationsConfiguration<MyContext> {    public MyConfiguration() {AutomaticMigrationsEnabled = true; // 启用自动迁移功能AutomaticMigrationDataLossAllowed = true; // 允许自动删字段,危险但是不加这个不能重命名字段}
}

Entity Framework提供的自动迁移有以下的特征,使用时应该注意

  • 如果字段重命名,旧的字段会被删除掉,推荐做好数据的备份和尽量避免重命名字段

  • 外键关联和字段类型都会自动变化,变化时有可能会导致原有的数据丢失

  • 自动迁移的记录和使用工具迁移一样,都会保存在__MigrationHistory表中,切勿混用否则代码将不能用到新的数据库中

总结Entity Framework的迁移可以保证实体和数据库之间很强的一致性,但是使用不当会导致原有数据的丢失,请务必做好数据库的定时备份。

Entity Framework Core的全自动迁移

Entity Framework Core去掉了SetInitializer选项,取而代之的是DatabaseFacade.MigrateDatabaseFacade.EnsureCreated
DatabaseFacade.Migrate可以应用使用ef命令生成的迁移代码,避免在生产环境中执行ef命令。
DatabaseFacade.EnsureCreated则从头创建所有数据表和字段,但只能创建不能更新,不会添加纪录到__MigrationHistory
这两个函数都不能实现全自动迁移,ZKWeb框架使用了EF内部提供的函数,完整代码可以查看这里

Entity Framework Core的自动迁移实现比较复杂,我们需要分两步走。

  • 第一步 创建迁移记录__ZKWeb_MigrationHistory表,这个表和EF自带的结构相同,但这个表是给自己用的不是给ef命令用的

  • 第二部 查找最后一条迁移记录,和当前的结构进行对比,找出差异并更新数据库

第一步的代码使用了EnsureCreated创建数据库和迁移记录表,其中EFCoreDatabaseContextBase只有迁移记录一个表。
创建完以后还要把带迁移记录的结构保留下来,用作后面的对比,如果这里不保留会导致迁移记录的重复创建错误。

using (var context = new EFCoreDatabaseContextBase(Database, ConnectionString)) {    // We may need create a new database and migration history table// It's done herecontext.Database.EnsureCreated();initialModel = context.Model;
}

在执行第二步之前,还需要先判断连接的数据库是不是关系数据库,
因为Entity Framework Core以后还会支持redis mongodb等非关系型数据库,自动迁移只应该用在关系数据库中。

using (var context = new EFCoreDatabaseContext(Database, ConnectionString)) {    var serviceProvider = ((IInfrastructure<IServiceProvider>)context).Instance;    var databaseCreator = serviceProvider.GetService<IDatabaseCreator>();    if (databaseCreator is IRelationalDatabaseCreator) {        // It's a relational database, create and apply the migrationMigrateRelationalDatabase(context, initialModel);} else {        // It maybe an in-memory database or no-sql database, do nothing}
}

第二步需要查找最后一条迁移记录,和当前的结构进行对比,找出差异并更新数据库。

先看迁移记录表的内容,迁移记录表中有三个字段

  • Revision 每次迁移都会+1

  • Model 当前的结构,格式是c#代码

  • ProductVersion 迁移时Entity Framework Core的版本号

Model存放的代码例子如下,这段代码记录了所有表的所有字段的定义,是自动生成的。
后面我将会讲解如何生成这段代码。

using System;using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Infrastructure;using Microsoft.EntityFrameworkCore.Metadata;using Microsoft.EntityFrameworkCore.Migrations;using ZKWeb.ORM.EFCore;namespace ZKWeb.ORM.EFCore.Migrations{[DbContext(typeof(EFCoreDatabaseContext))]    partial class Migration_636089159513819123 : ModelSnapshot{        protected override void BuildModel(ModelBuilder modelBuilder)        {modelBuilder.HasAnnotation("ProductVersion", "1.0.0-rtm-21431").HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);modelBuilder.Entity("Example.Entities.Foo", b =>{b.Property<Guid>("Id").ValueGeneratedOnAdd();b.Property<string>("Name").IsRequired();});}}}
}

接下来查找最后一条迁移记录:

var lastModel = initialModel;var histories = context.Set<EFCoreMigrationHistory>();var lastMigration = histories.OrderByDescending(h => h.Revision).FirstOrDefault();

存在时,编译Model中的代码并且获取ModelSnapshot.Model的值,这个值就是上一次迁移时的完整结构。
不存在时,将使用initialModel的结构。
编译使用的是另外一个组件,你也可以用Roslyn CSharp Scripting包提供的接口编译。

if (lastMigration != null) {    // Remove old snapshot code and assemblyvar tempPath = Path.GetTempPath();    foreach (var file in Directory.EnumerateFiles(tempPath, ModelSnapshotFilePrefix + "*").ToList()) {        try { File.Delete(file); } catch { }}    // Write snapshot code to temp directory and compile it to assemblyvar assemblyName = ModelSnapshotFilePrefix + DateTime.UtcNow.Ticks;    var codePath = Path.Combine(tempPath, assemblyName + ".cs");    var assemblyPath = Path.Combine(tempPath, assemblyName + ".dll");    var compileService = Application.Ioc.Resolve<ICompilerService>();    var assemblyLoader = Application.Ioc.Resolve<IAssemblyLoader>();File.WriteAllText(codePath, lastMigration.Model);compileService.Compile(new[] { codePath }, assemblyName, assemblyPath);    // Load assembly and create the snapshot instancevar assembly = assemblyLoader.LoadFile(assemblyPath);    var snapshot = (ModelSnapshot)Activator.CreateInstance(assembly.GetTypes().First(t =>        typeof(ModelSnapshot).GetTypeInfo().IsAssignableFrom(t)));lastModel = snapshot.Model;
}

和当前的结构进行对比:

// Compare with the newest modelvar modelDiffer = serviceProvider.GetService<IMigrationsModelDiffer>();var sqlGenerator = serviceProvider.GetService<IMigrationsSqlGenerator>();var commandExecutor = serviceProvider.GetService<IMigrationCommandExecutor>();var operations = modelDiffer.GetDifferences(lastModel, context.Model);if (operations.Count <= 0) {    // There no differencereturn;
}

如果有差异,生成迁移命令(commands)和当前完整结构的快照(modelSnapshot)。
上面Model中的代码由这里的CSharpMigrationsGenerator生成,modelSnapshot的类型是string

// There some difference, we need perform the migrationvar commands = sqlGenerator.Generate(operations, context.Model);var connection = serviceProvider.GetService<IRelationalConnection>();// Take a snapshot to the newest modelvar codeHelper = new CSharpHelper();var generator = new CSharpMigrationsGenerator(codeHelper,    new CSharpMigrationOperationGenerator(codeHelper),    new CSharpSnapshotGenerator(codeHelper));var modelSnapshot = generator.GenerateSnapshot(ModelSnapshotNamespace, context.GetType(),ModelSnapshotClassPrefix + DateTime.UtcNow.Ticks, context.Model);

插入迁移记录并执行迁移命令:

// Insert the history first, if migration failed, delete itvar history = new EFCoreMigrationHistory(modelSnapshot);
histories.Add(history);
context.SaveChanges();try {    // Execute migration commandscommandExecutor.ExecuteNonQuery(commands, connection);
} catch {histories.Remove(history);context.SaveChanges();    throw;
}

到这里就完成了Entity Framework Core的自动迁移,以后每次有更新都会对比最后一次迁移时的结构并执行更新。
Entity Framework Core的迁移特点和Entity Framework一样,可以保证很强的一致性但需要注意防止数据的丢失。

写在最后

全自动迁移数据库如果正确使用,可以增强项目中各个模块的独立性,减少开发和部署的工作量。
但是因为不能手动控制迁移内容,有一定的局限和危险,需要了解好使用的ORM迁移的特点。

写在最后的广告

ZKWeb网页框架已经在实际项目中使用了这项技术,目前来看迁移部分还是比较稳定的。
这项技术最初是为了插件商城而开发的,在下载安装插件以后不需要重新编译主程序,不需要执行任何迁移命令就能使用。
目前虽然没有实现插件商城,也减少了很多日常开发的工作。

如果你有兴趣,欢迎加入ZKWeb交流群522083886共同探讨。

原文地址:http://www.cnblogs.com/zkweb/p/5859536.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

全自动迁移数据库的实现 (Fluent NHibernate, Entity Framework Core)相关推荐

  1. 使用Entity Framework Core,Swagger和Postman创建ASP.NET Core Web API的分步指南

    目录 介绍 背景 第1步:创建一个新项目 第2步:添加模型类 第3步:使用Entity Framework Core 第4步:添加数据库上下文和控制器 步骤5:在Package Manager控制台中 ...

  2. [翻译] Entity Framework Core in Action 关于这本书

    Entityframework Core in action是 Jon P smith 所著的关于Entityframework Core 书籍.原版地址. 是除了官方文档外另一个学习EF Core的 ...

  3. 彻底征服 Entity Framework Core 优化!

    作者 | 喵叔 责编 | 胡巍巍 出品 | CSDN(ID:CSDNnews) 这篇文章我们来讲解一下 Entity Framework Core 的优化方案.Entity Framework Cor ...

  4. Entity Framework Core 之数据库迁移

    前言 最近打算用.NET Core写一份开源的简易CMS系统,来练练手 所以又去深入研究了一下Entity Framework Core 发现其实有些细节园子里还是很少讲到. 特意整理了几个细节. 正 ...

  5. Entity Framework Core系列教程-3为现有数据库生成实体模型

    在Entity Framework Core中为现有数据库创建模型 在这里,您将学习如何在Entity Framework Core中为现有数据库创建上下文和实体类.为现有数据库创建实体和上下文类称为 ...

  6. oracle精简版_使用Entity Framework Core访问数据库(Oracle篇)

    前言 哇..看看时间 真的很久很久没写博客了 将近一年了. 最近一直在忙各种家中事务和公司的新框架  终于抽出时间来更新一波了. 本篇主要讲一下关于Entity Framework Core访问ora ...

  7. 使用Entity Framework Core访问数据库(Oracle篇)

    前言 哇..看看时间 真的很久很久没写博客了 将近一年了. 最近一直在忙各种家中事务和公司的新框架  终于抽出时间来更新一波了. 本篇主要讲一下关于Entity Framework Core访问ora ...

  8. Entity Framework Core 2.0 使用代码进行自动迁移

    一.前言 我们在使用EF进行开发的时候,肯定会遇到将迁移更新到生产数据库这个问题,前面写了一篇文章介绍了Entity Framework Core 2.0的入门使用,这里面介绍了使用命令生成迁移所需的 ...

  9. 创建ASP.NET Core MVC应用程序(3)-基于Entity Framework Core(Code First)创建MySQL数据库表

    创建ASP.NET Core MVC应用程序(3)-基于Entity Framework Core(Code First)创建MySQL数据库表 创建数据模型类(POCO类) 在Models文件夹下添 ...

最新文章

  1. C++ algorithm中find系列函数总结
  2. libc-glibc
  3. 206. 反转链表 golang
  4. 关于联合体输出的问题(是否小端模式)
  5. 阿里布局物联网!开源操作系统 AliOS Things 喜提 1 亿芯片出货量
  6. c访问mysql数据库_C语言访问MySQL数据库的方法
  7. 浅谈 js 正则字面量 与 new RegExp 执行效率
  8. 专科python应届生工资多少-应届毕业生自述面试15K月薪的Python后端开发经历,希望对你有用...
  9. XVII Open Cup named after E.V. Pankratiev. GP of Siberia, Division 1
  10. WPS快捷键之 通用基础
  11. 56-狂拍灰太狼游戏
  12. steam for linux 安装目录,我该如何安装Steam?
  13. RK987蓝牙键盘使用说明书分享
  14. 在任意文件夹下以管理员的身份运行powershell
  15. 软件定义边界(SDP)简介
  16. win10开机提示服务未登录,无法加载用户配置文件
  17. 估值近百亿,“创维三把手”酷开科技能否顺利赶考?
  18. java单词排序_java中实现将输入的单词按首字母排序
  19. 如何生成Patch及打patch
  20. 遥感数字图像处理——原理与方法(朱文泉,林文鹏编著)

热门文章

  1. Linux脚本利器sed
  2. MapReduce经典案例——统计单词数
  3. C#学习笔记(十四):StatusBar控件
  4. 在 ASP.NET Core 中使用 HTTP 标头传播
  5. .NET 7 预览版 2 已发布,NativeAOT 正式可用
  6. 面试官: 平时开发中你用过读写锁吗?
  7. .NET 6 平台系列1 .NET Framework发展历程
  8. Code Runner for VS Code 突破 2000 万下载量!支持超过 50 种语言
  9. Winform 进度条弹窗和任务控制
  10. MongoDB via Dotnet Core数据映射详解