web-dev-qa-db-ja.com

Entity Framework Coreで動的に変化するスキーマ

[〜#〜] upd [〜#〜]ここ は、私が問題を解決する方法です。最高ではないかもしれませんが、私にとってはうまくいきました。


EF Coreでの作業に問題があります。プロジェクトのデータベース内のさまざまな会社のデータをスキーマメカニズムで分離したいと考えています。私の質問は、実行時にスキーマ名を変更するにはどうすればよいですか?私はこの問題について 同様の質問 を見つけましたが、それでも未回答であり、いくつかの異なる条件があります。だから私は必要に応じてdbコンテキストを許可するResolveメソッドを持っています

public static void Resolve(IServiceCollection services) {
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<DomainDbContext>()
        .AddDefaultTokenProviders();
    services.AddTransient<IOrderProvider, OrderProvider>();
    ...
}

OnModelCreatingにスキーマ名を設定できますが、以前に見つかったように、このメソッドは一度だけ呼び出されるため、スキーマ名をグローバルに設定できます

protected override void OnModelCreating(ModelBuilder modelBuilder) {
    modelBuilder.HasDefaultSchema("public");
    base.OnModelCreating(modelBuilder);
}

または属性を介してモデル内で

[Table("order", Schema = "public")]
public class Order{...}

しかし、実行時にスキーマ名を変更するにはどうすればよいですか?各リクエストごとにコンテキストを作成しますが、まず、データベースのスキーマ共有テーブルへのリクエストを介して、ユーザーのスキーマ名を推測します。そのメカニズムを整理する正しい方法は何ですか?

  1. ユーザー資格情報によってスキーマ名を把握します。
  2. 特定のスキーマからデータベースからユーザー固有のデータを取得します。

ありがとうございました。

追伸私はPostgreSqlを使用していますが、これが小文字のテーブル名の理由です。

19
user3272018

EntityTypeConfigurationをEF6で既に使用しましたか?

解決策は、次のようなDbContextクラスのOnModelCreatingメソッドのエンティティのマッピングを使用することだと思います。

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal;
using Microsoft.Extensions.Options;

namespace AdventureWorksAPI.Models
{
    public class AdventureWorksDbContext : Microsoft.EntityFrameworkCore.DbContext
    {
        public AdventureWorksDbContext(IOptions<AppSettings> appSettings)
        {
            ConnectionString = appSettings.Value.ConnectionString;
        }

        public String ConnectionString { get; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(ConnectionString);

            // this block forces map method invoke for each instance
            var builder = new ModelBuilder(new CoreConventionSetBuilder().CreateConventionSet());

            OnModelCreating(builder);

            optionsBuilder.UseModel(builder.Model);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.MapProduct();

            base.OnModelCreating(modelBuilder);
        }
    }
}

OnConfiguringメソッドのコードは、DbContextクラスのインスタンスの作成ごとにMapProductを強制的に実行します。

MapProductメソッドの定義:

using System;
using Microsoft.EntityFrameworkCore;

namespace AdventureWorksAPI.Models
{
    public static class ProductMap
    {
        public static ModelBuilder MapProduct(this ModelBuilder modelBuilder, String schema)
        {
            var entity = modelBuilder.Entity<Product>();

            entity.ToTable("Product", schema);

            entity.HasKey(p => new { p.ProductID });

            entity.Property(p => p.ProductID).UseSqlServerIdentityColumn();

            return modelBuilder;
        }
    }
}

上記のように、テーブルのスキーマと名前を設定する行があります。DbContextなどの1つのコンストラクターのスキーマ名を送信できます。

マジックストリングは使用しないでください。使用可能なすべてのスキーマを使用してクラスを作成できます。次に例を示します。

using System;

public class Schemas
{
    public const String HumanResources = "HumanResources";
    public const String Production = "Production";
    public const String Sales = "Production";
}

特定のスキーマでDbContextを作成するには、次のように記述します。

var humanResourcesDbContext = new AdventureWorksDbContext(Schemas.HumanResources);

var productionDbContext = new AdventureWorksDbContext(Schemas.Production);

明らかに、スキーマ名パラメーターの値に従ってスキーマ名を設定する必要があります。

entity.ToTable("Product", schemaName);
9
H. Herzl

これにはいくつかの方法があります。

  • モデルを外部で構築し、DbContextOptionsBuilder.UseModel()を介して渡します
  • IModelCacheKeyFactoryサービスを、スキーマを考慮したサービスに置き換えます
4
bricelam

このブログはあなたに役立つかもしれません。パーフェクト!:)

https://romiller.com/2011/05/23/ef-4-1-multi-tenant-with-code-first/

このブログはef4に基づいていますが、efコアで正常に動作するかどうかはわかりません。

public class ContactContext : DbContext
{
    private ContactContext(DbConnection connection, DbCompiledModel model)
        : base(connection, model, contextOwnsConnection: false)
    { }

    public DbSet<Person> People { get; set; }
    public DbSet<ContactInfo> ContactInfo { get; set; }

    private static ConcurrentDictionary<Tuple<string, string>, DbCompiledModel> modelCache
        = new ConcurrentDictionary<Tuple<string, string>, DbCompiledModel>();

    /// <summary>
    /// Creates a context that will access the specified tenant
    /// </summary>
    public static ContactContext Create(string tenantSchema, DbConnection connection)
    {
        var compiledModel = modelCache.GetOrAdd(
            Tuple.Create(connection.ConnectionString, tenantSchema),
            t =>
            {
                var builder = new DbModelBuilder();
                builder.Conventions.Remove<IncludeMetadataConvention>();
                builder.Entity<Person>().ToTable("Person", tenantSchema);
                builder.Entity<ContactInfo>().ToTable("ContactInfo", tenantSchema);

                var model = builder.Build(connection);
                return model.Compile();
            });

        return new ContactContext(connection, compiledModel);
    }

    /// <summary>
    /// Creates the database and/or tables for a new tenant
    /// </summary>
    public static void ProvisionTenant(string tenantSchema, DbConnection connection)
    {
        using (var ctx = Create(tenantSchema, connection))
        {
            if (!ctx.Database.Exists())
            {
                ctx.Database.Create();
            }
            else
            {
                var createScript = ((IObjectContextAdapter)ctx).ObjectContext.CreateDatabaseScript();
                ctx.Database.ExecuteSqlCommand(createScript);
            }
        }
    }
}

これらのコードの主なアイデアは、異なるスキーマによって異なるDbContextを作成し、それらを特定の識別子でキャッシュする静的メソッドを提供することです。

3
snys98

皆さん、申し訳ありませんが、私は以前に自分の解決策を投稿したはずですが、何らかの理由で投稿しなかったので、ここにあります。

[〜#〜]しかし[〜#〜]

誰もレビューしておらず、製造実績もないため、ソリューションに問題がある可能性があることを覚えておいてください。おそらく、ここでフィードバックを得ます。

プロジェクトで使用したASP .NET Core 1


私のデータベース構造について。 2つのコンテキストがあります。 1つ目はユーザーに関する情報(ユーザーが対処する必要のあるdbスキームを含む)、2つ目はユーザー固有のデータを含みます。

Startup.cs両方のコンテキストを追加します

public void ConfigureServices(IServiceCollection 
    services.AddEntityFrameworkNpgsql()
        .AddDbContext<SharedDbContext>(options =>
            options.UseNpgsql(Configuration["MasterConnection"]))
        .AddDbContext<DomainDbContext>((serviceProvider, options) => 
            options.UseNpgsql(Configuration["MasterConnection"])
                .UseInternalServiceProvider(serviceProvider));
...
    services.Replace(ServiceDescriptor.Singleton<IModelCacheKeyFactory, MultiTenantModelCacheKeyFactory>());
    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

UseInternalServiceProviderの部分に注意してください。これは Nero Sule によって次の説明で提案されました

EFC 1のリリースサイクルの最後に、EFチームはEFのサービスをデフォルトのサービスコレクション(AddEntityFramework()。AddDbContext())から削除することを決定しました。これは、サービスがアプリケーションサービスではなくEFの独自のサービスプロバイダーを使用して解決されることを意味しますプロバイダー。

EFでアプリケーションのサービスプロバイダーを強制的に使用するには、まずデータプロバイダーと一緒にEFのサービスをサービスコレクションに追加し、次に内部サービスプロバイダーを使用するようにDBContextを構成する必要があります。

MultiTenantModelCacheKeyFactoryが必要です

public class MultiTenantModelCacheKeyFactory : ModelCacheKeyFactory {
    private string _schemaName;
    public override object Create(DbContext context) {
        var dataContext = context as DomainDbContext;
        if(dataContext != null) {
            _schemaName = dataContext.SchemaName;
        }
        return new MultiTenantModelCacheKey(_schemaName, context);
    }
}

ここで、DomainDbContextはユーザー固有のデータのコンテキストです

public class MultiTenantModelCacheKey : ModelCacheKey {
    private readonly string _schemaName;
    public MultiTenantModelCacheKey(string schemaName, DbContext context) : base(context) {
        _schemaName = schemaName;
    }
    public override int GetHashCode() {
        return _schemaName.GetHashCode();
    }
}

また、コンテキスト自体をわずかに変更して、スキーマ対応にする必要があります。

public class DomainDbContext : IdentityDbContext<ApplicationUser> {
    public readonly string SchemaName;
    public DbSet<Foo> Foos{ get; set; }

    public DomainDbContext(ICompanyProvider companyProvider, DbContextOptions<DomainDbContext> options)
        : base(options) {
        SchemaName = companyProvider.GetSchemaName();
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.HasDefaultSchema(SchemaName);
        base.OnModelCreating(modelBuilder);
    }
}

共有コンテキストはsharedスキーマに厳密にバインドされています:

public class SharedDbContext : IdentityDbContext<ApplicationUser> {
    private const string SharedSchemaName = "shared";
    public DbSet<Foo> Foos{ get; set; }
    public SharedDbContext(DbContextOptions<SharedDbContext> options)
        : base(options) {}
    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.HasDefaultSchema(SharedSchemaName);
        base.OnModelCreating(modelBuilder);
    }
}

ICompanyProviderは、ユーザーのスキーマ名の取得を担当します。そして、はい、私はそれが完璧なコードからどれほど離れているかを知っています。

public interface ICompanyProvider {
    string GetSchemaName();
}

public class CompanyProvider : ICompanyProvider {
    private readonly SharedDbContext _context;
    private readonly IHttpContextAccessor _accesor;
    private readonly UserManager<ApplicationUser> _userManager;

    public CompanyProvider(SharedDbContext context, IHttpContextAccessor accesor, UserManager<ApplicationUser> userManager) {
        _context = context;
        _accesor = accesor;
        _userManager = userManager;
    }
    public string GetSchemaName() {
        Task<ApplicationUser> getUserTask = null;
        Task.Run(() => {
            getUserTask = _userManager.GetUserAsync(_accesor.HttpContext?.User);
        }).Wait();
        var user = getUserTask.Result;
        if(user == null) {
            return "shared";
        }
        return _context.Companies.Single(c => c.Id == user.CompanyId).SchemaName;
    }
}

そして、私が何も見逃していないなら、それだけです。これで、認証されたユーザーによるすべてのリクエストで、適切なコンテキストが使用されます。

お役に立てば幸いです。

2
user3272018

固定スキーマテーブルでテーブル属性を使用できます。

スキーマ変更テーブルで属性を使用することはできません。ToTableFluent APIを介して属性を構成する必要があります。
モデルキャッシュを無効にした場合(または独自のキャッシュを書き込んだ場合)、スキーマはリクエストごとに変更されるため、コンテキストの作成時(毎回)にスキーマを指定できます。

これは基本的なアイデアです

class MyContext : DbContext
{
    public string Schema { get; private set; }

    public MyContext(string schema) : base()
    {

    }

    // Your DbSets here
    DbSet<Emp> Emps { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Emp>()
            .ToTable("Emps", Schema);
    }
}

これで、コンテキストを作成する前に、いくつかの異なる方法でスキーマ名を決定できます。
たとえば、「システムテーブル」を別のコンテキストに置くことができるため、リクエストごとにシステムテーブルを使用してユーザー名からスキーマ名を取得し、適切なスキーマに作業コンテキストを作成します(共有することができます)コンテキスト間のテーブル)。
システムテーブルをコンテキストから切り離して、ADO .Netを使用してアクセスすることができます。
おそらく他にもいくつかの解決策があります。

こちらもご覧ください
コードが最初のEF6のマルチテナント

グーグルできますef multi tenant

[〜#〜]編集[〜#〜]
モデルキャッシングの問題もあります(忘れていました)。モデルのキャッシュを無効にするか、キャッシュの動作を変更する必要があります。

1
bubi

多分私はこの答えに少し遅れています

私の問題は、同じ構造を持つ異なるスキーマを処理することでした。

異なるスキーマに対して同じコンテキストの異なるインスタンスを作成しようとすると、Entity Frameworks 6が再生されるようになり、dbContextが最初に作成された後、次のインスタンスでは異なるスキーマ名で作成されましたが、onModelCreatingは意味と呼ばれていません各インスタンスは、以前にキャッチされた同じ生成済みビューを指し、最初のスキーマを指していました。

次に、スキーマごとにmyDBContextを継承する新しいクラスを作成すると、エンティティフレームワークがスキーマごとに1つの新しいコンテキストを作成する問題をキャッチして問題を解決しますが、ハードコードされたスキーマで終了して別の問題が発生するという問題が発生することに気付きました別のスキーマを追加する必要がある場合のコードスケーラビリティの条件。クラスを追加し、アプリケーションの新しいバージョンを再コンパイルして公開する必要があります。

そこで、ランタイムで現在のソリューションにクラスを作成、コンパイル、および追加することをもう少し行うことにしました。

これがコードです

public static MyBaseContext CreateContext(string schema)
{
    MyBaseContext instance = null;
    try
    {
        string code = $@"
            namespace MyNamespace
            {{
                using System.Collections.Generic;
                using System.Data.Entity;

                public partial class {schema}Context : MyBaseContext
                {{
                    public {schema}Context(string SCHEMA) : base(SCHEMA)
                    {{
                    }}

                    protected override void OnModelCreating(DbModelBuilder modelBuilder)
                    {{
                        base.OnModelCreating(modelBuilder);
                    }}
                }}
            }}
        ";

        CompilerParameters dynamicParams = new CompilerParameters();

        Assembly currentAssembly = Assembly.GetExecutingAssembly();
        dynamicParams.ReferencedAssemblies.Add(currentAssembly.Location);   // Reference the current Assembly from within dynamic one
                                                                            // Dependent Assemblies of the above will also be needed
        dynamicParams.ReferencedAssemblies.AddRange(
            (from holdAssembly in currentAssembly.GetReferencedAssemblies()
             select Assembly.ReflectionOnlyLoad(holdAssembly.FullName).Location).ToArray());

        // Everything below here is unchanged from the previous
        CodeDomProvider dynamicLoad = CodeDomProvider.CreateProvider("C#");
        CompilerResults dynamicResults = dynamicLoad.CompileAssemblyFromSource(dynamicParams, code);

        if (!dynamicResults.Errors.HasErrors)
        {
            Type myDynamicType = dynamicResults.CompiledAssembly.GetType($"MyNamespace.{schema}Context");
            Object[] args = { schema };
            instance = (MyBaseContext)Activator.CreateInstance(myDynamicType, args);
        }
        else
        {
            Console.WriteLine("Failed to load dynamic Assembly" + dynamicResults.Errors[0].ErrorText);
        }
    }
    catch (Exception ex)
    {
        string message = ex.Message;
    }
    return instance;
}

これが誰かが時間を節約するのに役立つことを願っています。

0
Randy

EFCoreでこれを理解するには数時間かかりました。これを実装する適切な方法については、多くの混乱があるようです。 EFCoreでカスタムモデルを処理するシンプルで正しい方法は、以下に示すように、デフォルトのIModelCacheKeyFactoryサービスを置き換えることだと思います。私の例では、カスタムテーブル名を設定しています。

  1. コンテキストクラスにModelCacheKey変数を作成します。
  2. コンテキストコンストラクターで、ModelCacheKey変数を設定します。
  3. IModelCacheKeyFactoryを継承するクラスを作成し、ModelCacheKey(MyModelCacheKeyFactory)を使用する
  4. OnConfiguringメソッド(MyContext)で、デフォルトのIModelCacheKeyFactoryを置き換えます
  5. OnModelCreatingメソッド(MyContext)で、ModelBuilderを使用して必要なものを定義します。
public class MyModelCacheKeyFactory : IModelCacheKeyFactory
{
    public object Create(DbContext context)
        => context is MyContext myContext ?
        (context.GetType(), myContext.ModelCacheKey) :
        (object)context.GetType();
}

public partial class MyContext : DbContext
{
     public string Company { get; }
     public string ModelCacheKey { get; }
     public MyContext(string connectionString, string company) : base(connectionString) 
     { 
         Company = company;
         ModelCacheKey = company; //the identifier for the model this instance will use
     }

     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     {
         //This will create one model cache per key
         optionsBuilder.ReplaceService<IModelCacheKeyFactory, MyModelCacheKeyFactory();
     }

     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         modelBuilder.Entity<Order>(entity => 
         { 
             //regular entity mapping 
         });

         SetCustomConfigurations(modelBuilder);
     }

     public void SetCustomConfigurations(ModelBuilder modelBuilder)
     {
         //Here you will set the schema. 
         //In my example I am setting custom table name Order_CompanyX

         var entityType = typeof(Order);
         var tableName = entityType.Name + "_" + this.Company;
         var mutableEntityType = modelBuilder.Model.GetOrAddEntityType(entityType);
         mutableEntityType.RemoveAnnotation("Relational:TableName");
         mutableEntityType.AddAnnotation("Relational:TableName", suffixTableName);
     }
}

その結果、コンテキストの各インスタンスは、ModelCacheKey変数に基づいてefcoreをキャッシュします。

0
C Rudolph

MVC Core 2.1のアップデート

複数のスキーマを持つデータベースからモデルを作成できます。システムは、その命名においてスキーマにとらわれないビットです。同じ名前のテーブルには「1」が追加されます。 「dbo」は想定されるスキーマであるため、テーブル名の前にPMコマンドを付けて、何も追加しません。

モデルファイル名とクラス名を自分で変更する必要があります。

PMコンソール

Scaffold-DbContext "Data Source=localhost;Initial Catalog=YourDatabase;Integrated Security=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -force -Tables TableA, Schema1.TableA
0
pixelda