如何以及在何处调用Database.EnsureCreated和Database.Migrate?

bai*_*ndo 59 asp.net asp.net-mvc entity-framework entity-framework-core

我有一个ASP.NET MVC 6应用程序,我需要调用Database.EnsureCreated和Database.Migrate方法.

但我应该在哪里打电话给他们?

Bas*_*ili 71

我认为这是一个重要的问题,应该得到很好的回答!

什么是Database.EnsureCreated?

context.Database.EnsureCreated()是新的EF核心方法,可确保上下文的数据库存在.如果存在,则不采取任何措施.如果它不存在,则创建数据库及其所有模式,并确保它与此上下文的模型兼容.

注意: 此方法不使用迁移来创建数据库.此外,以后无法使用迁移更新创建的数据库.如果要定位关系数据库并使用迁移,则可以使用该DbContext.Database.Migrate()方法确保创建数据库并应用所有迁移.

我们是如何使用EF 6做到的?

context.Database.EnsureCreated() 相当于下面列出的EF 6的方法:

  1. 包管理器控制台:

    启用 - 迁移-EnableAutomaticMigrations.添加迁移/更新的数据库.

  2. 来自代码:

    Database.SetInitializer CreateDatabaseIfNotExists

要么

使用DbMigrationsConfiguration并设置AutomaticMigrationsEnabled = true;

什么是Database.Migrate?

将上下文的任何挂起的迁移应用于数据库.如果数据库尚不存在,将创建它.

我们是如何使用EF 6做到的?

context.Database.Migrate() 相当于下面列出的EF 6的方法:

  1. 包管理器控制台:

    Update-Database -TargetMigration

  2. 使用自定义DbMigrationsConfiguration:

    AutomaticMigrationsEnabled = false; 或者使用DbMigrator.

结论:

如果您正在使用迁移context.Database.Migrate().如果您不想迁移并且只想要一个快速数据库(通常用于测试),那么请使用context.Database.EnsureCreated()/ EnsureDeleted().

  • 您好Bassam Alugili,谢谢您的回答!在我的项目中,我正在使用迁移,我不知道不应该同时使用这两种方法. (2认同)
  • 我在想 `Database.Migrate()` 创建迁移(如果需要),然后在此基础上更新基础。与 EF 6 中的自动迁移类似。但我错了。它仅应用数据库上的现有迁移(如果有)。 (2认同)
  • 你刚刚从未来的灾难中救了我.**荣誉 (2认同)

bai*_*ndo 18

根据James P和Bassam Alugili提供的信息,我最终做的是将这行代码添加到Startup.cs-> Configure方法中.

using (var scope = 
  app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
using (var context = scope.ServiceProvider.GetService<MyDbContext>())
    context.Database.Migrate();
Run Code Online (Sandbox Code Playgroud)

  • 这正是我一直在寻找的。大多数示例使用 .Net core 或 Web,我使用的是 .Net 4.6 的 Windows 窗体应用程序。数据库已创建(因为连接字符串中的用户没有创建数据库的权限)。上面的代码创建了所有表和迁移中的所有内容。 (2认同)

Jam*_*s P 13

正如foreward你应该阅读从罗恩·米勒:

... EnsureCreated完全绕过迁移并只为您创建架构,您不能将其与迁移混合在一起.EnsureCreated用于测试或快速原型设计,您可以在每次删除和重新创建数据库时使用.如果您正在使用迁移并希望在应用启动时自动应用它们,则可以context.Database.Migrate()改为使用.

根据这里的回答,您需要将Globals.EnsureDatabaseCreated();其添加到Startup.cs:

Startup.cs中的启动功能:

public Startup(IHostingEnvironment env)
{
    // Set up configuration sources.
    var builder = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .AddEnvironmentVariables();

    if (env.IsDevelopment())
    {
        // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
            builder.AddApplicationInsightsSettings(developerMode: true);
    }
    Configuration = builder.Build();
    Globals.Configuration = Configuration;
    Globals.HostingEnvironment = env;
    Globals.EnsureDatabaseCreated();
}
Run Code Online (Sandbox Code Playgroud)

并定义Globals.EnsureDatabaseCreated()如下:

public static void EnsureDatabaseCreated()
    {
        var optionsBuilder = new DbContextOptionsBuilder();
        if (HostingEnvironment.IsDevelopment()) optionsBuilder.UseSqlServer(Configuration["Data:dev:DataContext"]);
        else if (HostingEnvironment.IsStaging()) optionsBuilder.UseSqlServer(Configuration["Data:staging:DataContext"]);
        else if (HostingEnvironment.IsProduction()) optionsBuilder.UseSqlServer(Configuration["Data:live:DataContext"]);
        var context = new ApplicationContext(optionsBuilder.Options);
        context.Database.EnsureCreated();

        optionsBuilder = new DbContextOptionsBuilder();
        if (HostingEnvironment.IsDevelopment()) optionsBuilder.UseSqlServer(Configuration["Data:dev:TransientContext"]);
        else if (HostingEnvironment.IsStaging()) optionsBuilder.UseSqlServer(Configuration["Data:staging:TransientContext"]);
        else if (HostingEnvironment.IsProduction()) optionsBuilder.UseSqlServer(Configuration["Data:live:TransientContext"]);
        new TransientContext(optionsBuilder.Options).Database.EnsureCreated();
    }
Run Code Online (Sandbox Code Playgroud)

使用context.Database.Migrate()请看这里这里.

  • 同样,没有看到“全局”。这看起来像是尝试撬开这个的非标准方式 (2认同)

Kyl*_*ley 12

通常,DbContextStartup.ConfigureServices()像这样添加到依赖注入容器中:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Add DbContext to the injection container
        services.AddDbContext<MyDbContext>(options =>
                options.UseSqlServer(
                    this.Configuration.GetConnectionString("DefaultConnection")));
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,theIServiceCollection并不充当服务提供者,并且由于DbContext未在当前作用域 ( )之前向注入容器注册,因此Startup.ConfigureServices我们无法在此处通过依赖注入访问上下文。

Henk Mollema 在此处讨论了在启动期间手动解析服务,但提到...

手动解析服务(又名服务定位器)通常被认为是一种反模式...... [并且] 你应该尽可能避免它。

Henk 还提到Startup构造函数的依赖注入非常有限,不包括在 中配置的服务Startup.ConfigureServices(),因此通过应用程序其余部分使用的注入容器使用 DbContext 是最简单和最合适的。

运行时的托管服务提供者可以将某些服务注入到Startup类的构造函数中,例如IConfigurationIWebHostEnvironmentIHostingEnvironment在 3.0 之前的版本中)ILoggerFactoryIServiceProvider。请注意,后者是由托管层构建的实例,仅包含用于启动应用程序的基本服务。

为了调用Database.EnsureCreated()or Database.Migrate(),我们可以并且希望在 中自动解析 DbContext Startup.Configure(),我们配置的服务现在可以通过 DI 获得:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Add DbContext to the injection container
        services.AddDbContext<MyDbContext>(options =>
                options.UseSqlServer(
                    this.Configuration.GetConnectionString("DefaultConnection")));
    }

    public static void Configure(IApplicationBuilder app, IWebHostEnvironment env, MyDbContext context)
    {
        if (env.IsDevelopment())
        {
            context.Database.EnsureCreated();
            //context.Database.Migrate();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请记得巴萨姆Alugili的答案从EF核心文档引用了Database.EnsureCreated()Database.Migrate()并不意味着一起使用,因为一个确保现有迁移应用到数据库,这是必要时创建的。另一个只是确保数据库存在,如果不存在,则创建一个反映您的DbContext,包括在上下文中通过 Fluent API 完成的任何种子。

  • 我发现在内存数据库中播种的最优雅的方法,谢谢 (2认同)

小智 11

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<YourDbContext>(option => option.UseSqlServer(@"Data source=(localdb)\ProjectModels;Initial Catalog=YourDb;Integrated Security=True"));

var app = builder.Build();

// Configure the HTTP request pipeline.
YourDbContext dbcontext = app.Services.GetRequiredService<YourDbContext>();
dbcontext.Database.EnsureCreated();
Run Code Online (Sandbox Code Playgroud)

  • 我正在运行 .NET 6,并且收到“无法从根提供程序解析作用域服务‘CosmosTest.EfCore.CosmosContext’” (2认同)
  • @XWIKO我遇到了同样的问题,你必须首先创建一个范围(`using var range = app.Services.CreateScope()`),然后通过它获取所需的服务(`var db = scope.ServiceProvider.GetRequiredService&lt; MyDbContext&gt;()`): [来源](/sf/answers/5071322861/) (2认同)

小智 6

构造函数

public class AppDbContext : DbContext
{
    // Properties with entities of 
    // public DbSet<User> Users { get; set; }
    // public DbSet<Cart> Carts { get; set; }
    // public DbSet<CartItem> CartItems { get; set; }
    // public DbSet<Product> Products { get; set; }
    // ...
    
    public AppDbContext()
    {
        Database.Migrate();
        // Or Database.EnsureCreated();
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Model configuration
        // modelBuilder.Entity<CartItem>().HasKey(x => new { x.CartId, x.ProductId })

        base.OnModelCreating(modelBuilder);
    }
}
Run Code Online (Sandbox Code Playgroud)

优点

  • 这是最简单的调用Database.Migrate或 的地方Database.EnsureCreated
  • 这些方法保证在您开始使用之前被调用AppDbContext

缺点

  • 您不能使用这些方法的异步变体。在不接受使用线程阻塞操作的环境中,这是一个大问题。(例如单元测试或 UI 线程)。
  • 每次实例化时都会调用这些方法AppDbContext
  • 如果数据库模型、连接字符串甚至 DBMS 出现问题,您只能在实例化时才能发现它。对于 ASP.NET Core,它可能是对依赖于AppDbContext.

启动

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Service configurations

var app = builder.Build();
try 
{
    using (var serviceScope = app.Services.CreateScope())
    {
        var dbContext = serviceScope.ServiceProvider.GetRequiredService<AppDbContext>();
        await dbContext.Database.MigrateAsync();
        // or dbContext.Database.EnsureCreatedAsync();
    }
    
    // Middleware pipeline configuration

    app.Run();
} 
catch (Exception e) 
{
    app.Logger.LogCritical(e, "An exception occurred during the service startup");
}
finally
{
    // Flush logs or else you lose very important exception logs.
    // if you use Serilog you can do it via
    // await Log.CloseAndFlushAsync();
}
Run Code Online (Sandbox Code Playgroud)

优点

  • 这些方法保证在应用程序启动之前被调用。
  • 该方法被调用一次。
  • 您可以使用异步变量。
  • 如果出现问题,您会在应用程序启动之前发现它。

缺点

  • 如果您有一个需要花费大量时间才能应用的大型迁移,则应用程序可能会超过可在运行该应用程序的环境中使用的启动超时。

    如果发生这种情况,您可以暂时增加应用迁移的超时并将其恢复为默认值。


托管服务

// InitializationService.cs
public sealed class InitializationService : IHostedService
{
    #region Constructor and dependencies

    private readonly IServiceProvider _serviceProvider;
    private readonly Options _options;

    public InitializationService(IServiceProvider serviceProvider, IOptions<Options> options)
    {
        _serviceProvider = serviceProvider;
        _options = options.Value;
    }

    #endregion

    public async Task StartAsync(
        // Use this token to detect the application stopping
        CancellationToken cancellationToken
    )
    {
        using var serviceScope = _serviceProvider.CreateScope();
        var serviceProvider = serviceScope.ServiceProvider;

        if (!_options.SkipMigration)
        {
            var context = serviceProvider.GetRequiredService<AppDbContext>();

            await context.Database.MigrateAsync(cancellationToken);
        }

        // ... Other initialization logic of the application. (e.g. a seeding of an initial data)
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    public sealed class Options
    {
        public const string Position = "Initialization";

        public bool SkipMigration { get; set; }

        // ... Other options for initialization service
    }
}

public static class InitializationServiceExtensions
{
    public static void AddInitializationService(this IServiceCollection serviceCollection)
    {
        serviceCollection.AddHostedService<InitializationService>();

        serviceCollection
            .AddOptions<InitializationService.Options>()
            .BindConfiguration(InitializationService.Options.Position);
    }
}
Run Code Online (Sandbox Code Playgroud)

优点

  • 这些方法保证在应用程序启动之前被调用。
  • 该方法被调用一次。
  • 您可以使用异步变量。
  • 如果出现问题,您会在应用程序启动之前发现它。
  • 即使在初始化期间,您也可以正常关闭应用程序。

缺点

  • 您不能在之后立即使用数据库var app = builder.Build();

    当我尝试使用 Hangfire 仪表板并在 Hangfire 和 EF Core 之间共享连接字符串时,我遇到了这个问题。Hangfire 仪表板至少需要创建一个数据库。

    我通过以下代码解决了这个问题:

    /// <remarks>
    /// It cannot be done in <see cref="InitializationService"/>
    /// because Hangfire requires that db already has been created
    /// before Hangfire dashboard is configured
    /// </remarks>
    public static async Task CreateEmptyDbIfNotExists(WebApplication app)
    {
        using var serviceScope = app.Services.CreateScope();
        var serviceProvider = serviceScope.ServiceProvider;
    
        var skipMigration = serviceProvider
            .GetRequiredService<IOptions<InitializationService.Options>>()
            .Value.SkipMigration;
    
        if (skipMigration)
            return;
    
        var context = serviceProvider.GetRequiredService<AppDbContext>();
    
        var databaseCreator = context.GetService<IDatabaseCreator>() as RelationalDatabaseCreator;
        if (!await databaseCreator.ExistsAsync())
            await databaseCreator.CreateAsync();
    }
    
    Run Code Online (Sandbox Code Playgroud)