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

2023-05-16

在开发涉及到数据库的程序时,常会遇到一开始设计的结构不能满足需求需要再添加新字段或新表的情况,这时就需要进行数据库迁移。
实现数据库迁移有很多种办法,从手动管理各个版本的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.Database
Database.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 here
    context.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 migration
        MigrateRelationalDatabase(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 assembly
    var 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 assembly
    var 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 instance
    var 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 model
var 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 difference
    return;
}

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

// There some difference, we need perform the migration
var commands = sqlGenerator.Generate(operations, context.Model);
var connection = serviceProvider.GetService<IRelationalConnection>();
// Take a snapshot to the newest model
var 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 it
var history = new EFCoreMigrationHistory(modelSnapshot);
histories.Add(history);
context.SaveChanges();
try {
    // Execute migration commands
    commandExecutor.ExecuteNonQuery(commands, connection);
} catch {
    histories.Remove(history);
    context.SaveChanges();
    throw;
}

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

写在最后

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

写在最后的广告

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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • js 求字符串中,某个字符出现的位置和次数

    先看一个简单的例子 xff0c 主要是对str indexOf 某字符 函数的应用 var str 61 39 你好啊好啊 39 console log str indexOf 39 好 39 1 console log str index
  • 解决dialog在横竖屏切换时消失

    以AlertDialog为例 一 将AlertDialog视图封装在dialogfragment fragmentdeAlertDialog子类 实例中 package com example t import android app Al
  • PowerShell压缩与解压缩

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 1 压缩 因为Powershell中没有提供关于压缩的命令 xff0c 所以需要调用 Net中的对象来完成压缩功能 NAME zip DESCRIPTION 压缩文件 PA
  • opensuse12.1的gnome3“系统错误无法恢复”的解决办法

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 我的opensuse12 1的susu用户不知道折腾什么了又无法进入gnome3 老是提示 系统错误并且无法恢复 xff0c 说什么什么bar插件有问题 xff0c 然后只
  • 前端如何获取http状态码400的返回值

    axios get 34 check mobile and sent code 34 withCredentials span style color 0000ff true span params mobile formInline mo
  • 空间权重矩阵(SWM)

    基本原理 如何利用数学 xff08 如用面积 距离等 xff09 建立空间数据和非空间数据之间的关系 xff1f 对于空间统计而言 xff0c 空间权重矩阵是一种有效的表达空间关系的方式 因此 xff0c 它是用量化的方法表示了数据之间的
  • Oracle: 输入的ADMIN 口令不符合Oracle建议的标准

    安装oracle 11g 发行版2时 xff0c 会出现如下提示 xff1a 管理口令 P INS 30011 输入的 ADMIN 口令不符合 Oracle 建议的标准 这是怎么会事儿呢 xff1f 其实很简单 xff0c 这个是Oracl
  • 2016年7月21日学习笔记

    2016年 7 月 21 日学习笔记 Linux安装以后出现的文件目录的作用 xff1a 文件系统的类型 LINUX有四种基本文件系统类型 xff1a 普通文件 目录文件 连接文件和特殊文件 xff0c 可用 file 命令来识别 普通文件
  • Java架构进阶之路——阿里大牛强力推荐书单(附赠电子版)

    1 深入理解Java虚拟机 xff1a JVM高级特性与最佳实践 本书适合所有Java程序员 系统调优师和系统架构师阅读 共分为五大部分 xff0c 围绕内存管理 执行子系统 程序编译与优化 高效并发等核心主题对JVM进行了全面而深入的分析
  • 给Hexo搭建的博客绑定域名

    前言 前几天利用闲置时间 xff0c 利用hexo在GitHub上搭建了一个静态博客 xff0c 那么既然是个人博客 xff0c 当然要上自己的域名了 step 1 首先你得你得搭建 XXX github io 这样的博客 xff0c he
  • chrome浏览器提取网页视频

    http blog csdn net pipisorry article details 37728839 在我们平时上网看视频听音乐时都会产生缓存 xff0c 可是我们非常难通过一些软件把当中的视频和音乐文件提取出来 网页抓取视频的方法
  • js 统计一个字符串中,出现最多的字符和出现次数

    运用到的关键函数是str charAt index xff1a 根据位置返回字符 另外 xff0c 建立一个对象o xff0c 其中存放的key为字符串的各不重复的字母 xff0c 键值为出现的次数 代码 xff1a span class
  • IdentityServer4 配置负载均衡

    如果使用 IdentityServer4 做授权服务的负载均衡 xff0c 默认情况下是不可以的 xff0c 比如有两个授权服务站点 xff0c 一个资源服务绑定其中一个授权服务 xff08 Authority配置 xff09 xff0c
  • android 打开串口log,user版本如何打开uart,让android log从串口kernel log输出

    数据流控制是否正确 xff0c 一般需关闭 xff0c 下面为RS232的三种流控制模式介绍 xff1a DTR DSR xff1a 硬件上要有对应接口 xff0c 软件上实现对应协议 xff0c 才能实现此流控制 具体实现起来 xff0c
  • 安装flashplugin提示依赖libgdk-pixbuf2.0-0

    为什么80 的码农都做不了架构师 xff1f gt gt gt 今天安装flashplugin 出现问题 xff0c sudo apt get install adobe flashplugin 结果提示 xff1a 下列软件包有未满足的依
  • Python新手入门教程,从环境准备到掌握基本编程

    Lesson 1 准备好学习Python的环境 下载的地址是 xff1a www python org 为了咱们的便当 xff0c 我在校内作了copy xff1a http 10 1 204 2 tool compiler amp IDE
  • mariadb使用C语言编程,MHA实现mariadb的高可用的详细步骤及配置参数详解

    MHA实现mariadb的高可用的详细步骤及配置参数详解 A 实验环境说明 a 4台centos7主机 b 角色说明 xff1a a MHA xff1a 192 168 36 35 b Master mariadb xff1a 192 16
  • c语言中sizeof函数的作用是,c语言中sizeof函数的用法

    C语言sizeof函数如何使用 xff1f 怎样利用sizeofCSS布局HTML小编今天和大家分享各种数据类型占用的字节数 xff1f 1 sizeof不是函数 xff0c 它只是一个操作符 operator 2 sizeof的作用是返回
  • AutoCAD快捷键大全

    送给学习AutoCAD的朋友 xff0c 最后一张图片是可以打印的键盘标签 xff0c 可以打印出来贴在键盘上方便记住 xff01 如果感觉模糊的话 xff0c 可以单击文章图片进行查看 xff01
  • 如何恢复U盘里的删除文件?

    对于经常使用到U盘的用户来说 xff0c 误删U盘内重要数据的情况经常发生 xff0c U盘内重要的资料从电脑上被删除后 xff0c 不经过回收站 xff0c 我们很难从回收站中还原数据 那么 xff0c 如何恢复u盘删除文件 xff1f

随机推荐

  • maven maven.compiler.source和maven.compiler.target的坑

    最近建议产品组把jdk 1 7升级到1 8 xff0c 昨晚开发报了个问题过来 xff0c 说maven compiler source和maven compiler target改成1 8之后 xff0c 编译出来的代码还是1 7 xff
  • 1‘b0 什么意思

    在看datasheet 中有类似表达式如下 xff1a 3 39 b000 1 39 b1 1 39 b0 3 39 b000这个表示 xff1a b代表二進制 3代表位元數 1 39 b1 xff1a 宣告為一位元二進制之值為1 xff0
  • css 识别软件测出来的尺寸和代码设置的不一样

    这是我在模仿网页时遇到的问题 xff0c 困扰了一会儿 xff0c 情况是这样的 我下载了参考的网页html文件 xff0c 为了测量某个box的高度 xff0c 用了一款可以截图识别的软件 xff1a 量出来的box高度是30px xff
  • SpringBoot中注入ApplicationContext对象的三种方式

    在项目中 xff0c 我们可能需要手动获取spring中的bean对象 xff0c 这时就需要通过 ApplicationContext 去操作一波了 xff01 1 直接注入 xff08 Autowired xff09 span clas
  • vue中$attrs你会用吗?

    这篇文章的知识点是父子组件通讯 xff0c 如果你了解 props 但是还没了解过 attrs xff0c 那么建议你花1分钟时间阅读 xff0c 了解它的优点 xff0c 并学会在项目中使用 关于 attrs的介绍 包含了父作用域中不作为
  • 企业微信三方应用开发(二)授权开通及登录流程

    何谓企业微信第三方应用 一句话简介 一个S商场 企业微信 里入驻了D商站 xff08 服务商 xff09 在卖他的C产品 xff08 三方应用 xff09 三句话路径 我们申请成为企业微信服务商 xff0c 入驻到企业微信 然后经过 应用开
  • PHP+AJAX实现账号注册和登陆,附可用demo

    前言 登陆和注册已经是网站的标配了 xff0c 所以这是我们web开发学习过程中必学的了 其实很容易实现 xff0c 只需要懂数据库的增删改查 xff0c 还有if else的条件语句即可做出来 目录 css Login Reg css s
  • ftp服务器无法使用浏览器访问解决方法

    浏览器默认工作在被动模式 可能你的环境无法让ftp服务器工作在主动模式 这个很容易测试 你在IE浏览器的工具 internet选项 高级中取消使用被动ftp的设置 xff0c 看看浏览器是否还可以访问ftp服务器 xff0c 如果访问不了
  • Python抓取糗事百科成人版图片

    最近开始学习爬虫 xff0c 一开始看的是静觅 的爬虫系列文章 xff0c 今天看到糗事百科成人版 xff0c 心里就邪恶了一下 xff0c 把图片都爬下来吧 xff0c 哈哈 虽然后来实现了 xff0c 但还是存在一些问题 xff0c 暂
  • vc中调用Com组件的方法详解

    Requirement 1 创建myCom dll 该COM只有一个组件 两个接口IGetRes 方法Hello IGetResEx 方法HelloEx xff1b 2 在工程中导入组件或类型库 import 34 组件所在目录myCom
  • matlab练习程序(二值图像内外边界跟踪)

    目标内边界的像素全都在目标里面 xff0c 目标外边界的像素全都不在目标上 xff0c 是包围着目标的 二值图像内外边界的计算都是有两种方法的 xff0c 所以一共是4种算法 xff0c 不过实际用到跟踪的只有一个而已 首先是内边界跟踪 x
  • 如何使用.NET开发全版本支持的Outlook插件产品(三)——全面控制

    插件项目所有代码都已经上传至 https github com VanPan TestOutlookAdding 进阶基础 COM查看 首先 xff0c 对于Outlook对象模型 xff0c MSDN早就有非常详细的介绍 xff0c 请直
  • Android中应用程序如何获得系统签名权限

    最近在做一个控制电视界面的应用 xff0c 模拟电视遥控器操作 xff0c 代码如下 public static void simulateKeystroke final int KeyCode new Thread new Runnabl
  • 使用PostSharp在.NET平台上实现AOP

    摘要 本文首先介绍AOP xff08 面向方面编程 xff09 的相关概念及理论 xff0c 然后介绍如何使用PostSharp框架在 NET平台上实现AOP xff0c 最后对PostSharp的机制及AOP的优劣进行一个简单的分析 AO
  • python 带头节点的单链表相关函数

    链表用头结点还是头指针让我混乱 在写append函数时 xff0c 发现网上的写法各有不同 xff0c 而带头结点的会更好理解 xff0c 也更简洁 以下是带头结点的单链表一些简单的相关函数 span class token keyword
  • 使用 Beautiful Soup 解析网页内容

    安装Beautiful Soup Beautiful Soup是一个Python的HTML解析框架 xff0c 我们可以利用它方便的处理HTML和XML文档 Beautiful Soup有3和4两个版本 xff0c 目前3已经停止开发 所以
  • ibm服务器报警指示灯含意

    EVENT LOG指示灯报警 有台IBM服务器前面的光通路面板开始亮起了小黄灯 xff0c 推出这个小盒子一看 xff0c 是EVENT LOG指示灯报警 一时不知道是什么原因 xff0c 可能是日志错误 xff0c 要进Configura
  • [概念学习] Virtualization的几个概念

    1 Vitualization xff1a 某种东西的虚拟版本 xff0c 比如硬件平台 操作系统 存储设备 网络资源等 the creation of a virtual version of something such as a ha
  • Windows2012、windows2016配置多用户登录

    windows系统多用户登录配置方法如下 xff0c 但是120天后还是会提示缺少远程桌面授权服务器 xff0c 根本解决办法 xff0c 请参考 xff1a https blog 51cto com 13777088 2299170 服务
  • 全自动迁移数据库的实现 (Fluent NHibernate, Entity Framework Core)

    在开发涉及到数据库的程序时 xff0c 常会遇到一开始设计的结构不能满足需求需要再添加新字段或新表的情况 xff0c 这时就需要进行数据库迁移 实现数据库迁移有很多种办法 xff0c 从手动管理各个版本的ddl脚本 xff0c 到实现自己的