在Linq to SQL中处理DataContext后无法访问相关数据

5
重新表述我的问题,旧文如下:
由于我仍然希望得到答案,我想重新说明我的问题。假设我有一个 GUI,其中有两个列表,一个显示数据库 tblOrders 中所有条目的列表,另一个显示每个订单中的项目。
我可以使用 Linq2sql 或 EF 从数据库中获取所有订单,如下所示:
using (DataClasses1DataContext DataContext = new DataClasses1DataContext())
    ListOfOrders = DataContext.tblOrder.ToList();

我可以把这些订单以列表或datagridview的形式显示出来。然后,在选择其中一个工作时,我想要访问表格tblItems中的实体集合。我可以这样做:
ListOfOrders.First().tblItems.toList();

除非我需要已被处理的DataContext,否则我无法做到这一点。从某种意义上讲,这是有道理的,因为我无法保证自从我检索ListOfOrders以来是否添加了新项目到该订单中。因此,理想情况下,我想检查tblOrder.tblItems集合是否有添加,并且仅在必要时从服务器重新检索该集合。

背景是,我的模型有点复杂:它由订单组成,每个订单又由零件组成,每个零件又由任务组成。因此,要评估每个订单的进度,我必须检索属于订单的所有零件,并针对每个零件查看已完成多少任务。在具有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();

但是据我理解,这并不是可取的。
编辑:
我将我的例子扩展到了控制器-视图模式。
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块即可。 - nvoigt
这是正确的,并且对于这个例子有效,但如果例如我有一个控制器处理一个订单列表,由视图访问并显示该列表,那么这将成为一个问题。 - Jonidas
您不应该过快地处理上下文,而是可以将上下文作为表单的实例成员保留,并在表单被处理时处理上下文。 - Akash Kava
4个回答

4

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 表的所有表和外键内容。这可能需要一些时间并使用大量内存,具体取决于相关表的大小和数量。
(更正:我的错误,禁用LazyLoading并不会启用贪婪加载也没有默认的贪婪加载配置。对于错误信息,我深感抱歉。下面的 .Include 命令似乎是唯一的解决方法。) 包含其他表
    using (var context = new DataClasses1DataContext())
    {
        data.Configuration.LazyLoadingEnabled = false;
        _orders = context.tblOrder.Include("tblItems").ToList();
    }

这段话涉及到IT技术,意思是告诉Entity Framework在加载tblOrders表数据时,一并将tblItems表的所有相关数据预先加载。但EF仍然不会加载其他关联表的数据,所以在上下文被处理后,其他数据将不可用。
然而,这并没有解决过时数据的问题——随着时间的推移,dataGridView1中的记录将不再是最新的。您可以设置一个按钮或计时器来触发刷新。最简单的刷新方法是重新执行整个流程——重新加载_orders,然后有选择性地重新填充dataGridView1。

1
在你的第一个示例中,上下文将被处理。 - Backs
@Backs 是的,这就是想法。上下文将被正确处理,数据将存在对象列表中。 - MikeTV
@MikeTV 虽然include(包含)方法可以工作,但现在我想要关闭LazyLoading(延迟加载)(否则我需要太多的“includes”)。但是,如果我这样做,1:1关系为null,而1:Many关系为空列表。如果我只包括所有关系,它就可以工作。我做错了什么? - Jonidas
@Jonas 抱歉,我误解了在那种情况下 LazyLoadingEnabled 属性的工作方式!Include 看起来是唯一的选择。幸运的是,你可以像 .Include("foo").Include("foo.bar") 这样链接它们。更新了答案以更清晰地表达,并提供了支持链接。 - MikeTV

1
我建议您创建一个新的结果类,其中包含您的数据:
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();
        }
    }
}

然后将此集合绑定到网格视图。这样,在处理上下文之前,您就可以获取所有数据,而且不再有其他依赖项。


0

只是回答第一个问题。
就像EF所说的那样,在工作单元之后(在ASP中必须,在WinForm/WPF中是一个好习惯)释放了上下文。

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

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

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

EF的工作方式如下:
- 获取ListOfOrders中代表您对象的第一个元素的代理。
- 尝试使用LazyLoad获取与tblItems相关的内容。
此时,要检索tblItems需要从ListOfOrder第一个元素的代理中检索出一个上下文的数据库访问,但是该上下文已被处理。

因此,您不能使用此方法。

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

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();
}

0

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

EF 还提供了指定实体的急切加载方法:

// 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);
        }
    }
}

更多示例和多级包含,请查看此处

注意:在理解此机制时,使用帮助程序、控制器或存储库并不重要,因此我只是使用了静态方法。


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