代码优先EF6的多租户

时间:2013-10-18 20:58:35

标签: c# entity-framework

我们的组织需要一个单一的数据库,多租户 (按表架构,而不是租户ID )架构。

这里有一篇很棒的文章来介绍这种事情: http://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/

在文章的中间,写着:

  

你会注意到(可能有些沮丧)我们需要编写代码   为每个实体配置表模式。诚然,没有   许多神奇的独角兽在这个代码中放牧......在EF的未来版本中   我们将能够用更清洁的自定义约定替换它。

我们的目标是以最干净的方式拥有一个可用于连接到具有相同模型的多个模式的上下文类。
请注意,modelBuilder.HasDefaultSchema似乎不够,因为它仅适用于EF首次初始化上下文并运行OnModelCreating时)

上述清洁自定义约定是否存在于EF5或EF6中? 或者有一种更清洁的方式来处理这个问题吗?

注意:我也在开发论坛上提出了这个问题,因为它似乎更多地与EF的方向有关,但是想知道这里是否有人有替代方案。

注意2:我并不担心迁移,我们会分开处理。

4 个答案:

答案 0 :(得分:37)

modelBuilder.HasDefaultSchemaOnModelCreating中的媒体IDbModelCacheKeyProvider已足够,如果您在DbContext上实施EntityFramwork。模型创建一次,然后由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;
        }
    }
}

此外,我找到了一种使用EF迁移的方法。我对我的解决方案并不满意,但似乎现在还没有其他可用解决方案。

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();

的方式
{{1}}

来自德国,

托拜厄斯

答案 1 :(得分: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}]";

    }

}

答案 2 :(得分:1)

好吧,如果它不是代码优先,我会尝试这样做:

  • 在默认架构中生成表格,例如dbo

  • 基于现有数据库生成edmx

  • 将EF与POCO作为起始TT模板

  • 编辑TT文件并将新属性添加到名为schema的上下文中,并强制生成的类中的查询将其用于数据库对象。

这样您就可以为不同的模式创建上下文,甚至允许对象在上下文之间飞来飞去。

答案 3 :(得分:1)

感谢:Tobias!你救了我一年......

我在EF 6下对Oracle DB的修改:

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; }
    }
}