在开发涉及到数据库的程序时,常会遇到一开始设计的结构不能满足需求需要再添加新字段或新表的情况,这时就需要进行数据库迁移。

实现数据库迁移有很多种办法,从手动管理各个版本的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共同探讨。

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

  1. Entity Framework Core 之数据库迁移

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

  2. ASP.NET CORE系列【六】Entity Framework Core 之数据库迁移

    前言 最近打算用.NET Core写一份简单的后台系统,来练练手 然后又用到了Entity Framework Core 发现园子里有些文章讲得不是那么细节,对于新手小白来说,可能会有点懵. 特意整理 ...

  3. UWP: 在 UWP 中使用 Entity Framework Core 操作 SQLite 数据库

    在应用中使用 SQLite 数据库来存储数据是相当常见的.在 UWP 平台中要使用 SQLite,一般会使用 SQLite for Universal Windows Platform 和 SQLit ...

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

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

  5. Working with Data » Getting started with ASP.NET Core and Entity Framework Core using Visual Studio »迁移

    Migrations¶ 4 of 4 people found this helpful The Contoso University sample web application demonstra ...

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

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

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

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

  8. ASP.Net Core项目在Mac上使用Entity Framework Core 2.0进行迁移可能会遇到的一个问题.

    在ASP.Net Core 2.0的项目里, 我使用Entity Framework Core 2.0 作为ORM. 有人习惯把数据库的连接字符串写在appSettings.json里面, 有的习惯写 ...

  9. ASP.NET CORE系列【六】Entity Framework Core 之数据迁移

    原文:ASP.NET CORE系列[六]Entity Framework Core 之数据迁移 前言 最近打算用.NET Core写一份简单的后台系统,来练练手 然后又用到了Entity Framew ...

随机推荐

  1. expect用法

    1. [#!/usr/bin/expect]  这一行告诉操作系统脚本里的代码使用那一个shell来执行.这里的expect其实和linux下的bash.windows下的cmd是一类东西.  注意: ...

  2. 通过ProGet搭建一个内部的Nuget服务器

    .NET Core项目完全使用Nuget 管理组件之间的依赖关系,Nuget已经成为.NET 生态系统中不可或缺的一个组件,从项目角度,将项目中各种组件的引用统统交给NuGet,添加组件/删除组件/以 ...

  3. .NET Core系列 :3 、使用多个项目

    通过前面的两篇文章,我们已经知道如何创建新的项目,如何生成并运行我们的应用程序,也知道(大致) project.json 文件中的内容是什么意思.但大多数项目往往也需要多个项目或引用的类库.我们要创建 ...

  4. Sublime Text3配置在可交互环境下运行python快捷键

    安装插件 在Sublime Text3下面写代码感觉很不错,但是写Python的时候遇到了一些问题. 用Sublime Text3打开python文件,或者在Sublime Text3下写好pytho ...

  5. .NET 对接JAVA 使用Modulus,Exponent RSA 加密

    最近有一个工作是需要把数据用RSA发送给Java 虽然一开始标准公钥 net和Java  RSA填充的一些算法不一样 但是后来这个坑也补的差不多了 具体可以参考 http://www.cnblogs. ...

  6. WebApi接口 - 响应输出xml和json

    格式化数据这东西,主要看需要的运用场景,今天和大家分享的是webapi格式化数据,这里面的例子主要是输出json和xml的格式数据,测试用例很接近实际常用情况:希望大家喜欢,也希望各位多多扫码支持和点 ...

  7. [C#] 回眸 C# 的前世今生 - 见证 C# 6.0 的新语法特性

    回眸 C# 的前世今生 - 见证 C# 6.0 的新语法特性 序 目前最新的版本是 C# 7.0,VS 的最新版本为 Visual Studio 2017 RC,两者都尚未进入正式阶段.C# 6.0 ...

  8. 【原创分享·微信支付】C# MVC 微信支付教程系列之现金红包

            微信支付教程系列之现金红包           最近最弄这个微信支付的功能,然后扫码.公众号支付,这些都做了,闲着无聊,就看了看微信支付的其他功能,发现还有一个叫“现金红包”的玩意,想 ...

  9. 真假4K电视验证:一张图足矣

    国庆期间笔者逛了一下电视卖场,考虑到国内电视台以及宽带的情况,1080P至少还能用十年,所以只想要个2k电视就够了.然而事与愿违,卖场中八成的都是4k电视,清一色的4k电视让人眼花缭乱.难道4k面板技 ...

  10. jQuery个性化图片轮播效果

    jQuery个性化图片轮播效果 购物产品展示:图片轮播器<效果如下所示> 思路说明: 每隔一段时间,实现图片的自动切换及选项卡选中效果,鼠标划入图片动画停止,划出或离开动画开始 两个区域: ...