Entity Framework:一个数据库,多个DbContexts。这是一个不好的想法吗?

273

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

然而,一些同事想将功能区域拆分成单独的DbContext类。

我认为这源于善意——希望保持代码更加清晰——但它似乎不稳定。我的直觉告诉我这是个坏主意,但不幸的是,我的直觉并不足以成为设计决策的充分条件。

所以我正在寻找:

A)具体例子说明这可能是个坏主意;

B)保证这将会很好地解决问题。


1
请查看我的回答:https://dev59.com/SWsy5IYBdhLWcg3w5CFr#18714942 - Mohsen Alikhani
11个回答

214

你可以为单个数据库拥有多个上下文。这在数据库包含多个数据库架构且你想将它们作为独立的区域处理时非常有用。

问题在于当你想使用Code First创建你的数据库时,应用程序中只能有一个上下文来完成。通常解决方法是添加一个额外的上下文,其中包含所有实体,仅用于数据库创建。你真正的应用程序上下文只包含实体的子集,必须将其数据库初始化器设置为 null。

在使用多个上下文类型时,你还会遇到其他问题,例如共享实体类型及其从一个上下文传递到另一个上下文等。一般而言,这是可能的,它可以使你的设计更加清晰,并将不同的功能区域分离,但它会增加额外的复杂性成本。


27
如果应用程序具有许多实体/表,则使用每个应用程序一个上下文可能会很昂贵。因此,根据模式,拥有多个上下文也可能是有意义的。 - DarthVader
9
由于我没有订阅Pluralsight,所以在这个问题的回答之后,我发现了这篇由Julie Lerman撰写的精彩文章([她的评论](http://stackoverflow.com/a/12625918/65716) ),它非常适合:http://msdn.microsoft.com/en-us/magazine/jj883952.aspx - Dave T.
我建议Entity Framework通过命名约定来支持在同一数据库中使用多个DbContext。因此,为了实现模块化应用程序的目的,我仍在编写自己的ORM。很难相信它会强制单个应用程序使用单个数据库。特别是在Web农场中,您只有有限数量的数据库可用。 - freewill
11
不确定在你发表评论和现在之间是否有所改变,但是现在Enable-Migrations -ContextTypeName MyContext -MigrationsDirectory Migrations\MyContextMigrations已经可以使用了。 - Zack
为什么必须将数据库初始化器设置为null? +1。 - w0051977
显示剩余5条评论

99

我大约四年前写过这篇答案,我的观点没有改变。但是自那以后,微服务领域出现了重大的发展。我在结尾处添加了微服务特定的注释...

我将反对这个想法,并用真实世界的经验来支持我的观点。

我被引进到一个有五个上下文的单个数据库的大型应用程序中。最终,我们除了一个上下文之外,删除了所有上下文-回归到一个上下文。

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

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

这使我们面临几个糟糕的选择。我们可以在各种情况下复制这些表。我们尝试过这样做。这创建了几个映射问题,包括EF约束,要求每个实体都有一个唯一的名称。因此,在不同的上下文中,我们最终得到了名为Person1和Person2的实体。有人可能会认为这是我们设计不良的结果,但是尽管我们尽了最大努力,但实际上我们的应用程序就是这样在现实世界中发展的。

我们还尝试查询两个上下文以获取所需的数据。例如,我们的业务逻辑从上下文1和上下文2分别查询部分所需内容。这产生了一些主要问题。与单个上下文执行一次查询不同,我们必须在不同的上下文中执行多个查询。这具有真正的性能惩罚。

最后,好消息是,轻松剥离多个上下文。上下文旨在成为轻量级对象。所以我认为性能不是多个上下文的好论点。在几乎所有情况下,我认为一个单一的上下文更简单,更少复杂,并且可能表现更好,而且您不必实现一堆解决方法才能使其起作用。

我想到了一种使用多个上下文(context)的情况。可以使用一个单独的上下文来解决数据库中实际包含多个域名(domain)的物理问题。理想情况下,一个上下文应该是一对一地与一个域名相关联,而这个域名又应该是一对一地与一个数据库相关联。换句话说,如果一组表与给定数据库中的其他表没有任何关系,它们可能应该被拆分到一个单独的数据库中。我意识到这并不总是切实可行的,但如果一组表如此不同以至于您感到舒适将它们分开成一个单独的数据库(但您选择不这样做),那么我可以看出使用单独的上下文的理由,仅因为实际上有两个独立的域。

关于微服务(micro-services),仍然需要使用一个单一的上下文(context)。但是,对于微服务,每个服务都将拥有其自己的上下文,其中仅包括与该服务相关的数据库表。换句话说,如果服务x访问表1和2,而服务y访问表3和4,则每个服务将拥有其自己独特的上下文,其中只包括特定于该服务的表。

我很想听听您的想法。


13
我必须在这里同意,特别是当针对现有数据库时。我正在解决这个问题,目前我的直觉是:
  1. 在多个上下文中拥有相同的物理表是一个坏主意。
  2. 如果我们无法决定一个表属于哪个上下文,那么这两个上下文并不足够不同以进行逻辑分离。
- jkerak
3
在进行命令查询责任分离(CQRS)时,我认为你不会在上下文之间建立任何关系(每个视图都可以拥有自己的上下文),因此这个警告并不适用于每个可能需要多个上下文的情况。取而代之的是,使用数据复制来处理每个上下文,而非联接和引用。- 然而,这并不否定这个答案的有用性 :) - urbanhusky
1
我深刻地感受到你所面临的痛苦!:/ 我也认为一个上下文对于简单性来说是更好的选择。 - ahmet
10
在我看来,似乎你没有完全按照领域驱动设计(DDD)的理念实施,如果你的聚合需要了解其他聚合,那么就需要引用它们。如果你需要引用某些东西,有两个原因:它们可能属于同一个聚合,这意味着它们必须在同一个事务中被更改;或者它们不是同一个聚合,这意味着你的边界划分有误。 - modmoto
1
当你为每个客户端使用单独的数据库,并且采用Azure Function的消费计划等无服务器方法时,这会带来真正的性能惩罚。除非您以某种方式缓存连接,在每次函数调用时初始化多个数据库连接将会是一个真正的开销。然而,每次加载整个模式的单个上下文也会产生一些负担。 - tehmas
显示剩余2条评论

62

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

在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


关于上下文键的简短说明

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

然而,我更喜欢通过像上面解释的那样指定默认架构来拥有多个 MigrationHistory表。


使用单独的迁移文件夹

在这种情况下,您可能还希望在项目中使用不同的“迁移”文件夹。 您可以使用 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";
    }
}


摘要

总体而言,你可以说一切都被清晰地分开了:在项目中的上下文、迁移文件夹和数据库中的表格。

如果有一些实体组是属于更大主题的一部分,但它们彼此之间没有关联(通过外键),我会选择这种解决方案。

如果这些实体组彼此之间没有任何关系,我将为每个实体组创建一个单独的数据库,并在不同的项目中访问它们,可能每个项目只使用一个单一的上下文。


1
当您需要更新位于不同上下文中的2个实体时,您该怎么办? - yakya
我会创建一个新的(服务)类,该类了解两个上下文,考虑好它的名称和职责,并在其中一个方法中进行更新。 - Martin

9

以下是实现此目标的简单示例:

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

只需在主上下文中限定属性:(用于创建和维护数据库) 注意:只需使用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);
    }
}

监视上下文:在此处公开单独实体

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中将所有实体标记为受保护的,然后根据需要为每个模式分离创建额外的上下文。

它们都使用相同的连接字符串,但是它们使用单独的连接,因此不会交叉事务,并且要注意锁定问题。通常情况下,您设计分离以使其不发生。


2
这非常有帮助。"次要"上下文不需要声明共享表。只需手动添加它的 DbSet<x> 定义即可。我在与 EF 设计器相匹配的部分类中执行此操作。 - Glen Little
1
您解决了我的大问题,先生!您提供了一个具体的解决方案,而不是被接受的答案。非常感谢! - ˈvɔlə

6

@JulieLerman 2013年MSDN杂志上的DDD文章的启发

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")
  {}
}   

"If you’re doing new development and you want to let Code First create or migrate your database based on your classes, you’ll need to create an “uber-model” using a DbContext that includes all of the classes and relationships needed to build a complete model that represents the database. However, this context must not inherit from BaseContext." JL
如果您正在进行新开发,并希望让Code First根据您的类创建或迁移数据库,则需要使用包含所有必要类和关系以构建表示数据库的完整模型的DbContext创建“超级模型”。但是,此上下文不得继承自BaseContext。

有一个基础上下文和两个派生上下文(例如ShippingContext和SomeOtherContext),是否可以在派生的上下文之间定义查询,例如_context.Shipments.Select(shipment => shipment.OtherContexts).ToList()?我需要如何将这个baseContext添加到DI中呢? - SNO
很好的问题。如果你自己尝试过并通过实验找到了答案,可以随意在这里发布答案。你也可以在SO上发布一个新的问题。原始的MSDNMagazine文章在这里:https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/january/data-points-shrink-ef-models-with-ddd-bounded-contexts - OzBob

6

提醒:如果您要组合多个上下文,请确保将各种功能从您的各个RealContexts.OnModelCreating()剪切并粘贴到您的单个CombinedContext.OnModelCreating()中。

我刚刚浪费了时间寻找为什么我的级联删除关系没有被保存,最后才发现我没有将modelBuilder.Entity<T>()....WillCascadeOnDelete();代码从我的实际上下文传递到我的组合上下文。


9
您可以直接从合并的上下文中调用OtherContext.OnModelCreating(),而不是剪切和粘贴代码。 - Alex

5

当我看到这个设计时,我的直觉也告诉我同样的事情。

我正在处理一个代码库,其中有三个dbContext与一个数据库相关联。其中3个dbcontexts中的2个是依赖于1个dbcontext提供的管理数据的信息的。这种设计对查询数据的方式施加了限制。我遇到了这样的问题:您不能跨越dbcontexts进行连接。相反,您需要查询两个单独的dbcontexts,然后在内存中进行连接或迭代这两个结果集以获取这两个结果集的组合作为结果集。问题在于,现在您不仅要查询特定的结果集,而且还要将所有记录加载到内存中,然后在内存中针对两个结果集进行连接。这可能会使速度变慢。

我想问的问题是“你能做到吗,你应该这样做吗?”

查看此文章,了解我遇到与此设计相关的问题。 指定的LINQ表达式包含与不同上下文相关联的查询的引用


3
我曾经在一个大型系统上工作,其中我们有多个上下文。我发现有时你需要在多个上下文中包含相同的DbSet。这一方面破坏了一些纯洁性方面的考虑,但它确实允许你完成查询。例如,在需要读取某些管理员表的情况下,您可以将它们添加到基本的DbContext类中,并在应用程序模块上下文中进行继承。您的“真正的”管理上下文可能被重新定义为“为管理员表提供维护”,而非提供对它们的所有访问权限。 - JMarsch
2
说实话,我总是在权衡利弊。一方面,使用单独的上下文,对于只想在一个模块上工作的开发人员来说,需要了解的内容更少,而且您可以更安全地定义和使用自定义投影(因为您不必担心它对其他模块的影响)。另一方面,当您需要跨上下文共享数据时,确实会遇到一些问题。 - JMarsch
2
你不需要在两者中都包含实体,你可以始终获取ID并向不同上下文提出第二个查询。对于小系统来说,这是不好的,对于具有许多开发人员的大型数据库/系统而言,多表结构的一致性比2个查询更难维护和解决问题。 - user1496062

3

另一条“智慧”的建议。我有一个面向互联网和内部应用程序的数据库。我为每个界面都设置了上下文,这可以帮助我保持严格的安全隔离。


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

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

现在对于用户数据库,我已经通过EF实现了两个模式。一个是AspNet Identity框架提供的默认模式。另一个是我们自己实现的其他与用户相关的内容。我更喜欢这种解决方案,而不是扩展ApsNet模式,因为我可以轻松处理将来对AspNet Identity的更改,并且同时分离使程序员清楚,“我们自己的用户信息”进入我们定义的特定用户模式。


2
我没有在我的回复中看到任何问题。我并不是在问一个单一的问题!相反,我分享了一个情景,在这个讨论的主题是有意义的。 - freilebt

2
在Code First中,您可以拥有多个DBContext,但只有一个数据库。您只需要在构造函数中指定连接字符串即可。
public class MovieDBContext : DbContext
{
    public MovieDBContext()
        : base("DefaultConnection")
    {

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

1
可以的,但是你怎么从不同的数据库上下文中查询不同的实体呢? - Reza
数据库优先方法是什么意思? - m0a

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接