2013-10-18 59 views
25

我們的組織需要有一個單一的數據庫,多租戶
按表架構,而不是租戶ID)架構。代碼優先的多租戶EF6

這裏有一個偉大的文章上開始使用這種事情在這裏: http://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/

在文章的中間,這樣寫:

你會(可能與通知有些沮喪)我們需要編寫代碼到 爲每個實體配置表模式。無可否認,目前還沒有許多魔法獨角獸在這個代碼周圍放牧......在EF 的未來版本中,我們將能夠用一個更清潔的自定義約定替換它。

我們的目標是讓最簡潔的方式擁有一個上下文類,我們可以使用它來連接到具有相同模型的多個模式。
注意modelBuilder.HasDefaultSchema似乎沒有足夠的,因爲它僅適用於第一時間EF初始化上下文和運行OnModelCreating)

請問上述清潔習俗慣例存在EF5或EF6?
或者有沒有更好的方法來處理這個問題?

注意:我在開發論壇上也提過這個問題,因爲它似乎與EF的方向更相關,但想看看這裏是否有人有替代方案。注2:我並不擔心遷移,我們會分開處理。

+0

只是好奇 - 有多少住戶?同時有多少用戶? – tsells

+0

租戶可能在100幾歲。同時使用的用戶數量很少,最多可能有100個是我的猜測。 –

+1

爲什麼不在數據庫設計級別處理它?例如每個表中的列都有租戶Id,並在存儲庫級別處理它。 –

回答

33

酒店modelBuilder.HasDefaultSchemaOnModelCreating如果您在DbContext實施IDbModelCacheKeyProvider就足夠了。模型只創建一次,而不是由EntityFramwork在內部緩存,您可以爲緩存定義自己的密鑰。將模式名稱作爲模型緩存鍵,EF將通過每個不同的緩存鍵(本例中爲模式)創建一個模型。這裏是我對概念代碼的證明:

using System; 
using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.Entity.Infrastructure; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 

using TenantDataModel; 

namespace TenantDataContext 
{ 
    public class TenantDataCtx : DbContext, IDbModelCacheKeyProvider 
    { 
     #region Construction 

     public static TenantDataCtx Create(string databaseServer, string databaseName, string databaseUserName, string databasePassword, Guid tenantId) 
     { 
      var connectionStringBuilder = new System.Data.SqlClient.SqlConnectionStringBuilder(); 
      connectionStringBuilder.DataSource = databaseServer; 
      connectionStringBuilder.InitialCatalog = databaseName; 
      connectionStringBuilder.UserID = databaseUserName; 
      connectionStringBuilder.Password = databasePassword; 

      string connectionString = connectionStringBuilder.ToString(); 
      return new TenantDataCtx(connectionString, tenantId); 
     } 

     // Used by EF migrations 
     public TenantDataCtx() 
     { 
      Database.SetInitializer<TenantDataCtx>(null); 
     } 

     internal TenantDataCtx(string connectionString, Guid tenantId) 
      : base(connectionString) 
     { 
      Database.SetInitializer<TenantDataCtx>(null); 
      this.SchemaName = tenantId.ToString("D"); 
     } 

     public string SchemaName { get; private set; } 

     #endregion 

     #region DataSet Properties 

     public DbSet<TestEntity> TestEntities { get { return this.Set<TestEntity>(); } } 

     #endregion 

     #region Overrides 

     protected override void OnModelCreating(DbModelBuilder modelBuilder) 
     { 
      if (this.SchemaName != null) 
      { 
       modelBuilder.HasDefaultSchema(this.SchemaName); 
      } 

      base.OnModelCreating(modelBuilder); 
     } 

     #endregion 

     #region IDbModelCacheKeyProvider Members 

     public string CacheKey 
     { 
      get { return this.SchemaName; } 
     } 

     #endregion 
    } 
} 

此外我找到了一種使用EF遷移的方法。我對我的解決方案並不滿意,但現在似乎沒有其他解決方案可用。

using System; 
using System.Collections.Generic; 
using System.Data.Entity.SqlServer; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 

namespace TenantDatabaseManager 
{ 
    public class SqlServerSchemaAwareMigrationSqlGenerator : SqlServerMigrationSqlGenerator 
    { 
     private string _schema; 

     public SqlServerSchemaAwareMigrationSqlGenerator(string schema) 
     { 
      _schema = schema; 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.AddColumnOperation addColumnOperation) 
     { 
      string newTableName = _GetNameWithReplacedSchema(addColumnOperation.Table); 
      var newAddColumnOperation = new System.Data.Entity.Migrations.Model.AddColumnOperation(newTableName, addColumnOperation.Column, addColumnOperation.AnonymousArguments); 
      base.Generate(newAddColumnOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.AddPrimaryKeyOperation addPrimaryKeyOperation) 
     { 
      addPrimaryKeyOperation.Table = _GetNameWithReplacedSchema(addPrimaryKeyOperation.Table); 
      base.Generate(addPrimaryKeyOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.AlterColumnOperation alterColumnOperation) 
     { 
      string tableName = _GetNameWithReplacedSchema(alterColumnOperation.Table); 
      var newAlterColumnOperation = new System.Data.Entity.Migrations.Model.AlterColumnOperation(tableName, alterColumnOperation.Column, alterColumnOperation.IsDestructiveChange); 
      base.Generate(newAlterColumnOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.DropPrimaryKeyOperation dropPrimaryKeyOperation) 
     { 
      dropPrimaryKeyOperation.Table = _GetNameWithReplacedSchema(dropPrimaryKeyOperation.Table); 
      base.Generate(dropPrimaryKeyOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.CreateIndexOperation createIndexOperation) 
     { 
      string name = _GetNameWithReplacedSchema(createIndexOperation.Table); 
      createIndexOperation.Table = name; 
      base.Generate(createIndexOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.CreateTableOperation createTableOperation) 
     { 
      string newTableName = _GetNameWithReplacedSchema(createTableOperation.Name); 
      var newCreateTableOperation = new System.Data.Entity.Migrations.Model.CreateTableOperation(newTableName, createTableOperation.AnonymousArguments); 
      newCreateTableOperation.PrimaryKey = createTableOperation.PrimaryKey; 
      foreach (var column in createTableOperation.Columns) 
      { 
       newCreateTableOperation.Columns.Add(column); 
      } 

      base.Generate(newCreateTableOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.RenameTableOperation renameTableOperation) 
     { 
      string oldName = _GetNameWithReplacedSchema(renameTableOperation.Name); 
      string newName = renameTableOperation.NewName.Split(new char[] { '.' }).Last(); 
      var newRenameTableOperation = new System.Data.Entity.Migrations.Model.RenameTableOperation(oldName, newName, renameTableOperation.AnonymousArguments); 
      base.Generate(newRenameTableOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.RenameIndexOperation renameIndexOperation) 
     { 
      string tableName = _GetNameWithReplacedSchema(renameIndexOperation.Table); 
      var newRenameIndexOperation = new System.Data.Entity.Migrations.Model.RenameIndexOperation(tableName, renameIndexOperation.Name, renameIndexOperation.NewName); 
      base.Generate(newRenameIndexOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.AddForeignKeyOperation addForeignKeyOperation) 
     { 
      addForeignKeyOperation.DependentTable = _GetNameWithReplacedSchema(addForeignKeyOperation.DependentTable); 
      addForeignKeyOperation.PrincipalTable = _GetNameWithReplacedSchema(addForeignKeyOperation.PrincipalTable); 
      base.Generate(addForeignKeyOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.DropColumnOperation dropColumnOperation) 
     { 
      string newTableName = _GetNameWithReplacedSchema(dropColumnOperation.Table); 
      var newDropColumnOperation = new System.Data.Entity.Migrations.Model.DropColumnOperation(newTableName, dropColumnOperation.Name, dropColumnOperation.AnonymousArguments); 
      base.Generate(newDropColumnOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.RenameColumnOperation renameColumnOperation) 
     { 
      string newTableName = _GetNameWithReplacedSchema(renameColumnOperation.Table); 
      var newRenameColumnOperation = new System.Data.Entity.Migrations.Model.RenameColumnOperation(newTableName, renameColumnOperation.Name, renameColumnOperation.NewName); 
      base.Generate(newRenameColumnOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.DropTableOperation dropTableOperation) 
     { 
      string newTableName = _GetNameWithReplacedSchema(dropTableOperation.Name); 
      var newDropTableOperation = new System.Data.Entity.Migrations.Model.DropTableOperation(newTableName, dropTableOperation.AnonymousArguments); 
      base.Generate(newDropTableOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.DropForeignKeyOperation dropForeignKeyOperation) 
     { 
      dropForeignKeyOperation.PrincipalTable = _GetNameWithReplacedSchema(dropForeignKeyOperation.PrincipalTable); 
      dropForeignKeyOperation.DependentTable = _GetNameWithReplacedSchema(dropForeignKeyOperation.DependentTable); 
      base.Generate(dropForeignKeyOperation); 
     } 

     protected override void Generate(System.Data.Entity.Migrations.Model.DropIndexOperation dropIndexOperation) 
     { 
      dropIndexOperation.Table = _GetNameWithReplacedSchema(dropIndexOperation.Table); 
      base.Generate(dropIndexOperation); 
     } 

     private string _GetNameWithReplacedSchema(string name) 
     { 
      string[] nameParts = name.Split('.'); 
      string newName; 

      switch (nameParts.Length) 
      { 
       case 1: 
        newName = string.Format("{0}.{1}", _schema, nameParts[0]); 
        break; 

       case 2: 
        newName = string.Format("{0}.{1}", _schema, nameParts[1]); 
        break; 

       case 3: 
        newName = string.Format("{0}.{1}.{2}", _schema, nameParts[1], nameParts[2]); 
        break; 

       default: 
        throw new NotSupportedException(); 
      } 

      return newName; 
     } 
    } 
} 

這就是我如何使用SqlServerSchemaAwareMigrationSqlGenerator

// Update TenantDataCtx 
var tenantDataMigrationsConfiguration = new DbMigrationsConfiguration<TenantDataContext.TenantDataCtx>(); 
tenantDataMigrationsConfiguration.AutomaticMigrationsEnabled = false; 
tenantDataMigrationsConfiguration.SetSqlGenerator("System.Data.SqlClient", new SqlServerSchemaAwareMigrationSqlGenerator(schemaName)); 
tenantDataMigrationsConfiguration.SetHistoryContextFactory("System.Data.SqlClient", (existingConnection, defaultSchema) => new HistoryContext(existingConnection, schemaName)); 
tenantDataMigrationsConfiguration.TargetDatabase = new System.Data.Entity.Infrastructure.DbConnectionInfo(connectionString, "System.Data.SqlClient"); 
tenantDataMigrationsConfiguration.MigrationsAssembly = typeof(TenantDataContext.TenantDataCtx).Assembly; 
tenantDataMigrationsConfiguration.MigrationsNamespace = "TenantDataContext.Migrations.TenantData"; 

DbMigrator tenantDataCtxMigrator = new DbMigrator(tenantDataMigrationsConfiguration); 
tenantDataCtxMigrator.Update(); 

問候來自德國,

托比亞斯

+0

嗨Tobias。你在哪裏調用'這是我如何使用SqlServerSchemaAwareMigrationSqlGenerator'中陳述的代碼?你在配置中插入這個嗎?或者你在別處運行?來自荷蘭,彼得。 –

+3

嗨,彼得。此代碼將'tenantDataMigrationsConfiguration.MigrationsAssembly'中存在的所有遷移應用於由'tenantDataMigrationsConfiguration.TargetDatabase'標識的數據庫。你可以在任何你想要的地方執行該代碼。我有一個小的控制檯應用程序,它通過命令行參數獲取所有必需的參數,並與它建立一個連接字符串並執行代碼。 –

+3

一個重要的建議:SqlServerSchemaAwareMigrationSqlGenerator絕對不完整!幾天前,我不得不重寫'Generate(RenameTableOperation renameTableOperation)'的重載。爲了獲得完整性,您必須覆蓋與模式有關的所有「生成」重載。 –

1

好吧,如果它不會首先是代碼,我會嘗試做這種方式:

  • 生成默認模式的表,說DBO

  • 基於現有的數據庫

    上產生EDMX與POCO
  • 採取EF爲起始TT模板

  • 編輯TT文件和新屬性添加到上下文卡萊d schema,並強制生成的類中的查詢將其用於數據庫對象。

這樣你就可以爲不同的模式創建上下文,甚至允許對象在上下文之間飛來飛去。

+0

這是有趣的感謝!但是我的目標(上面)是用單個上下文來完成這個工作,所以我可以輕鬆地共享模型,並根據需要重新連接到單個應用程序運行中的不同模式。除此之外,我可能有50或100個不同的模式(所有模型都是相同的),所以我不想在每次想要配置新租戶(模式)時都生成新的上下文。 - 儘管如果我只有幾個模式,你的解決方案將工作得很好! –

+0

@KevinRadcliffe好吧,無論如何,你會經常重新創建上下文,因爲在大多數情況下,他們實現了Unit Of Work模式,並且不應該長時間生活。 – vittore

+0

@KevinRadcliffe關於模式 - 我真的會單獨做,就像調用create schema腳本一樣,因爲無論如何你都要分別管理遷移。 – vittore

2

非常好的辦法,它讓我獲得更直接的解決方案。 你可能只覆蓋了名稱,方法,它是在每一個作家用.... 對不起新的答案,但我不能發表評論....

public class SqlServerSchemaAwareMigrationSqlGenerator:SqlServerMigrationSqlGenerator 
{ 

    private string _schema; 

    public accountMigrationSqlGenerator(string schema) 
    { 
     _schema = schema; 
    } 

    protected override string Name(string name) 
    { 

     int p = name.IndexOf('.'); 
     if(p>0) 
     { 
      name = name.Substring(p + 1); 
     } 

     return $"[{_schema}].[{name}]"; 

    } 

} 
+0

不幸的是,並非所有函數都使用Name屬性,例如Generate for RenameIndexOperation忽略它。所以重寫所有Generate方法更安全。 – Anatoliy

1

感謝:Tobias!你救我一年前...

我修改了Oracle數據庫下EF 6:

public class IntegrationDbContext : DbContext, IDbModelCacheKeyProvider 
{ 
    private static readonly ILog __log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); 

    /// <summary> 
    /// Factory method 
    /// </summary> 
    public static IntegrationDbContext Create(string connectionStringName) 
    { 
     return new IntegrationDbContext(connectionStringName, GetDBSchema(connectionStringName)); 
    } 

    /// <summary> 
    /// Constructor 
    /// </summary> 
    public IntegrationDbContext() 
    { 
     Database.SetInitializer<IntegrationDbContext>(null); 
    } 

    /// <summary> 
    /// Constructor 
    /// </summary> 
    internal IntegrationDbContext(string connectionString, string schemaName) 
     : base("name={0}".Fill(connectionString)) 
    { 
     Database.SetInitializer<IntegrationDbContext>(null); 
     SchemaName = schemaName; 
    } 

    /// <summary> 
    /// DB schema name 
    /// </summary> 
    public string SchemaName { get; private set; } 

    #region Tables 
    /// <summary> 
    /// Integration table "SYNC_BONUS_DISTRIBUTION" 
    /// </summary> 
    public virtual DbSet<SYNC_BONUS_DISTRIBUTION> SYNC_BONUS_DISTRIBUTION { get; set; } 

    /// <summary> 
    /// Integration table "SYNC_MESSAGE_DISTRIBUTION" 
    /// </summary> 
    public virtual DbSet<SYNC_MESSAGE_DISTRIBUTION> SYNC_MESSAGE_DISTRIBUTION { get; set; } 

    /// <summary> 
    /// Integration table "IMPORT_TEMPLATES" 
    /// </summary> 
    public virtual DbSet<IMPORT_TEMPLATE> IMPORT_TEMPLATES { get; set; } 

    #endregion //Tables 

    private static Dictionary<string, string> __schemaCache = new Dictionary<string, string>(); 
    private static object __schCacheLock = new object(); 
    /// <summary> 
    /// Gets DB schema name from connection string, or default from config 
    /// </summary> 
    private static string GetDBSchema(string connectionStringName) 
    { 
     string result; 
     if (!__schemaCache.TryGetValue(connectionStringName, out result)) 
     { 
      lock (__schCacheLock) 
      { 
       if (!__schemaCache.TryGetValue(connectionStringName, out result)) 
       { 
        DbConnectionStringBuilder builder = new DbConnectionStringBuilder(); 
        builder.ConnectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString; 
        result = builder.ContainsKey("User ID") ? builder["User ID"] as string : ConfigurationManager.AppSettings["DefaultIntegrationSchema"]; 
        __schemaCache.Add(connectionStringName, result); 
       } 
      } 
     } 
     return result; 
    } 

    /// <summary> 
    /// Context initialization 
    /// </summary> 
    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
     __log.DebugFormat("OnModelCreating for integration model in schema: {0}", SchemaName); 
     if (SchemaName != null) 
     { 
      modelBuilder.HasDefaultSchema(SchemaName); 
     } 
     //### CLOB settings 
     modelBuilder.Properties().Where(p => p.PropertyType == typeof(string) && 
              p.GetCustomAttributes(typeof(MaxLengthAttribute), false).Length == 0) 
               .Configure(p => p.HasMaxLength(2000)); 

     base.OnModelCreating(modelBuilder); 
    } 

    /// <summary> 
    /// Implementation of <see cref="IDbModelCacheKeyProvider.CacheKey"/> - thanks by this is 'OnModelCreating' calling for each specific schema. 
    /// </summary> 
    public string CacheKey 
    { 
     get { return SchemaName; } 
    } 
} 
相關問題