可以在现有的数据库中使用EF Core(称为"数据库优先")。在许多使用EF Core的场景中,数据库已经存在。数据库的模型类更新独立于应用程序,并且在数据库更改完成后更新应用程序。在这种情况下,EF Core迁移没有多大帮助。
如果使用应用程序创建数据库,EF Core迁移将非常有用。更改代码模型时,可以自动更新数据库。如果客户都有自己的数据库,并且使用应用程序的更新版本更改数据库模式,那么使用旧的应用程序版本更新客户可能是一个挑战。EF Core迁移可以解决这个问题:通过迁移,可以轻松地从版本x升级到版本y。数据库的当前版本是从数据库中读取的,而迁移则包含了升级到最新版本的每一步所需的信息。还可以升级或降级到特定的版本。
有不同的选项来升级数据库。使用升级命令可以直接从应用程序迁移。还可以使用dotnet命令从命令行上更新数据库。另一个选项是创建一个SQL Server脚本,数据库管理员可以使用该脚本更新数据库。
显示迁移的示例应用程序存在于.NET标准库和.NET Core Web应用程序中。通常,数据访问代码是在库中实现的,需要一些额外的命令行选项来处理这个问题,这就是为什么在这样的场景中演示迁移的原因。
1. 准备项目文件
下面从.NET 标准 2.0 库开始。这个库包含定义模型的Menu和MenuCard类、实现接口IEntityTypeConfiguration来配置对应模型类型的映射的MenuConfiguration和MenuCardConfiguration类,以及上下文类MenusContext。
项目文件需要准备的不仅仅是引用NuGet包Microsoft.EntityFrameworkCore和Microsoft.EntityFrameworkCore.SqlServer,还需要EF Core工具扩展以使用dotnet命令行,这是一个引用Microsoft.EntityFrameworkCore.Tools.Dotnet的工具:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.1" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.Dotnet"
Version="2.0.0"/>
</ItemGroup>
</Project>
为了使用Web应用程序中的上下文类(使用依赖注入),实现MenusContext,并需要DbContextOptions类型作为参数的构造函数,以使用依赖注入:
public class MenusContext : DbContext
{
public MenusContext(DbContextOptions<MenusContext> options)
: base(options) { }
public DbSet<Menu> Menus { get; set; }
public DbSet<MenuCard> MenuCards { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.HasDefaultSchema("mc");
modelBuilder.ApplyConfiguration(new MenusConfiguration());
modelBuilder.ApplyConfiguration(new MenuCardConfiguration());
}
}
public class MenusConfiguration : IEntityTypeConfiguration<Menu>
{
public void Configure(EntityTypeBuilder<Menu> builder)
{
builder.ToTable("Menus")
.HasKey(m => m.MenuId);
builder.Property(m => m.MenuId)
.ValueGeneratedOnAdd();
builder.Property(m => m.Text)
.HasMaxLength(120);
builder.Property(m => m.Price)
.HasColumnType("Money");
builder.HasOne(m => m.MenuCard)
.WithMany(m => m.Menus)
.HasForeignKey(m => m.MenuCardId);
}
}
public class MenuCardConfiguration : IEntityTypeConfiguration<MenuCard>
{
public void Configure(EntityTypeBuilder<MenuCard> builder)
{
builder.ToTable("MenuCards")
.HasKey(m => m.MenuCardId);
builder.Property(m => m.MenuCardId)
.ValueGeneratedOnAdd();
builder.Property(m => m.Title)
.HasMaxLength(50);
builder.HasMany(m => m.Menus)
.WithOne(m => m.MenuCard);
}
}
2. 利用ASP.NET Core MVC 托管应用程序
在新的ASP.NET Core MVC Web应用程序中,使用Microsoft.Entensions.DependencyInjection的依赖注入已经构建到模版中。要启用迁移,只需要使用扩展方法AddDbContext添加EF Core DB上下文,并使用UseSqlServer配置SQL Server。需要在配置文件中配置到数据库的连接字符串。这样,就可以使用命令进行迁移:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddDbContext<MenusContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MenusConnection")));
}
3. 托管.NET Core控制台应用程序
如果EF Core DB上下文没有使用依赖注入,就可以使用配置了工具的上下文类。在这种情况下,可以使用上下文的OnConfiguration方法检索连接字符串。使用依赖注入,连接字符串从外部注入。ASP.NET Core在访问Web应用程序的Main()方法,以通过依赖注入容器获取连接字符串方面有工具的特殊支持。控制台应用程序(以及UWP、WPF和Xamarin应用程序)中的Main()方法看起来有所不同。在这里,需要实现一个工厂类,通过实现接口IDesignTimeDbContextFactory返回DbContext。当访问程序集时,在.NET Core工具中自动检测到实现该接口的类。这个接口定义的CreateDbContext方法需要返回一个已配置的上下文:
public class MenusContextFactory : IDesignTimeDbContextFactory<MenusContext>
{
private const string ConnectionString =
@"server=(locadb)\MSSQLLocalDb;database=ProCSharpMigrations;trusted_connection=true";
public MenusContext CreateDbContext(string[] args)
{
var optionssBuilder = new DbContextOptionsBuilder<MenusContext>();
optionssBuilder.UseSqlServer(ConnectionString);
return new MenusContext(optionssBuilder.Options);
}
}
.NET Core工具使用的控制台应用程序的项目文件需要引用NuGet包Microsoft.EntityFrameworkCore.Design。该工具只需要设计库,不需要从调用应用程序中引用,这就是为什么可以设置PrivateAssets特性的原因(运行示例,发现设置PrivateAssets="All"后,迁移没有引发异常,但新创建的迁移类相关方法Up()、Down()内容为空。应用迁移后,数据库表中也没有更新内容):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MigrationsLib\MigrationsLib.csproj" />
</ItemGroup>
</Project>
4. 创建迁移
有了这些,就可以创建一个初始迁移。使用以下命令,当前目录必须是库的目录——定义dotnet工具引用的目录。以下命令创建名为InitMenus的初始迁移。使用选项--startup-project引用的启动项目包含初始代码,其中包括到服务器的连接字符串——使用从ASP.NET Core Web应用程序的默认项目模版中生成的Main()方法,或实现IDesignTimeContextFactory的对象,如前一节所示:
> dotnet ef migrations add InitMenus --startup-project ...\MigrationsConsoleApp
如果项目包含多个DbContext,就需要提供附加选项--context,并提供DbContext类的名称。
运行此命令,会创建一个带有快照的Migrations文件夹,以基于模型创建完整的数据库模式:
[DbContext(typeof(MenusContext))]
partial class MenusContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("mc")
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("MigrationsLib.Menu", b =>
{
b.Property<int>("MenuId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("Allergens");
b.Property<string>("Content");
b.Property<int>("MenuCardId");
b.Property<decimal>("Price")
.HasColumnType("Money");
b.Property<string>("Text")
.HasMaxLength(120);
b.HasKey("MenuId");
b.HasIndex("MenuCardId");
b.ToTable("Menus");
});
modelBuilder.Entity("MigrationsLib.MenuCard", b =>
{
b.Property<int>("MenuCardId")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("Title")
.HasMaxLength(50);
b.HasKey("MenuCardId");
b.ToTable("MenuCards");
});
modelBuilder.Entity("MigrationsLib.Menu", b =>
{
b.HasOne("MigrationsLib.MenuCard", "MenuCard")
.WithMany("Menus")
.HasForeignKey("MenuCardId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
对于每次迁移,都会创建一个从基类Migration派生的迁移类。这个基类定义了Up和Down方法,以允许将迁移应用到这个迁移版本中,或者向后一部:
public partial class InitMenus : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "mc");
migrationBuilder.CreateTable(
name: "MenuCards",
schema: "mc",
columns: table => new
{
MenuCardId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Title = table.Column<string>(maxLength: 50, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MenuCards", x => x.MenuCardId);
});
migrationBuilder.CreateTable(
name: "Menus",
schema: "mc",
columns: table => new
{
MenuId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Text = table.Column<string>(maxLength: 120, nullable: true),
Price = table.Column<decimal>(type: "Money", nullable: false),
MenuCardId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Menus", x => x.MenuId);
table.ForeignKey(
name: "FK_Menus_MenuCards_MenuCardId",
column: x => x.MenuCardId,
principalSchema: "mc",
principalTable: "MenuCards",
principalColumn: "MenuCardId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Menus_MenuCardId",
schema: "mc",
table: "Menus",
column: "MenuCardId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Menus",
schema: "mc");
migrationBuilder.DropTable(
name: "MenuCards",
schema: "mc");
}
}
在对模型进行更改后,例如向Menu类添加Allergens:
public class Menu
{
public int MenuId { get; set; }
public string Text { get; set; }
public decimal Price { get; set; }
public string Allergens { get; set; }
public int MenuCardId { get; set; }
public MenuCard MenuCard{get;set;}
public override string ToString() => Text;
}
就需要一个新迁移:
> dotnet ef migrations add AddAllergens --startup-project ...\MigrationsConsoleApp
使用新的迁移,将更新快照类,以显示当前状态,并添加新的Migrations类型,以使用Up和Down方法添加和删除Allergens列:
public partial class AddAllergens : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Allergens",
schema: "mc",
table: "Menus",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Allergens",
schema: "mc",
table: "Menus");
}
}
在应用新的迁移之前,请注意构建库。否则,新的迁移就可能是空的。
注意:
对于所做的每一个更改,都可以创建另一个迁移。新的迁移只定义了从旧版本到新版本所需的更改。如果客户的数据库需要从任何早期版本更新,那么在迁移数据库时将调用必要的迁移。
在开发过程中,可能会出现许多生产中不需要的迁移。只需要为可能在客户站点上运行的所有版本保留迁移。要从开发时删除迁移,可以调用dotnet ef migrations remove来删除最新的迁移代码。然后添加新的较大迁移,其中包含自上次迁移以来的所有更改。
5. 以编程方式应用迁移
配置后迁移后,可以直接在应用程序中启动数据库的迁移过程。为此,控制台应用程序配置为使用依赖注入容器来检索DbContext,然后调用Database属性的Migrate(或MigrateAsync)方法:
class Program
{
static async Task Main(string[] args)
{
RegisterServices();
//await Container.GetService<MenusContext>().Database.EnsureCreatedAsync();
var context = Container.GetService<MenusContext>();
await context.Database.MigrateAsync();
var result = context.Database.GetMigrations();
foreach (var item in result)
{
Console.WriteLine(item);
}
}
private const string ConnectionString =
@"server=(localdb)\MSSQLLocalDb;database=ProCSharpMigrations;trusted_connection=true";
private static void RegisterServices()
{
var service = new ServiceCollection();
service.AddDbContext<MenusContext>(options =>
options.UseSqlServer(ConnectionString));
Container = service.BuildServiceProvider();
}
public static IServiceProvider Container { get; set; }
}
如果数据库还不存在,那么MigrateAsync方法将创建数据库——使用模型定义的模式——以及一个_EFMigrationsHistory表,该表列出了已应用到数据库的所有迁移。不能像前面那样使用EnsureCreatedAsync方法来创建数据库,因为该方法不向数据库应用迁移信息。
使用现有的数据库,数据库将更新到迁移的当前版本。通过编程,可以使用GetMigrations方法在应用程序中获得所有可用的迁移。要查看所有应用的迁移,可以使用GetAppliedMigrations方法。对于数据库中丢失的所有迁移,请使用GetPendingMigrations方法。
6. 应用迁移的其他方法
除了以编程方式应用迁移之外,还可以使用命令行来应用迁移:
> dotnet ef database update --startup-project ...\MigrationsConsoleApp
该命令将最新的迁移应用到数据库。还可以向该命令提供迁移的名称,以便将数据库放到迁移的特定版本中。
如果数据库管理员需要对数据库进行完全控制,且不允许进行编程更改,不允许使用.NET Core CLI命令行之类的工具进行任何更改,就可以创建一个SQL脚本,并将其提交或自己使用。
下面的命令行创建SQL脚本migrationsscript.sql,其中包括从最初创建的数据库到最近的迁移。还可以为脚本中应该应用的迁移范围提供特定的from/to值:
> dotnet ef migrations script --output migrationsscript.sql --startup-project ...\MigrationsConsoleApp
创建的SQL脚本:
IF OBJECT_ID(N'[__EFMigrationsHistory]') IS NULL
BEGIN
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
END;
GO
IF SCHEMA_ID(N'mc') IS NULL EXEC(N'CREATE SCHEMA [mc];');
GO
CREATE TABLE [mc].[MenuCards] (
[MenuCardId] int NOT NULL IDENTITY,
[IsDeleted] bit NOT NULL,
[LastUpdated] datetime2 NOT NULL,
[Title] nvarchar(50) NULL,
CONSTRAINT [PK_MenuCards] PRIMARY KEY ([MenuCardId])
);
GO
CREATE TABLE [mc].[Menus] (
[MenuId] int NOT NULL IDENTITY,
[MenuCardId] int NOT NULL,
[Price] Money NOT NULL,
[Text] nvarchar(120) NULL,
CONSTRAINT [PK_Menus] PRIMARY KEY ([MenuId]),
CONSTRAINT [FK_Menus_MenuCards_MenuCardId] FOREIGN KEY ([MenuCardId]) REFERENCES [mc].[MenuCards] ([MenuCardId]) ON DELETE CASCADE
);
GO
CREATE INDEX [IX_Menus_MenuCardId] ON [mc].[Menus] ([MenuCardId]);
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20171102201121_InitialMenus', N'2.1.1-rtm-30846');
GO
ALTER TABLE [mc].[Menus] ADD [Allergens] nvarchar(max) NULL;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20171102212604_AddAllergens', N'2.1.1-rtm-30846');
GO