您的位置:时时app平台注册网站 > 编程知识 > 全自动迁移数据库的实现 (Fluent NHibernate, Entity

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

2019-10-30 04:20

写在最后的广告

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

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

实体框架简介

第一个示例使用单个Book类型,并将此类型映射到SQL Server数据库中的Books表。可以将记录写入数据库,然后读取,更新和删除它们。 

在第一个示例中,首先创建数据库。可以使用Visual Studio 2015中的SQL Server对象资源管理器执行此操作。选择数据库实例(与Visual Studio一起安装的(localdb) MSSQLLocalDB),单击树视图中的数据库节点,然后选择“添加新数据库”。示例数据库只有一个名为Books的表。 

选择Books数据库中的表节点,然后选择"添加新表"来创建表Books。使用图38.1中所示的设计器,或者通过在T-SQL编辑器中输入SQL DDL语句,都可以创建表Books。以下代码段显示了用于创建表的T-SQL代码。单击“更新”按钮可以将更改提交到数据库。

CREATE TABLE [dbo].[Books]
(
  [BookId] INT NOT NULL PRIMARY KEY IDENTITY,
  [Title] NVARCHAR(50) NOT NULL,
  [Publisher] NVARCHAR(25) NOT NULL
)

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

从数据库读取

从C#代码读取数据只需要调用BooksContext并访问Books属性。访问此属性会创建一个SQL语句从数据库中检索所有图书(代码文件BooksSample / Program.cs):

private void ReadBooks()
{
  using (var context = new BooksContext())
  {
    var books = context.Books;
    foreach (var b in books)
    {
      WriteLine($"{b.Title} {b.Publisher}");
    }
  }
  WriteLine();
}

在调试期间打开 IntelliTrace Events窗口,可以看到发送到数据库的SQL语句(需要Visual Studio 企业版):

SELECT [b].[BookId], [b].[Publisher], [b].[Title]
FROM [Books] AS [b]

Framework提供了一个LINQ提供程序,可以创建LINQ查询访问数据库。可以使用如下所示语法的方法:

private void QueryBooks()
{
  using (var context = new BooksContext())
  {
    var wroxBooks = context.Books.Where(b => b.Publisher =="Wrox Press");
    foreach (var b in wroxBooks)
    {
      WriteLine($"{b.Title} {b.Publisher}");
    }
  }
  WriteLine();
}

或使用LINQ查询语法:

var wroxBooks = from b in context.Books
                where b.Publisher =="Wrox Press"
                select b;

使用这两种不同的语法,都将发送下面的SQL语句到数据库:

SELECT [b].[BookId], [b].[Publisher], [b].[Title]
FROM [Books] AS [b]
WHERE [b].[Publisher] = 'Wrox Press'

*注意 在第13章“语言集成查询”中详细讨论了LINQ。
*

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

写入数据库

现在已创建了有Books表的数据库,也定义了模型和上下文类,然后可以用数据填充表。创建AddBookAsync方法将Book对象添加到数据库。首先,BooksContext对象被实例化,这里使用using语句确保数据库连接关闭。使用Add方法将对象添加到上下文之后,实体被写入调用SaveChangesAsync的数据库(代码文件BooksSample / Program.cs):

private async Task AddBookAsync(string title, string publisher)
{
  using (var context = new BooksContext())
  {
    var book = new Book
    {
      Title = title,
      Publisher = publisher
    };
    context.Add(book);
    int records = await context.SaveChangesAsync();

    WriteLine($"{records} record added");
  }
  WriteLine();
} 

要添加书籍列表,可以使用AddRange方法(代码文件BooksSample / Program.cs):

private async Task AddBooksAsync()
{
  using (var context = new BooksContext())
  {
    var b1 = new Book
    {
      Title ="Professional C# 5 and .NET 4.5.1",
      Publisher ="Wrox Press"
    };
    var b2 = new Book
    {
      Title ="Professional C# 2012 and .NET 4.5",
      Publisher ="Wrox Press"
    };
    var b3 = new Book
    {
      Title ="JavaScript for Kids",
      Publisher ="Wrox Press"
    };
    var b4 = new Book
    {
      Title ="Web Design with HTML and CSS",
      Publisher ="For Dummies"
    };
    context.AddRange(b1, b2, b3, b4);
    int records = await context.SaveChangesAsync();
    WriteLine($"{records} records added");
  }
  WriteLine();
} 

 运行应用程序并调用这些方法后,可以使用SQL Server对象资源管理器查看写入到数据库的数据。

实现全自动迁移的思路

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

  • 获取数据库现有的结构
  • 获取代码中现有的结构
  • 对比结构之间的差异并生成迁移

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

**译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(上)),不对的地方欢迎指出与交流。** 

章节出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位阅读时仔细分辨,唯望莫误人子弟。

附英文版原文:Professional C# 6 and .NET Core 1.0 - 38 Entity Framework Core

本章节译文分为上下篇,下篇见: C# 6 与 .NET Core 1.0 高级编程 - 38 章 实体框架核心(下)


本章内容

  • Entity Framework Core 1.0简介
  • 使用依赖注入实体框架
  • 创建关系模型
  • 使用.NET CLI工具和MSBuild进行迁移
  • 对象跟踪
  • 更新对象和对象树
  • 冲突处理与更新
  • 使用事务

Wrox.Com关于本章的源代码下载

本章的wrox.com代码下载位于 www.wrox.com/go/professionalcsharp6 下载代码选项卡。本章的代码主要有以下示例:

  • Books Sample
  • Books Sample with DI
  • Menus Sample
  • Menus with Data Annotations
  • Conflict Handling Sample
  • Transactions Sample 

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一样,可以保证很强的一致性但需要注意防止数据的丢失。

创建上下文 

创建的BooksContext类完成Book表与数据库的关联。这个类派生自基类DbContext,BooksContext类定义Books属性类型为DbSet <Book>。此类型允许创建查询并添加Book实例以将其存储在数据库中。要定义连接字符串,可以重写DbContext的OnConfiguring方法。UseSqlServer扩展方法将上下文映射到SQL Server数据库(代码文件BooksSample / BooksContext.cs):

public class BooksContext: DbContext
{
  private const string ConnectionString =  @"server= (localdb)MSSQLLocalDb;database=Books;trusted_connection=true";
  public DbSet<Book> Books { get; set; }
  protected override void OnConfiguring(DbContextOptionsBuilder  optionsBuilder)
  {
    base.OnConfiguring(optionsBuilder);
    optionsBuilder.UseSqlServer(ConnectionString);
  }
}

定义连接字符串的另一个选项是使用依赖注入,将在本章后面介绍。 

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的自动迁移只会添加表和字段,基本不会修改原有的结构,有一定的限制但是比较安全。

删除记录

最后,让我们清理数据库并删除所有记录。可以通过检索所有记录并调用Remove或RemoveRange方法来设置上下文中要删除的对象的状态。然后调用SaveChangesAsync方法即可从数据库中删除记录,DbContext会为每个要删除的对象调用SQL Delete语句(代码文件BooksSample / Program.cs):

private async Task DeleteBooksAsync()
{
  using (var context = new BooksContext())
  {
    var books = context.Books;
    context.Books.RemoveRange(books);
    int records = await context.SaveChangesAsync();
    WriteLine($"{records} records deleted");
  }
  WriteLine();
}

*注意 对象关系映射工具(如Entity Framework)在并非在所有方案中都可用。使用示例代码无法有效地删除所有对象。您可以使用一个SQL语句删除所有而不是逐条删除记录。在第37章“ADO.NET”中解释了如何做到这一点。*

了解了如何添加、查询、更新和删除记录,本章将介绍幕后的功能,并使用Entity Framework进入高级场景。

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的迁移可以保证实体和数据库之间很强的一致性,但是使用不当会导致原有数据的丢失,请务必做好数据库的定时备份。

从数据库创建模型  

从模型可以创建数据库,相反从数据库也可以创建模型。 

要从SQL Server数据库执行此操作,除了其他包,还必须将NuGet包添加到DNX项目中,EntityFramework.MicrosoftSqlServer.Design。然后可以在开发人员命令提示符使用以下命令:

> dnx ef dbcontext scaffold 
"server=(localdb)MSSQLLocalDb;database=SampleDatabase; trusted_connection=true""EntityFramework.MicrosoftSqlServer"

dbcontext命令能够从项目中列出DbContext对象,同时也创建DBContext对象。命令scaffold创建DbContext派生类以及模型类。 dnx ef dbcontext scaffold 需要两个必要的参数:数据库的连接字符串和使用的提供程序。前面所示的语句中,在SQL Server(localdb) MSSQLLocalDb上访问数据库SampleDatabase。使用的提供程序是EntityFramework.MicrosoftSqlServer。这个NuGet包以及具有相同名称和设计后缀的NuGet包必须添加到项目中。 

运行此命令后,可以看到DbContext派生类以及生成的模型类型。默认情况下,模型的配置使用fluent API完成。但是也可以将其更改为使用提供-a选项的数据批注。还可以影响生成的上下文类名称以及输出目录。只需使用选项-h检查不同的可用选项。

 

----------------未完待续

写在最后

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

创建关系

让我们开始创建一个模型。示例项目使用MenuCard和Menu类型定义一对多关系。MenuCard包含Menu对象的列表。这种关系由List <Menu>类型的Menu属性简单定义(代码文件MenusSample / MenuCard.cs):

public class MenuCard
{
  public int MenuCardId { get; set; }
  public string Title { get; set; }
  public List<Menu> Menus { get; } = new List<Menu>();

  public override string ToString() => Title;
}

该关系也可以从另一个角度访问,菜单可以使用MenuCard属性访问MenuCard。指定 MenuCardId 属性去定义外键关系(代码文件MenusSample / Menu.cs):

public class Menu
{
  public int MenuId { get; set; }
  public string Text { get; set; }
  public decimal Price { get; set; }

  public int MenuCardId { get; set; }
  public MenuCard MenuCard { get; set; }

  public override string ToString() => Text;
}

到数据库的映射由MenusContext类完成。这个类定义为与上一个上下文类型类似的类型,它只包含两个属性来映射两个对象类型:属性Menus和MenuCards(代码文件MenusSamples / MenusContext.cs):

public class MenusContext: DbContext
{
  private const string ConnectionString = @"server=(localdb)MSSQLLocalDb;"       "Database=MenuCards;Trusted_Connection=True";
  public DbSet<Menu> Menus { get; set; }
  public DbSet<MenuCard> MenuCards { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder  optionsBuilder)
  {
    base.OnConfiguring(optionsBuilder);
    optionsBuilder.UseSqlServer(ConnectionString);
  }
}

使用.NET CLI进行迁移

要使用C#代码自动创建数据库,可以使用enet工具使用package dotnet-ef扩展.NET CLI工具。此软件包包含用于为迁移创建C#代码的命令。通过安装dotnet-ef NuGet包可以使命令可用。您可以通过从项目配置文件(代码文件MenusSample / project.json)中的工具部分引用此软件包来安装它:

"tools": {
  "dotnet-ef":"1.0.0-*"
 }

ef命令提供以下命令:数据库、dbcontext和迁移。数据库命令用于将数据库升级到特定的迁移状态。 dbcontext命令列出项目中的所有DbContext派生类型(dbcontext list),并从数据库(dbcontext scaffold)创建上下文和实体。 migrations命令则创建和删除迁移,以及创建SQL脚本去创建包含所有迁移的数据库。如果生产数据库只能从SQL管理员使用SQL代码创建和修改,可以将生成的脚本移交给SQL管理员。 

为了创建初始迁移以从代码创建数据库,可以从开发人员命令提示符调用以下命令,该命令创建名为InitMenuCards的迁移:

>dotnet ef migrations add InitMenuCards

命令migrations add使用反射以及相反的引用模型访问DbContext派生类。此信息创建两个类来创建和更新数据库。使用Menu,MenuCard和MenusContext类创建两个类,MenusContextModelSnapshot和InitMenuCards。命令成功后可以在Migrations文件夹中找到这两种类型。

MenusContextModelSnapshot类包含构建数据库的模型的当前状态:

[DbContext(typeof(MenusContext))]
partial class MenusContextModelSnapshot: ModelSnapshot
{
  protected override void BuildModel(ModelBuilder modelBuilder)
  {
    modelBuilder
     .HasAnnotation("ProductVersion","7.0.0-rc1-16348")
     .HasAnnotation("SqlServer:ValueGenerationStrategy",
       SqlServerValueGenerationStrategy.IdentityColumn);

     modelBuilder.Entity("MenusSample.Menu", b =>
     {
       b.Property<int>("MenuId")
        .ValueGeneratedOnAdd();
       b.Property<int>("MenuCardId");
       b.Property<decimal>("Price");
       b.Property<string>("Text");
       b.HasKey("MenuId");
     });

     modelBuilder.Entity("MenusSample.MenuCard", b =>
     {
       b.Property<int>("MenuCardId")
        .ValueGeneratedOnAdd();

       b.Property<string>("Title");
       b.HasKey("MenuCardId");
     });
     modelBuilder.Entity("MenusSample.Menu", b =>
     {
       b.HasOne("MenusSample.MenuCard")
        .WithMany()
        .HasForeignKey("MenuCardId");
     });
  }
}

InitMenuCards类定义了Up和Down方法。 Up方法列出了创建MenuCard和菜单表所需的所有操作,包括主键、列和关系。 Down方法删除两个表:

public partial class InitMenuCards: Migration
{
  protected override void Up(MigrationBuilder migrationBuilder)
  {
    migrationBuilder.CreateTable(
      name:"MenuCard",
      columns: table => new
      {
        MenuCardId = table.Column<int>(nullable: false)
          .Annotation("SqlServer:ValueGenerationStrategy",
            SqlServerValueGenerationStrategy.IdentityColumn),
        Title = table.Column<string>(nullable: true)
      },
      constraints: table =>
      {
        table.PrimaryKey("PK_MenuCard", x => x.MenuCardId);
      });

    migrationBuilder.CreateTable(
      name:"Menu",
      columns: table => new
      {
        MenuId = table.Column<int>(nullable: false)
          .Annotation("SqlServer:ValueGenerationStrategy",
            SqlServerValueGenerationStrategy.IdentityColumn),
        MenuCardId = table.Column<int>(nullable: false),
        Price = table.Column<decimal>(nullable: false),
        Text = table.Column<string>(nullable: true)
      },
      constraints: table =>
      {
        table.PrimaryKey("PK_Menu", x => x.MenuId);
        table.ForeignKey(
          name:"FK_Menu_MenuCard_MenuCardId",
          column: x => x.MenuCardId,
          principalTable:"MenuCard",
          principalColumn:"MenuCardId",
          onDelete: ReferentialAction.Cascade);
      });
  }

  protected override void Down(MigrationBuilder migrationBuilder)
  {
    migrationBuilder.DropTable("Menu");
    migrationBuilder.DropTable("MenuCard");
  }
}

注意 正在进行的每个更改都可以创建另一个迁移。新迁移仅定义从先前版本到新版本所需的更改。如果客户的数据库需要从任意早期的版本更新,迁移数据库时调用必要的迁移。 

在开发过程中,也行不需要所有的迁移,可能需要从项目中创建,因为可能没有该类临时状态的数据库存在。在这种情况下可以删除迁移并创建一个较大的新迁移。

使用MSBuild进行迁移  

如果您正在使用基于MSBuild的项目Entity Framework迁移而不是DNX,迁移命令是不同的。使用完整框架控制台应用程序、WPF应用程序或ASP.NET 4.6项目类型,需要在NuGet包管理器控制台中指定迁移命令,而不是开发人员命令提示符。从Visual Studio通过 工具➪库管理器控制台➪包管理器控制台 启动包管理器控制台。

在包管理器控制台可以使用PowerShell脚本添加和删除迁移。命令如下

> Add-Migration InitMenuCards

创建一个Migrations文件夹,其中包含如前所示的迁移类。

创建数据库 

随着迁移类型到位,可以创建数据库。 DbContext派生类MenusContext包含一个返回DatabaseFacade对象的Database属性。使用DatabaseFacade可以创建和删除数据库。如果数据库不存在,EnsureCreated方法会创建数据库;如果数据库已存在,则不执行任何操作。方法EnsureDeletedAsync删除数据库。以下代码片段创建数据库(如果它不存在)(代码文件MenusSample / Program.cs):

private static async Task CreateDatabaseAsync()
{
  using (var context = new MenusContext())
  {
bool created = await context.Database.EnsureCreatedAsync();
    string createdText = created ?"created":"already exists";
    WriteLine($"database {createdText}");
  }
}

注意 如果数据库存在但是一个较旧的结构版本,EnsureCreatedAsync方法不会应用结构更改。这时可以通过调用Migrate方法来进行结构升级。 Migrate是Microsoft.Data.Entity命名空间中定义的DatabaseFacade类的扩展方法。

运行程序将创建表MenuCard和Menu。基于默认约定,表与实体类型是相同的名称。另一个约定用于创建主键:MenuCardId列会被定义为主键,因为属性名以Id结束。

CREATE TABLE [dbo].[MenuCard] (
  [MenuCardId] INT            IDENTITY (1, 1) NOT NULL,
  [Title]      NVARCHAR (MAX) NULL,
  CONSTRAINT [PK_MenuCard] PRIMARY KEY CLUSTERED ([MenuCardId] ASC)
);

Menu表定义了MenuCardId,它是MenuCard表的外键。由于DELETE CASCADE,删除MenuCard也会删除所有关联的Menu行:

CREATE TABLE [dbo].[Menu] (
  [MenuId]     INT             IDENTITY (1, 1) NOT NULL,
  [MenuCardId] INT             NOT NULL,
  [Price]      DECIMAL (18, 2) NOT NULL,
  [Text]       NVARCHAR (MAX)  NULL,
  CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED ([MenuId] ASC),
  CONSTRAINT [FK_Menu_MenuCard_MenuCardId] FOREIGN KEY ([MenuCardId])
  REFERENCES [dbo].[MenuCard] ([MenuCardId]) ON DELETE CASCADE
);

在创建代码中有一些部分改变是有用的。例如,Text 和 Title 列的大小可以从NVARCHAR(MAX)减小,SQL Server定义了可用于Price列的Money类型,并且结构名称可以从dbo更改。 Entity Framework提供了两个选项从代码中执行这些更改:数据批注和Fluent API,下面将讨论。

数据批注

影响生成的数据库的一种方法是向实体类型添加数据注释。可以利用Table属性更改表的名称。要更改结构名称,Table属性定义Schema属性。如果要为字符串类型指定不同的长度,可以使用MaxLength属性(代码文件MenusWithDataAnnotations / MenuCard.cs):

[Table("MenuCards", Schema ="mc")]
public class MenuCard
{
  public int MenuCardId { get; set; }
  [MaxLength(120)]
  public string Title { get; set; }
  public List<Menu> Menus { get; }
}

Menu类的Table和MaxLength属性同样可以应用。使用Column属性更改SQL类型(代码文件MenusWithDataAnnotations / Menu.cs):

[Table("Menus", Schema ="mc")]
public class Menu
{
  public int MenuId { get; set; }
  [MaxLength(50)]
  public string Text { get; set; }
  [Column(TypeName ="Money")]
  public decimal Price { get; set; }
  public int MenuCardId { get; set; }
  public MenuCard MenuCard { get; set; }
}

应用迁移创建数据库后可以看到结构名称下表的新名称,以及Title、Text 和 Price 字段中已更改的数据类型:

CREATE TABLE [mc].[MenuCards] (
  [MenuCardId] INT            IDENTITY (1, 1) NOT NULL,
  [Title]      NVARCHAR (120) NULL,
  CONSTRAINT [PK_MenuCard] PRIMARY KEY CLUSTERED ([MenuCardId] ASC)
);

CREATE TABLE [mc].[Menus] (
  [MenuId]     INT           IDENTITY (1, 1) NOT NULL,
  [MenuCardId] INT           NOT NULL,
  [Price]      MONEY         NOT NULL,
  [Text]       NVARCHAR (50) NULL,
  CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED ([MenuId] ASC),
  CONSTRAINT [FK_Menu_MenuCard_MenuCardId] FOREIGN KEY ([MenuCardId])
    REFERENCES [mc].[MenuCards] ([MenuCardId]) ON DELETE CASCADE
);

使用依赖注入  

Entity Framework Core 1.0内置了对依赖注入的支持。连接和SQL Server选择可以通过使用依赖注入框架注入,而非定义和然后使用DbContext派生类的SQL Server连接。 

要查看此操作,BooksSampleWithDI示例项目对上一个代码示例项目进行了修改。 

此示例使用以下依赖项和命名空间:

  依赖项

NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.Framework.DependencyInjection 

  命名空间

Microsoft.EntityFrameworkCore
System.Linq
System.Threading.Tasks
static System.Console

BooksContext类现在看起来很简单,只需定义Books属性(代码文件BooksSampleWithDI / BooksContext.cs):

public class BooksContext: DbContext
{
  public DbSet<Book> Books { get; set; }
}

BooksService是使用BooksContext的新类。BooksContext通过注入构造函数注入。方法AddBooksAsync和ReadBooks与上一个示例中的这些方法非常相似,但他们使用BooksService类的上下文成员,而不是创建一个新的(代码文件BooksSampleWithDI / BooksService.cs):

public class BooksService
{
  private readonly BooksContext _booksContext;
  public BooksService(BooksContext context)
  {
    _booksContext = context;
  }

  public async Task AddBooksAsync()
  {
    var b1 = new Book
    {
      Title ="Professional C# 5 and .NET 4.5.1",
      Publisher ="Wrox Press"
    };
    var b2 = new Book
    {
      Title ="Professional C# 2012 and .NET 4.5",
      Publisher ="Wrox Press"
    };
    var b3 = new Book
    {
      Title ="JavaScript for Kids",
      Publisher ="Wrox Press"
    };
    var b4 = new Book
    {
      Title ="Web Design with HTML and CSS",
      Publisher ="For Dummies"
    };
    _booksContext.AddRange(b1, b2, b3, b4);
    int records = await _booksContext.SaveChangesAsync();

    WriteLine($"{records} records added");
  }

  public void ReadBooks()
  {
    var books = _booksContext.Books;
    foreach (var b in books)
    {
      WriteLine($"{b.Title} {b.Publisher}");
    }
    WriteLine();
  }
} 

依赖注入框架的容器在 InitializeServices 方法中初始化。创建一个ServiceCollection实例,将BooksService类添加到此集合中,并进行临时生命周期管理。这样,每次请求该服务时都会实例化 ServiceCollection。对于注册Entity Framework和SQL Server,可以用扩展方法AddEntityFramework,AddSqlServer和AddDbContext。 AddDbContext方法需要一个Action委托作为参数,其中接收到一个DbContextOptionsBuilder参数。有了该选项参数,可以使用UseSqlServer扩展方法配置上下文。这里用Entity Framework注册SQL Server与上一个示例是类似的功能(代码文件BooksSampleWithDI / Program.cs):

private void InitializeServices()
{
  const string ConnectionString =@"server= (localdb)MSSQLLocalDb;database=Books;trusted_connection=true";
  var services = new ServiceCollection();
  services.AddTransient<BooksService>();
  services.AddEntityFramework()
    .AddSqlServer()
    .AddDbContext<BooksContext>(options =>
      options.UseSqlServer(ConnectionString));
  Container = services.BuildServiceProvider();
}

public IServiceProvider Container { get; private set; }

服务的初始化以及BooksService的使用是从Main方法完成的。通过调用IServiceProvider的GetService方法来检索BooksService(代码文件BooksSampleWithDI / Program.cs):

static void Main()
{
  var p = new Program();
  p.InitializeServices();

  var service = p.Container.GetService<BooksService>();
  service.AddBooksAsync().Wait();
  service.ReadBooks();
}

运行应用程序可以看到记录已添加到图书数据库中然后从中读取记录。

*注意 在第31章“XAML应用程序的模式”中阅读有关依赖注入和Microsoft.Framework.DependencyInjection包的更多信息,还可以参见第40章“ASP.NET Core”和第41章“ ASP.NET MVC“。*

Fluent API  

影响创建的表的另一种方法是使用Fluent API中DbContext派生类的OnModelCreating方法。它的优点是可以保持实体类型简单,而不添加任何属性,Fluent API还提供了比应用属性更多的选项。 

以下代码片段显示了BooksContext类重写OnModelCreating方法。作为参数接收的ModelBuilder类提供了一些方法,并定义了几种扩展方法。 HasDefaultSchema是其中一个扩展方法,它将默认结构应用于目前所有类型的模型。 Entity方法返回一个EntityTypeBuilder,使您能够自定义实体,例如将其映射到特定的表名和定义键和索引(代码文件MenusSample / MenusContext.cs):

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  base.OnModelCreating(modelBuilder);

  modelBuilder.HasDefaultSchema("mc");

  modelBuilder.Entity<MenuCard>()
    .ToTable("MenuCards")
    .HasKey(c => c.MenuCardId);

  // etc.

  modelBuilder.Entity<Menu>()
    .ToTable("Menus")
    .HasKey(m => m.MenuId);

  // etc.
}

EntityTypeBuilder定义了一个Property方法来配置属性。 Property方法返回PropertyBuilder,能够依次配置具有最大长度值,必要的设置和SQL类型的属性,并指定是否应自动生成值(例如标识列):

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  // etc.

  modelBuilder.Entity<MenuCard>()
    .Property<int>(c => c.MenuCardId)
    .ValueGeneratedOnAdd();

  modelBuilder.Entity<MenuCard>()
    .Property<string>(c => c.Title)
    .HasMaxLength(50);

  modelBuilder.Entity<Menu>()
    .Property<int>(m => m.MenuId)
    .ValueGeneratedOnAdd();

  modelBuilder.Entity<Menu>()
.Property<string>(m => m.Text)
    .HasMaxLength(120);

  modelBuilder.Entity<Menu>()
    .Property<decimal>(m => m.Price)
    .HasColumnType("Money");

  // etc.
} 

EntityTypeBuilder定义映射方法去定义一对多映射。HasMany 结合 WithOne 方法定义了多Menus 和一个Menu Card 的映射。 HasMany需要与WithOne链接,即HasOne方法需要一个带WithMany或WithOne的链。链接 HasOne 和 WithMany定义了一对多关系,链接HasOne与WithOne定义了一对一的关系:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  // etc.

  modelBuilder.Entity<MenuCard>()
    .HasMany(c => c.Menus)
    .WithOne(m => m.MenuCard);
  modelBuilder.Entity<Menu>()
    .HasOne(m => m.MenuCard)
    .WithMany(c => c.Menus)
    .HasForeignKey(m => m.MenuCardId);
}

在OnModelCreating方法中创建映射之后可以创建如前所示的迁移。

创建模型 

用于访问Books数据库的示例应用程序BookSample是一个控制台应用程序(Package)。此示例使用以下依赖项和命名空间:

  依赖项

NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer

   命名空间

Microsoft.EntityFrameworkCore
System.ComponentModel.DataAnnotations.Schema
System
System.Linq
System.Threading.Tasks
static System.Console

 时时app平台注册网站 1

图 38.1  

Book类是一个简单的实体类型,它定义了三个属性。 BookId属性映射到表的主键,Title属性指向标题列,Publisher属性指向Publisher列。Table属性应用于类型将类型映射到Books表(代码文件BooksSample / Book.cs):

[Table("Books")]
public class Book
{
  public int BookId { get; set; }
  public string Title { get; set; }
  public string Publisher { get; set; }
}

更新记录

只需更改已加载上下文的对象并调用SaveChangesAsync即可轻松实现更新记录(代码文件BooksSample / Program.cs):

private async Task UpdateBookAsync()
{
  using (var context = new BooksContext())
  {
    int records = 0;
    var book = context.Books.Where(b => b.Title =="Professional C# 6")
      .FirstOrDefault();
    if (book != null)
    {
      book.Title ="Professional C# 6 and .NET Core 5";
      records = await context.SaveChangesAsync();
    }
    WriteLine($"{records} record updated");
  }
  WriteLine();
}

创建模型  

本章的第一个示例映射单个表。第二个例子显示了创建表之间的关系。在本节中使用C#代码创建数据库而没有使用SQL DDL语句(或通过使用设计器)创建数据库。 

示例应用程序MenusSample使用以下依赖项和命名空间:

  依赖项

NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer

  命名空间

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.ChangeTracking
System
System.Collections.Generic
System.ComponentModel.DataAnnotations
System.ComponentModel.DataAnnotations.Schema
System.Linq
System.Threading
System.Threading.Tasks
static System.Console

实体框架的历史

实体框架是提供实体到关系的映射的框架。通过这种方式,可以创建映射到数据库表的类型,使用LINQ创建数据库查询,创建和更新对象,并将它们写入数据库。 

经过多年对Entity Framework的少量修改,最新的版本是一个完全的重写。一起来看看Entity Framework的历史,以及重写的原因。

  • Entity Framework 1—Entity Framework的第一个版本没有准备好与.NET 3.5兼容,但它很快就可以与.NET 3.5 SP1兼容。另一个产品LINQ to SQL提供了一些类似的功能,且已经可用于.NET 3.5。 LINQ to SQL和Entity Framework在很大程度上提供了类似的功能。LINQ to SQL更易于使用,但只能用于访问SQL Server。实体框架是基于提供程序的,并提供了对不同关系数据库的访问。它包含更多的功能,例如多对多映射而不需要映射对象,n对n映射是可能的。 Entity Framework的一个缺点是它的模型类型需要由EntityObject基类派生。将对象映射到关系使用包含XML的EDMX文件完成的。包含的XML由三个模式组成:概念模式定义(CSD)定义具有其属性和关联的对象类型;存储模式定义(SSD)定义数据库表、列和关系;以及映射模式语言(MSL)定义CSD和SSD如何相互映射。

  • Entity Framework 4—Entity Framework 4 在.NET 4中兼容,并且获得了重大改进,其中许多来自LINQ到SQL的想法。由于变化较大,版本2和3已被跳过。这个版本里增加了延迟加载以获取访问属性的关系。在使用SQL数据定义语言(DDL)设计模型之后,可以创建数据库。现在使用Entity Framework的两个模型是Database First或Model First。也许最重要的特性是支持简单对象类(POCO),因此不再需要从基类EntityObject派生。

随着更新(例如Entity Framework 4.1,4.2),NuGet包增加了额外的功能,因此能更快地添加功能。 Entity Framework 4.1提供了Code First模型,其中用于定义映射的EDMX文件不再使用。相反,所有的映射都使用C#代码定义

  • 使用属性或Fluent API来定义的映射。

Entity Framework 4.3增加了对迁移的支持。有了这一点,就可以使用C#代码定义数据库结构的更改。使用数据库从应用程序自动应用数据库更新。

  • Entity Framework 5—Entity Framework 5的NuGet包支持.NET 4.5和.NET 4应用程序。但是,Entity Framework 5的许多功能都可用于.NET 4.5。 Entity Framework仍然基于.NET 4.5在系统上安装的类型。此版本的新增功能是性能改进以及支持新的SQL Server功能,例如空间数据类型。

  • Entity Framework 6—Entity Framework 6解决了Entity Framework 5的一些问题,其中一部分是安装在系统上的框架的一部分,一部分通过NuGet扩展提供。目前Entity Framework的全部代码已移至NuGet包。为了不造成冲突,使用了一个新的命名空间。将应用程序移植到新版本时,必须更改命名空间。

本书讨论Entity Framework的最新版本,Entity Framework Core 1.0。此版本是一个删除旧的行为全面重写,不再支持CSDL,SSDL和MSL的XML文件映射,只支持Code First - 使用Entity Framework 4.1添加的模型。Code First 并不意味着数据库不能先存在。您可以先创建数据库,或者仅从代码中定义数据库,以上两种选项都是可行的。

注意 Code First 这个名称某些程度上让人误会。Code First 先创建代码或先数据库都是可行的。最初Code First的测试版本名称是Code Only。因为其他模型选项在名称中有First,所以“Code Only”的名称也被更改。

Entity Framework 的全面重写不仅支持关系数据库,还支持NoSql数据库 - 只需要一个提供程序。在撰写本文时,提供程序支持有限,但相信会随时间而增加。 

新版本的Entity Framework基于.NET Core,因此在Linux和Mac系统上也可以使用此框架。 

Entity Framework Core 1.0不完全支持Entity Framework 6提供的所有功能。随着时间的推移,Entity Framework的新版本将提供更多功能,留意所使用的Entity Framework的版本。尽管使用Entity Framework 6 很多有力的理由,但在非Windows平台上使用ASP.NET Core 1.0、Entity Framework和通用Windows平台(UWP),以及非关系数据存储,都需要使用Entity Framework Core 1.0。 

本章介绍Entity Framework Core 1.0。从一个简单的模型读取和SQL Server中写入信息开始,稍后会介绍添加关系,在写入数据库时将介绍更改跟踪器和冲突处理。利用迁移创建和修改数据库结构是本章的另一个重要部分。 

注意 本章使用Books数据库,此数据库包含在示例代码的下载包中 www.wrox.com/go/professionalcsharp6. 

本文由时时app平台注册网站发布于编程知识,转载请注明出处:全自动迁移数据库的实现 (Fluent NHibernate, Entity

关键词: