在 C# 开发中,Linq(Language Integrated Query)提供了强大的数据查询能力,尤其是在处理集合间的关联操作时。本文将详细解析 C# Linq 中的左关联查询,并通过实际案例说明其用法。
左关联查询基础
左关联(Left Join)是 SQL 中的一种关联查询方式,它会返回左表中的所有记录,无论右表中是否有匹配记录。在 Linq 中实现左关联需要使用GroupJoin
和DefaultIfEmpty
方法组合,或者使用查询语法中的into
和DefaultIfEmpty
关键字。
基本语法结构
csharp
var query = from left in leftTable
join right in rightTable
on left.Key equals right.Key into tempGroup
from temp in tempGroup.DefaultIfEmpty()
select new {
LeftProp = left.Property,
RightProp = temp?.Property
};
这里的关键点是:
- 使用
into
关键字将右表分组到临时集合 - 使用
DefaultIfEmpty()
处理可能为空的情况 - 在结果选择器中使用空条件运算符
?.
处理可能的 null 值
实际案例分析
让我们通过一个实际案例来理解左关联的应用场景。假设我们有一个企业资产管理系统,需要查询特定日期范围内有资产检查记录的企业,并标记哪些企业有记录。
以下是完整的示例代码:
csharp
using System;
using System.Collections.Generic;
using System.Linq;
namespace LinqLeftJoinDemo
{
// 企业信息类
public class Enterprise
{
public string Code { get; set; }
public string Name { get; set; }
public string Industry { get; set; }
}
// 资产检查记录类
public class AssetCheckRecord
{
public string EnterpriseID { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string MonthEndType { get; set; }
public decimal CheckScore { get; set; }
}
// 资产类型枚举
public enum PigAssets
{
FixedAssets,
CurrentAssets,
IntangibleAssets
}
class Program
{
static void Main(string[] args)
{
// 模拟企业数据
var enterprises = new List<Enterprise>
{
new Enterprise { Code = "E001", Name = "ABC公司", Industry = "制造业" },
new Enterprise { Code = "E002", Name = "XYZ公司", Industry = "零售业" },
new Enterprise { Code = "E003", Name = "123公司", Industry = "服务业" },
new Enterprise { Code = "E004", Name = "TEST公司", Industry = "科技业" }
};
// 模拟资产检查记录数据
var checkRecords = new List<AssetCheckRecord>
{
new AssetCheckRecord
{
EnterpriseID = "E001",
StartDate = new DateTime(2023, 10, 1),
EndDate = new DateTime(2023, 10, 31),
MonthEndType = PigAssets.FixedAssets.ToString(),
CheckScore = 95.5m
},
new AssetCheckRecord
{
EnterpriseID = "E003",
StartDate = new DateTime(2023, 10, 1),
EndDate = new DateTime(2023, 10, 31),
MonthEndType = PigAssets.FixedAssets.ToString(),
CheckScore = 88.0m
}
};
// 查询条件
DateTime startDate = new DateTime(2023, 10, 1);
DateTime endDate = new DateTime(2023, 10, 31);
string assetType = PigAssets.FixedAssets.ToString();
// 第一步:筛选符合条件的检查记录
var filteredRecords = checkRecords
.Where(r => r.StartDate.Date == startDate.Date
&& r.EndDate.Date == endDate.Date
&& r.MonthEndType.Equals(assetType))
.ToList();
// 第二步:执行左关联查询
var result = from e in enterprises
join r in filteredRecords
on e.Code equals r.EnterpriseID into enterpriseGroup
from g in enterpriseGroup.DefaultIfEmpty()
select new
{
EnterpriseID = e.Code,
Name = e.Name,
HasCheckRecord = g != null,
CheckScore = g?.CheckScore ?? 0
};
// 输出结果
Console.WriteLine("企业资产检查记录查询结果:");
Console.WriteLine("企业代码\t企业名称\t是否有检查记录\t检查得分");
foreach (var item in result)
{
Console.WriteLine($"{item.EnterpriseID}\t{item.Name}\t{item.HasCheckRecord}\t\t{item.CheckScore}");
}
}
}
}
代码解析
上述代码实现了一个典型的左关联查询场景:
数据准备:创建了两个实体类
Enterprise
和AssetCheckRecord
,并初始化了示例数据。条件筛选:首先筛选出特定日期范围内且资产类型匹配的检查记录:
csharp
var filteredRecords = checkRecords .Where(r => r.StartDate.Date == startDate.Date && r.EndDate.Date == endDate.Date && r.MonthEndType.Equals(assetType)) .ToList();
左关联查询:使用查询语法实现左关联,确保返回所有企业信息,并标记是否有检查记录:
csharp
var result = from e in enterprises join r in filteredRecords on e.Code equals r.EnterpriseID into enterpriseGroup from g in enterpriseGroup.DefaultIfEmpty() select new { ... };
结果处理:通过
DefaultIfEmpty()
方法处理可能的空值情况,并使用空合并运算符??
提供默认值。
执行结果分析
运行上述代码,输出结果如下:
plaintext
企业资产检查记录查询结果:
企业代码 企业名称 是否有检查记录 检查得分
E001 ABC公司 True 95.5
E002 XYZ公司 False 0
E003 123公司 True 88
E004 TEST公司 False 0
可以看到,即使某些企业没有对应的检查记录(如 E002 和 E004),它们仍然会出现在结果集中,并且HasCheckRecord
字段被标记为False
,检查得分为 0。
方法语法实现左关联
除了查询语法,Linq 还提供了方法语法来实现左关联,以下是等效的方法语法实现:
csharp
var result = enterprises
.GroupJoin(
filteredRecords,
e => e.Code,
r => r.EnterpriseID,
(e, group) => new { Enterprise = e, Records = group })
.SelectMany(
temp => temp.Records.DefaultIfEmpty(),
(temp, r) => new
{
EnterpriseID = temp.Enterprise.Code,
Name = temp.Enterprise.Name,
HasCheckRecord = r != null,
CheckScore = r?.CheckScore ?? 0
});
这种方法语法虽然更加函数式,但理解起来可能稍复杂一些。