在 SqlSugar 中,导航查询(Include)与子查询同时使用时出现无数据的情况,通常与 查询别名冲突 或 导航查询结构被导航加载干扰 有关。以下通过具体测试案例分析原因并提供解决方案。
测试场景复现
假设我们有两个实体类,模拟项目(Project)和系统文件(SysFile)的关系,其中 Project 包含自引用的子级导航属性 children:
csharp
// 项目实体
public class Project
{
public int Id { get; set; }
public string title { get; set; }
public string hetong { get; set; } // 关联合同文件URL,关联SysFile.Url
public int? pid { get; set; } // 父级项目ID(自引用)
// 导航属性:子级项目(自引用)
[Navigate(NavigateType.OneToMany, nameof(pid))]
public List<Project> children { get; set; }
}
// 系统文件实体
public class SysFile
{
public int Id { get; set; }
public string FileName { get; set; }
public string Url { get; set; } // 与Project.hetong关联
}
// 输出DTO
public class ProjectOutput
{
public int Id { get; set; }
public string title { get; set; }
public List<elfile> hetongAttachment { get; set; } // 子查询结果
public List<Project> children { get; set; } // 导航属性
}
public class elfile
{
public string name { get; set; }
public string url { get; set; }
}
问题查询代码(子查询无数据)
csharp
// 有问题的查询:同时使用Include(children)和子查询
var query = db.Queryable<Project>()
.Includes(u => u.children) // 加载子级导航属性
.Select(u => new ProjectOutput
{
Id = u.Id,
title = u.title,
// 子查询:通过hetong关联SysFile
hetongAttachment = SqlFunc.Subqueryable<SysFile>()
.Where(f => f.Url == u.hetong)
.Select(f => new elfile { name = f.FileName, url = f.Url })
.ToList(),
children = u.children // 导航属性
});
var result = await query.ToListAsync();
// 现象:hetongAttachment始终为空(即使有匹配数据)
问题原因分析
通过输出 SQL 日志(配置方式见下文),发现 Include(children) 会改变主表的别名,导致子查询中的条件失效:
- 导航查询(Include)的 SQL 生成逻辑:
当使用Includes(u => u.children)时,SqlSugar 会自动生成自连接查询,主表Project会被赋予别名(如u1),而子级children会被赋予另一个别名(如u2)。 - 子查询中的别名冲突:
子查询中使用u.hetong时,SqlSugar 会默认引用主表原始别名(如u),但实际主表已被重命名为u1,导致条件f.Url == u.hetong实际被解析为f.Url == u.hetong(而非u1.hetong),条件不匹配,子查询无结果。
解决方案
方案 1:拆分查询(推荐)
将 “导航属性加载” 和 “子查询” 拆分为两步,避免别名冲突:
csharp
// 第一步:查询主数据+子查询(不加载children)
var query = db.Queryable<Project>()
.Select(u => new ProjectOutput
{
Id = u.Id,
title = u.title,
hetongAttachment = SqlFunc.Subqueryable<SysFile>()
.Where(f => f.Url == u.hetong)
.Select(f => new elfile { name = f.FileName, url = f.Url })
.ToList()
});
var result = await query.ToListAsync();
// 第二步:单独加载children导航属性
if (result.Any())
{
var parentIds = result.Select(r => r.Id).ToList();
// 查询所有子级项目
var children = await db.Queryable<Project>()
.Where(c => parentIds.Contains(c.pid ?? 0))
.ToListAsync();
// 手动关联子级
foreach (var item in result)
{
item.children = children.Where(c => c.pid == item.Id).ToList();
}
}
方案 2:强制子查询引用主表实际别名
通过 SQL 日志获取主表在 Include 后的实际别名(如 u1),使用 SqlFunc.GetSelfColumn 强制子查询引用正确别名:
csharp
var query = db.Queryable<Project>()
.Includes(u => u.children)
.Select(u => new ProjectOutput
{
Id = u.Id,
title = u.title,
// 关键:使用主表实际别名(从SQL日志获取,如u1)
hetongAttachment = SqlFunc.Subqueryable<SysFile>()
.Where(f => f.Url == SqlFunc.GetSelfColumn("u1.hetong"))
.Select(f => new elfile { name = f.FileName, url = f.Url })
.ToList(),
children = u.children
});
注意:主表别名可能随查询复杂度变化(如 u1、u2),需通过日志确认后调整,不推荐在生产环境硬编码。
方案 3:禁用导航查询的自动别名(谨慎使用)
通过配置 Aop 禁用导航查询的别名生成(可能影响其他逻辑,需测试):
csharp
// 初始化SqlSugar时配置
db.Aop.ConfigureJoinTable = (joinItems) =>
{
// 强制主表别名固定为"u"
foreach (var item in joinItems)
{
if (item.IsMainTable)
{
item.Alias = "u"; // 固定主表别名为u
}
}
};
关键排查工具:输出 SQL 日志
配置 SqlSugar 日志,查看生成的 SQL 语句,确认主表别名和子查询条件:
csharp
// 初始化SqlSugar时添加日志配置
db.Aop.OnLogExecuting = (sql, pars) =>
{
Console.WriteLine("生成的SQL:" + sql);
Console.WriteLine("参数:" + string.Join(",", pars.Select(p => $"{p.ParameterName}={p.Value}")));
};

对比 “不加 Include” 和 “加 Include” 的 SQL,重点观察:
- 主表的别名是否变化(如
u→u1)。 - 子查询中的
u.hetong是否正确关联到主表别名。
总结
导航查询(Include)导致子查询无数据的核心原因是 主表别名被改写,导致子查询条件关联失效。推荐采用 拆分查询 的方式,避免导航属性与复杂子查询在同一语句中冲突。若必须同时使用,需通过日志确认主表别名并调整子查询条件。