异步 LINQ to SQL





0/5 (0投票)
如何通过不必等待 LINQ to SQL 从服务器返回结果来构建响应式应用程序
引言
这将是一个关于将 Linq2Sql 与 WPF 结合使用的三部分系列文章的一部分。
虽然异步执行 SQL 查询即使在 ASP 中也可能带来好处,但我能看到的主要好处是保持富客户端 (WPF) 的响应性(这也是为什么 Silverlight 不允许 Linq2Sql 或任何其他同步网络访问的原因)。
在接下来的两篇文章中,我将介绍另外两个基于本文的类,以将所有功能保留在 presenter 层。
SynchedCollection
- 一个维护其自身CurrentItem
的类EntityCollection
-EntitySet
的包装器,用于修复向EntitySet
添加和删除操作不会在绑定中显示的问题
用法
在本文中,我只介绍一个类 - AsynchronousLinq
类。它可以直接使用,无需任何其他类。您可以将 CS 文件复制到您的项目中,也可以引用该项目或 DLL。无论哪种方式都可以。
该类有两个主要属性 - LinqList
和 ObservableList
。在过去几个月使用 LINQ 和 WPF 的过程中,我意识到几乎每次我编写 LINQ 语句或访问实体相关的元素(如 contact.Emails
)时,我都会将语句包装在 ObservableCollection
中以辅助 DataBinding
。但我同时也经常希望能够返回到 LINQ 语句,以便更改表达式或在其他查询中使用它。那么,为什么不创建一个可重用的方法来同时维护可绑定集合和原始查询呢?
因此,在您的 ViewModel
中,您可以根据需要创建任意数量的 AsynchronousLinq
实例,然后绑定到每个实例的 ObservableList
。然后,当您想填充列表时,您可以设置 LinqList
,后者将填充 ObservableList
。
<ListBox ItemsSource="{Binding Emails.ObservableList}">...</ListBox>
public AsynchronousLinq<ContactMethod> Emails { get; set; }
object SynchronizationObject = new object();
Emails = new AsynchronousLinq<ContactMethod> {Wait = SynchronizationObject };
Emails.LinqList = from cm in contact.ContactMethods where cm.Type == 1 select cm
异步部分
Linq2Sql 是一项强大的技术。然而,归根结底,它只是将 SQL 语句传递给您的服务器并检索数据。数据的检索可能需要一段时间,具体取决于网络延迟、查询的复杂性以及返回的数据量。在此过程中,您的用户界面可能会冻结。这是因为应用程序的主线程正在等待数据返回,因此无法处理用户的交互(或其他任何事情)。然而,如果您将数据检索 offload 到另一个线程,并且仅在完成后才联系主线程告知其显示数据,那么您的主线程就可以自由地与用户进行交互,直到数据准备好。
现在,有些人会将异步数据加载与 Linq2Sql 的延迟执行混淆。然而,事实是延迟执行实际上使得预测数据检索的确切时间更加困难。但当它发生时,它同样会阻塞您的主线程。
延迟执行是通过 IQueryable
接口实现的。只要您调用 IQueryable
接口中的函数,您就不会获得请求的数据,而是会得到一个新的 IQueryable
,其中包含能够告知计算机(在我们的例子中是 SQL Server)您想要什么数据的必要表达式。只有当您调用 IEnumerable
中的函数(如 .ToList()
或将 IQueryable
包装在 ObservableCollection
中)时,表达式才会被转换为实际数据。
如果您依赖 WPF 绑定来调用其中一个函数,那么就没有办法知道数据检索何时会发生。但是,如果您在代码中自己调用它们,那么您就可以确保代码不会因为将其 offload 到另一个线程而受到阻碍。
注意事项
我们首先需要处理几件事情。
- WPF 只能由初始化它的线程访问。
- Linq2Sql 本身不是线程安全的
进一步解释这些概念
我在 Google 上搜索了异步 Linq,并找到了许多示例。但没有一个真正起作用(至少在 WPF 中不行)。有些使用了 Thread
类,有些使用了异步 delegates
,但它们都有相同的问题 - 上面列出的问题。
UI 线程
任何时候访问 WPF 的任何部分,都必须在 UI 线程上。在 INotifyPropertyChanged
接口中引发 PropertyChanged
事件会 **直接** 调用绑定到该属性的任何 WPF 元素并导致它们更新。从另一个线程执行此操作违反了跨线程规则,并会导致程序崩溃。
为了解决这个问题,我使用了一个很棒的内置类,名为 BackgroundWorker
。它接受两个委托 - 一个在自己的线程上执行,第二个在第一个完成后被调用(与异步委托的 ondone 委托不同),**在 UI 线程上**。
因此,在后台线程中,我将 IQueryable
包装在 ObservableCollection
中(导致数据检索发生),并将其存储在一个临时变量中。而在 UI 线程上执行的 ondone 委托中,我将集合分配给 ObservableList
并引发 PropertyChanged
事件,从而更新显示。
现在来到下一个问题 - Linq2Sql 不是线程安全的!
如果您同时执行 2 个 Linq2Sql 查询,您会遇到大量奇怪的错误。
BackgroundWorker
无法解决这个问题,因为这一切的根本目的是将数据检索移出主线程。
因此,我需要做的是确保任何特定的 DataContext
在任何特定时间只有一个查询在执行。这并不意味着程序不能有其他事情发生 - 它可以更新显示,甚至可以使用不同的 DataContext
查询同一个数据库。
我本可以使用 DataContext
来做些什么,但我不想将此解决方案直接绑定到特定技术。
这就是 Wait
属性的用处。您可以将任何对象分配给 Wait
属性,只要包含对同一 DataContext
对象的查询的每个 AsynchronousLinq
都引用同一个对象即可。如果您的程序拓扑结构是 AsynchronousLinq
s 都可被一个也具有 DataContext
访问权限的类访问,那么您可以直接使用 DataContext
作为 Wait
对象。但如果您无法访问 DataContext
,您可以使用任何其他对象,只要它始终是同一个对象。
我如何处理那个对象?
每次我调用 IEnumerable
上的函数(如创建 ObservalbeCollection
)时,我都会将其包装在一个 lock 中。
lock(Wait)
{
oc = new ObservableCollection(LinqList);
}
现在是关键 - **您也必须只在 lock 中访问 Datacontext!**
这意味着,任何时候您调用一个将要在具有 AsynchronousLinq
的相同 DataContext
上执行 SQL 语句的函数,您都必须将该代码包装在与分配给 Wait
属性相同的对象的 lock 中。
另一方面,请也 **切勿** 将对 AsynchronousLinq.LinqList
的调用(**即使是间接的**)包装在与分配给 Wait
相同的对象的 lock 调用中。因为如果您这样做,您将导致死锁,因为 LinqList
将尝试等待您释放 lock,但这永远不会发生,因为该调用永远不会返回,因为它正在等待。
好吧,我想这篇文章已经够长了。比我预期的要长很多。我写代码比写文章要好得多,所以请阅读代码,您可能会从中获得比文章更多的收获。
改进
这个类仍然可以进行一些改进
- 撰写更好的帮助文章,我意识到这篇文章并不那么好,代码也应该得到更好的解释
- 添加一个演示项目。以演示如何使用这个类
- 添加分页数据检索。这对于大量数据集合很有用。它可以循环
LinqList
使用.Take(pageSize)
历史
- 2010 年 4 月 16 日:初始帖子