写在前面
将LINQ to DataSet单独放一篇,是因为随后的LINQ to SQL默认只支持MS SQL Server。只有LINQ to DataSet能在没有相应Data Provider帮助的情况下,与其他数据库平台进行交互。而且LINQ to DataSet中的许多基本概念,是引导我们深入LINQ to SQL以及Entity Framework的基础。
P341 DataRow的集合运算符
通过LINQ与数据库交互,仍需要LINQ的三个基本要素:DataTable是序列,DataRow是元素,以及相关的操作符。
但是DataRow与Object的显著区别在于如何唯一性地标识一个DataRow。Object可以通过GetHashCode()来保证其被唯一性地识别,而DataRow只会导致引用Reference间的比较,而这并不是我们所期望的。因为不同时刻我们从数据库中加载的同一条DataRow,可能对应完全不同的引用。为此,在DataRow的LINQ运算符中,出现了一个默认的比较器DataRowComparer.Default。使用的方式与LINQ to Objects一致:
DataTable dt = new DataTable(); IEnumerable<DataRow> except = dt.AsEnumerable().Except(subtable, DataRowComparer.Default);
有了DataRowComparer.Default的帮助,当我们把IQueryable接口的DataTable利用AsEnumerable()转换为IEnumerable<>后,即可采取类似LINQ to Objects的运算符进行操作了。如果没有DataRowComparer.Default,则会在DataRow的对象筛选时得到出乎意料的结果。
需要说明的是,DataRowComparer<T>是对应的泛型。可以自由定制自己的DataRow比较子。
P353 DataRow的Field运算符
问题来源于下面这样的一种情形:
string anthonysClass = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" from c in seq2 where c["Id"] == s["Id"] select (string) c["Class"]) .SingleOrDefault<string>();
由于DataRow[列名]的引用,本来是int类型的列值返回后被装箱成为object,其中的c["Id"] == s["Id"]将是两个object之间的比较,从而导致结果错误。当我们将比较条件修改为(int) c["Id"] == (int) s["Id"]之后,才能返回正确的结果。但从另一个方面考虑,当该字段值为DBNull时(int)c["Id"]这样的强制类型转换便不再有效,因此引入了Field<>运算符。需要说明的是,Field<>取出的值有可能是null。
其存在三种引用方式:
row.Field<字段值类型>(列序号) | row.Field<int>(0) |
row.Field<字段值类型>(列名) | row.Field<int>(“Id”) |
row.Field<字段值类型>(Column引用) | row.Field<int>(dt.Columns[0]) |
同时,还引入了Field<>的DataRowVersion,这与ADO.NET中的DataRowState是相互呼应的。
Original | 取原始值 |
Current | 取当前值 |
Proposed | 取建议值 |
Default | Added、Modified或Current=>Delete Detached=>Proposed |
例如这样:
int id = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s.Field<int>("Id", DataRowVersion.Current)) .SingleOrDefault<int>();
P362 DataRow的SetField<T>运算符
SetField<>(列, 值),其中列的引用方式同Field<>。不同于Field<>的,是null值将可能引发DBNull异常。
特别是对于string类型的列,由于不支持string? name这样的可空声明,因此要特别注意处理,避免陷入DBNull异常。
(from s in seq1 where s.Field<string>("Name") == "Michael Bluth" select s).Single<DataRow>().SetField("Name", "Tony Wonder");
P365 DataTable相关运算符
AsEnumerable()相对简单,稍微复杂一点的是CopyToDataTable()。后者有一个类似于DataRowState的LoadOption选择方式。每个列,都有Current与Original两个版本的数据。这个LoadOption决定哪个版本的数据被拷贝到新的DataTable中。
OverwriteChanges | Current与Original都拷贝 |
PreserveChanges | 只拷贝Original |
Upsert | 只拷贝Current |
一个需要考虑的情形是,在没有定义新表主键的情况下,在利用DataTable.AcceptChanges()将所有DataRow的State复位之前,如果象下面这样执行2次CopyToDataTable(),将会导致DataRow被重复添加。其中第2次拷贝的DataRow将没有Original版本。
Console.WriteLine("Before upserting DataTable:"); foreach (DataRow dataRow in newTable.AsEnumerable()) { Console.WriteLine("Student Id = {0} : original {1} : current {2}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name", DataRowVersion.Original), dataRow.Field<string>("Name", DataRowVersion.Current)); } (from s in dt1.AsEnumerable() where s.Field<string>("Name") == "Anthony Adams" select s).Single<DataRow>().SetField("Name", "George Oscar Bluth"); dt1.AsEnumerable().CopyToDataTable(newTable, LoadOption.Upsert); Console.WriteLine("{0}After upserting DataTable:", System.Environment.NewLine); foreach (DataRow dataRow in newTable.AsEnumerable()) { Console.WriteLine("Student Id = {0} : original {1} : current {2}", dataRow.Field<int>("Id"), dataRow.HasVersion(DataRowVersion.Original) ? dataRow.Field<string>("Name", DataRowVersion.Original) : "-does not exist-", dataRow.Field<string>("Name", DataRowVersion.Current)); }
运行结果:
--------------------------------------------------------------- Before upserting DataTable: Student Id = 1 : original Joe Rattz : current Joe Rattz Student Id = 7 : original Anthony Adams : current Anthony Adams Student Id = 13 : original Stacy Sinclair : current Stacy Sinclair Student Id = 72 : original Dignan Stephens : current Dignan Stephens After upserting DataTable: Student Id = 1 : original Joe Rattz : current Joe Rattz Student Id = 7 : original Anthony Adams : current Anthony Adams Student Id = 13 : original Stacy Sinclair : current Stacy Sinclair Student Id = 72 : original Dignan Stephens : current Dignan Stephens Student Id = 1 : original -does not exist- : current Joe Rattz Student Id = 7 : original -does not exist- : current George Oscar Bluth Student Id = 13 : original -does not exist- : current Stacy Sinclair Student Id = 72 : original -does not exist- : current Dignan Stephens ---------------------------------------------------------------
因此,指定新表的主键是必要的。就象下面这样:
DataTable newTable = dt1.AsEnumerable().CopyToDataTable(); newTable.PrimaryKey = new DataColumn[] { newTable.Columns[0] };
不知道列的基本特征、约束等会否一并被Copy?待解!
P373 LINQ to 类型化DataSet
类型化DataSet,基于编译时的类型检查和直观的表、列引用语法等优势,曾经是NET 2.0时代数据库应用的利器。但受限于其Cache的特性,和缺乏一致的查询机制,在N层应用中上下穿梭的能力有限,逐渐被新兴的LINQ与Entity所取代。但在小型的数据库应用中,DataSet仍有一定的生存空间。
string name = studentsDataSet.Students .Where(student => student.Id == 7) .Single().Name;
特别要提的是,类SQL语法的形式与扩展方法调用的形式相比较,仍是后者的效能胜出。