EF 和存储库模式 - 最终在一个控制器中出现多个 DbContext - 有任何问题(性能、数据完整性)吗?

2023-11-24

我对 ASP.NET MVC 3 的大部分了解来自于阅读 Adam Freeman 和 Steven Senderson 所著的《Pro ASP.NET MVC 3 Framework》一书。对于我的测试应用程序,我尝试非常严格地遵循他们的示例。我使用存储库模式加上 Ninject 和 Moq,这意味着单元测试工作得很好(即不需要从数据库中提取数据)。

在书中,存储库的使用方式如下:

public class EFDbTestChildRepository
{
    private EFDbContext context = new EFDbContext();

    public IQueryable<TestChild> TestChildren
    {
        get { return context.TestChildren; }
    }

    public void SaveTestChild(TestChild testChild)
    {
        if (testChild.TestChildID == 0)
        {
            context.TestChildren.Add(testChild);
        }
        else
        {
            context.Entry(testChild).State = EntityState.Modified;
        }
        context.SaveChanges();
    }
}

这是与之配套的 DbContext:

public class EFDbContext : DbContext
{
    public DbSet<TestParent> TestParents { get; set; }
    public DbSet<TestChild> TestChildren { get; set; }
}

请注意:为了在这个提取的示例中保持简单,我在这里省略了 Ninject 将使用的接口 ITestChildRepository。

在其他来源中,我看到了一种更通用的存储库方法,其中一个存储库足以满足整个应用程序的需要。显然,就我而言,我的应用程序中最终得到了相当多的存储库列表 - 基本上我的域模型中的每个实体都有一个存储库列表。不确定这两种方法的优缺点 - 我只是为了安全起见而遵循这本书。

最后回答我的问题:每个存储库都有自己的 DbContext -private EFDbContext context = new EFDbContext();。我是否有可能在一个请求中面临多个 DbContext 的风险?这会导致任何显着的性能开销吗?上下文之间潜在的冲突以及对数据完整性的任何后果如何?

这是一个示例,我最终在一个控制器中拥有多个存储库。

我的两个数据库表通过外键关系链接。我的领域模型类:

public class TestParent
{
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual ICollection<TestChild> TestChildren { get; set; }
}

public class TestChild
{
    public int TestChildID { get; set; }
    public int TestParentID { get; set; }
    public string Name { get; set; }
    public string Comment { get; set; }

    public virtual TestParent TestParent { get; set; }
}

Web 应用程序包含一个允许用户创建新 TestChild 的页面。其上有一个选择框,其中包含可供选择的可用 TestParents 列表。这就是我的控制器的样子:

public class ChildController : Controller
{
    private EFDbTestParentRepository testParentRepository = new EFDbTestParentRepository();
    private EFDbTestChildRepository testChildRepository = new EFDbTestChildRepository();

    public ActionResult List()
    {
        return View(testChildRepository.TestChildren);
    }

    public ViewResult Edit(int testChildID)
    {
        ChildViewModel cvm = new ChildViewModel();
        cvm.TestChild = testChildRepository.TestChildren.First(tc => tc.TestChildID == testChildID);
        cvm.TestParents = testParentRepository.TestParents;
        return View(cvm);
    }

    public ViewResult Create()
    {
        ChildViewModel cvm = new ChildViewModel();
        cvm.TestChild = new TestChild();
        cvm.TestParents = testParentRepository.TestParents;
        return View("Edit", cvm);
    }

    [HttpPost]
    public ActionResult Edit(TestChild testChild)
    {
        try
        {
            if (ModelState.IsValid)
            {
                testChildRepository.SaveTestChild(testChild);
                TempData["message"] = string.Format("Changes to test child have been saved: {0} (ID = {1})",
                                                        testChild.Name,
                                                        testChild.TestChildID);
                return RedirectToAction("List");
            }
        }
        catch (DataException)
        {
            //Log the error (add a variable name after DataException)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
        }

        // something wrong with the data values
        return View(testChild);
    }
}

仅有可用的 EFDbTestChildRepository 还不够,我还需要 EFDbTestParentRepository。它们都被分配给控制器的私有变量 - 瞧,在我看来,已经创建了两个 DbContext。或者说这是不正确的?

为了避免这个问题,我尝试使用 EFDbTestChildRepository 来访问 TestParents。但这显然只会带来那些已经连接到至少一个 TestChild 的人 - 所以不是我想要的。

这是视图模型的代码:

public class ChildViewModel
{
    public TestChild TestChild { get; set; }
    public IQueryable<TestParent> TestParents { get; set; }
}

如果我忘记添加一些相关代码,请告诉我。非常感谢您的建议!


不会出现性能问题(除非我们谈论的是纳秒,实例化上下文非常便宜)并且您不会损坏数据完整性(在此之前您会遇到异常)。

但这种方法非常有限,只能在非常简单的情况下起作用。多个上下文会导致许多场景下的问题。举个例子:假设您想为现有的父级创建一个新的子级,并尝试使用以下代码:

var parent = parentRepo.TestParents.Single(p => p.Id == 1);
var child = new Child { TestParent = parent };
childrenRepo.SaveTestChild(child);

这个简单的代码不起作用,因为parent已经附加到里面的上下文parentRepo but childrenRepo.SaveTestChild将尝试将其附加到内部的上下文childrenRepo这将导致异常,因为实体不得附加到另一个上下文。 (这实际上是一个解决方法,因为您可以设置 FK 属性而不是加载parent: child.TestParentID = 1。但如果没有 FK 属性,这将是一个问题。)

遇到这样的问题怎么解决呢?

一种方法可能是延长EFDbTestChildRepository通过一个新属性:

public IQueryable<TestParent> TestParents
{
    get { return context.TestParents; }
}

在上面的示例代码中,您可以只使用一个存储库,并且该代码可以工作。但正如您所看到的,名称“EFDbTestChildRepository”不再真正适合新存储库的目的。现在应该是“EFDbTest父子存储库”。

我会称之为聚合根一种方法,这意味着您不仅为一个实体创建一个存储库,而且为几个彼此密切相关且在它们之间具有导航属性的实体创建一个存储库。

另一种解决方案是将上下文注入存储库(而不是在存储库中创建它),以确保每个存储库都使用相同的上下文。 (上下文通常被抽象为IUnitOfWork接口。)示例:

public class MyController : Controller
{
    private readonly MyContext _context;
    public MyController()
    {
        _context = new MyContext();
    }

    public ActionResult SomeAction(...)
    {
        var parentRepo = new EFDbTestParentRepository(_context);
        var childRepo = new EFDbTestChildRepository(_context);

        //...
    }

    protected override void Dispose(bool disposing)
    {
        _context.Dispose();
        base.Dispose(disposing);
    }
}

这为每个控制器提供了一个上下文,您可以在多个存储库中使用。

下一步可能是通过依赖注入为每个请求创建一个上下文,例如......

private readonly MyContext _context;
public MyController(MyContext context)
{
    _context = context;
}

...然后配置 IOC 容器以创建单个上下文实例,该实例可能会注入到多个控制器中。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

EF 和存储库模式 - 最终在一个控制器中出现多个 DbContext - 有任何问题(性能、数据完整性)吗? 的相关文章

随机推荐