【问题标题】:Can't access related data after disposal of DataContext in Linq to SQL在 Linq to SQL 中处理 DataContext 后无法访问相关数据
【发布时间】:2015-11-03 12:24:21
【问题描述】:

重述了我的问题,下面是旧文本

由于我仍然希望得到答案,我想重申我的问题。图片 我有一个带有两个列表的 GUI,一个显示数据库 tblOrders 的所有条目的列表,另一个显示每个订单中的项目。

我可以使用 Linq2sql 或 EF 从数据库中获取所有订单,如下所示:

using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    ListOfOrders = DataContext.tblOrder.ToList();

我可以在列表或datagridview 中显示这些订单。然后选择我想从表tblItems 访问实体集合的作业之一。我可以这样做:

ListOfOrders.First().tblItems.toList();

除非我不能,因为我需要已处理的DataContext。这在某种程度上是有道理的,因为我无法保证没有新项目已添加到该订单中,因为我检索了 ListOfOrders. So ideally I would like to check if there has been an addition to a tblOrder.tblItems 集合,并且仅在需要时才从服务器重新检索该集合。

背景是,我的模型稍微复杂一些:它由 Orders 组成,Orders 由 Parts 组成,而 Parts 又由 Tasks 组成。因此,为了评估每个订单的进度,我必须检索属于订单的所有部分,并且对于每个部分,我必须查看已完成的任务数量。在一个有 200 个工作的数据库中,每个工作有 1 到 10 个部分,这只会让我的程序在响应能力方面变慢......

谁能帮帮我?

原始问题

我发现了很多关于DataContext 的问题,但我还没有找到解决问题的方法。 如果我执行以下操作:

using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    ListOfOrders = DataContext.tblOrder.ToList();

这给了我tblOrder 表中的实体列表。 但现在我想这样做:

DataTable Orders = new DataTable();
Orders.Columns.Add("Order-ID", typeof(int));
Orders.Columns.Add("Order-Name", typeof(string));
Orders.Columns.Add("Order-Items", typeof(string));

dataGridView1.DataSource = Orders;

foreach (tblOrder Order in ListOfOrders)
{
    var newRow = Orders.NewRow();
    newRow["Order-ID"] = Order.orderID;
    newRow["Order-Name"] = Order.orderName;
    newRow["Order-Items"] = string.Join(", ", Order.tblItem.Select(item=> item.itemName).ToList());
        // System.ObjectDisposedException
    (dataGridView1.DataSource as DataTable).Rows.Add(newRow);
}

我不能因为访问tblItem 表中与外键订单相关的所有实体似乎没有被存储。

什么是有效的:

DataClasses1DataContext DataContext = new DataClasses1DataContext();
ListOfOrders = DataContext.tblOrder.ToList();

DataTable Orders = new DataTable();
Orders.Columns.Add("Order-ID", typeof(int));
Orders.Columns.Add("Order-Name", typeof(string));
Orders.Columns.Add("Order-Items", typeof(string));

dataGridView1.DataSource = Orders;

foreach (tblOrder Order in ListOfOrders)
{
    var newRow = Orders.NewRow();
    newRow["Order-ID"] = Order.orderID;
    newRow["Order-Name"] = Order.orderName;
    newRow["Order-Items"] = string.Join(", ", Order.tblItem.Select(item=> item.itemName).ToList()); 
    (dataGridView1.DataSource as DataTable).Rows.Add(newRow);
}

DataContext.Dispose();

但据我了解,这是不可取的。

编辑

我将示例扩展为 Controller-View-Pattern。

using System.Collections.Generic;
using System.Linq;

namespace TestApplication
{
    class Controller
    {
        private List<tblOrder> _orders;
        public IList<tblOrder> Orders
        {
            get
            {
                return _orders;
            }
        }

        public Controller()
        {
            using (var DataContext = new DataClasses1DataContext())
            {
                _orders = DataContext.tblOrder.ToList();
            }
        }
    }
}

视图现在从控制器中检索订单:

using System.Data;
using System.Linq;
using System.Windows.Forms;

namespace TestApplication
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            Controller controller = new Controller();

            DataTable Orders = new DataTable();
            Orders.Columns.Add("Order-ID", typeof(int));
            Orders.Columns.Add("Order-Name", typeof(string));
            Orders.Columns.Add("Order-Items", typeof(string));

            dataGridView1.DataSource = Orders;

            foreach (tblOrder Order in controller.Orders)
            {
                var newRow = Orders.NewRow();
                newRow["Order-ID"] = Order.orderID;
                newRow["Order-Name"] = Order.orderName;
                newRow["Order-Items"] = string.Join(", ", Order.tblItem.Select(item=> item.itemName).ToList());
                (dataGridView1.DataSource as DataTable).Rows.Add(newRow);
            }
        }
    }
}

遗憾的是,问题依旧……

【问题讨论】:

  • 手动调用 Dispose 而不是使用 using 块是不可取的。但你可以轻松解决这个问题。只需放入 using 块即可。
  • 这是正确的并且适用于这个例子,但是如果我有一个控制器处理一个由视图访问的订单列表,那么这就会成为一个问题,视图会​​显示该列表。
  • 你不应该太快地释放上下文,而是你可以将上下文作为表单的实例成员,并在释放表单时释放上下文。

标签: c# sql-server entity-framework linq-to-sql datacontext


【解决方案1】:

Entity Framework 延迟加载对象数据,这意味着它尽可能晚地加载它必须的最小数据量。接受您的查询:

ListOfOrders = context.tblOrder.ToList();

您在此处请求tblOrder 表中的所有记录。 Entity Framework 不会在您的程序中预读并了解您将在处理上下文后查看tblItem 表,因此它假定它可以稍后加载tblItem 数据。由于比较懒惰,它会加载最低要求:tblOrder 中的记录列表。

有两种方法是一种解决方法:

禁用延迟加载

    using (var context = new DataClasses1DataContext())
    {
        data.Configuration.LazyLoadingEnabled = false;
        _orders = context.tblOrder.ToList();
    }

使用LazyLoadingEnabled=false Entity Framework 将选择tblOrder 表的全部内容以及通过外键与其连接的所有表。这可能需要一段时间并使用大量内存,具体取决于相关表的大小和数量。

(编辑:我的错误,disabling LazyLoading does not enable eager loadingthere is no default configuration for eager loading。为错误信息道歉。下面的 .Include 命令看起来是唯一的方法。)

包括其他表格

    using (var context = new DataClasses1DataContext())
    {
        data.Configuration.LazyLoadingEnabled = false;
        _orders = context.tblOrder.Include("tblItems").ToList();
    }

这告诉实体框架在加载tblOrders 表数据时预先从tblItems 加载所有相关数据。 EF 仍然不会从其他相关表中加载任何数据,因此在上下文处理后其他数据将不可用。

但是,这并不能解决过时数据的问题——也就是说,随着时间的推移,dataGridView1 中的记录将不再是最新的。您可以有一个触发刷新的按钮或计时器。最简单的刷新方法是重新执行整个过程——重新加载_orders,然后有选择地重新填充dataGridView1

【讨论】:

  • 在您的第一个示例上下文中将被处理
  • @Backs 是的,就是这样。上下文将被正确处理,数据将出现在对象列表中。
  • @MikeTV 虽然包含确实有效,但我现在想转而使用 LazyLoading(否则我需要很多“包含”)。但如果我这样做,1:1 关系为空,1:Many 关系为空列表。如果我只包括所有关系,它就可以工作。我做错了什么?
  • @Jonas 抱歉,我误解了 LazyLoadingEnabled 属性在这种情况下的工作原理! Include 看起来这是唯一的出路。谢天谢地,您可以像.Include("foo").Include("foo.bar") 一样将它们链接起来,更新了答案,使其更加清晰,并带有支持链接。
【解决方案2】:

我建议您创建包含您的数据的新结果类:

class Result 
{
    int ID {get;set;}
    string OrderName {get;set;}
    IEnumerable<string> ItemNames {get;set;}
}

选择需要的数据:

class Controller
{
    private List<Result> _orders;
    public IList<Result> Orders
    {
        get
        {
            return _orders;
        }
    }

    public Controller()
    {
        using (var DataContext = new DataClasses1DataContext())
        {
            _orders = DataContext.tblOrder.Select(o=> 
                      new Result
                      {
                          ID = o.orderID,
                          OrderName = o.orderName,
                          ItemNames = o.tblItem.Select(item=> item.itemName)
                      }).ToList();
        }
    }
}

然后绑定这个集合做网格视图。因此,您将在处理上下文之前获取所有数据,并且不再有依赖关系。

【讨论】:

    【解决方案3】:

    只是第一个问题的答案。
    就像 EF 所说,您在 [unit of] 工作之后处理了上下文(在 ASP 中是必须的,在 WinForm/WPF 中是一个很好的做法)。

    using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
        ListOfOrders = DataContext.tblOrder.ToList();
    

    在此之后,如果您尝试运行此语句

    ListOfOrders.First().tblItems.toList();
    

    EF 以这种方式工作:
    - 获取 ListOfOrders 的第一个元素,它是您的对象的代理。
    - 尝试获取与 LazyLoad 相关的 tblItems。
    此时,要检索 tblItems 需要使用从 ListOfOrder 第一个元素的代理检索到的上下文的数据库访问权限,但上下文已被释放。

    所以你不能使用这种方法。

    有 2 种解决方案以及它们的一些变体:
    - 您可以在处理上下文之前阅读所有 tblItems(您可以在另一个答案中看到它),但这是一种不可扩展的方法。
    - 当您需要访问 tblItems 时,您可以从新的上下文中检索 Order 并在处理它之前执行您需要的操作。 你的情况

    using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    {
        int Id = ListOfOrders.First().Id;
        var myListOftblItems = DataContext.tblOrder.Find(Id).tblItems.ToList();
    }
    

    这种方法的一个变体是只读 tblItems。如果 tblItems 还公开了外键字段而不仅仅是导航属性(通常我不公开),您可以这样做。

    using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    {
        int Id = ListOfOrders.First().Id;
        var myListOftblItems = DataContext.tblItems.Where(t => t.IdOrder == Id).ToList();
    }
    

    【讨论】:

      【解决方案4】:

      EF 的默认行为是延迟加载实体。 总的来说,这是一个好主意,如果没有很好的理由,我认为不应该禁用它。

      EF 还提供了Eager Loading指定实体的方法:

      // Add this!
      using System.Data.Entity;
      

      然后就可以使用 Include 方法了:

      public static IList<Order> GetOrdersAndItems()
      {
          List<Order> orders;
      
          using (var context = new ShopDC())
          {
              context.Database.Log = Console.WriteLine;
      
              Console.WriteLine("Orders: " + context.Orders.Count());
      
              orders = context.Orders
                  .Where(o => o.OrderID > 0)
                  // Tells EF to eager load all the items
                  .Include(o => o.Items)
                  .ToList();
          }
      
          return orders;
      }
      

      现在您可以使用 GetOrdersAndItems 加载包含所有订单的列表,每个订单将包含所有商品:

      public static void Run()
      {
          IList<Order> disconnectedOrders = GetOrdersAndItems();
      
          foreach (var order in disconnectedOrders)
          {
              Console.WriteLine(order.Name);
      
              foreach (var item in order.Items)
              {
                  Console.WriteLine("--" + item.Name);
              }
          }
      }
      

      更多示例和多级包括查看here

      注意:使用助手或控制器或存储库对于理解这种机制并不重要,因此我只是使用了静态方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-01-22
        • 1970-01-01
        • 2010-10-23
        • 2012-12-25
        • 2011-01-13
        • 1970-01-01
        相关资源
        最近更新 更多