目录

  1. 什么是 LINQ to DataSet?
  2. 准备工作:环境配置
  3. 核心概念:AsEnumerable() 方法
  4. 基本查询操作
    • 1 查询所有行 (Select)
    • 2 筛选数据 (Where)
    • 3 排序数据 (OrderBy, ThenBy)
    • 4 分组数据 (GroupBy)
    • 5 投影数据 (Select 的进阶用法)
  5. 高级查询操作
    • 1 聚合函数 (Count, Sum, Average, Max, Min)
    • 2 连接查询 (Join, GroupJoin)
    • 3 集合操作 (Union, Intersect, Except)
  6. 处理 DataRow 对象
  7. 性能优化技巧
  8. 完整示例:从数据库到查询结果

什么是 LINQ to DataSet?

LINQ to DataSet 是 LINQ to Objects 的一个特例,它允许你使用强大的 LINQ 查询语法来查询和操作 ADO.NET DataSetDataTable 中的数据。

linq to dataset教程
(图片来源网络,侵删)

核心优势:

  • 声明式语法:使用类似 SQL 的 Where, Select, OrderBy 等关键字,代码更易读、更易维护。
  • 编译时检查:查询在编译时进行检查,可以提前发现错误,而不是等到运行时。
  • 丰富的功能:除了基本的查询,还支持聚合、连接、分组等复杂操作。
  • 与现有 ADO.NET 代码无缝集成:可以轻松地将 LINQ 查询结果与传统的 ADO.NET 代码结合使用。

与传统 DataViewDataTable.Select() 的对比:

  • DataTable.Select():功能有限,语法笨拙,不支持复杂的排序和分组。
  • DataView:主要用于绑定和排序,查询能力较弱。
  • LINQ to DataSet:功能最强大,语法最优雅,是处理内存中 DataSet 数据的首选方式。

准备工作:环境配置

要使用 LINQ to DataSet,你的项目需要满足以下条件:

  1. .NET Framework:LINQ 是 .NET Framework 3.5 引入的,因此你的项目目标框架必须是 3.5 或更高版本。

    linq to dataset教程
    (图片来源网络,侵删)
  2. 必要的命名空间:在你的代码文件顶部(如 usingImports)添加以下引用:

    using System.Data;        // 用于 DataSet, DataTable, DataRow 等
    using System.Data.SqlClient; // 如果使用 SQL Server
    using System.Linq;        // LINQ 的核心命名空间
    using System.Linq.Expressions; // 有时用于高级查询

核心概念:AsEnumerable() 方法

这是 LINQ to DataSet 中最重要的一个方法。

DataTable 本身并不直接实现 IEnumerable<T> 接口,因此不能直接对其使用 LINQ 查询。AsEnumerable() 方法的作用就是将 DataTable 转换为一个 IEnumerable<DataRow> 序列,一旦你有了这个序列,你就可以对其应用所有标准的 LINQ 操作。

语法:

// 将 DataTable 转换为可查询的 DataRow 序列
IEnumerable<DataRow> query = from row in myTable.AsEnumerable()
                             // ... 在这里添加查询子句
                             select row;

基本查询操作

我们假设有一个名为 ProductsDataTable,其结构如下:

ProductID ProductName CategoryID UnitPrice UnitsInStock
1 Chair 1 00 100
2 Table 1 00 50
3 Keyboard 2 00 200
4 Mouse 2 00 500
5 Monitor 3 00 30

1 查询所有行 (Select)

虽然 Select 在简单查询中是可选的(默认行为),但明确使用它可以清晰地表明你的意图,并且是进行数据投影的关键。

// 查询所有产品,并返回 DataRow 对象
var allProducts = from product in productsTable.AsEnumerable()
                  select product;
// 遍历结果
foreach (var row in allProducts)
{
    Console.WriteLine($"ID: {row["ProductID"]}, Name: {row["ProductName"]}");
}

2 筛选数据 (Where)

这是最常见的查询操作,用于根据指定条件过滤行。

// 查询所有单价大于 100 的产品
var expensiveProducts = from product in productsTable.AsEnumerable()
                        where product.Field<decimal>("UnitPrice") > 100
                        select product;
foreach (var row in expensiveProducts)
{
    Console.WriteLine($"Name: {row["ProductName"]}, Price: {row["UnitPrice"]}");
}

注意:推荐使用 Field<T>() 方法来访问列值,它比直接使用 row["ColumnName"] 更安全,因为它:

  • 提供了编译时类型检查
  • 正确处理数据库中的 NULL 值,避免 NullReferenceException

3 排序数据 (OrderBy, ThenBy)

使用 OrderBy 进行升序排序,使用 ThenBy 进行次级排序。

// 查询所有产品,按单价升序排序
var productsSortedByPrice = from product in productsTable.AsEnumerable()
                           orderby product.Field<decimal>("UnitPrice") ascending
                           select product;
// 查询所有产品,先按类别升序,再按单价降序排序
var productsSortedByCategoryAndPrice = from product in productsTable.AsEnumerable()
                                       orderby product.Field<int>("CategoryID") ascending,
                                               product.Field<decimal>("UnitPrice") descending
                                       select product;
foreach (var row in productsSortedByCategoryAndPrice)
{
    Console.WriteLine($"Category: {row["CategoryID"]}, Name: {row["ProductName"]}, Price: {row["UnitPrice"]}");
}

4 分组数据 (GroupBy)

GroupBy 将数据按指定的键进行分组,返回一个 IGrouping<TKey, TElement> 的集合。

// 按类别ID对产品进行分组
var productsByCategory = from product in productsTable.AsEnumerable()
                         group product by product.Field<int>("CategoryID");
foreach (var group in productsByCategory)
{
    Console.WriteLine($"Category ID: {group.Key}");
    foreach (var product in group)
    {
        Console.WriteLine($"  - {product["ProductName"]}");
    }
}

5 投影数据 (Select 的进阶用法)

查询结果不一定非要是 DataRow,你可以使用 Select 将数据投影到新的类型中,例如匿名类型、自定义类或简单类型,这使得数据更易于在 UI 层或服务层使用。

使用匿名类型:

// 查询产品名称和单价,并将结果投影为匿名类型
var productDetails = from product in productsTable.AsEnumerable()
                     select new
                     {
                         Name = product.Field<string>("ProductName"),
                         Price = product.Field<decimal>("UnitPrice")
                     };
foreach (var detail in productDetails)
{
    Console.WriteLine($"Product: {detail.Name}, Price: {detail.Price:C}");
}

使用自定义类:

public class ProductInfo
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
// 查询并投影到自定义类
var productInfos = from product in productsTable.AsEnumerable()
                   select new ProductInfo
                   {
                       Name = product.Field<string>("ProductName"),
                       Price = product.Field<decimal>("UnitPrice")
                   };
foreach (var info in productInfos)
{
    Console.WriteLine($"Product: {info.Name}, Price: {info.Price:C}");
}

高级查询操作

1 聚合函数

LINQ 提供了丰富的聚合函数。

// 查询产品总数
int totalProducts = productsTable.AsEnumerable().Count();
Console.WriteLine($"Total Products: {totalProducts}");
// 计算所有产品的总库存
int totalStock = productsTable.AsEnumerable().Sum(p => p.Field<int>("UnitsInStock"));
Console.WriteLine($"Total Stock: {totalStock}");
// 计算平均单价
decimal averagePrice = productsTable.AsEnumerable().Average(p => p.Field<decimal>("UnitPrice"));
Console.WriteLine($"Average Price: {averagePrice:C}");
// 找出最贵的产品
var mostExpensiveProduct = productsTable.AsEnumerable().OrderByDescending(p => p.Field<decimal>("UnitPrice")).FirstOrDefault();
Console.WriteLine($"Most Expensive: {mostExpensiveProduct["ProductName"]}");

2 连接查询 (Join, GroupJoin)

假设我们还有一个 Categories 表。

CategoryID CategoryName
1 Furniture
2 Electronics
3 Computers
// 假设我们有两个 DataTable: productsTable 和 categoriesTable
var productsWithCategories = from product in productsTable.AsEnumerable()
                             join category in categoriesTable.AsEnumerable()
                             on product.Field<int>("CategoryID") equals category.Field<int>("CategoryID")
                             select new
                             {
                                 ProductName = product.Field<string>("ProductName"),
                                 CategoryName = category.Field<string>("CategoryName"),
                                 Price = product.Field<decimal>("UnitPrice")
                             };
foreach (var item in productsWithCategories)
{
    Console.WriteLine($"{item.ProductName} ({item.CategoryName}) - {item.Price:C}");
}

3 集合操作

这些操作需要两个集合。

// 假设我们有另一个 DataTable: discontinuedProductsTable
// 获取所有产品,但排除已停产的产品
var availableProducts = productsTable.AsEnumerable()
                                    .Except(discontinuedProductsTable.AsEnumerable(), DataRowComparer.Default);
// 获取既在主产品列表又在促销列表中的产品 (交集)
var promotionalProducts = productsTable.AsEnumerable()
                                       .Intersect(promotionalProductsTable.AsEnumerable(), DataRowComparer.Default);
// 获取所有产品,并合并促销列表中的新产品 (并集)
var allProductsCombined = productsTable.AsEnumerable()
                                       .Union(promotionalProductsTable.AsEnumerable(), DataRowComparer.Default);

注意DataRowComparer.Default 用于比较 DataRow 对象,因为它需要比较所有列的值。


处理 DataRow 对象

当你查询返回的是 DataRow 时,你可能需要修改它们。DataRow 是一个引用类型,所以当你修改查询结果中的 DataRow 时,你实际上是在修改原始 DataTable 中的数据。

// 查询所有单价低于 50 的产品,并将它们的价格提高 10%
var cheapProducts = from product in productsTable.AsEnumerable()
                    where product.Field<decimal>("UnitPrice") < 50
                    select product;
foreach (var product in cheapProducts)
{
    decimal newPrice = product.Field<decimal>("UnitPrice") * 1.1m;
    product.SetField("UnitPrice", newPrice); // 修改原始数据
}
// productsTable 中的数据已经被永久修改了

性能优化技巧

  1. 使用 AsEnumerable():始终使用它来启动查询。

  2. 尽早筛选:在查询链的早期使用 Where 子句,可以减少后续操作需要处理的数据量。

    // 好:先筛选,再排序
    var query = from p in productsTable.AsEnumerable()
                where p.Field<bool>("IsActive") // 先过滤掉不活跃的
                orderby p.Field<string>("Name")
                select p;
    // 不好:先排序,再筛选
    var query = from p in productsTable.AsEnumerable()
                orderby p.Field<string>("Name")
                where p.Field<bool>("IsActive") // 排序时所有数据都在内存中
                select p;
  3. 考虑使用 DataTable.Copy():如果你需要对原始 DataTable 进行大量修改,但又不希望影响原始数据,可以先创建一个副本,然后在副本上进行查询和修改。

    DataTable workingCopy = productsTable.Copy();
    // 在 workingCopy 上进行 LINQ 操作和修改
  4. 避免在循环中查询:不要在 foreach 循环内部对同一个 DataTable 执行 LINQ 查询,应该一次性获取所需数据。


完整示例:从数据库到查询结果

这个示例展示了如何从数据库填充 DataSet,然后使用 LINQ to DataSet 进行查询。

using System;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
public class LinqToDataSetExample
{
    public static void Main()
    {
        // 1. 连接数据库并填充 DataSet
        string connectionString = "Your_Connection_String";
        string query = "SELECT ProductID, ProductName, CategoryID, UnitPrice, UnitsInStock FROM Products; SELECT CategoryID, CategoryName FROM Categories;";
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            SqlDataAdapter adapter = new SqlDataAdapter(query, connection);
            DataSet dataSet = new DataSet();
            adapter.Fill(dataSet);
            // 获取 DataTable
            DataTable productsTable = dataSet.Tables[0];
            DataTable categoriesTable = dataSet.Tables[1];
            Console.WriteLine("--- 查询所有单价大于 100 的产品 ---");
            var expensiveProducts = from p in productsTable.AsEnumerable()
                                    where p.Field<decimal>("UnitPrice") > 100
                                    orderby p.Field<decimal>("UnitPrice") descending
                                    select new
                                    {
                                        Name = p.Field<string>("ProductName"),
                                        Price = p.Field<decimal>("UnitPrice")
                                    };
            foreach (var p in expensiveProducts)
            {
                Console.WriteLine($"{p.Name}: {p.Price:C}");
            }
            Console.WriteLine("\n--- 按类别分组产品并计算每个类别的产品数量 ---");
            var categoryGroups = from p in productsTable.AsEnumerable()
                                 join c in categoriesTable.AsEnumerable()
                                 on p.Field<int>("CategoryID") equals c.Field<int>("CategoryID")
                                 group p by c.Field<string>("CategoryName") into g
                                 select new
                                 {
                                     CategoryName = g.Key,
                                     ProductCount = g.Count()
                                 };
            foreach (var group in categoryGroups)
            {
                Console.WriteLine($"{group.CategoryName}: {group.ProductCount} 个产品");
            }
        }
    }
}

LINQ to DataSet 是一个极其强大和优雅的工具,用于处理内存中的 ADO.NET 数据,通过掌握 AsEnumerable() 和 LINQ 的标准查询操作符,你可以以声明式、类型安全且富有表现力的方式完成各种复杂的数据检索、筛选、排序、分组和聚合任务。

关键要点回顾:

  • 入口是 AsEnumerable():没有它,LINQ 查询无法工作。
  • 优先使用 Field<T>():为了类型安全和处理 NULL
  • Select 用于投影:让查询结果更符合你的业务逻辑。
  • 组合使用Where, OrderBy, GroupBy 等可以自由组合,构建复杂的查询。
  • 性能至上:尽早 Where,减少数据集大小。

希望这份详尽的教程能帮助你顺利上手 LINQ to DataSet!