本篇目录
本系列的源码本人已托管于Coding上:点击查看,想要注册Coding的可以点击该连接注册。
先附上codeplex上EF的源码:entityframework.codeplex.com,此外,本人的实验环境是VS 2013 Update 5,windows 10,MSSQL Server 2008/2012。
这一篇,我们会学习使用EF的迁移API来记录结构型数据库的改变。之前,我们都是使用的数据库初始化器销毁然后重新创建数据库来处理这些改变。现在,我们使用EF的迁移API没有数据损失地完成同样的最终结果。我们也会讨论对一个已存在的数据库集成EF的过程,而不是只允许EF从头开始创建数据库。最后也会介绍一些其他的功能。
本篇会覆盖以下知识点:
- 在使用了EF的项目上开启数据库迁移
- 使用自动迁移
- 创建显式的迁移
- 添加数据库工件,例如索引
- 对已存在的数据库添加迁移
- 使用EF的其他功能(前面的章节没有介绍的)
开启并运行迁移
EF是一个ORM工具,因此它使用数据库工作,我们已经看到了目前面临了这么一个挑战,那就是保持RDBMS和EF实体类同步。之前,我们都是使用一个初始化器来销毁并重建数据库以使新的结构匹配上下文和实体。显然,我们不能在生产环境中这样做。因此,我们有两种选择:第一种是我们可以挑选其他工具(如SQL Server的SSDT)来单独维护和升级数据库;第二种选择就是我们这篇要说的,当数据库结构发生改变时,使用EF本身来更新数据库。要使用这项技术,我们必须在项目上启用迁移(migration)。
先前的例子,我们都是为整个应用程序和EF的实体类使用了单独一个项目。这在真实的解决方案中没有意义,很有可能我们要将EF的对象分离成它们自己的一个项目,这个项目一般是类库类型的。在本篇的例子中,我们就会这么做了。
现在就开始动手了,按照之前做的例子的步骤创建一个类库项目,取名“Data”,然后添加EF的Nuget包的引用,其次写实体类和上下文类。最后,将该类库项目的引用添加到应用程序的主项目中(Console项目)。项目结构如下,代码就不贴了,大家可看源码。
完成上面的步骤之后,下一步就是为Data项目开启迁移了。打开Nuget包管理控制台窗口,默认项目选择刚才创建的项目Data,然后在窗口中输入Enable-Migrations
,最后按下Enter键即可:
如果想要获取PowerShell命令的详细帮助信息,比如Enable-Migration
,只需要输入Get-Help Enable-Migrations
,其他命令依次类推。我们会找到参数信息,有些参数会将迁移指向一个特定的项目或连接字符串。在我们的例子中,我们不需要指定任何参数,因为我们在控制台项目的配置文件中添加了目标连接字符串。运行该命令后,我们会看到Data项目中多了一个名叫Migrations的文件夹,该文件夹里面有一个类,它指定了迁移配置,通过泛型参数将它连接到我们的数据库上下文类,默认生成的配置类如下所示:
internal sealed class Configuration : DbMigrationsConfiguration<Data.Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(Data.Context context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
该类也有Seed
方法,每当迁移应用到数据库时都会调用该方法,这样,开发者就可以执行各种各样的任务,比如插入种子数据。因为该方法会在数据库上运行多次,所以我们需要确保种子数据不重复,因而我们需要在插入数据之前检查一下数据是否已经存在。
现在我们准备创建数据库了,如果在本地工作,只是创建或者更新本地数据库,那么我们在包管理器控制台中执行Update-Database命令,还有,可以使用Get-Help
命令查看可以使用的参数(后面不再啰嗦这个命令)。下面,我们对-script
参数感兴趣,该参数很有用,因为它可以生成迁移的SQL脚本,我们可以将该脚本交给DBA或者我们自己运行。当运行Update-Database
命令时,它会比较实体类、上下文创建的数据库和物理数据库的结构。运行该命令之后:
该错误是告诉我们应该启用自动迁移生成,就像错误提示的那样,将配置类中的AutomaticMigrationsEnabled = false;
设置为true,生成解决方案,再次运行生成脚本的命令。我们会看到生成的脚本会在VS中打开,然后可以通过运行该脚本生成目标数据库。当我们和DBA共事时,他需要复审我们的升级脚本,这个功能就派上用场了。如果我们只是创建本地数据库的话,只需要运行Update-Database
,无需带任何参数。运行之后,打开数据库,发现已经创建了新的数据库,名为“DatabaseMigrationApp”。
生成的脚本如下:
自动迁移很容易使用,我们只需要更改代码,然后重新运行Update-Database
将发生的变化传播到SQL Server数据库中。要验证自动迁移没有数据丢失,我们手动在SSMS中添加一条数据,如下图:
现在,我们给Donator添加一个新属性Message(表示打赏者的留言),定义如下:
public class Donator
{
public int Id { get; set; }
[StringLength(10)]
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime DonateDate { get; set; }
public int ProvinceId { get; set; }
public virtual Province Province{ get; set; }
public string Message { get; set; }
}
执行Update-Database
,我们会看到之前手动添加的数据依然保留着,而且Message列的值是NULL,这是EF自动处理的默认值,如果数值类型,默认值为0。一般来讲,对于非空列,EF实际上会尝试该类型的默认值。比如,我们要给Message属性添加一个限制,即最大长度为50(默认的长度是MAX),然后更新数据库,结果会报错:
产生这个错误的道理很简单,字符串长度从最大变成50,肯定会造成数据丢失的。如果你知道会造成数据丢失,还要这么做,可以在后面加参数-Force
,这个参数会强制更新数据库。或者,我们可以开启数据丢失支持,正如EF暴露的这个设置Set AutomaticMigrationDataLossAllowed to 'true'(错误信息中提到的)。
我们看到,简单的情景推荐使用自动迁移,当迁移变得复杂时,自动迁移就不那么好使了,后面会看到。
使用迁移API
假如我还要给Donator实体添加一个非空的创建时间CreationTime属性:
public DateTime CreationTime { get; set; }
我想要这个新列的默认值为当前插入数据的日期。如果就这样更新数据库的话,会发现该列的值是1900/1/1。很显然,我们并不需要这样的值,因此,这个时候就需要我们切换到显式迁移了。一般来说,显式迁移比自动迁移更加灵活,虽然需要写更多的代码,但是对于迁移有了更多控制权,比如迁移名称和回滚过程等等。如果我们混用了自动迁移和显式迁移,就会把自己搞糊涂。比如,必须通过搜索项目来检查列是否通过自动迁移或手动迁移添加了。因此,为了提供一致性和维护目的,我们需要标准化显式迁移,此时,我们应该关闭自动迁移。
开始显式迁移之前,先要删除刚才SSMS中创建的数据库,然后使用迁移配置类中 AutomaticMigrationsEnabled = false;
关闭自动迁移。要创建初始化数据库迁移,需要使用新的命令Add-Migration
,如下所示:
InitialMigration只是一个迁移名称,可以随便起名字。这条指令会在Migrations文件夹中添加一个新类。物理文件会被命名为类似 201606100435228_InitalMigration的东西,文件名以迁移创建的时间为前缀,便于我们在文件夹中组织迁移。生成的代码如上图所示。
进一步看一下生成的代码,EF自动使用DbMigration
基类帮我们实现了自己的迁移,重写了Up
和Down
方法。Up方法将数据库结构向前移动,例如,我们这里创建了两张新的表;Down方法帮助我们撤销更改,以防我们发现了软件问题需要回滚到之前的数据库结构。现在使用Update-Database
命令更新数据库,我们会发现数据库中多了两张新表。
再仔细看的话,会发现多了一张不是我们创建的表__MigrationHistory
,如下所示:
__MigrationHistory
这张表,顾名思义,是记录迁移历史的。可以看到,MigrationId对应于初次迁移的文件名,Model列包含了上下文的哈希值,ContextKey包含了上下文配置类的类名。
现在回到之前例子,我们要给Donator实体添加CreationTime
属性,然后,我们需要给这个新属性添加新的迁移,再次使用Add-Migration
指令,执行指令Add-Migration Donator_Add_CreationTime
,EF生成的代码行如下:
public partial class Donator_Add_CreationTime : DbMigration
{
public override void Up()
{
AddColumn("dbo.Donators", "CreationTime", c => c.DateTime(nullable: false));
}
public override void Down()
{
DropColumn("dbo.Donators", "CreationTime");
}
}
之前我们看到了AddTable方法,现在,又看到了AddColumn方法。该方法需要提供表名和列名以及列类型(由相应的.Net类型指定)。这次我们要添加一个自定义的默认值,迁移支持硬编码默认值和识别为字符串的数据库引擎默认值。要指定硬编码默认值,我们可以使用defaultValue
参数。下面我们会使用defaultValueSql
,如下代码:
public override void Up()
{
AddColumn("dbo.Donators", "CreationTime", c => c.DateTime(nullable: false,defaultValueSql:"GetDate()"));
}
上面的代码中,我们使用了SQL Server中的GetDate函数使用当前日期填充新加入的列。实际上,我们也可以在EntityTypeConfiguration
类中做许多相同的事情。
DbMigration
基类支持许多数据库工件的维护(不仅仅是列和表),我们可以执行下面的东西:
- 创建、删除和更改存储过程
- 添加和删除外键
- 移动数据库模式之间的工件,例如表和存储过程
- 重命名对象,如表、存储过程和列
- 维护主键约束
- 创建、重命名和删除索引
最后,当遇到所说的这些方法不起作用时,可以使用Sql
或者SqlFile
方法。前者方法需要传入sql字符串,后者需要一个sql文件名作为参数。
所有迁移默认都以事务的一部分运行,确保所有的迁移操作要么成功,要么什么都不做。这对SQL Server是成立的,但是对于其他RDBMS可能不成立。比如,Oracle不支持DDL(Data Definition Language)定义的结构化操作事务。DDL指的是定义数据库结构的sql语句。还有DML(Data Manipulation Language),它指的是CRUD操作语句或者其他操作数据的语句。
要创建迁移,我们不一定非要有未处理的更改。比如,要创建一个索引,不需要未处理的更改。另外,我们可以使用EF 6.1中引入的API,它允许我们通过model builder API创建索引。为了这个例子的需要,我们使用迁移API。我们仍然使用之前的指令Add-Migration
,这样就给项目添加了一个迁移,但是Up和Down方法都是空的。现在,我们需要添加创建索引的自定义的代码,如下所示:
public partial class Donator_Add_Index_Name : DbMigration
{
public override void Up()
{
CreateIndex(
"Donators",
new []{"Name"},
name:"Index_Donator_Name"
);
}
public override void Down()
{
DropIndex("Donators", "Index_Donator_Name");
}
}
在上面的迁移中,我创建了一个新的索引,命名为“Donator_Add_Index_Name”,该索引是建立在Donators表上的,包含了Name列,如果需要多个列,只需要在字符串数组中添加列就可以了,至于是聚集索引还是非聚集索引,大家也可以自行配置。在Down方法中,我撤销了上面的更改,删除了索引。更新数据库,在MMSM中可以看到,生成了自己创建的索引:
到现在,我们还没有使用Down方法。事实上,EF数据库迁移支持目标迁移,也就是说,开发者可以将数据库结构移动到任何版本,这就是迁移,可以前进,也可以后退,就像版本控制工具一样。迁移实际上是按照文件名的创建时间进行排序的,该文件名被编码到了迁移的设计器文件中,如201606100502209_InitalMigration.Designer.cs。在解决方案资源管理器中,我们可以看到每个迁移都包含了三个文件,第一个文件是实际的迁移代码,第二个包含了单词Designer,指定了迁移Id和一些其他属性,第三个文件是资源文件,包含了模式名称和迁移目标哈希值。
现在,尝试通过指定创建索引迁移之前的目标迁移删除该索引,创建索引之前的迁移名称是Donator_Add_CreationTime,要退回上一个迁移,我们可以使用下面的指令:
Update-Database -TargetMigration Donator_Add_CreationTime
EF会立即通知我们会撤销哪一个迁移,我们的例子中,就是Donator_Add_Index_Name,如果我们在MMSM中查看数据库结构的话,会看到之前的索引已经不存在了。
应用迁移
至今,我们使用VS应用了所有迁移,在VS内部使用这些功能没有问题,但是当提到更新,测试或生产环境时,这种方式就不奏效了。为了更新这些软件安装,提供了以下更多的选择:
- 生成更改脚本
- 使用migrate.exe
- 使用迁移初始化器
通过脚本应用迁移
在包管理器控制台窗口中,我们可以通过Update-Database -Script
生成脚本,该命令一执行完成,生成的脚本就会在VS中打开。这个脚本包含了目标数据库从上次生成到生成脚本这段时间内发生的更改。我们只需要将这个脚本交给DBA,他会使用该脚本维护生产环境的。
需要注意的是,我们要指定匹配目标环境的数据库的正确连接字符串,因为迁移API会使用上下文比较实时数据库。我们要么在Update-Database
后面带上连接字符串,要么在配置文件中使用正确的连接字符串。
通过migrate.exe应用迁移
Migrate.exe是伴随EF发布的工具。使用Nuget安装EF时,也会将该工具放到EF的安装包所在的文件夹下。我们只需要将这个工具发布到应用程序的二进制文件夹下(也就是bin目录),使得该工具可以找到它需要的所有程序集。这个工具需要和Update-Database
指令相同的参数,例如:
migrate.exe Data
/connectionString="Data Source=.;Initial Catalog=DatabaseMigrationApp;Integrated Security=SSPI"
/connectionProviderName="System.Data.SqlClient"
/startupConfigurationFile=DatabaseMigrationApp.exe.config
为了清晰明了,我们将命令行分成多行,为了可读性,每个参数自成一行。第一个参数是包含上下文和迁移的程序集。然后,指定了连接字符串、provider和配置文件。之所以需要配置文件,是因为上下文构造函数使用了配置文件中的连接字符串名字。
通过初始化器应用迁移
当数据库模式发生变化时,我们可以使用初始化器重建数据库。EF自带了可应用未处理迁移的初始化器基类,这个基类是MigrateDatabaseToLatestVersion
。下面定义初始化器:
internal class Initializer:MigrateDatabaseToLatestVersion<Context,Configuration>
{
public override void InitializeDatabase(Context context)
{
base.InitializeDatabase(context);
}
}
这是个很简单的类,我们不需要写任何代码。如果你想在应用迁移时运行一些代码,那么可以使用InitializeDatabase
方法。该方法会获得上下文的一个实例,因此我们可以在该方法内将数据添加到数据库或者执行其他的一些函数。或者,可以使用迁移配置类,它也有Seed
方法,可以使用一些种子数据填充数据库。
现在,只需要在程序启动时将这个新的初始化器插入到EF中,然后调用上下文强制应用迁移,如下所示:
Database.SetInitializer(new Initializer());
using (var context = new Context())
{
context.Database.Initialize(true);
}
使用其他的初始化器时,我们已经看到了相似的代码,这里,我们也调用了数据库的Initialize
方法强制对已存在的数据库执行模式验证和迁移应用。如果数据库不存在,就会创建数据库。
给已存在的数据库添加迁移
有时,我们想为一个已存在的数据库添加EF迁移,为的是将处理模式变化从一种方式移动到迁移API。当然,因为数据库已存在于生产环境,所以我们需要让迁移知道迁移起始的已知状态。使用 Add-Migration -IgnoreChanges
指令处理这个是相当简单的,当执行该命令时,EF会创建一个空的迁移,它会假设上下文和实体定义的模型和数据库是兼容的。一旦通过运行这个迁移更新了数据库,数据库模式不会发生变化,但是会在 _MigrationHistory
表中添加一条新的数据来对应初次迁移。这个完成之后,我们就可以安全地切换到EF的迁移API来维护数据库模式变化了。
一些数据库系统不支持表名的首字母为下划线,EF允许开发者自定义该表名。
另一个想要解决的用例是为已存在的数据库创建实体类,然后不仅要给已存在的数据库添加EF,还要给已存在的软件添加。这个任务可以使用VS的Entity Framework Power Tools插件完成。一旦安装了该插件,在项目的右键菜单上会多个选项Reverse Engineer Code First(工程转换为Code First)。开发者需要做的是将这个工具指向想要使用EF支持的数据库,然后该工具会将数据库中的所有表转换为实体类、上下文和配置类。我们也可以使用Entity Framework Tools。这个工具集也支持从数据库生成Code First。为了使用这个功能,只需要从Add New Item对话框选择ADO.NET Entity Data Model,然后按向导步骤进行即可。
EF的其他功能
下面看一下更多至今还没有讨论的功能,这些功能并不经常使用,但是知道这些功能存在很重要。
自定义约定
有时,我们想做出应用于很多实体类型或者表的全局更改。比如,我们想使得所有的decimal字段默认是确定的大小,除非我们明确指定为其他大小;美国人也可能想要全局设置所有的字符串属性为非Unicode,因为他们的应用只打算为讲英语的人用。我们可以通过使用全局配置API或者自定义约定完成这些任务。比如,下面是如何设置所有的string属性在数据库中存储为非Unicode列:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<string>().Configure(config=>config.IsUnicode(false));
}
我们也可以写相同的代码作为自定义约定,然后在model builder中加入到约定集合中。要这么做的话,首先创建一个继承自Convention
的类,重写构造函数,然后使用之前相同的代码,调用Convention
类的Properties
方法。代码如下:
public class CustomConventions:Convention
{
public CustomConventions()
{
Properties<string>().Configure(config=>config.IsUnicode(false));
}
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//modelBuilder.Properties<string>().Configure(config=>config.IsUnicode(false));
modelBuilder.Conventions.Add<CustomConventions>();
}
必须要记住,要将自定义的约定添加到modelBuilder的Conventions的集合中。
这些类型的约定指的是配置约定。EF中的模型约定API可以创建两种类型约定:存储模型和概念模型约定。这些约定允许我们在模型中的许多地方全局地应用更改,而不是一个个实体或者一个个属性地修改。每种.Net类型也可以写多个约定,因为EF允许我们控制应用的约定的顺序。
地理空间数据
除了标量类型数据,如string或decimal,EF也通过.Net中的DbGeometry
和DbGeography
支持地理空间数据。这些类型有支持地理空间查询的内置支持和正确翻译,例如地图上两点之间的距离。这些特定的查询方法对于具有地理空间属性的实体的查询很有用,换言之,当使用空间类型时,我们仍编写.Net代码。
依赖注入DI和日志记录
EF现在已经实现了服务定位模式,因此开启了依赖注入。DI用于支持配置方法。比如,可以使用我们自定义方式来创建依赖解析器Resolver,然后创建通用的EF对象,例如IDbConnectionFactory
。请通过阅读EF的文档找出可以注入到正在运行的应用中的类或接口,强制EF使用它们而不是使用默认的实现。
也可以将一个自定义的logger注入EF,这样就可以记录EF执行的所有actions到自定义的日志源。要创建自定义日志,开发者可以设置Database对象的Log属性。Log属性需要赋予一个带有一个字符串参数的方法,如context.Database.Log = System.Console.Write;
,这样日志就会在控制台中输出。如果你不喜欢这种记录日志的方式,也可以创建自定义的方式。
启动性能
有时,对于大型数据库和上下文启动时间可能相对较长,Entity Framework Power Tools允许我们通过暴露一种预生成视图的能力加速这个过程。这里我们说的不是数据库视图,而指的是EF生成的语句可以创建CRUD操作语句。要生成这些预编译的视图,我们要做的是在含有派生自DbContext的类所在的文件上右键(前提是安装了Power Tools),在Entity Framework菜单下选择 Generate Views操作。这个操作会创建需要编译到程序集的所有代码。
一个数据库,多个上下文
我们不必总是将映射到表的所有实体集合放到一个上下文里。使用多个DbContext类有很多优点。这种方式可能会减少启动时间,因为这个时间一般是和上下文第一次访问的集合的数量成比例的,也会减少每个上下文对开发者暴露的数据面。还有,它会帮助开发者将数据组织到数据模块中。当然,如果我们使用迁移,我们仍然需要一个包含每个集合或表的上下文,因为我们会使用这个上下文用于支持迁移。这是我们需要实际配置的唯一上下文。当我们使用多个上下文并在一个事务中将数据保存到多个上下文时,需要做一些额外的事情。每个SaveChanges
调用都是事务的,但我们需要为所有的SaveChanges
调用创建一个首要的事务。我们也许会发现,对于涉及多个模块的保存操作,将所有的集合放到单个大型的DbContext中是更简单的。
本章小结
这篇博客,我们学会了如何使用EF维护数据库模式,学习了在Nuget包管理器控制台里运行Enable-Migration
命令在一个项目上启用迁移。一旦启用了迁移,会生成一个配置类,我们可以开始将数据库模式向前移动了。对于迁移,开发者有两种选择,可以依赖自动迁移或者创建显式迁移。自动迁移有许多限制,例如不可能设置默认值。为了确保迁移一致性,开发者只能选择使用显式迁移。所有的显式迁移继承自DbMigration
类,该类包含了允许更新目标数据库模式的方法。该类的方法允许我们创建或删除表,创建、删除和更改列,创建和删除索引等等。最后,如果找不到合适的方法或者只需要对数据改变时,可以使用Sql
方法来运行任意的SQL命令。如果需要在一个已存在的数据库上开启迁移,那么只需要创建一个空的迁移,这样就将数据库对应的上下文标记为最新的。一旦这个空的初始迁移创建之后,我们就可以像平常那样编写迁移了。在VS内部使用Update-Database
命令可以相当容易地更新开发环境中的数据库。当更新生产环境的数据库时,我们可以使用初始化器来迁移数据库,也可以使用migrate.exe或者在VS里生成一个迁移脚本。
最后,我们还介绍了一些EF中额外的功能,这些功能平时可能不怎么用,但是知道一些总是好的。因此我们快速浏览了一下这些功能。现在EF支持地理空间数据了,也可以使用日志功能捕获EF对数据库创建并执行的一些命令,通过使用多个数据库上下文类或者预生成视图可以加速EF的启动时间。
自我测试
- 要发挥EF内置的模式更新的优势,你必须在项目上开启迁移,对吗?
- 自动迁移百分之百的时间都会奏效,所以不需要使用显式迁移,对吗?
- 要生成最新版本的迁移脚本,不需要访问目标数据库,对吗?
- 为了在已存在的生产数据库上添加迁移,你需要怎么做?
- 要更新本地开发环境的数据库,不能使用VS,对吗?
- EF迁移不支持存储过程,因此必须使用其他的工具来实现,对吗?
- 要为所有的实体类和表的所有decimal字段设置一个通用的精度,必须为每个实体的每个字段指定这个精度,对吗?
- 如果想要通过EF对RDBMS记录已经触发的命令,只可以使用数据库工具,例如SQL Server profiler,对吗?
- 要确定两个地理坐标(使用SQL Server地理数据类型可以识别坐标存储)的距离,必须使用存储过程,对吗?
如果您觉得这篇文章对您有价值或者有所收获,请点击右下方的店长推荐,然后查看答案,谢谢!
参考书籍:
《Mastering Entity Framework》
《Code-First Development with Entity Framework》
《Programming Entity Framework Code First》