实体框架:一个数据库,多个dbcontext。这是个坏主意吗?

到目前为止,我的印象是DbContext代表你的数据库,因此,如果你的应用程序使用一个数据库,你只需要一个DbContext

然而,一些同事希望将功能区域分解为单独的DbContext类。

我相信这是出于一个好的原因——希望保持代码更干净——但它似乎不稳定。我的直觉告诉我这是个坏主意,但不幸的是,我的直觉并不是设计决策的充分条件。

所以我在寻找:

A)为什么这可能是一个坏主意的具体例子;

B)保证这一切都会顺利解决。

155658 次浏览

单个数据库可以有多个上下文。例如,如果您的数据库包含多个数据库模式,并且您希望将每个模式作为独立的自包含区域来处理,那么它就很有用。

问题是,当您想首先使用代码来创建数据库时,只有应用程序中的单个上下文可以做到这一点。这样做的技巧通常是一个额外的上下文,其中包含仅用于数据库创建的所有实体。仅包含实体子集的实际应用程序上下文必须将数据库初始化式设置为null。

在使用多种上下文类型时,还会遇到其他问题——例如共享实体类型及其从一个上下文传递到另一个上下文等等。一般来说,这是可能的,它可以使你的设计更干净,并分离不同的功能区域,但它的代价是额外的复杂性。

提醒:如果你组合了多个上下文,请确保你剪切粘贴了各种RealContexts.OnModelCreating()中的所有功能到你的单个CombinedContext.OnModelCreating()中。

我只是浪费时间寻找为什么我的级联删除关系没有被保存,却发现我没有将modelBuilder.Entity<T>()....WillCascadeOnDelete();代码从我的真实上下文中移植到我的组合上下文中。

当我看到这个设计时,我的直觉告诉我同样的事情 我正在编写一个代码库,其中一个数据库有三个dbcontext。3个dbcontext中的2个依赖于来自1个dbcontext的信息,因为它提供管理数据。这种设计限制了查询数据的方式。我遇到了这个问题,你不能跨dbcontext连接。相反,您需要做的是查询两个单独的dbcontext,然后在内存中进行连接或遍历这两个dbcontext,以获得两个dbcontext的组合作为结果集。这样做的问题是,您现在不是查询特定的结果集,而是将所有记录加载到内存中,然后对内存中的两个结果集进行连接。它真的会让事情变慢。

我会问"仅仅因为你可以,你应该吗?"

关于我遇到的与此设计相关的问题,请参阅这篇文章。 指定的LINQ表达式包含对与不同上下文关联的查询的引用 < / p >

首先,在代码中,可以有多个DBContext和一个数据库。您只需在构造函数中指定连接字符串。

public class MovieDBContext : DbContext
{
public MovieDBContext()
: base("DefaultConnection")
{


}
public DbSet<Movie> Movies { get; set; }
}

实现以下简单示例:

    ApplicationDbContext forumDB = new ApplicationDbContext();
MonitorDbContext monitor = new MonitorDbContext();

只在主上下文中作用域属性:(用于创建和维护DB) 注意:只使用protected:(此处未暴露实体)

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext()
: base("QAForum", throwIfV1Schema: false)
{


}
protected DbSet<Diagnostic> Diagnostics { get; set; }
public DbSet<Forum> Forums { get; set; }
public DbSet<Post> Posts { get; set; }
public DbSet<Thread> Threads { get; set; }
public static ApplicationDbContext Create()
{
return new ApplicationDbContext();
}


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
< p > MonitorContext: 在这里暴露单独的实体

public class MonitorDbContext: DbContext
{
public MonitorDbContext()
: base("QAForum")
{


}
public DbSet<Diagnostic> Diagnostics { get; set; }
// add more here
}

诊断模型:

public class Diagnostic
{
[Key]
public Guid DiagnosticID { get; set; }
public string ApplicationName { get; set; }
public DateTime DiagnosticTime { get; set; }
public string Data { get; set; }
}

如果您愿意,可以在主ApplicationDbContext中将所有实体标记为受保护的,然后根据需要为每个模式分离创建额外的上下文。

它们都使用相同的连接字符串,但是它们使用不同的连接,因此不要交叉事务并注意锁定问题。一般来说,你的设计是分离的,所以这种情况不应该发生。

我四年前写了这个答案,我的观点一直没有改变。但自那以后,微服务领域有了重大发展。我在最后添加了特定的微服务注释……

我会权衡反对这个想法,用实际经验来支持我的投票。

我曾参与一个大型应用程序,该应用程序为单个数据库提供了五个上下文。最后,我们删除了所有的上下文,只有一个例外——返回到单个上下文。

起初,多重上下文的想法似乎是个好主意。我们可以将数据访问划分为域,并提供几个干净的轻量级上下文。听起来像DDD,对吧?这将简化我们的数据访问。另一个理由是为了性能,因为我们只访问我们需要的上下文。

但在实践中,随着应用程序的增长,许多表在各种上下文之间共享关系。例如,对上下文1中的表A的查询也需要连接上下文2中的表B。

这给我们留下了一些糟糕的选择。我们可以在不同的上下文中复制这些表。我们试过了。这产生了几个映射问题,包括一个EF约束,它要求每个实体都有唯一的名称。因此,我们在不同的上下文中得到了名为Person1和Person2的实体。有人可能会说这是我们糟糕的设计,但尽管我们尽了最大的努力,这就是我们的应用程序在现实世界中实际发展的方式。

我们还尝试查询两个上下文以获得所需的数据。例如,我们的业务逻辑将从上下文1查询所需内容的一半,从上下文2查询所需内容的另一半。这有一些主要的问题。我们必须跨不同的上下文执行多个查询,而不是针对单个上下文执行一个查询。这有一个真正的性能损失。

最后,好消息是很容易剥离出多个上下文。上下文是一个轻量级对象。所以我不认为性能在多种情况下都是一个很好的理由。在几乎所有情况下,我认为单个上下文更简单、更简单,而且可能性能更好,您不必实现一堆变通方法来让它工作。

我想到了一种情况,在这种情况下,多个上下文可能是有用的。可以使用单独的上下文来修复数据库中实际包含多个域的物理问题。理想情况下,上下文与域是一对一的,域与数据库是一对一的。换句话说,如果一组表与给定数据库中的其他表没有任何关联,则可能应该将它们提取到单独的数据库中。我知道这并不总是实际的。但是,如果一组表如此不同,以至于您可以放心地将它们分离到一个单独的数据库中(但您选择不这样做),那么我可以看到使用单独上下文的情况,但这只是因为实际上有两个单独的域。

对于微服务,单一的上下文仍然是有意义的。然而,对于微服务,每个服务都有自己的上下文,其中只包括与该服务相关的数据库表。换句话说,如果服务x访问表1和表2,服务y访问表3和表4,那么每个服务都有自己的唯一上下文,其中包含特定于该服务的表。

我对你的想法很感兴趣。

另一点“智慧”。我有一个面向互联网和内部应用程序的数据库。每一个面向都有一个上下文。这有助于我保持纪律严明的隔离。

通过设置默认模式来区分上下文

在EF6中,你可以有多个上下文,只需在你的DbContext派生类的OnModelCreating方法中指定默认数据库模式的名称(Fluent-API配置所在的地方)。 这将在EF6工作:

public partial class CustomerModel : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("Customer");


// Fluent API configuration
}
}
这个例子将使用“Customer”作为数据库表的前缀(而不是“dbo”)。 更重要的是,它还将作为__MigrationHistory表的前缀,例如Customer.__MigrationHistory。 因此,在一个数据库中可以有多个__MigrationHistory表,每个上下文对应一个。 所以你对一个上下文所做的改变不会影响到另一个上下文

添加迁移时,在add-migration命令中指定配置类的完全限定名称(派生自DbMigrationsConfiguration)作为参数:

add-migration NAME_OF_MIGRATION -ConfigurationTypeName FULLY_QUALIFIED_NAME_OF_CONFIGURATION_CLASS

< br >

上下文键上的一个简短的单词

根据这篇MSDN文章,“章节-针对同一数据库的多个模型”EF 6可能会处理这种情况,即使只有一个MigrationHistory表存在,因为在表中有一个ContextKey列来区分迁移。

然而,我更喜欢通过指定如上所述的默认模式来拥有多个MigrationHistory表。

< br >

使用单独的迁移文件夹

在这种情况下,您可能还想在项目中使用不同的“Migration”文件夹。你可以使用MigrationsDirectory属性相应地设置你的DbMigrationsConfiguration派生类:

internal sealed class ConfigurationA : DbMigrationsConfiguration<ModelA>
{
public ConfigurationA()
{
AutomaticMigrationsEnabled = false;
MigrationsDirectory = @"Migrations\ModelA";
}
}


internal sealed class ConfigurationB : DbMigrationsConfiguration<ModelB>
{
public ConfigurationB()
{
AutomaticMigrationsEnabled = false;
MigrationsDirectory = @"Migrations\ModelB";
}
}

< br >

总结

总而言之,您可以说所有内容都被清晰地分离了:项目中的上下文、迁移文件夹和数据库中的表。

我会选择这样的解决方案,如果有一组实体是一个更大的主题的一部分,但彼此不相关(通过外键)。

如果实体组之间没有任何关联,我会为它们每个创建一个单独的数据库,并在不同的项目中访问它们,可能每个项目中都有一个上下文。

灵感来自@JulieLerman的DDD MSDN杂志文章2013

public class ShippingContext : BaseContext<ShippingContext>
{
  public DbSet<Shipment> Shipments { get; set; }
  public DbSet<Shipper> Shippers { get; set; }
  public DbSet<OrderShippingDetail> Order { get; set; } //Orders table
  public DbSet<ItemToBeShipped> ItemsToBeShipped { get; set; }
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    modelBuilder.Ignore<LineItem>();
    modelBuilder.Ignore<Order>();
    modelBuilder.Configurations.Add(new ShippingAddressMap());
  }
}

public class BaseContext<TContext>
DbContext where TContext : DbContext
{
static BaseContext()
{
Database.SetInitializer<TContext>(null);
}
protected BaseContext() : base("DPSalesDatabase")
{}
}

如果你正在进行新的开发,你想让Code First基于你的类来创建或迁移你的数据库,你将需要使用DbContext创建一个“超级模型”,其中包括构建一个表示数据库的完整模型所需的所有类和关系。但是,此上下文不能继承自BaseContext."莱托

我想分享一个案例,我认为在同一个数据库中有多个dbcontext的可能性是很有意义的。

我有一个解决方案与两个数据库。一个是除用户信息外的域数据。另一个仅用于用户信息。这种划分主要是由EU 一般保障资料规例驱动的。通过拥有两个数据库,我可以自由地移动域数据(例如,从Azure移动到我的开发环境),只要用户数据保持在一个安全的地方。

现在,对于用户数据库,我已经通过EF实现了两个模式。一个是AspNet身份框架提供的默认身份。另一个是我们自己实现任何与用户相关的东西。与扩展ApsNet模式相比,我更喜欢这种解决方案,因为我可以轻松地处理未来对AspNet身份的更改,同时这种分离使程序员清楚地知道,“我们自己的用户信息”应该放在我们定义的特定用户模式中。

嗯,花了相当多的时间在每个DB模式的独立DB上下文的问题上,希望它能帮助其他人…

我最近开始从事一个项目,其中一个数据库有3个模式(DB优先方法),其中一个用于用户管理。每个单独的模式都有一个DB上下文。当然,用户也与其他模式相关,例如。schema KB有一个表Topic,它有“由谁创建的”,“最后修改的是谁”等。FK到标识模式,表appuser。

这些对象是在c#中单独加载的,首先,topic是从一个上下文加载的,然后用户是通过用户id从另一个db上下文加载的-不太好,必须修复这个! (类似于使用多个dbcontext在同一个数据库与EF 6)

首先,我尝试将身份模式中缺失的FK指令添加到KB模式中,添加到KB DB上下文中的EF modelBuilder中。就像只有1个上下文一样,但我把它分离成2个。

modelBuilder.Entity<Topic>(entity =>
{
entity.HasOne(d => d.Creator)
.WithMany(p => p.TopicCreator)
.HasForeignKey(d => d.CreatorId)
.HasConstraintName("fk_topic_app_users");

它没有工作,因为kb db context没有任何关于用户对象的信息,postgres返回错误relation "AppUsers" does not exist。选择语句没有正确的模式,字段名等信息。

我几乎放弃了,但后来我注意到在运行dotnet ef dbcontext scaffold时切换了“-d”。它是-data-annotations的缩写-使用属性配置模型(在可能的情况下)。如果省略,则只使用fluent API。 通过指定这个开关,对象属性不是在db上下文OnModelCreating()中定义的,而是在对象本身上定义的,带有属性

通过这种方式,EF获得了足够的信息,可以用正确的字段名和模式生成正确的SQL语句。

单独的DB上下文不能很好地处理它们之间的关系(FKs),每个上下文只有关于自己实体的信息。 当在dotnet ef dbcontext scaffold上指定"-data-annotations"开关时,这些信息不会存储在每个单独的上下文中,而是存储在DB对象本身