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

实现数据库迁移有很多种办法,从手动管理各个版本的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. 【.net 深呼吸】细说CodeDom(2):表达式、语句

    在上一篇文章中,老周厚着脸皮给大伙介绍了代码文档的基本结构,以及一些代码对象与CodeDom类型的对应关系. 在评论中老周看到有朋友提到了 Emit,那老周就顺便提一下.严格上说,Emit并不是针对代 ...

  2. 【原创经验分享】WCF之消息队列

    最近都在鼓捣这个WCF,因为看到说WCF比WebService功能要强大许多,另外也看了一些公司的招聘信息,貌似一些中.高级的程序员招聘,都有提及到WCF这一块,所以,自己也关心关心一下,虽然目前工作 ...

  3. 4.Android 打包时出现的Android Export aborted because fatal error were founds [closed]

    Android 程序开发完成后,如果要发布到互联网上供别人使用,就需要将自己的程序打包成Android 安装包文件(Android Package,APK),其扩展名为.apk.使用run as 也能 ...

  4. iOS 键盘添加完成按钮,delegate和block回调

    这个是一个比较初级一点的文章,新人可以看看.当然实现这个需求的时候自己也有一点收获,记下来吧. 前两天产品要求在工程的所有数字键盘弹出时,上面带一个小帽子,上面安装一个“完成”按钮,这个完成按钮也没有 ...

  5. 6_Win7下Chrome主页被流氓网站hao123.com劫持后的解决方法。

    今天安装了一个PDF阅读器,免费的,你懂的,结果自己安装的时候没有将默认的选项取消,就被hao123流氓网站劫持啦. 说实话某免费PDF阅读器还算好的,有一个可以供你选择的项.不想某些软件直接就默认选 ...

  6. 多线程条件通行工具——CountDownLatch

    CountDownLatch的作用是,线程进入等待后,需要计数器达到0才能通行. CountDownLatch(int)构造方法,指定初始计数. await()等待计数减至0. await(long, ...

  7. 使用Microsoft的IoC框架:Unity来对.NET应用进行解耦

    1.IoC/DI简介 IoC 即 Inversion of Control,DI 即 Dependency Injection,前一个中文含义为控制反转,后一个译为依赖注入,可以理解成一种编程模式,详 ...

  8. swift开发新项目总结

    新项目用swift3.0开发,现在基本一个月,来总结一下遇到的问题及解决方案   1,在确定新项目用swift后,第一个考虑的问题是用纯swift呢?还是用swift跟OC混编      考虑到新项目 ...

  9. Android之数据存储的五种方法

    1.Android数据存储的五种方法 (1)SharedPreferences数据存储 详情介绍:http://www.cnblogs.com/zhangmiao14/p/6201900.html 优 ...

  10. win10系统下连接无线网络掉线问题解决办法

    打开驱动精灵----系统诊断 找一个可修复的驱动点击 选择连不上网中的查看更多 有连不上网络,网络连接受限,解决无线间歇性掉网问题 进入计算机管理----设备管理 修改无线网络属性(名称含有wirel ...