5.3 LINQ (Language Integrated Query) 第五章:数据访问 (Data Access) - 5.3 LINQ (Language Integrated Query) 5.3.1 LINQ 概述:数据访问的革新 LINQ 是 .NET Framework 3.5 中引入的一项强大技术,它为 .NET 语言(如 C# 和 VB.NET)提供了统一的查询语法,可以用于各种数据源。这意味着,无论你的数据存储在内存中的集合、数据库、XML 文档还是其他形式的数据源中,你都可以使用相似的 LINQ 查询语句进行操作。
LINQ 是 .NET Framework 3.5 中引入的一项强大技术,它为 .NET 语言(如 C# 和 VB.NET)提供了统一的查询语法,可以用于各种数据源。这意味着,无论你的数据存储在内存中的集合、数据库、XML 文档还是其他形式的数据源中,你都可以使用相似的 LINQ 查询语句进行操作。
LINQ 的核心价值在于:
统一的查询语法: 不再需要学习多种不同的查询语言(如 SQL, XML 查询语言等),LINQ 提供了一种通用的、基于语言的查询方式,降低了学习成本和开发复杂度。
类型安全: LINQ 查询在编译时进行类型检查,可以及早发现类型错误,避免运行时异常,提高了代码的健壮性。
强大的查询能力: LINQ 提供了丰富的查询操作符,可以实现复杂的过滤、排序、分组、连接等数据操作,满足各种业务需求。
代码可读性和可维护性: LINQ 查询语句通常更加简洁、易懂,提高了代码的可读性和可维护性,方便团队协作和后续维护。
延迟执行: LINQ 查询默认采用延迟执行策略,只有在真正需要数据时才执行查询,提高了性能,尤其是在处理大量数据时。
在 ASP.NET 开发中,LINQ 主要应用于以下几个方面:
LINQ to Objects: 查询和操作内存中的对象集合,例如 List, Array 等。常用于处理业务逻辑中的数据筛选、排序、转换等。
LINQ to Entities (Entity Framework Core): 与 Entity Framework Core (EF Core) 结合,查询和操作关系型数据库。这是 ASP.NET Core 中最常用的数据访问方式,LINQ to Entities 允许开发者使用 LINQ 查询语法操作数据库,而无需编写原生 SQL 语句。
LINQ to XML: 查询和操作 XML 文档。虽然在 ASP.NET 应用中相对较少使用,但在处理配置文件、数据交换等场景仍然有用。
LINQ to SQL (已过时,不推荐新项目使用): 早期 .NET Framework 中的 ORM 技术,已被 Entity Framework Core 取代。
本章节将重点关注 LINQ to Objects 和 LINQ to Entities 在 ASP.NET 应用中的实践应用。
LINQ 提供了两种主要的语法风格来编写查询:查询表达式 (Query Syntax) 和 方法语法 (Method Syntax)。
查询表达式是一种声明式的、类似 SQL 的语法,更接近自然语言,易于理解和编写简单的查询。它使用 from, where, select, orderby, groupby, join 等关键字来构建查询。
示例代码 (C#):
// 假设有一个用户列表 List<User> users = new List<User>() { new User { Id = 1, Name = "Alice", Age = 25, City = "Beijing" }, new User { Id = 2, Name = "Bob", Age = 30, City = "Shanghai" }, new User { Id = 3, Name = "Charlie", Age = 22, City = "Beijing" }, new User { Id = 4, Name = "David", Age = 35, City = "Guangzhou" } }; // 使用查询表达式筛选年龄大于 25 岁的用户,并选择他们的姓名 var adultUserNames = from user in users where user.Age > 25 select user.Name; // 遍历结果 foreach (var name in adultUserNames) { Console.WriteLine(name); // 输出 Bob, David }
代码详解:
from user in users: 指定数据源 users 和迭代变量 user,类似于 SQL 中的 FROM 子句。
where user.Age > 25: 指定过滤条件,只选择 Age 属性大于 25 的用户,类似于 SQL 中的 WHERE 子句。
select user.Name: 指定要选择的结果,这里选择用户的 Name 属性,类似于 SQL 中的 SELECT 子句。
方法语法是一种更灵活、更底层的语法,它使用扩展方法和 Lambda 表达式来构建查询。方法语法更加强大,可以实现更复杂的查询逻辑。
示例代码 (C#):
// 使用方法语法实现与查询表达式相同的功能 var adultUserNamesMethod = users.Where(user => user.Age > 25) .Select(user => user.Name); // 遍历结果 foreach (var name in adultUserNamesMethod) { Console.WriteLine(name); // 输出 Bob, David }
代码详解:
users.Where(user => user.Age > 25): 使用 Where 扩展方法对 users 集合进行过滤。user => user.Age > 25 是一个 Lambda 表达式,表示过滤条件,类似于查询表达式中的 where 子句。
.Select(user => user.Name): 使用 Select 扩展方法对过滤后的结果进行投影,选择用户的 Name 属性,类似于查询表达式中的 select 子句。
选择查询表达式还是方法语法?
简单查询: 查询表达式通常更简洁易懂,适合简单的过滤、选择、排序等操作。
复杂查询: 方法语法更加灵活,可以实现更复杂的逻辑,例如链式调用、匿名类型、更复杂的 Lambda 表达式等。
个人偏好: 两种语法都可以实现相同的功能,选择哪种语法更多取决于个人偏好和团队约定。
在实际开发中,经常会混合使用两种语法,根据具体情况选择最合适的语法风格。
LINQ 提供了丰富的操作符,可以实现各种数据查询和操作。以下列举一些常用的操作符,并结合代码示例进行详细解释。
WhereWhere 操作符用于根据指定的条件筛选序列中的元素。
示例代码 (C#):
// 筛选城市为 "Beijing" 的用户 var beijingUsers = users.Where(user => user.City == "Beijing"); // 查询表达式写法 var beijingUsersQuery = from user in users where user.City == "Beijing" select user; // 输出结果 Console.WriteLine("北京用户:"); foreach (var user in beijingUsers) { Console.WriteLine($"姓名:{user.Name},城市:{user.City}"); }
Select, SelectManySelect: 将序列中的每个元素投影到新的形式。可以用于选择元素的属性,或者将元素转换为新的对象。示例代码 (C#):
// 选择所有用户的姓名和年龄,并创建匿名对象 var userInfos = users.Select(user => new { UserName = user.Name, UserAge = user.Age }); // 查询表达式写法 var userInfosQuery = from user in users select new { UserName = user.Name, UserAge = user.Age }; // 输出结果 Console.WriteLine("用户姓名和年龄:"); foreach (var userInfo in userInfos) { Console.WriteLine($"姓名:{userInfo.UserName},年龄:{userInfo.UserAge}"); }
SelectMany: 将序列中的每个元素投影到一个序列,并将所有结果序列合并成一个序列。常用于处理嵌套集合。示例代码 (C#):
// 假设用户类新增了 Orders 属性,表示用户的订单列表 public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public string City { get; set; } public List<Order> Orders { get; set; } // 新增订单列表 } public class Order { public int OrderId { get; set; } public string ProductName { get; set; } public decimal Price { get; set; } } // 初始化用户数据,包含订单信息 List<User> usersWithOrders = new List<User>() { new User { Id = 1, Name = "Alice", Age = 25, City = "Beijing", Orders = new List<Order>() { new Order { OrderId = 101, ProductName = "Product A", Price = 10.00m }, new Order { OrderId = 102, ProductName = "Product B", Price = 20.00m } } }, new User { Id = 2, Name = "Bob", Age = 30, City = "Shanghai", Orders = new List<Order>() { new Order { OrderId = 201, ProductName = "Product C", Price = 30.00m } } } }; // 使用 SelectMany 获取所有用户的订单列表 var allOrders = usersWithOrders.SelectMany(user => user.Orders); // 查询表达式写法 var allOrdersQuery = from user in usersWithOrders from order in user.Orders select order; // 输出结果 Console.WriteLine("所有订单:"); foreach (var order in allOrders) { Console.WriteLine($"订单号:{order.OrderId},商品名称:{order.ProductName},价格:{order.Price}"); }
OrderBy, OrderByDescending, ThenBy, ThenByDescendingOrderBy: 升序排序。
OrderByDescending: 降序排序。
ThenBy: 在 OrderBy 或 OrderByDescending 之后,进行次要排序(升序)。
ThenByDescending: 在 OrderBy 或 OrderByDescending 之后,进行次要排序(降序)。
示例代码 (C#):
// 按年龄升序排序用户列表 var usersOrderByAgeAsc = users.OrderBy(user => user.Age); // 查询表达式写法 var usersOrderByAgeAscQuery = from user in users orderby user.Age ascending select user; // 按年龄降序,再按姓名升序排序 var usersOrderByAgeDescThenNameAsc = users.OrderByDescending(user => user.Age) .ThenBy(user => user.Name); // 查询表达式写法 var usersOrderByAgeDescThenNameAscQuery = from user in users orderby user.Age descending, user.Name ascending select user; // 输出结果 (按年龄降序,再按姓名升序) Console.WriteLine("用户列表(按年龄降序,再按姓名升序):"); foreach (var user in usersOrderByAgeDescThenNameAsc) { Console.WriteLine($"姓名:{user.Name},年龄:{user.Age},城市:{user.City}"); }
GroupByGroupBy 操作符用于根据指定的键将序列中的元素分组。
示例代码 (C#):
// 按城市分组用户 var usersGroupedByCity = users.GroupBy(user => user.City); // 查询表达式写法 var usersGroupedByCityQuery = from user in users group user by user.City into cityGroup select cityGroup; // 输出结果 Console.WriteLine("用户按城市分组:"); foreach (var cityGroup in usersGroupedByCity) { Console.WriteLine($"城市:{cityGroup.Key}"); // 输出城市名称 (分组的键) foreach (var user in cityGroup) // 遍历每个分组中的用户 { Console.WriteLine($" - 姓名:{user.Name}"); } }
Join, GroupJoinJoin: 基于键选择器联接两个序列的元素,类似于 SQL 中的 INNER JOIN。示例代码 (C#):
// 假设有城市列表 List<City> cities = new List<City>() { new City { CityName = "Beijing", Population = 21540000 }, new City { CityName = "Shanghai", Population = 26320000 }, new City { CityName = "Guangzhou", Population = 18670000 }, new City { CityName = "Shenzhen", Population = 17560000 } }; public class City { public string CityName { get; set; } public int Population { get; set; } } // 使用 Join 联接用户列表和城市列表,基于城市名称 var userCityInfos = users.Join(cities, user => user.City, // 用户列表的键选择器 (城市名称) city => city.CityName, // 城市列表的键选择器 (城市名称) (user, city) => new { // 结果选择器,将用户和城市信息合并 UserName = user.Name, UserCity = user.City, CityPopulation = city.Population }); // 查询表达式写法 var userCityInfosQuery = from user in users join city in cities on user.City equals city.CityName select new { UserName = user.Name, UserCity = user.City, CityPopulation = city.Population }; // 输出结果 Console.WriteLine("用户和城市信息联接结果:"); foreach (var info in userCityInfos) { Console.WriteLine($"姓名:{info.UserName},城市:{info.UserCity},城市人口:{info.CityPopulation}"); }
GroupJoin: 基于键选择器联接两个序列的元素,并对第一个序列的每个元素创建一组匹配的第二个序列的元素,类似于 SQL 中的 LEFT JOIN。示例代码 (C#):
// 使用 GroupJoin 联接用户列表和城市列表,并分组城市用户 var cityUserGroups = cities.GroupJoin(users, city => city.CityName, // 城市列表的键选择器 (城市名称) user => user.City, // 用户列表的键选择器 (城市名称) (city, userGroup) => new { // 结果选择器 CityName = city.CityName, CityPopulation = city.Population, UsersInCity = userGroup.ToList() // 将用户分组为列表 }); // 查询表达式写法 var cityUserGroupsQuery = from city in cities join user in users on city.CityName equals user.City into userGroup select new { CityName = city.CityName, CityPopulation = city.Population, UsersInCity = userGroup.ToList() }; // 输出结果 Console.WriteLine("城市和用户分组联接结果:"); foreach (var cityGroup in cityUserGroups) { Console.WriteLine($"城市:{cityGroup.CityName},人口:{cityGroup.CityPopulation}"); Console.WriteLine(" 用户:"); foreach (var user in cityGroup.UsersInCity) { Console.WriteLine($" - 姓名:{user.Name}"); } }
Count, Sum, Average, Min, Max聚合操作符用于对序列中的元素进行聚合计算。
Count: 计数序列中的元素数量。
Sum: 计算序列中数值元素的总和。
Average: 计算序列中数值元素的平均值。
Min: 查找序列中的最小值。
Max: 查找序列中的最大值。
示例代码 (C#):
// 统计用户总数 int userCount = users.Count(); Console.WriteLine($"用户总数:{userCount}"); // 计算所有用户的平均年龄 double averageAge = users.Average(user => user.Age); Console.WriteLine($"平均年龄:{averageAge}"); // 查找年龄最大的用户 User oldestUser = users.OrderByDescending(user => user.Age).First(); // 使用 OrderByDescending 和 First User oldestUser2 = users.MaxBy(user => user.Age); // 或者使用 MaxBy (C# 6+) Console.WriteLine($"最年长的用户:{oldestUser.Name},年龄:{oldestUser.Age}");
Any, All, Contains量词操作符用于判断序列中的元素是否满足某些条件。
Any: 判断序列中是否至少有一个元素满足条件。
All: 判断序列中是否所有元素都满足条件。
Contains: 判断序列中是否包含指定的元素。
示例代码 (C#):
// 判断是否有年龄大于 30 岁的用户 bool hasAdultUser = users.Any(user => user.Age > 30); Console.WriteLine($"是否有年龄大于 30 岁的用户:{hasAdultUser}"); // 判断是否所有用户都来自中国 (假设所有城市都在中国) bool allUsersFromChina = users.All(user => user.City != null); // 假设 City 不为空表示来自中国 Console.WriteLine($"是否所有用户都来自中国:{allUsersFromChina}"); // 判断用户列表中是否包含名为 "Bob" 的用户 bool containsBob = users.Any(user => user.Name == "Bob"); bool containsBob2 = users.Contains(users.FirstOrDefault(u => u.Name == "Bob")); // 或者使用 Contains 和 FirstOrDefault Console.WriteLine($"是否包含名为 'Bob' 的用户:{containsBob}");
Distinct, Union, Intersect, Except集合操作符用于执行集合运算,例如去重、并集、交集、差集等。
Distinct: 去除序列中的重复元素。
Union: 合并两个序列,并去除重复元素 (并集)。
Intersect: 返回两个序列的交集。
Except: 返回第一个序列中存在,但在第二个序列中不存在的元素 (差集)。
示例代码 (C#):
List<int> numbers1 = new List<int>() { 1, 2, 2, 3, 4, 5 }; List<int> numbers2 = new List<int>() { 3, 5, 6, 7, 8 }; // 去除 numbers1 中的重复元素 var distinctNumbers = numbers1.Distinct(); // 输出 1, 2, 3, 4, 5 Console.WriteLine($"去重后的 numbers1:{string.Join(", ", distinctNumbers)}"); // 求 numbers1 和 numbers2 的并集 var unionNumbers = numbers1.Union(numbers2); // 输出 1, 2, 3, 4, 5, 6, 7, 8 Console.WriteLine($"numbers1 和 numbers2 的并集:{string.Join(", ", unionNumbers)}"); // 求 numbers1 和 numbers2 的交集 var intersectNumbers = numbers1.Intersect(numbers2); // 输出 3, 5 Console.WriteLine($"numbers1 和 numbers2 的交集:{string.Join(", ", intersectNumbers)}"); // 求 numbers1 相对于 numbers2 的差集 var exceptNumbers = numbers1.Except(numbers2); // 输出 1, 2, 4 Console.WriteLine($"numbers1 相对于 numbers2 的差集:{string.Join(", ", exceptNumbers)}");
Skip, TakeSkip: 跳过序列中指定数量的元素。
Take: 从序列的开头选取指定数量的元素。
这两个操作符常用于实现分页功能,例如在 Web 应用中显示数据列表时,每次只显示一部分数据。
示例代码 (C#):
int pageSize = 2; // 每页显示 2 条数据 int pageNumber = 2; // 获取第二页数据 // 跳过前 (pageNumber - 1) * pageSize 条数据,然后选取 pageSize 条数据 var pagedUsers = users.Skip((pageNumber - 1) * pageSize).Take(pageSize); // 输出第二页用户 Console.WriteLine($"第 {pageNumber} 页用户:"); foreach (var user in pagedUsers) { Console.WriteLine($"姓名:{user.Name}"); }
LINQ 查询的一个重要特性是 延迟执行 (Deferred Execution)。这意味着,当您定义一个 LINQ 查询时,查询并不会立即执行。只有在您真正需要查询结果时,例如通过循环遍历、调用 ToList(), ToArray() 等方法时,查询才会执行。
延迟执行的优势:
性能优化: 避免不必要的数据库查询或数据处理,只有在真正需要数据时才执行查询,提高了性能。
组合查询: 可以将多个 LINQ 操作符链式组合起来,最终只执行一次查询,减少了数据库访问次数。
立即执行 (Immediate Execution):
有些 LINQ 操作符会强制查询立即执行,并返回结果。这些操作符通常会返回一个非序列的结果,例如单个值、集合等。常见的立即执行操作符包括:
ToList(), ToArray(), ToDictionary(), ToLookup(): 将查询结果转换为 List, Array, Dictionary, Lookup 等集合类型。
Count(), Sum(), Average(), Min(), Max(): 聚合操作符。
First(), FirstOrDefault(), Last(), LastOrDefault(), Single(), SingleOrDefault(): 单元素操作符。
Any(), All(), Contains(): 量词操作符。
示例代码 (C#):
// 延迟执行示例 var query = users.Where(user => user.Age > 25).Select(user => user.Name); // 查询定义,但未执行 Console.WriteLine("查询定义完成,但未执行"); // 循环遍历结果,触发查询执行 Console.WriteLine("开始遍历结果,触发查询执行:"); foreach (var name in query) { Console.WriteLine(name); // 查询在此处执行 } // 立即执行示例 List<string> adultUserNamesList = users.Where(user => user.Age > 25).Select(user => user.Name).ToList(); // ToList() 强制查询立即执行 Console.WriteLine("使用 ToList() 强制查询立即执行:"); foreach (var name in adultUserNamesList) { Console.WriteLine(name); // 结果已在 ToList() 中获取 }
Mermaid Graph TD 图示延迟执行流程:
图示解释:
LINQ Query Definition: 开发者编写 LINQ 查询语句。
Query Not Executed Yet: 查询定义完成,但尚未执行,处于延迟执行状态。
Need to Access Result?: 程序是否需要访问查询结果?
Yes: 如果需要访问结果 (例如遍历、调用立即执行操作符),则执行查询。
Query Execution: LINQ 查询引擎执行查询。
Result Retrieved: 查询结果被检索出来。
No: 如果不需要访问结果,查询仍然保持延迟状态,可以继续进行其他操作。
理解 LINQ 的延迟执行机制对于优化性能至关重要。在开发中,应尽量利用延迟执行的优势,避免不必要的查询执行,提高应用程序的效率。
在 ASP.NET Core 应用中,Entity Framework Core (EF Core) 是最常用的 ORM 框架,用于访问关系型数据库。LINQ to Entities 是 EF Core 的核心组成部分,它允许开发者使用 LINQ 查询语法来操作数据库,而无需编写原生 SQL 语句。
使用 LINQ to Entities 的优势:
类型安全: LINQ 查询在编译时进行类型检查,避免了 SQL 字符串拼接可能导致的类型错误和 SQL 注入风险。
数据库抽象: 开发者无需关注底层数据库的具体细节,可以使用统一的 LINQ 查询语法操作不同的数据库系统。
代码可维护性: LINQ 查询语句通常更加简洁易懂,提高了代码的可读性和可维护性。
ORM 功能: EF Core 提供了完整的 ORM 功能,包括对象关系映射、数据上下文管理、事务处理、迁移等,简化了数据库操作。
示例代码 (ASP.NET Core with EF Core):
假设我们有一个 ASP.NET Core Web API 项目,使用 EF Core 连接数据库,并定义了 User 实体类和 ApplicationDbContext 上下文。
Controller 代码 (UserController.cs):
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using YourNamespace.Data; // 假设 ApplicationDbContext 在 YourNamespace.Data 命名空间下 using YourNamespace.Models; // 假设 User 模型在 YourNamespace.Models 命名空间下 using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace YourNamespace.Controllers { [ApiController] [Route("api/[controller]")] public class UserController : ControllerBase { private readonly ApplicationDbContext _context; public UserController(ApplicationDbContext context) { _context = context; } [HttpGet] public async Task<ActionResult<IEnumerable<User>>> GetUsers() { // 使用 LINQ to Entities 查询所有用户 var users = await _context.Users.ToListAsync(); return users; } [HttpGet("{id}")] public async Task<ActionResult<User>> GetUser(int id) { // 使用 LINQ to Entities 查询指定 ID 的用户 var user = await _context.Users.FindAsync(id); if (user == null) { return NotFound(); } return user; } [HttpGet("adults")] public async Task<ActionResult<IEnumerable<User>>> GetAdultUsers() { // 使用 LINQ to Entities 筛选年龄大于 25 岁的用户 var adultUsers = await _context.Users .Where(u => u.Age > 25) .ToListAsync(); return adultUsers; } [HttpGet("search")] public async Task<ActionResult<IEnumerable<User>>> SearchUsers(string name) { // 使用 LINQ to Entities 模糊搜索用户姓名 var searchResults = await _context.Users .Where(u => u.Name.Contains(name)) .ToListAsync(); return searchResults; } } }
代码详解:
_context.Users: _context 是 ApplicationDbContext 的实例,Users 是 DbSet<User> 属性,代表数据库中的 Users 表。
.ToListAsync(): 将 LINQ 查询结果转换为 List<User> 并异步执行数据库查询。
.Where(u => u.Age > 25): 使用 Where 操作符筛选年龄大于 25 岁的用户,LINQ to Entities 会将此操作转换为 SQL 的 WHERE 子句。
.Name.Contains(name): 使用 Contains 方法进行模糊搜索,LINQ to Entities 会将其转换为 SQL 的 LIKE 子句。
.FindAsync(id): 使用主键查找实体,EF Core 会优化查询性能。