在实体框架中重复创建和删除数据库

Posted

技术标签:

【中文标题】在实体框架中重复创建和删除数据库【英文标题】:Repeatedly creating and deleting databases in Entity Framework 【发布时间】:2015-03-28 16:29:03 【问题描述】:

在为我们的应用程序编写一些单元测试时,我偶然发现了 EF6 中的一些奇怪行为(用 6.1 和 6.1.2 测试):显然不可能在相同的应用程序上下文。

测试设置:

public class A

    public int Id  get; set; 
    public string Name  get; set; 


class AMap : EntityTypeConfiguration<A>

    public AMap()
    
        HasKey(a => a.Id);
        Property(a => a.Name).IsRequired().IsMaxLength().HasColumnName("Name");
        Property(a => a.Id).HasColumnName("ID");
    


public class SomeContext : DbContext

    public SomeContext(DbConnection connection, bool ownsConnection) : base(connection, ownsConnection)
    

    

    public DbSet<A> As  get; set; 

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    
        base.OnModelCreating(modelBuilder);
        modelBuilder.Configurations.Add(new AMap());
    


[TestFixture]
public class BasicTest

    private readonly HashSet<string> m_databases = new HashSet<string>();

    #region SetUp/TearDown

    [TestFixtureSetUp]
    public void SetUp()
    
        System.Data.Entity.Database.SetInitializer(
            new CreateDatabaseIfNotExists<SomeContext>());
    


    [TestFixtureTearDown]
    public void TearDown()
    
        foreach (var database in m_databases)
        
            if (!string.IsNullOrWhiteSpace(database))
                DeleteDatabase(database);
        
    

    #endregion


    [Test]
    public void RepeatedCreateDeleteSameName()
    
        var dbName = Guid.NewGuid().ToString();
        m_databases.Add(dbName);
        for (int i = 0; i < 2; i++)
        
            Assert.IsTrue(CreateDatabase(dbName), "failed to create database");
            Assert.IsTrue(DeleteDatabase(dbName), "failed to delete database");
        

        Console.WriteLine();
    

    [Test]
    public void RepeatedCreateDeleteDifferentName()
    
        for (int i = 0; i < 2; i++)
        
            var dbName = Guid.NewGuid().ToString();
            if (m_databases.Add(dbName))
            
                Assert.IsTrue(CreateDatabase(dbName), "failed to create database");
                Assert.IsTrue(DeleteDatabase(dbName), "failed to delete database");
            
        

        Console.WriteLine();
    

    [Test]
    public void RepeatedCreateDeleteReuseName()
    
        var testDatabases = new HashSet<string>();
        for (int i = 0; i < 3; i++)
        
            var dbName = Guid.NewGuid().ToString();
            if (m_databases.Add(dbName))
            
                testDatabases.Add(dbName);
                Assert.IsTrue(CreateDatabase(dbName), "failed to create database");
                Assert.IsTrue(DeleteDatabase(dbName), "failed to delete database");
            
        
        var repeatName = testDatabases.OrderBy(n => n).FirstOrDefault();
        Assert.IsTrue(CreateDatabase(repeatName), "failed to create database");
        Assert.IsTrue(DeleteDatabase(repeatName), "failed to delete database");

        Console.WriteLine();
    

    #region Helpers

    private static bool CreateDatabase(string databaseName)
    
        Console.Write("creating database '" + databaseName + "'...");
        using (var connection = CreateConnection(CreateConnectionString(databaseName)))
        
            using (var context = new SomeContext(connection, false))
            
                var a = context.As.ToList(); // CompatibleWithModel must not be the first call
                var result = context.Database.CompatibleWithModel(false);
                Console.WriteLine(result ? "DONE" : "FAIL");
                return result;
            
        
    


    private static bool DeleteDatabase(string databaseName)
    
        using (var connection = CreateConnection(CreateConnectionString(databaseName)))
        
            if (System.Data.Entity.Database.Exists(connection))
            
                Console.Write("deleting database '" + databaseName + "'...");
                var result = System.Data.Entity.Database.Delete(connection);
                Console.WriteLine(result ? "DONE" : "FAIL");
                return result;
            
            return true;
        
    

    private static DbConnection CreateConnection(string connectionString)
    
        return new SqlConnection(connectionString);
    

    private static string CreateConnectionString(string databaseName)
    
        var builder = new SqlConnectionStringBuilder
        
            DataSource = "server",
            InitialCatalog = databaseName,
            IntegratedSecurity = false,
            MultipleActiveResultSets = false,
            PersistSecurityInfo = true,
            UserID = "username",
            Password = "password"
        ;
        return builder.ConnectionString;
    

    #endregion


RepeatedCreateDeleteDifferentName 成功完成,其他两个失败。据此,您不能创建一个以前已经使用过的同名数据库。第二次尝试创建数据库时,测试(和应用程序)抛出 SqlException,指出登录失败。这是 Entity Framework 中的错误还是故意的这种行为(有什么解释)?

我在 Ms SqlServer 2012 和 Express 2014 上对此进行了测试,尚未在 Oracle 上进行测试。 顺便说一句:EF 似乎对 CompatibleWithModel 是对数据库的第一次调用存在问题。

更新: 在 EF 错误跟踪器上提交了一个问题 (link)

【问题讨论】:

你的助手都是静态方法有什么原因吗? @timothyclifford 也许是因为它们不依赖任何状态,所以可以将它们标记为静态?你有什么理由反对静态方法? 在单元测试方面,通常只是不喜欢静态类/方法,但这只是个人喜好:) 看代码看起来不错,我会自己做一些测试,看看我能不能了解更多 我承认,静态助手在这里没有多大意义,尽管它们不会造成伤害 ;-) 在我们的单元测试套件中,它比这个例子更广泛;-),我们使用一个帮助类来提供集中的数据库访问。这有助于在不同的测试数据库之间切换,甚至是提供者(我们针对 MsSql、Oracle、SqlCE 进行测试)。我承认把这个例子放在一起有点草率。 【参考方案1】:

数据库初始化程序在每个 AppDomain 的每个上下文中仅运行一次。因此,如果您在某个任意点删除数据库,它们将不会自动重新运行并重新创建数据库。您可以使用DbContext.Database.Initialize(force: true) 强制初始化程序再次运行。

【讨论】:

谢谢,解决了我的问题。我不知道我怎么会错过。继续与 EF 合作!但似乎仍然存在对连接字符串的依赖性,因为数据库创建确实在相同的 AppDomain 中使用相同的上下文但不同的数据库名称。尽管如此,它现在就像一个魅力。干杯:)【参考方案2】:

几天前,我编写了包括通过 EF6 访问数据库的集成测试。为此,我必须在每个测试用例上创建和删除一个 LocalDB 数据库,它对我有用。

我没有使用 EF6 数据库初始化程序功能,而是在 this post 的帮助下执行了一个 DROP/CREATE DATABASE 脚本 - 我在此处复制了示例:

using (var conn = new SqlConnection(@"Data Source=(LocalDb)\v11.0;Initial Catalog=Master;Integrated Security=True"))
 
    conn.Open();
    var cmd = new SqlCommand();
    cmd.Connection = conn;
    cmd.CommandText =  string.Format(@"
        IF EXISTS(SELECT * FROM sys.databases WHERE name='0')
        BEGIN
            ALTER DATABASE [0]
            SET SINGLE_USER
            WITH ROLLBACK IMMEDIATE
            DROP DATABASE [0]
        END

        DECLARE @FILENAME AS VARCHAR(255)

        SET @FILENAME = CONVERT(VARCHAR(255), SERVERPROPERTY('instancedefaultdatapath')) + '0';

        EXEC ('CREATE DATABASE [0] ON PRIMARY 
            (NAME = [0], 
            FILENAME =''' + @FILENAME + ''', 
            SIZE = 25MB, 
            MAXSIZE = 50MB, 
            FILEGROWTH = 5MB )')", 
        databaseName);

    cmd.ExecuteNonQuery();

以下代码负责根据模型创建数据库对象:

var script = objectContext.CreateDatabaseScript();

using ( var command = connection.CreateCommand() )

    command.CommandType = CommandType.Text;
    command.CommandText = script;

    connection.Open();
    command.ExecuteNonQuery();

测试之间无需更改数据库名称。

【讨论】:

感谢您的回答。您的解决方法版本应该可以工作,事实上,我已经为我的应用程序实现了类似的东西。但是,这并不能真正回答我的问题。在我看来,强迫开发人员编写和执行硬编码 SQL 脚本的 ORM 并没有起到作用。当然,出于性能原因,您可以这样做,但这不是必需的。特别是如果框架提供了旨在完成这项工作的功能...... 第 2 部分:在我对此的研究中,我发现了各种需要的解决方案,例如将数据库设置为单用户模式以终止现有连接,或调用 ClearAllPools。但真正的问题不在于删除数据库,而在于(重新)创建它们。无法创建(不再)不存在的数据库让我感到困惑。如前所述,我不仅在 MsSQL 上使用 EF,还在 Oracle 和 SqlCE 上使用 EF(并且在同一应用程序上下文中)... 第 3 部分:所以我真的很想尽可能地使用 EF 功能,而不必为每个数据库提供程序、每个版本……等等而回退到 SQL 脚本。 该死的评论限制 好的,我明白了。你不是在寻找一种让它发挥作用的方法,而是在寻找正确的方法。是的,EF 不擅长层抽象,有时我认为抽象不在它的设计目标之列。这正是我目前面临的问题。我目前的解决方案是为业务逻辑提供一个 API,该 API 完全从底层 ORM 中抽象出来。然后,为特定的 ORM 提供一个适配层。任何依赖于 EF 的东西都驻留在适配层中。这是另一个例子:http://***.com/a/28403546/4544845

以上是关于在实体框架中重复创建和删除数据库的主要内容,如果未能解决你的问题,请参考以下文章

从实体框架中删除重复的 Context.vb 文件

如何在实体框架中循环创建和删除记录

数据存储区 - 如果您删除一个实体,将来是不是可以在新创建的实体上重复使用它的实体 ID?

实体框架 6 和 SQLite - 无法创建条目 PK 之前删除的条目

实体框架手动删除的表不能从EF迁移生成

如何使用实体框架代码优先从数据库中删除所有相关实体