如何在EF Code First中处理一个用于多个数据库上下文的类?

3
我正在尝试在我的仓库层中使用EF创建一些模块类库API。为了使所有这些工作正常,我需要在每个类库中都有一个dbcontext类。但是如果我需要一个类被每个模块引用怎么办?例如,假设我有一个用户模块,其db上下文包括:
  • 用户
  • 角色
然后我有一个位置模块,包括:
  • 建筑物
  • 位置
  • 房间
接下来是一个设备的第三个模块,它包括:
  • 设备
  • 设备类型
  • 工单
后面两个模块仍然需要与用户相关联,这几乎是每个模块必不可少的部分。但是我不能向指向同一个数据库的两个上下文添加两个不同的用户类,因为它们可能会失去同步。所以,很明显的解决方案是使后两个模块需要用户模块,并且那些需要用户的模块中的任何类都引用userID。虽然这样做会破坏规范化,因为它不是外键,所以我不确定这个想法有多好。
我脑海中另一个可能性是让每个模块的dbcontext使用一个接口,并允许使用该模块的人声明自己的dbcontext并实现所有这些成员,但我不确定这也能起作用。
我基本上只想制作一个类库模块集合,定义一组通用类和API调用供其他程序员使用,并以EF为基础来存储所有内容在一个DB中。但是我不太确定如何通过DbContexts使其实现。当您有多个模块需要相同的对象时会发生什么?

不确定这是否正是您所需要的,但请参阅引用此处的Lehrman文章:https://dev59.com/coLba4cB1Zd3GeqPir1m - Steve Greene
那个答案看起来很有趣,但是当他们尝试为每个上下文更新迁移时,我可以预见到问题。不过也许还是有解决方法的。 - SventoryMang
她在文章中提到的“超级模型”中解决了这个问题。其他上下文不进行迁移。 - Steve Greene
2个回答

4

您所代表的三种上下文通常与Bounded Context的设计相匹配,正如Steeve所指出的那样,这是在领域驱动设计中设计的。

很明显,有多种方法来实现这种情况,每种方法都有其优缺点。

我建议采用两种方法来遵守领域驱动设计的最佳实践,并具有极大的灵活性。

方法一:软分离

我在第一个有界上下文中定义了User类,并在第二个有界上下文中定义了表示用户引用的接口。

让我们定义用户:

class User
{
    [Key]
    public Guid Id { get; set; }

    public string Name { get; set; }
}

其他引用用户的模型实现了 IUserRelated 接口:

interface IUserRelated
{
    [ForeignKey(nameof(User))]
    Guid UserId { get; }
}

设计模式建议不直接链接来自两个分离的有界上下文的两个实体,而是存储它们各自的引用。
Building类的代码如下:
class Building : IUserRelated
{
    [Key]
    public Guid Id { get; set; }

    public string Location { get; set; }
    public Guid UserId { get; set; }
}

正如您所看到的,Building模型仅知道User的引用。尽管如此,该接口充当外键并约束插入到UserId属性中的值。
现在让我们定义数据库上下文...
class BaseContext<TContext> : DbContext where TContext : DbContext
{
    static BaseContext()
    {
        Database.SetInitializer<TContext>(null);
    }

    protected BaseContext() : base("Demo")
    {

    }
}

class UserContext : BaseContext<UserContext>
{
    public DbSet<User> Users { get; set; }
}

class BuildingContext : BaseContext<BuildingContext>
{
    public DbSet<Building> Buildings { get; set; }
}

还有初始化数据库的db上下文:

class DatabaseContext : DbContext
{
    public DbSet<Building> Buildings { get; set; }
    public DbSet<User> Users { get; set; }

    public DatabaseContext() : base("Demo")
    {
    }
}

最后,创建用户和建筑物的代码:

// Defines some constants
const string userName = "James";
var userGuid = Guid.NewGuid();

// Initialize the db
using (var db = new DatabaseContext())
{
    db.Database.Initialize(true);
}

// Create a user
using (var userContext = new UserContext())
{
    userContext.Users.Add(new User {Name = userName, Id = userGuid});
    userContext.SaveChanges();
}

// Create a building linked to a user
using (var buildingContext = new BuildingContext())
{
    buildingContext.Buildings.Add(new Building {Id = Guid.NewGuid(), Location = "Switzerland", UserId = userGuid});
    buildingContext.SaveChanges();
}

方法二:硬分离

在每个有界上下文中,我定义了一个User类。接口强制实现共同属性。Martin Fowler在下面的示例中说明了这种方法:

enter image description here

用户有界上下文:

public class User : IUser
{
    [Key]
    public Guid Id { get; set; }

    public string Name { get; set; }
}

public class UserContext : BaseContext<UserContext>
{
    public DbSet<User> Users { get; set; }
}

构建有界上下文:

public class User : IUser
{
    [Key]
    public Guid Id { get; set; }
}

public class Building
{
    [Key]
    public Guid Id { get; set; }

    public string Location { get; set; }

    public virtual User User { get; set; }
}

public class BuildingContext : BaseContext<BuildingContext>
{
    public DbSet<Building> Buildings { get; set; }

    public DbSet<User> Users { get; set; }
}

在这种情况下,在BuildingContext中拥有一个Users属性是完全可以接受的,因为用户也存在于建筑物的上下文中。
用法:
    // Defines some constants
    const string userName = "James";
    var userGuid = Guid.NewGuid();

    // Create a user
    using (var userContext = new UserContext())
    {
        userContext.Users.Add(new User { Name = userName, Id = userGuid });
        userContext.SaveChanges();
    }

    // Create a building linked to a user
    using (var buildingContext = new BuildingContext())
    {
        var userReference = buildingContext.Users.First(user => user.Id == userGuid);

        buildingContext.Buildings.Add(new Building { Id = Guid.NewGuid(), Location = "Switzerland", User = userReference });
        buildingContext.SaveChanges();
    }

操作EF迁移非常容易。以下是用户边界上下文的迁移脚本(由EF生成):

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Users",
            c => new
                {
                    Id = c.Guid(nullable: false),
                    Name = c.String(),
                })
            .PrimaryKey(t => t.Id);

    }

    public override void Down()
    {
        DropTable("dbo.Users");
    }
}

建筑有界上下文的迁移脚本(由 EF 生成)。我必须删除表 Users 的创建,因为其他有界上下文有创建它的责任。您仍然可以在创建它之前检查表是否存在,以实现模块化方法:

public partial class Initial : DbMigration
{
    public override void Up()
    {
        CreateTable(
            "dbo.Buildings",
            c => new
                {
                    Id = c.Guid(nullable: false),
                    Location = c.String(),
                    User_Id = c.Guid(),
                })
            .PrimaryKey(t => t.Id)
            .ForeignKey("dbo.Users", t => t.User_Id)
            .Index(t => t.User_Id);
    }

    public override void Down()
    {
        DropForeignKey("dbo.Buildings", "User_Id", "dbo.Users");
        DropIndex("dbo.Buildings", new[] { "User_Id" });
        DropTable("dbo.Users");
        DropTable("dbo.Buildings");
    }
}

应用 Upgrade-Database 命令,为两个上下文(contexts)更新数据库即可!

针对“用户”类(User)新增属性的请求进行编辑。

当一个有界上下文(bounded context)向“用户”类(User)添加新属性时,它会逐步在后台添加新列,而不是重新定义整个表。这就是为什么此实现非常灵活的原因。

以下是迁移脚本示例,在有界上下文(bounded context)“Building”中给“用户”类(User)添加了新属性 Accreditation

public partial class Accreditation : DbMigration
{
    public override void Up()
    {
        AddColumn("dbo.Users", "Accreditation", c => c.String());
    }

    public override void Down()
    {
        DropColumn("dbo.Users", "Accreditation");
    }
}

这种方法的问题在于它仍然需要一个第三个总体“全合一”的上下文来执行迁移。这有点违背了模块式的选择所需的方法。你仍然需要创建自己的上下文并将所有对象放入其中。这并不是很糟糕,除非你已经有了其他特定的上下文。 - SventoryMang
嘿@DOTang,我编辑了我的答案,提供了* Hard-separation *方法,使您可以自由地使用EF迁移脚本。 - Jämes
但是,当应用第三个迁移时,它不会从第二个模块的迁移中删除用户特定属性吗?假设第二个和第三个都有唯一的用户特定属性。假设建筑上下文的用户引用了一个新的“薪资”对象,而第三个上下文的用户引用了一个新的“状态”对象。如果我运行第二个然后是第三个迁移,那么它不会删除薪资对象吗? - SventoryMang
等一下,我认为我可以回答“不是”。因为第三个迁移脚本没有关于Salary对象的知识,所以也就没有需要删除它的代码行。我说得对吗? - SventoryMang
当然可以!迁移脚本是逐步执行的(这非常强大)。我已经编辑了我的答案,请在底部查看。 - Jämes
显示剩余3条评论

-1
  1. 关于DDD有一个非常好的解释,你可以在这里找到:https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/january/data-points-shrink-ef-models-with-ddd-bounded-contexts
  2. 但是我自己使用了一种非常简单的方法来处理两个分离的数据库:
  • 为所有类定义一个数据模型,
  • 为所有类定义一个基础上下文,
  • 使用迁移工具生成两个相同的分离数据库,每个数据库都有自己的上下文
  • 将共享数据从数据库1复制到数据库2 => 这可能是一种非常原始的方法,但它非常稳定!

这对于任何在线系统都行不通,而数据库复制对于这个问题来说过于复杂了。 - Gert Arnold

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