EntityFramework代码优先自定义连接字符串和迁移。

24
当我使用从app.config中读取的默认连接字符串创建上下文时,数据库被创建并且迁移工作正常——基本上一切都是正确的。然而,当程序化地创建连接字符串(使用SqlConnectionStringBuilder)时:
  • 如果不存在数据库(场景A),则不会创建数据库;
  • CreateDbIfNotExists()会创建数据库模型的最新版本,但迁移机制未被调用(场景B)。
在情况A中,当我希望访问数据库时,会抛出异常,因为很明显数据库不存在。在情况B中,数据库被正确创建,但迁移机制没有被调用,就像标准连接字符串的情况一样。 app.config: "Data Source=localhost\\SQLEXPRESS;Initial Catalog=Db13;User ID=xxx;Password=xxx" builder:
sqlBuilder.DataSource = x.DbHost;
sqlBuilder.InitialCatalog = x.DbName;
sqlBuilder.UserID = x.DbUser;
sqlBuilder.Password = x.DbPassword;

初始化器:

Database.SetInitializer(
    new MigrateDatabaseToLatestVersion<
        MyContext,
        Migrations.Configuration
    >()
);

规格: 实体框架(Entity Framework):5.0,数据库(DB):SQL Server Express 2008


我正在为一个类似的问题苦苦挣扎,我已经在https://dev59.com/NnDYa4cB1Zd3GeqPBo9t上提问了。 - Kirsten
7个回答

19

如果您的迁移不正常工作,尝试在DbContext构造函数中设置Database.Initialize(true)

public CustomContext(DbConnection connection)
: base(connection, true)    
{    
        Database.Initialize(true);    
}    

我也有类似的迁移问题。在我的解决方案中,我必须始终在构造函数中设置数据库初始化器,如下所示

public CustomContext(DbConnection connection)
: base(connection, true)    
{    
        Database.SetInitializer(new CustomInitializer());
        Database.Initialize(true);    
}    

在自定义的初始化器中,你需要实现InitalizeDatabase(CustomContex context)方法,例如:

class CustomInitializer : IDatabaseInitializer<CustomContext>
{
    public void InitializeDatabase(CustomContext context)
    {
        if (!context.Database.Exists || !context.Database.CompatibleWithModel(false))
        {
            var configuration = new Configuration();
            var migrator = new DbMigrator(configuration);
            migrator.Configuration.TargetDatabase = new DbConnectionInfo(context.Database.Connection.ConnectionString, "System.Data.SqlClient");
            var migrations = migrator.GetPendingMigrations();
            if (migrations.Any())
            {
                var scriptor = new MigratorScriptingDecorator(migrator);
                string script = scriptor.ScriptUpdate(null, migrations.Last());
                if (!String.IsNullOrEmpty(script))
                {
                    context.Database.ExecuteSqlCommand(script);
                }
            }
        }
    }
}

更新


太好了,看起来完成了任务!有点绕路,但仍然达到了我的期望。 - Red XIII
@Radek,你能展示一下CustomInitializer类继承自哪里吗? - Kirsten
谢谢 @Radek。你有 IDbContextFactory 的实现吗?我得到了一个 MigrationsException,内容是`Context is not constrictible. Add a default constructor or provide an implementation of IDbContextFactory'。 - Kirsten
你还能使用包管理器成功创建迁移吗? 我已经在这里发布了一个新的问题:https://dev59.com/IHDYa4cB1Zd3GeqPD8S4 - Kirsten
如果数据库不存在,我会在ExecuteSqlCommand(script)这一行中收到"The underlying provider failed on Open"的错误提示 - 当我检查脚本时,它只包含表的创建而不是数据库的创建。如果数据库不存在,这段代码对您是否有效?您有什么建议我可能遗漏了什么吗? - user3141326
显示剩余7条评论

15
他是一个解决方案,在app.config中没有连接字符串。使用自动迁移和2个使用相同上下文的数据库。真正运行时提供连接的方法。

APP.CONFIG (使用EF 6)

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework,     Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
 </configSections>
 <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
 <entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
  <parameters>
    <parameter value="Data Source=localhost; Integrated Security=True; MultipleActiveResultSets=True" />
  </parameters>
</defaultConnectionFactory>
 </entityFramework>
</configuration>

我重写了代码,以便在演示时尽可能地减小大小:

using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;

namespace Ef6Test {
    public class Program {
    public static void Main(string[] args) {
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<Ef6Ctx, Ef6MigConf>());
        WhichDb.DbName = "HACKDB1";
        var sqlConn = GetSqlConn4DBName(WhichDb.DbName);
        var context = new Ef6Ctx(sqlConn);
        context.Database.Initialize(true);
        AddJunk(context);
        //sqlConn.Close();  //?? whatever other considerations, dispose of context etc...

        Database.SetInitializer(new MigrateDatabaseToLatestVersion<Ef6Ctx, Ef6MigConf>()); // yes its default again reset this !!!!
        WhichDb.DbName = "HACKDB2";
        var sqlConn2 = GetSqlConn4DBName(WhichDb.DbName);
        var context2 = new Ef6Ctx(sqlConn2);
        context2.Database.Initialize(true);
        AddJunk(context2);
    }
    public static class WhichDb { // used during migration to know which connection to build
        public static string DbName { get; set; }
    }
    private static void AddJunk(DbContext context) {
        var poco = new pocotest();
        poco.f1 = DateTime.Now.ToString();
      //  poco.f2 = "Did somebody step on a duck?";  //comment in for second run
        context.Set<pocotest>().Add(poco);
        context.SaveChanges();
    }
    public static DbConnection GetSqlConn4DBName(string dbName) {
        var sqlConnFact =
            new SqlConnectionFactory(
                "Data Source=localhost; Integrated Security=True; MultipleActiveResultSets=True");
        var sqlConn = sqlConnFact.CreateConnection(dbName);
        return sqlConn;
    }
}
public class MigrationsContextFactory : IDbContextFactory<Ef6Ctx> {
    public Ef6Ctx Create() {
        var sqlConn = Program.GetSqlConn4DBName(Program.WhichDb.DbName); // NASTY but it works
        return new Ef6Ctx(sqlConn);
    }
}
public class Ef6MigConf : DbMigrationsConfiguration<Ef6Ctx> {
    public Ef6MigConf() {
        AutomaticMigrationsEnabled = true;
        AutomaticMigrationDataLossAllowed = true;
    }
}
public class pocotest {
    public int Id { get; set; }
    public string f1 { get; set; }
 //   public string f2 { get; set; } // comment in for second run
}
public class Ef6Ctx : DbContext {
    public DbSet<pocotest> poco1s { get; set; }
    public Ef6Ctx(DbConnection dbConn) : base(dbConn, true) { }
}
}

1
我非常了解那种感觉,Mike :-D - phil soady
3
从EF 6开始,可以使用“MigrateDatabaseToLatestVersion<TContext, TMigrationsConfiguration>(true)”来强制迁移使用触发初始化的上下文中的连接信息。我认为这实现了与您的代码相同的目标? - Tim Iles
"MigrateDatabaseToLatestVersion<TContext, TMigrationsConfiguration>" 是示例中的一部分。 - phil soady
你使用了MigrateDatabaseToLatestVersion<TContext, TMigrationsConfigur‌​ation>,但没有使用正确的参数来重用调用上下文。 - Koen van der Linden

3
我已经能够使用以下技术在多个连接之间进行切换:
1)在app.config中定义多个连接字符串名称。
2)在上下文中有一个构造函数,该构造函数需要连接字符串名称。
public Context(string connStringName)
        : base(connStringName)
    {

    }

3) 设置上下文的Create方法,并使其能够接收连接名称(使用一点技巧)

  public class ContextFactory : IDbContextFactory<Context>
  {
    public Context Create()
    {
        var s = (string)AppDomain.CurrentDomain.GetData("ConnectionStringName");
        var context = new Context(s);
        return context;
    }
}

4) 我的迁移配置...

 public sealed class Configuration : DbMigrationsConfiguration<SBD.Syrius.DataLayer.Context>
{
   etc
}

5) 设置一个函数来创建上下文。

 private static Context MyCreateContext(string connectionStringName )
    {
        // so that we can get the connection string name to the context create method 
       AppDomain.CurrentDomain.SetData("ConnectionStringName", connectionStringName);

        // hook up the Migrations configuration
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<Context, Configuration>());

        // force callback by accessing database
        var db = new Context(connectionStringName);
        var site = db.Sites.FirstOrDefault()  // something to access the database

        return db;
    }

似乎需要添加ContextFactory类才能使其正常工作,这看起来有些奇怪。 - Kirsten
还有一件事 - 为了创建迁移,您需要恢复上下文构造函数 - 这样在创建迁移时就知道要与哪个数据库进行比较。 - Kirsten
在我的问题中,我无法从app.config读取connectionString,因为我的连接字符串需要动态创建(以编程方式)。我没有要支持的静态数据库列表。如果我有一个在app.config中定义了connectionStrings的静态数据库列表,我会选择DbContext(string nameOrConnectionString)构造函数来更改connectionStrings。您的解决方案似乎过于复杂... - Red XIII
我同意@Radek的解决方案看起来更好。但是我还无法使其工作-请参见我的评论。 - Kirsten
你看到上面的帖子了吗?我遇到了相同的问题并找到了解决方案。 - phil soady

1

看一下这个链接: 它可以让你更自由地为每个数据库激活迁移。

我通过在默认构造函数中使用静态连接字符串来解决这个问题,连接到特定的数据库。

假设我有几个数据库,都基于相同的架构:myCatalog1、myCatalog2等。 我只在构造函数中使用第一个数据库的连接字符串,像这样:

public MyContext() : base("Data Source=.\SQLEXPRESS;Initial Catalog=myCatalog1;Integrated Security=True")
{
   // Can leave the rest of the constructor function itself empty
}

此构造函数仅用于使Add-Migration命令正常工作并创建迁移。请注意,对于其余数据库没有任何副作用,如果您需要另一个构造函数初始化上下文(除了迁移之外的其他目的),它也可以正常工作。
在我像这样运行Add-Migration之后:
Add-Migration -ConfigurationTypeName YourAppName.YourNamespace.Configuration "MigrationName"

我可以调用下面的代码(从开始提供的链接中获取),以便更新基于与我的Catalog1相同模式的每个数据库的迁移。
YourMigrationsConfiguration cfg = new YourMigrationsConfiguration(); 
cfg.TargetDatabase = 
   new DbConnectionInfo( 
      theConnectionString, 
      "provider" );

DbMigrator dbMigrator = new DbMigrator( cfg );
if ( dbMigrator.GetPendingMigrations().Count() > 0 )
{
   // there are pending migrations
   // do whatever you want, for example
   dbMigrator.Update(); 
}

1

我得出了类似的结论。

昨天我们就这个问题进行了长时间的讨论,你可以看一下。

如果通过DbContext构造函数调用连接,就会出现问题(简化)。因为DbMigrator实际上调用了您的“默认空”构造函数,所以您会得到一些混合的东西。我曾经从中获得了一些非常奇怪的效果。我的结论是正常的初始化程序CreateDb...可以工作,但迁移不行(甚至在某些情况下会失败,抛出错误)。

底线是要以某种方式创建一个“单例”连接 - 无论是通过@kirsten使用的DbContext工厂还是在您的DbContext中创建和更改静态连接或类似方法。不确定是否解决了所有问题,但应该有所帮助。


0

对于迁移,您可以使用以下两种方法:(1) 使用MigrateDatabaseToLatestVersion,当您开始使用上下文中的任何实体时,它将自动启动;(2) 使用DbMigrator显式告诉EF启动迁移。 (2)的优点是您不必执行虚拟操作(例如@philsoady示例中的AddJunk),甚至可以使用MigratorScriptingDecorator提取迁移SQL(请参见代码中的示例2)。

(2)的技巧似乎在于确保DbMigrationsConfigurationDbContext类始终使用相同的连接字符串。请注意,在DbMigration.Update过程中会实例化多个上下文 - 所有这些上下文都调用上下文的默认构造函数(因此如果您有多个构造函数,请小心)。在这里,您还有两个选项 - 您可以在app.config中使用connection string name(但然后您无法以编程方式定义连接字符串),或者构建\硬编码\加载等完整的connection string。请参见下面代码中的注释。

在EF 6.0.1和6.0.2中进行了测试

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;

namespace ConsoleApplication1
{
    // Models
    public class Foo
    {
        [Key]
        public int Id { get; set; }
        public string Column1 { get; set; }
        public string Column2 { get; set; }
    }

    // Configuration
    public class Configuration : DbMigrationsConfiguration<Context>
    {
        public static string StaticConnectionString; // use connection string

        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
            TargetDatabase = new DbConnectionInfo(StaticConnectionString, "System.Data.SqlClient"); // use connection string
            //TargetDatabase = new DbConnectionInfo("ConnectionStringName"); // use connection string name in app.config
        }

        protected override void Seed(Context context)
        {
        }
    }

    // Context
    public class Context : DbContext
    {
        public Context()
            //: base("ConnectionStringName") // use connection string name in app.config
            : base(ConsoleApplication1.Configuration.StaticConnectionString) // use connection string
        {
        }

        public IDbSet<Foo> Foos { get; set; }
    }

    // App
    class Program
    {
        static void Main(string[] args)
        {
            // Example 1 - migrate to test1 DB
            Configuration.StaticConnectionString = "Data Source=localhost;Initial Catalog=test1;Integrated Security=True;MultipleActiveResultSets=True";
            var configuration = new Configuration();
            var migrator = new DbMigrator(configuration);
            migrator.Update();
            Console.WriteLine("Migration 1 complete");

            // Example 2 - create migrate SQL and migrate to test2 DB
            // NOTE: You can't do this if you use a connection string name in app.config
            // Generate migrate sql script for migration to test2 DB
            Configuration.StaticConnectionString = "Data Source=localhost;Initial Catalog=test2;Integrated Security=True;MultipleActiveResultSets=True";
            configuration = new Configuration();
            migrator = new DbMigrator(configuration);
            var scriptor = new MigratorScriptingDecorator(migrator);
            string sql = scriptor.ScriptUpdate(null, null);
            Console.WriteLine("Migration 2 SQL:\n" + sql);

            // Perform migration to test2 DB
            configuration = new Configuration();
            migrator = new DbMigrator(configuration);
            migrator.Update();
            Console.WriteLine("Migration 2 complete");
        }
    }
}

0

我想在DEBUG模式下自动迁移,以便为开发人员提供方便(生产安装程序通常会执行迁移),但是遇到了同样的问题:当迁移时,代码指定的连接字符串被忽略。

我的方法是从这个通用的上下文中派生出迁移上下文,该上下文处理“保存”连接字符串:

public class MigrateInitializeContext<TDbContext, TMigrationsConfiguration> : DbContext
    where TDbContext : DbContext
    where TMigrationsConfiguration : DbMigrationsConfiguration<TDbContext>, new()
{
    // ReSharper disable once StaticFieldInGenericType
    private static string nameOrConnectionString = typeof(TDbContext).Name;

    static MigrateInitializeContext()
    {
        Database.SetInitializer(new MigrateDatabaseToLatestVersion<TDbContext, TMigrationsConfiguration>());
    }

    protected MigrateInitializeContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
        MigrateInitializeContext<TDbContext,TMigrationsConfiguration>.nameOrConnectionString = nameOrConnectionString;
    }

    protected MigrateInitializeContext() : base(nameOrConnectionString)
    {
    }
}

ReSharper警告是因为泛型类中的静态字段仅针对每个具体类型是静态的,这正是我们想要的

上下文定义如下:

public class MyContext : MigrateInitializeContext<MyContext, Migrations.Configuration>
{
    public MyContext()
    {
    }

    public MyContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
    }

    public virtual DbSet<MyType> MyTypes { get; set; }
}

可以正常使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接