You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Testcontainers启动SQL Server容器但无法创建数据库与执行迁移

SQL Server Testcontainers 无法创建数据库及执行EF迁移的解决方法

问题现象

使用Testcontainers启动SQL Server容器做集成测试时,容器能正常启动,但无法自动创建数据库及执行EF Core迁移。手动创建数据库后,迁移操作仍然失败。

代码问题分析

  1. 未等待SQL Server完全就绪:启动容器后直接执行数据库创建和迁移,此时SQL Server可能还未完成初始化
  2. 迁移时未指定目标数据库:原ApplyMigrations方法使用连接master库的字符串,导致迁移不会作用到新建的NewDb
  3. 数据库创建后未等待初始化完成:SQL Server创建数据库后需要短暂时间完成启动,直接迁移可能失败
  4. 不必要的端口绑定:硬绑定1433端口可能导致本地端口冲突,Testcontainers会自动处理端口映射

修复后的代码

public class CustomApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
    private const string Database = "NewDb";
    private readonly MsSqlContainer _dbContainer;

    public CustomApiFactory()
    {
        _dbContainer = new MsSqlBuilder()
            .WithImage("mcr.microsoft.com/mssql/server:2022-CU10-ubuntu-22.04")
            .WithPassword("Password123")
            .WithCleanUp(true)
            .Build();
    }

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();
        
        // 先等待SQL Server服务完全就绪
        var masterConnectionString = _dbContainer.GetConnectionString();
        await WaitForSqlServerToBeReady(masterConnectionString);
        
        // 创建目标数据库
        await CreateDatabaseIfNotExists(masterConnectionString);
        
        // 使用目标数据库的连接字符串执行迁移
        var targetConnectionString = $"{masterConnectionString};Database={Database}";
        // 等待新数据库初始化完成
        await WaitForSqlServerToBeReady(targetConnectionString);
        await ApplyMigrations(targetConnectionString);
    }

    public new async Task DisposeAsync()
    {
        await _dbContainer.StopAsync();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Test");
        builder.ConfigureServices(services =>
        {
            services.AddTestAuthentication();
        });

        builder.ConfigureLogging(logging =>
        {
            logging.ClearProviders();
        });

        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll(typeof(DbContextOptions<ApplicationContext>));
            services.AddDbContext<ApplicationContext>(opts =>
            {
                opts.UseSqlServer($"{_dbContainer.GetConnectionString()};Database={Database}");
            });
        });
    }

    private async Task CreateDatabaseIfNotExists(string masterConnectionString)
    {
        using (var conn = new SqlConnection(masterConnectionString))
        {
            await conn.OpenAsync();
            var cmd = conn.CreateCommand();
            // 使用参数化查询避免SQL注入风险
            cmd.CommandText = "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = @dbName) CREATE DATABASE [@dbName]";
            cmd.Parameters.AddWithValue("@dbName", Database);
            await cmd.ExecuteNonQueryAsync();
        }
    }

    private async Task WaitForSqlServerToBeReady(string connectionString)
    {
        var retryCount = 10;

        for (int i = 0; i < retryCount; i++)
        {
            try
            {
                using (var conn = new SqlConnection(connectionString))
                {
                    await conn.OpenAsync();
                    // 执行简单查询验证数据库是否可用
                    using var cmd = conn.CreateCommand();
                    cmd.CommandText = "SELECT 1";
                    await cmd.ExecuteScalarAsync();
                    Console.WriteLine($"数据库连接就绪: {connectionString}");
                    return;
                }
            }
            catch (SqlException)
            {
                Console.WriteLine("数据库未就绪,5秒后重试...");
                await Task.Delay(5000);
            }
        }

        throw new Exception("数据库超时未就绪");
    }

    private async Task ApplyMigrations(string targetConnectionString)
    {
        var optionsBuilder = new DbContextOptionsBuilder<ApplicationContext>();
        optionsBuilder.UseSqlServer(targetConnectionString);

        using (var context = new ApplicationContext(optionsBuilder.Options))
        {
            try
            {
                await context.Database.MigrateAsync();
                Console.WriteLine("迁移执行成功");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"迁移失败: {ex.Message}");
                throw;
            }
        }
    }
}

关键修改说明

  • 添加服务就绪等待:启动容器后先等待SQL Server服务可用,再执行后续操作
  • 迁移指定目标数据库:迁移时使用包含NewDb的连接字符串,确保迁移作用到目标库
  • 数据库创建后等待:新建数据库后再次调用等待方法,确保数据库完成初始化
  • 移除硬绑定端口:删除WithPortBinding(1433),避免端口冲突
  • SQL注入防护:创建数据库时使用参数化查询,替代字符串拼接

Docker日志说明

从日志可以看到NewDb已被成功创建并启动,但之前的迁移未作用到该库,修复后迁移会正确在NewDb中创建Schema和表结构。

内容的提问来源于stack exchange,提问作者Leviathan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 23:35:55