11. 迁移

可以在现有的数据库中使用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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值