使用 LINQ 比较 DataSets
使用 LINQ 在 Visual Studio 2008 中比较 DataSets。
引言
我最近的一个项目包括一些简单且我认为是常见的任务。我们正在使用 Microsoft Visual Studio Professional 版和 C# 3.0 实现一个项目,任务是在一个特定的时间间隔内比较从同一个表中获取的两个快照。此任务的目的是确定在给定的时间间隔内服务器端是否进行了任何更改。
我想到的第一个方法是使用 DataSet.Merge()
方法。理想情况下,它应该这样工作:
在
应用程序初始化过程中的某个时候,我们“获取”数据的第一个快照并将其加载到原始 DataSet
(我们称之为 dsOriginal
)。然后,在计时器事件中,当经过给定的时间间隔后,我们获取第二个快照(我们称之为 dsChanged
)。其余的应该是一个相当常规的过程:
- 创建一个空的
DataSet
(我们称之为dsDifferences
)。 - 将
dsOriginal
与dsChanged
合并。 - 最后一步是使用
DataSet.GetChanges()
方法获取修改过的或添加的行。dsDifferences = dsOriginal.GetChanges();
DataSet dsDifferences = new Dataset();
dsOriginal.Merge(dsChanged);
这是一种非常“时尚”且简单的解决方案,但并不像概念上那么容易。当我们发现合并数据集不会更改数据集中行的 RowSatus
属性时,我们感到非常不快。为了确保我们没有做错什么,我再次查阅了 MSDN 库,MSDN 的材料证实了:
Merge
方法在没有主键的情况下合并行(相当于 SQL Union 语句),或者在定义了主键的情况下,使用源DataSet
更新目标DataSet
中的行。
为了解决这个问题,我们联系了 Microsoft 支持团队,经过几天的邮件往来,我们收到了以下消息:
我认为问题的根源在于 Merge 不会更改行状态。如果您创建一个
DataSet
,然后手动更改一个行值,那么行状态就会被调整,GetChanges()
就会按预期工作。”
但是 Merge
只进行“傻瓜式”合并,不会设置其他标志,我正在尝试查找文档来支持这一点。如果是这样,唯一的选择就是手动比较。我会及时向您汇报我的发现……
正如您从消息中看到的,Microsoft 的建议是逐行手动遍历两个 DataSet
,比较值,并根据情况使用 Row.SetAdded()
或 Row.SetModified()
方法手动更新结果 DataSet
中的设置——这是一种不太优雅的解决方案。考虑到上面提供的方法从 Framework 2.0 开始就可供开发人员使用,可以获得额外的灵活性,很难想象这些方法在实际中何时可以使用。经过一两天额外的研究和与 Microsoft 的沟通,我们收到了支持团队的另一封电子邮件,其中我们得到了一个线索,这个问题无法解决:“我已经将这个问题上报给了相关人员,正在等待他们的回复。一旦我了解更多信息,我会通知您。”
LINQ 解决方案
这时我们转向了 LINQ。我之前对这个新功能没有太多经验,但几个小时后,我发现它非常容易学习和应用。有几种使用 LINQ 来实现相同结果的方法。对于我们的目的,我们选择了 Union 方法。因此,这是“顿悟时刻”:
- 在我们的应用程序初始化过程中,我们仍然创建一个初始数据快照;在下面的示例中,我们将称之为
dsOriginal
。 - 在计时器的
Elapsed
事件中,我们创建当前数据状态的另一个快照;我们将称之为dsChanged
。 - 为了使用 LINQ,我们需要将两个数据集中的表获取到一个可枚举的行集合中。这可以使用
AsEnumerable()
方法来完成(参见下面的代码示例),这样我们就可以使用 LINQ 的魔力了。
下面是完成此任务的代码:
var orig = dsOriginal.Tables[0].AsEnumerable();
var updated = dsChanged.Tables[0].AsEnumerable();
//First, getting new records if any
var newRec = from u in dsChanged
where !(from o in orig
select o.Field<decimal>("PRIMARY_KEY_FIELD"))
.Contains(u.Field<decimal>(" PRIMARY_KEY_FIELD"))
select new
{
prim_key = u.Field<decimal>("PRIMARY_KEY_FIELD"),
field1 = u.Field<decimal>("FIELD1"),
field2=u.Field<decimal>("FIELD2"),
field3 = u.Field<decimal>("FIELD3"),
field4 = u.Field<decimal>("FIELD4"),
rec_type="A"//Added
};
//Secondly, getting updated records
var updRec = from u in updated
join o in orig
on u.Field<decimal>("PRIMARY_KEY_FIELD")
equals o.Field<decimal>("PRIMARY_KEY_FIELD")
where (u.Field<decimal>("FIELD1") !=
o.Field<decimal>("FIELD1")) ||
(u.Field<decimal>("FIELD2") !=
o.Field<decimal>("FIELD2"))
select new
{
prim_key = u.Field<decimal>("PRIMARY_KEY_FIELD"),
field1 = u.Field<decimal>("FIELD1"),
field2=u.Field<decimal>("FIELD2"),
field3 = u.Field<decimal>("FIELD3"),
field4 = u.Field<decimal>("FIELD4"),
rec_type = "M"//Mofified
};
var Union = newRec.Union(updRec);
代码片段简单且不言自明。在第一个 select 语句中,我们使用 Contains()
方法获取新记录,并在其前面加上“ ! ”运算符,这只给我们新添加的行。在第二个语句中,我们使用 where 子句与之前的比较来获取具有更新值的行,最后,我们使用 Union()
方法比较结果。
您可能已经注意到在两个查询中都使用了变量 rec_type
。当您需要知道哪个记录被修改了,哪个是新添加的(added)时,Union
运算符非常有用,因为它允许您在 DataRow
上创建一个自定义标志来存储此信息,但如果您只需要获取差异而无需知道行是添加还是修改的,那么您可以“一气呵成”地获取所有数据,下面的代码片段演示了如何操作:
var AddedAndModif = from u in updated
where !(from o in orig
select o.Field<decimal>("PRIMARY_KEY"))
.Contains(u.Field<decimal>("PRIMARY_KEY"))
|| !(from o in orig
select o.Field<decimal>("FIELD1"))
.Contains(u.Field<decimal>(“FIELD1"))
select new
{
prim_key = u.Field<decimal>("PRIMARY_KEY"),
field1 = u.Field<decimal>("FIELD1"),
field2=u.Field<decimal>("FIELD2"),
field3 = u.Field<decimal>("FIELD3"),
field4 = u.Field<decimal>("FIELD4"),
};
从这个例子中可以看出,使用两次 Contains()
方法解决了任务,并为您提供了修改过和新添加的行的集合。
我们认为,在所有其他选项中,这个解决方案似乎是最快的。它易于阅读和根据需要进行修改,并且与 MS 支持专业人员在本文开头提供的建议方法(逐行比较并使用 Row.SetAdded()
或 Row.SetModified()
方法手动更改 RowState
)相比,性能提高了约 20%。
由于本文讨论中提供的情况很常见,我们决定分享我们的解决方案,希望能让其他开发人员的生活变得更轻松。