Entity Framework 的模型缓存使其无法处理大量数据库模式
Entity Framework's model caching makes it useless with large amounts of database schemas
我使用的 SaaS 产品具有较大的用户群。到目前为止,我们隔离客户数据的方法一直是拥有客户特定的数据库。这对 Entity Framework 6 非常有效,因为我们需要做的就是将客户特定的连接字符串传递给 DbContext
,一切正常。
由于与这个问题无关的原因,我们需要摆脱每个客户模型的这个数据库。从数据隔离的角度来看,每个客户拥有一个数据库模式而不是每个客户拥有一个数据库似乎是个好主意。在做了一些测试之后,当我们谈论大量不同的模式时,它似乎几乎无法使用。
这是一个关于我们目前如何使用 DbContext
:
的简化示例
public class CustomDbContext : DbContext
public CustomDbContext(IConnectionStringProvider provider)
: base(provider.ConnectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new SomeEntityMap());
modelBuilder.Configurations.Add(new SomeOtherEntityMap());
}
}
下面是我们认为它如何工作的示例:
public class CustomDbContext : DbContext, IDbModelCacheKeyProvider
public CustomDbContext(IConnectionStringProvider provider)
: base(provider.ConnectionString)
{
CacheKey = provider.Schema;
}
public string CacheKey { get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(CacheKey);
modelBuilder.Configurations.Add(new SomeEntityMap());
modelBuilder.Configurations.Add(new SomeOtherEntityMap());
}
}
Microsoft 非常友好地允许绕过数据库模型的默认缓存。使用模式名称作为缓存键强制 Entity Framework 为每个模式创建一个新模型。理论上这是可行的。实际上,并非如此。我创建了一个测试应用程序,该应用程序向导致实例化 DbContext
的服务发出请求。它从一组 5000 个键中随机化 CacheKey
,所以基本上当应用程序首次启动时,几乎每个请求都会导致调用 OnModelCreating()
。在发出几百次请求后,IIS 工作进程耗尽了所有可用内存(使用了大约 9 GB),CPU 使用率接近 100%,服务几乎停止。
我查看了 Entity Framework 源代码,并希望将空字符串与模型构建器的 HasDefaultSchema()
一起使用会使 EF 使用数据库用户的默认架构。然后我们可以只缓存一个模型并通过为每个客户的数据库凭据设置默认模式来获得模式 "defined in connection string"。但是,如果架构为空字符串,EF 将引发异常。
所以问题是,有没有人遇到同样的问题,如果有,你是如何解决的?如果解决方案只是分叉 Entity Framework,我将不胜感激任何关于所需更改范围的见解。
感谢 Ivan Stoev 为我指明了正确的方向。拦截器绝对是解决这个问题最简单的方法。测试了 1000 个连续请求,使用拦截器时对执行时间没有明显影响。如果没有额外的工作,此方法将不适用于 EF 迁移,但由于我们不使用它,所以这不是问题。
编辑:对示例做了一些修复
这是一个似乎可以解决问题的示例:
public class CustomDbContext : DbContext
{
static CustomDbContext()
{
Database.SetInitializer<CustomDbContext>(null);
DbInterception.Add(new SchemaInterceptor());
}
public CustomDbContext(IConnectionStringProvider provider)
: base(provider.ConnectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("RemoveThisDefaultSchema");
modelBuilder.Configurations.Add(new SomeEntityMap());
modelBuilder.Configurations.Add(new SomeOtherEntityMap());
}
}
public class SchemaInterceptor : IDbCommandInterceptor
{
public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
}
public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
command.CommandText = command.CommandText.Replace("[RemoveThisDefaultSchema].", string.Empty);
}
public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
}
public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
command.CommandText = command.CommandText.Replace("[RemoveThisDefaultSchema].", string.Empty);
}
public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
}
public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
command.CommandText = command.CommandText.Replace("[RemoveThisDefaultSchema].", string.Empty);
}
}
我使用的 SaaS 产品具有较大的用户群。到目前为止,我们隔离客户数据的方法一直是拥有客户特定的数据库。这对 Entity Framework 6 非常有效,因为我们需要做的就是将客户特定的连接字符串传递给 DbContext
,一切正常。
由于与这个问题无关的原因,我们需要摆脱每个客户模型的这个数据库。从数据隔离的角度来看,每个客户拥有一个数据库模式而不是每个客户拥有一个数据库似乎是个好主意。在做了一些测试之后,当我们谈论大量不同的模式时,它似乎几乎无法使用。
这是一个关于我们目前如何使用 DbContext
:
public class CustomDbContext : DbContext
public CustomDbContext(IConnectionStringProvider provider)
: base(provider.ConnectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new SomeEntityMap());
modelBuilder.Configurations.Add(new SomeOtherEntityMap());
}
}
下面是我们认为它如何工作的示例:
public class CustomDbContext : DbContext, IDbModelCacheKeyProvider
public CustomDbContext(IConnectionStringProvider provider)
: base(provider.ConnectionString)
{
CacheKey = provider.Schema;
}
public string CacheKey { get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(CacheKey);
modelBuilder.Configurations.Add(new SomeEntityMap());
modelBuilder.Configurations.Add(new SomeOtherEntityMap());
}
}
Microsoft 非常友好地允许绕过数据库模型的默认缓存。使用模式名称作为缓存键强制 Entity Framework 为每个模式创建一个新模型。理论上这是可行的。实际上,并非如此。我创建了一个测试应用程序,该应用程序向导致实例化 DbContext
的服务发出请求。它从一组 5000 个键中随机化 CacheKey
,所以基本上当应用程序首次启动时,几乎每个请求都会导致调用 OnModelCreating()
。在发出几百次请求后,IIS 工作进程耗尽了所有可用内存(使用了大约 9 GB),CPU 使用率接近 100%,服务几乎停止。
我查看了 Entity Framework 源代码,并希望将空字符串与模型构建器的 HasDefaultSchema()
一起使用会使 EF 使用数据库用户的默认架构。然后我们可以只缓存一个模型并通过为每个客户的数据库凭据设置默认模式来获得模式 "defined in connection string"。但是,如果架构为空字符串,EF 将引发异常。
所以问题是,有没有人遇到同样的问题,如果有,你是如何解决的?如果解决方案只是分叉 Entity Framework,我将不胜感激任何关于所需更改范围的见解。
感谢 Ivan Stoev 为我指明了正确的方向。拦截器绝对是解决这个问题最简单的方法。测试了 1000 个连续请求,使用拦截器时对执行时间没有明显影响。如果没有额外的工作,此方法将不适用于 EF 迁移,但由于我们不使用它,所以这不是问题。
编辑:对示例做了一些修复
这是一个似乎可以解决问题的示例:
public class CustomDbContext : DbContext
{
static CustomDbContext()
{
Database.SetInitializer<CustomDbContext>(null);
DbInterception.Add(new SchemaInterceptor());
}
public CustomDbContext(IConnectionStringProvider provider)
: base(provider.ConnectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("RemoveThisDefaultSchema");
modelBuilder.Configurations.Add(new SomeEntityMap());
modelBuilder.Configurations.Add(new SomeOtherEntityMap());
}
}
public class SchemaInterceptor : IDbCommandInterceptor
{
public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
}
public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
command.CommandText = command.CommandText.Replace("[RemoveThisDefaultSchema].", string.Empty);
}
public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
}
public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
{
command.CommandText = command.CommandText.Replace("[RemoveThisDefaultSchema].", string.Empty);
}
public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
}
public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
{
command.CommandText = command.CommandText.Replace("[RemoveThisDefaultSchema].", string.Empty);
}
}