LINQ 挑战和 SQL Server Compact Edition






4.81/5 (39投票s)
克服 LINQ to SQL 的挑战并在 SQL Server Compact Edition 中使用 LINQ。
目录
- 引言
- LINQ to SQL
- 判决理由(示例)
- LINQ 和 SQL Server Compact Edition
- 在 LINQ to SQL 中使用枚举
- LINQ to SQL 中的字段子集挑战
- LINQ to SQL 中的更改跟踪挑战
- LINQ to SQL 中的非连接挑战
- LINQ to SQL 中的预取挑战
- 其他值得关注的点
- 总结
- 参考资料和资源
引言
开发人员时刻面临挑战。这是我们工作的一部分。开发人员面临的最常见挑战之一是管理数据。有时数据可以通过文档格式存储。然而,我们很多人处理的数据都具有个性、特征、关系……我相信你已经领会了拟人化的双关语。当然,我指的是关系型数据。
关系型数据在应用程序中如此常见,以至于我们有多种选择来存储、组织、查询、操作它,这令人惊叹。微软自己的 SQL Server 2005 产品就有六个版本!每个提供程序对 SQL ANSI 标准也略有不同。关键是,有如此多的选项可供选择,难怪许多开发人员将大部分精力投入到“数据访问层”中。我指的是为与应用程序数据使用的存储引擎通信而编写的库、服务和/或代码片段,无论其开发方案可能多么不正确。数据仅仅存储在那里是毫无意义的。我们必须在应用程序中与它进行交换,才能使其具有任何实际用途。
LINQ to SQL
LINQ to SQL(最初称为 DLINQ)登场。现在我们可以使用我们已经熟悉的语言(目前是 C# 或 VB)查询各种数据。当我听说 LINQ(语言集成查询)发布时,我非常兴奋,尤其是在 SQL 方面。它减少代码并创建一致性系统的潜力对所有开发人员来说都是无价的。Visual Studio 2008 的最新增强功能进一步放大了 LINQ 的优点。
LINQ 还提供了一个对象关系映射 (ORM) 工具,使生活更轻松。我们可以将该工具指向数据库,并享受干净生成的代码带来的便利。它为我们生成的代码非常容易扩展,允许我们编写业务逻辑、验证、辅助方法、复杂的事务场景等等。我使用对象关系映射 (ORM) 已有一段时间了,并且一直在努力使用一些可用的免费实现。当然,有一些写得很好的 ORM 工具,但手头有一个,并且在框架内部,这简直是太棒了。
再加上 Visual Studio 2008 中添加的所有其他新功能,LINQ 是这个家族中一个受欢迎的补充。LINQ 带来了一些新挑战,但这也在意料之中。毕竟我们是开发人员。
判决理由(示例)
为了开始讨论 LINQ to SQL 可能遇到的挑战,我们需要建立一个我可以用来演示的示例。作为开发人员,我们需要彻底量化我们的产出。我们中的一些人是独立承包商,一些人拥有咨询/开发公司,还有一些人则为这些公司工作。无论如何,都需要报告我们花费在项目上的时间,以便准确地向“客户”收费。因此,对于我们来说,没有什么比一个跟踪我们工作时间的应用程序更适合用 LINQ to SQL 进行实验的示例了。
这个应用程序将不同于市面上许多允许您输入项目时间的应用程序。我们的示例应用程序旨在实际实时跟踪时间。我希望非开发人员也能使用它。任何在现场或出差携带笔记本电脑的专业人士都可以通过点击几个菜单来获得详细的时间跟踪。我甚至希望可以选择让程序检测我何时离开电脑,这样它就可以暂停并从持续时间中扣除“离开时间”。总而言之,我认为最好创建一个桌面应用程序。
我还认为这是一个使用 SQL Server Compact Edition 作为数据存储引擎的绝佳机会。我们当然可以使用 XML 甚至纯文本文件,但我希望能够快速查询数据集以获取聚合信息等等。为什么不使用 SQL Server Express Edition 呢?嗯,如果应用程序在没有任何系统依赖项(除了显而易见的 .NET Framework 3.5 Express Edition 无法嵌入到我们的应用程序中,并且需要比我们用户可能拥有的更多权限才能安装)的情况下安装,那将是非常好的。在工作环境中尤其如此。
有关选择 SQL Server Compact 和 Express Edition 之间差异的更多信息,请参阅 Microsoft 发布的文章和白皮书。
另请注意,示例应用程序绝不是完整的。它仅作为示例场景,引导我们决定将 LINQ to SQL 与 SQL Server Compact Edition 3.5 结合使用。可供下载的代码是一个骨架应用程序,展示了本文其余部分讨论的主题。
LINQ 和 SQL Server Compact Edition
寻求熟悉 LINQ to SQL 的开发人员遇到的挑战之一是如何将其与 SQL Server Compact Edition (SSCE) 3.5 版本一起使用。我们被告知 LINQ 暂时将与 SQL Server 配合使用,未来将有更多提供程序(可能要等到 LINQ to Entities 发布)。但是,如果您尝试将 Compact Edition 数据连接中的一些表拖到新的 LINQ to SQL 设计器画布上,您将看到一个讨厌的错误对话框,指出不支持该提供程序!
更准确地说,LINQ to SQL **设计器**不支持 SSCE 提供程序。您仍然可以使用命令行工具 SQLMetal 来生成数据实体、数据访问和其他 ORM 代码。如果您喜欢命令行工具,那就万事俱备了。如果您喜欢实体的可视化表示,仍然有希望。
我建议创建一个批处理文件或 PowerShell 脚本,通过 SQLMetal 生成您的文件。这让您在不可避免地需要由于模式更改而重新生成数据访问层时,可以快速执行。我已包含一个可供下载的示例。
对于我们的示例应用程序,所需的命令相当简单。
SqlMetal.exe TimeApp.sdf /dbml:TimeApp.dbml /namespace:TimeApp.DataAccess /pluralize
注意:SQLMetal 工具默认位于主驱动器上的:_Program Files\Microsoft SDKs\Windows\V6.0A\Bin\SqlMetal.exe_。
请注意,我为生成的代码的命名空间指定了一些选项,以使实体类名复数化并生成 DBML 文件。生成的 DBML 文件对于那些想要视觉设计器支持的人来说极其重要。有了它,您可以通过设计器对架构中发生的微小更改进行修改,或者选择编辑 DBML 文件本身。它只是 XML,所以请随意深入研究。一旦您将文件添加到项目中,Visual Studio 2008 将自动为您的数据访问层生成相应的代码。
您可以在图 2 中看到 LINQ to SQL 设计器中表的非常简单的布局。示例时间跟踪应用程序将允许用户选择一个项目,并可选地选择一个任务,以开始跟踪时间。需要注意的是:项目表和任务表几乎完全相同,并且可以通过一个父字段合并成一个表以实现层次结构;但是,我选择在此示例中将它们分开,以便您可以看到在类似设计中可能遇到的一些挑战。
在 LINQ to SQL 中使用枚举
很多时候,一个带有约束或业务规则的简单数字列就足够了,而不需要使用外键指向查找表。如果值会经常更改或需要由用户更改,查找表是非常好的。在其他情况下,我们开发人员会确定此类字段的确切域。我们仍然希望为用户提供选择可能值之一的能力,但我们完全控制这些可能性。这是在应用程序中使用枚举来表示这些字段值的绝佳机会。
那么,问题出在哪里?当 ORM 生成代表数据库表的实体时,它会用数字属性来表示这些数字字段。在编写用户界面时,我们可以将用户的选择转换为适当的数字。如果我们要使用枚举,我们仍然需要将枚举值转换为对应的数字值。您的第一反应可能是利用 ORM 提供的功能来创建一个新的代码文件,其中包含一个局部类,该类包含一个处理这些转换的新属性。
public enum EstimationComparison : byte {
Overall = 1,
Repeating = 2
}
public partial class Project {
public EstimationComparison EstimationComparison {
get { return (EstimationComparison)this.EstimationComparisonType; }
set {
if (Enum.IsDefined(typeof(EstimationComparison), value))
this.EstimationComparisonType = (byte)value;
}
}
}
Public Enum EstimationComparison As Byte
Overall = 1
Repeating = 2
End Enum
Partial Public Class Project
Public Property EstimationComparison() As EstimationComparison
Get
Return CType(Me.EstimationComparisonType, EstimationComparison)
End Get
Set(ByVal value As EstimationComparison)
If [Enum].IsDefined(GetType(EstimationComparison), value) Then
Me.EstimationComparisonType = CType(value, Byte)
End If
End Set
End Property
End Class
这很好,并且利用了 LINQ ORM 为我们提供的工具。然而,我们可以做得更好。我们实际上可以告诉 ORM 为属性使用不同的类型,它将为我们处理转换。只要我们通知它使用的类型与数据库中用于存储值的数据类型兼容,就不会有问题。这可以通过几种方式实现。一种是手动编辑 DBML 文件,然后使用 SQLMetal 从编辑后的文件重新生成代码。另一种方法是在设计器中打开 DBML,并通过属性页更改实体属性的类型。由于我们还希望此字段为 `Nullable`,因此我们必须确保相应地设置属性类型。
一旦做出此更改,我们就可以直接将枚举值赋给 `Project` 实体的 `EstimationComparisonType` 属性。框架会负责转换。
Project Project = DataContext.Projects.Single(P => P.ProjectId == 1);
Project.EstimationComparisonType = EstimationComparisonType.Overall;
Dim Project As Proejct = DataContext.Projects.Single(Function(P) P.ProjectId = 1)
Project.EstimationComparisonType = EstimationComparisonType.Overall
LINQ to SQL 中的字段子集挑战
我们应该在数据库开发生涯早期就学到的一件事是,将我们检索的数据集限制为我们需要的,不多不少。这意味着,我们应该通过经过深思熟虑的 `where
` 子句限制行数,并通过 SQL 语句的 `select
` 子句中的实际列列表限制列数。
-- No restrictions at all.
select *
from Project
order by [Name]
-- Limit the columns to just those needed.
-- Limit the rows as needed.
select ProjectId, [Name]
from Project
where IsActive = 1
order by [Name]
清单 3 中有两个不同的 `Project` 表查询。如果我们的目的是用这些数据填充一个简单的列表控件,我们不需要表中的所有字段。我们真正关心的列表是主键和每个记录的文本表示。第二个查询当然更理想。
在 LINQ to SQL 的 Beta 版本中,我们**能够**强制加载哪些字段到我们正在检索的实体对象中。结果发现,这是一个错误。为了避免混淆,这个所谓的“功能”从未被 intended。想法是,如果开发人员从另一个开发人员编写的库、层或其他代码中调用数据,那么如果检索到的实体缺少属性中的数据,将会造成巨大的混乱。
例如,如果我从另一个开发人员编写的库中调用一个方法,该方法检索我们的项目列表并只获取 `ProjectId` 和 `Name`,那么当我尝试访问 `EstimatedDuration` 属性时,我可能会非常困惑。如果字段不可为空且数据未初始化,情况会很糟糕,但想象一下 `EstimatedDuration` 属性**是**可空的。我可能会认为数据库中没有这样的值,而实际上可能存在。我无法根据收到的 `Product` 实体来判断。
因此,当 LINQ to SQL 发布到生产环境(从 Beta 版到正式版)时,这种行为不再被允许。不过,我仍然需要能够从表中只检索一些字段,而且我可以做到。我可以在本地使用匿名类型,但如果我想传递结果,我需要创建一个新类来只包含我想要的那些值,然后在检索数据时使用该类的新实例。
public class ProjectSummary {
public int ProjectId { get; set; }
public string Name { get; set; }
public List<TaskSummary> Tasks { get; set; }
}
Public Class ProjectSummary
Private _projectId As Integer
Private _name As String
Private _tasks As List(Of TaskSummary)
Public Property ProjectId As Integer
Get
Return _projectId
End Get
Set(ByVal value As Integer)
_projectId = value
End Set
End Property
Public Property Name As String
Get
Return _name
End Get
Set(ByVal value As String)
_name = value
End Set
End Property
Public Property Tasks As List(Of TaskSummary)
Get
Return _tasks
End Get
Set(ByVal value As List(Of TaskSummary))
_tasks = value
End Set
End Property
End Class
var ProjectList = from P in DataContext.Projects
orderby P.Name
select new ProjectSummary {
ProjectId = P.ProjectId,
Name = P.Name
};
Dim ProjectList = From P In DataContext.Projects _
Order By P.Name _
Select ProjectSummary = New With { _
.ProjectId = P.ProjectId, _
.Name = P.Name _
}
当我们创建一个新类来仅保存我们想要的字段时,调整 LINQ 语法以将数据填充到我们的类的新实例中是相当容易的。在我们的示例中,清单 4 声明了一个新的 `ProjectSummary` 类,其中只包含 `ProjectId` 和 `Name` 属性,清单 5 显示了填充 `ProjectSummary` 新实例的查询,其中包含适当的数据。
LINQ to SQL 中的变更跟踪挑战
在某些情况下,LINQ to SQL 的变更跟踪可能非常具有挑战性;特别是,如果您刚刚开始使用这项新技术。让我们来看看示例时间跟踪应用程序中的一个此类情况。
“项目”窗口通过生成的数据上下文从数据库检索当前项目的摘要列表。获取的项目列表直接绑定到图 4 所示的列表框。当用户按下“编辑”按钮时,整个 `Project` 被检索并传递给“编辑项目”窗口。
“编辑项目”窗口接收一个 `Project` 实体对象的实例,并将其所有属性绑定到表单上的控件,`Tasks` 子列表除外。如果我们将 `Project.Tasks` 列表绑定到列表框,那么如果用户选择“取消”对话框,我们将无法撤销对该列表所做的更改。
尽管数据上下文可以跟踪对象更改以确定哪些内容已被修改,但我们无法调用公共方法来“撤销”到目前为止所做的更改。对于这个缺点,有几种解决方案。
首先,您可以指示 ORM 让实体类继承您选择的基类。您有权创建一个支持深层克隆的基类。有了这个功能,当任何属性发生更改时,您可以克隆对象的原始状态。此外,您可以添加一个方法来撤销对对象所做的更改,将其恢复到原始值。然而,实现深层克隆有点耗时,并且肯定会导致性能损失。这会随着每个子对象深度而加剧。当然,您可以实现最大深度,并允许根据具体情况指定。祝您好运!
或者,您可以使用标记对象进行修改和/或删除的方法。我们的示例应用程序最关心的是撤销已删除的任务。我们可以初始化一个独立的包含项目任务的列表。这样,当用户点击任务的“删除”按钮时,`Task` 对象将被“标记”为删除并从我们的独立列表(该列表绑定到列表框)中移除。这会将对象保留在原始 `Project.Tasks` 列表中,但会将其从用于在列表框中显示任务的列表中移除。如果用户点击“编辑项目”对话框的“确定”按钮,我们可以在将更改提交到项目之前删除那些已标记为删除的任务。
LINQ to SQL 中的非连接挑战
一些开发人员遇到的另一个问题涉及非连接场景。假设您通过 LINQ to SQL 数据上下文在一个独立的物理层中检索实体或实体集合。在这种情况下,用于检索数据的原始数据上下文在数据传输到用户界面层并返回进行更新时将不复存在。对于这种非常常见的架构,存在一些支持。
如果您倾向于使用 ORM 构建的生成实体类,则可以在各层之间传递这些实例并获取结果。但请注意其局限性。您可以将对象重新附加到新的数据上下文以进行更新。然而,上下文的 `Table` 实例的 `Attach` 方法不支持深层对象图。实际上,它只支持自身。也就是说,当您附加一个对象时,只有该对象被附加。如果您的对象通过外键关联着子对象,那些子对象将**不会**被附加。您必须单独附加子对象,才能使其参与更新。
LINQ to SQL 通常使用对象的原始状态来在更新期间维护乐观并发控制。由于在断开连接场景中对象跟踪被切断,因此在重新连接对象进行更新时,您将不得不提供该原始对象或替代对象。重新连接**必须**接收原始对象和修改后的对象,**或者**您的实体**必须**具有版本成员。版本成员通常在数据库中定义为时间戳字段,并且您肯定会希望将生成的属性标记为只读。
LINQ to SQL 需要这些要求来确定您希望更新的数据是否已被其他用户修改。如果您希望在各层之间传递生成的实体对象而不是消息,请牢记所有这些。
LINQ to SQL 中的预取挑战
LINQ to SQL 具有许多出色的性能优化。在数据检索方式上投入了大量精力。如果您花点时间在调试模式下或通过数据上下文的 `Log` 属性检查生成的 SQL,您将看到语句中体现的细致考量。
另一个性能特性是延迟加载,或“惰性加载”。延迟加载可以防止某些数据在实际需要之前被检索。您的实体上的每个属性都有一个选项,用于“延迟加载”属性值。但是,所有子对象默认都采用此行为。在我们的示例应用程序中,`Project` 实体有一个为其下定义的 `Task`s 的属性。当通过 LINQ to SQL 检索项目时,只检索来自相应表的数据。但是,如果我访问 `Tasks` 属性,则会向数据库发出另一个查询以检索这些任务。这是为了防止在“以防万一”的情况下对数据库造成不必要的负载。但是,有时我们**知道**我们希望加载子对象。
时间跟踪应用程序在其通知图标(系统托盘图标)上显示一个菜单,其中包含数据库中的所有项目。每个项目菜单项还包含每个关联任务的子菜单项列表。我们知道需要提前构建这些菜单,这使我们能够预取所有必要的任务,同时获取所有项目。如果我们可以指示 LINQ 在一个查询中完成所有这些操作,我们可以通过减少多次查询(1 个查询而不是 1 个用于项目,加上每个项目 1 个用于获取任务)来节省一些性能。
我们将使用 LINQ to SQL 的一个特性来实现这种“预先加载”。数据上下文有一个名为 `LoadOptions` 的属性,它接受 `DataLoadOptions` 的实例。这就是我们指定要在一次查询中加载一个或多个相关子实体的方式。
// Pre-fetch the tasks for each project.
DataLoadOptions Options = new DataLoadOptions();
Options.LoadWith<Project>(P => P.Tasks);
DataContext.LoadOptions = Options;
var ProjectList = from P in DataContext.Projects
orderby P.Name
select new ProjectSummary {
ProjectId = P.ProjectId,
Name = P.Name,
Tasks = (
from T in P.Tasks
orderby T.Name
select new TaskSummary {
TaskId = T.TaskId,
Name = T.Name
}
).ToList()
};
' Pre-fetch the tasks for each project.
Dim Options As New DataLoadOptions()
Options.LoadWith(Of Project)(Function(P) P.Tasks)
DataContext.LoadOptions = Options
Dim ProjectList = From P In DataContext.Projects _
Order By P.Name _
Select ProjectSummary = New With { _
.ProjectId = P.ProjectId, _
.Name = P.Name, _
.Tasks = ( _
From T In P.Tasks _
Order By T.Name _
Select TaskSummary = New With { _
.TaskId = T.TaskId, _
.Name = T.Name _
} _
).ToList() _
}
这真是个很棒的功能!但是,当我们进行此设置后检查生成的 SQL 时,可能会出现性能问题。请注意图 6 中 LINQ 生成的 SQL 如何使用 join
从 *Project* 和 *Task* 表获取数据。
我最初看到上面生成的 SQL 时有点惊讶。我惊讶的原因是检索到的数据。从 *Project* 表中选择的每个字段都将为检索到的每个 `Task` 行重复。
我期望看到两个查询:一个用于检索必要的项目,另一个用于检索那些项目的匹配任务(如果项目查询存在 `where
` 子句,则通过相关子查询)。我期望这两个查询的结果将用于在构建对象图时将任务映射到项目。
我并非一个轻易接受这种结果的人,于是我给微软的总经理 Scott Guthrie 发了一封电子邮件,他写过几篇关于 LINQ 的博客文章。我问他为什么在 LINQ 中预取相关实体时会出现这种行为。Scott 将问题转给了 LINQ 项目的项目经理 Dinesh Kulkarni。Dinesh 专门负责 LINQ to SQL。他的回复总结了他的团队在性能和预加载或预取方面考虑的许多选项。该团队显然对不同大小的数据集进行了几次测试。一些解决方案在某些数据集上效果很好,而在其他数据集上则不行。经过深思熟虑,他们选择了当前的解决方案,尽管他表示他们对性能不满意。
Dinesh 还引用了高级性能架构师 Rico Mariani 的一篇详细博客文章。我强烈建议您有空时阅读这篇博客文章。该文章确实触及了这些性能问题。
阅读了 Dinesh 的回复和博客文章后,我得出的结论是,没有“万能药”。我完全可以理解,在某些情况下,由于相关子查询,多次查询不会产生良好的性能。想象一个父查询,其 `where
` 子句中包含一些复杂的过滤器。每个预取子查询都必须包含一个子查询,通过相同的复杂过滤器将其与父查询关联。在这些情况下,这种行为会大大增加复杂性。
其他兴趣点
您可以在实体内部响应某些事件,这非常棒。在探索其中一些选项时,我尝试了在将新对象的某个属性设置为相关实体实例时,为新对象设置一些默认值的想法。这出人意料地奏效了。
public partial class Task {
partial void OnCreated() {
// Subscribe to the PropertyChanged event when created.
this.PropertyChanged += new
System.ComponentModel.PropertyChangedEventHandler(Task_PropertyChanged);
}
void Task_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e) {
if (e.PropertyName == "Project") {
if (Project != null && TaskId <= 0) {
// Set the defaults of this new Task based on the assigned Project.
EstimationComparisonType = Project.EstimationComparisonType;
DetectAway = Project.DetectAway;
IsBillable = Project.IsBillable;
}
}
}
}
Partial Public Class Task
Private Sub OnCreated()
' Subscribe to the PropertyChanged event when created.
AddHandler Me.PropertyChanged, AddressOf Task_PropertyChanged
End Sub
Sub Task_PropertyChanged(sender As Object,
e As System.ComponentModel.PropertyChangedEventArgs)
If e.PropertyName = "Project"
If Not Project Is Nothing AndAlso TaskId <= 0
' Set the defaults of this new Task based on the assigned Project.
EstimationComparisonType = Project.EstimationComparisonType
DetectAway = Project.DetectAway
IsBillable = Project.IsBillable
End If
End If
End Sub
End Class
如果 `Task` 对象的新实例通过其相关属性被分配一个现有的 `Project` 值,则清单 7 中的代码将被执行。在这种情况下,`Task` 的一些属性将从父 `Project` 对象继承默认值。
在使用 LINQ to SQL 时,还需要注意一个我收到过几次非常模糊的错误。当您在未更新 DBML 文件的情况下编辑数据库的架构时,此错误更容易发生。我在运行示例应用程序时多次收到 `ChangeConflictException`,消息为“未找到行或已更改”。结果发现是非常简单的问题。我更改了示例架构,允许一些字段为空,但忘记为我的 LINQ 实体更新 DBML 文件。我所要做的就是指示允许相应的实体属性为空。在我的自定义枚举属性的情况下,我还必须在指定枚举类型时使用 `System.Nullable` 泛型类型。我期望 ORM 或 DBML 设计器在我将属性的 `Nullable` 标志设置为“True”时暗示 `Nullable` 类型。但是,它没有,所以我必须像前面提到的那样设置属性的 `Type`,“`System.Nullable<EstimationComparisonType>`”。
总结
在一个**充满**缩略词的领域里,.NET Framework 最新成员之一的 LINQ 极其强大。它为我们的数据提供了简单的接口。尽管它带来了挑战,但它仍然是开发人员“工具包”中非常有价值的工具。将 LINQ to SQL 与 SQL Server Compact Edition 结合使用,无疑会加速您下一个需要嵌入式数据的应用程序的开发过程。您所需要的只是关于如何使其发挥最佳作用的一些知识和一些空闲时间来尝试一下。
祝您好运,玩得开心!