Entity Framework 上下文生命周期相关问题

22

我对在ASP.NET MVC应用程序中使用Entity Framework上下文的期望生命周期有一些疑问。将上下文保持存活状态的时间尽可能缩短难道不是最好的吗?

考虑以下控制器操作:

public ActionResult Index()
{
    IEnumerable<MyTable> model;

    using (var context = new MyEntities())
    {
        model = context.MyTable;
    }

    return View(model);
}

上面的代码不起作用是因为实体框架上下文在视图渲染页面时已经超出了作用域。其他人会如何构建上面的代码结构?


model = context.MyTable.ToList() - ToList() 会执行你的查询。在你的情况下,IQueryable确实不能在上下文范围之外工作。 - Andrei
更新:有一篇来自微软的专门文章,内容完全相同:https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/在许多Web应用程序中,每个HTTP请求对应于一个单元工作。这使得将上下文生命周期绑定到请求的生命周期成为Web应用程序的良好默认设置。ASP.NET Core应用程序使用依赖注入进行配置。可以在Startup.cs的ConfigureServices方法中使用AddDbContext将EF Core添加到此配置中。例如:... - Avi
4个回答

53

让我们有些争议!

我不同意一般的MVC + EF共识,即在整个请求过程中保持上下文的活动状态是一个好事情,原因如下:

性能提升微不足道 你知道创建一个新的数据库上下文有多么昂贵吗?好吧... "DataContext轻量级,创建起来并不昂贵" 这是从 MSDN 上得出的结论。

IoC容器设置错误会导致问题隐藏直到正式上线 如果您设置了IoC容器以代替您处理上下文的销毁,并且设置错误,那么后果将非常严重。我现在已经两次看到由于IoC容器未始终正确地处理上下文销毁而导致的大规模内存泄漏。您不会意识到您设置了错误,直到您的服务器在正常并发用户水平下开始崩溃。这种问题在开发阶段不会出现,因此请进行一些负载测试!

意外的延迟加载 您返回一个最近文章的IQueryable,以便在主页上列出它们。有一天,别人被要求显示每篇文章旁边的评论数量。于是他们在视图中添加了一段简单的代码来显示评论计数,如下所示...

@foreach(var article in Model.Articles) {
    <div>
        <b>@article.Title</b> <span>@article.Comments.Count() comments</span>
    </div>
}

看起来不错,运行也正常。但是实际上您没有包含返回数据中的评论,因此现在每篇文章都会进行新的数据库调用。SELECT N+1 问题。10篇文章=11个数据库调用。好吧,代码是错误的,但这是一个容易犯的错误,所以会发生。

您可以通过在数据层关闭上下文来防止这种情况发生。但文章.Comments.Count()会不会出现NullReferenceException呢?是的,它会强制您编辑Data层以获得View层所需的数据。这就是应该的做法。

代码异味 从View中访问数据库就有些不对劲了。您知道IQueryable实际上还没有访问数据库吗?所以忘掉那个对象。确保在离开数据层之前访问您的数据库。

所以答案是

我的意见是,您的代码应该像这样:

DataLayer:

public List<Article> GetArticles()
{
    List<Article> model;

    using (var context = new MyEntities())
    {
        //for an example I've assumed your "MyTable" is a table of news articles
        model = (from mt in context.Articles
                select mt).ToList();
        //data in a List<T> so the database has been hit now and data is final
    }

    return model;
}

控制器:

public ActionResult Index()
{
    var model = new HomeViewModel(); //class with the bits needed for you view
    model.Articles = _dataservice.GetArticles(); //irrelevant how _dataService was intialised
    return View(model);
}

一旦你已经完成并理解了这个,也许你可以开始尝试使用一个IoC容器来处理上下文,但绝对不要在此之前。请听取我的警告-我见过两个大规模失败的案例 :)

但坦白说,做你想做的,编程是有趣的,并且应该是个人喜好的问题。我只是告诉你我的看法。但无论你做什么,不要因为"所有酷酷的孩子都在用它"而开始每个控制器或请求一个IoC上下文。要做到这点,是因为你真正关心它的好处,并理解如何正确地使用它。


4
我认为值得引用MSDN中的整段内容:“一般来说,DataContext实例设计用于持续一个“工作单元”,无论你的应用程序如何定义它。DataContext是轻量级的,创建成本不高。典型的LINQ to SQL应用程序在方法范围内或作为代表相关数据库操作逻辑集合的短暂类的成员创建DataContext实例。” - David Hammond
1
@BritishDeveloper:问题关乎System.Data.Entity命名空间下的DbContext(实体框架DbContext类),我在 MSDN 的描述中并没有找到任何“轻量级”的字眼。您有其他的证据吗? - Vladislav Kostenko
1
啊,这是两年前的事了,我懒得再为你找更多证据了。不过,我确实有几年的专业经验,使用这种方法在大型网站上从未出现过任何问题或性能方面的担忧。 - BritishDeveloper
1
回顾这个问题(几年后),我只想再加上一个想法:这个问题不仅仅局限于创建数据库上下文。在大型项目中,我经常看到同样的数据被多个地方引用。如果每次都创建一个新的数据库上下文,那么数据必须每次都重新检索,而不是将其缓存在请求期间打开的上下文中。 - Jonathan Wood
Linq2Sql上下文是轻量级的。EF上下文则不然。你从一开始就引用了错误的DataContext类,因此整个答案都是可疑的。 - Andrei
显示剩余3条评论

6

我认同每个请求只处理一个上下文,我们通常使用Ninject绑定.InRequestScope的上下文,这样做效果非常好,具体步骤如下:

Bind<MyContext>().ToSelf().InRequestScope();

此外,尽可能靠近查询时枚举集合是非常好的实践,例如:
public ActionResult Index()
{
    IEnumerable<MyTable> model;

    using (var context = new MyEntities())
    {
        model = (from mt in context.MyTable
                select mt).ToArray();
    }
    return View(model);
}

这将帮助您避免意外地从您的视图中增加查询。

2

首先,您应该考虑将数据库访问放在单独的类中。

其次,我最喜欢的解决方案是使用“每个请求一个上下文”(如果您使用MVC,则是每个控制器一个上下文)。

请求编辑:

看一下这个答案,也许对你有帮助。请注意,我正在使用Webforms,所以目前无法在MVC中验证它,但它可能对您有所帮助,或者至少给您一些指针。https://dev59.com/UGw15IYBdhLWcg3w4_3k#10153406

这里是一些使用此dbcontext的示例:

public class SomeDataAccessClass
{
    public static IQueryable<Product> GetAllProducts()
    {
        var products = from o in ContextPerRequest.Current.Products
                       select o;
        return products;
    }
}

然后你可以像这样做:
public ActionResult Index()
{
     var products = SomeDataAccessClass.GetProducts();
     return View(products);
}

简单,对吧?你不必再担心处理上下文了,你只需要编写你真正需要的代码。

有些人喜欢通过添加UnitOfWork模式或IoC容器来进一步增加一些调味料...但我更喜欢这种方法,因为它很简单。


谢谢,但我正在尝试弄清楚人们如何构建代码,以便每个请求只有一个上下文。我应该在哪里创建它,以及如何确保在请求完成时及时处理它? - Jonathan Wood

1

你能这样利用LINQ的.ToList()扩展方法吗:

public ActionResult Index()
{
    IEnumerable<MyTable> model;

    using (var context = new MyEntities())
    {
        model = (from mt in context.MyTable
                select mt).ToList();
    }
    return View(model);
}

可以,但这并未解决在单个HTTP请求期间多次重新创建上下文可能出现的性能问题。这就是为什么我正在寻找其他人如何做到这一点的示例的原因。 - Jonathan Wood
1
你有测量性能影响吗?它是否符合你的期望/要求? - Jesse C. Slicer
我还没有测量过性能。我只是试图弄清楚其他人如何处理EF的这个特定方面。我很难找到许多例子。 - Jonathan Wood

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