LINQ
LINQ(Language Integrated Query,语言集成查询)是 .NET 框架中的一种查询技术,它允许开发者使用统一的语法 查询不同类型的数据源(如集合、数据库、XML 等,像List这样的集合类也是可以用LINQ的!)。
查询构建
- 查询表达式 :使用类似 SQL 的语法(如
from、where、select) - 方法语法 :使用 LINQ 扩展方法(如
Where()、Select()、OrderBy()) - 两者可以混合使用,功能等价
这是查询表达式语法,很像SQL:
c#
var query = from device in _context.Devices
where device.Status == "online"
orderby device.CreateTime descending
select new DeviceDTO
{
Id = device.Id,
Name = device.Name
// 其他属性...
};
var result = query.ToList(); // 执行查询
注意:LINQ SQL是直接写到C#代码里的!也就是说,它可以做编译期检查,而不像SQL语句,语法错误要到运行期才能发现(这在spring 里使用mybatis时是很常见的问题)。
这是LINQ方法语法:
c#
// 构建查询(延迟执行)
var query = _context.Devices
.Where(d => d.Status == "online") // 过滤条件
.OrderByDescending(d => d.CreateTime); // 排序
// 执行查询(立即执行)
var onlineDevices = query.ToList(); // 此时才执行 SQL 查询
对于集合类操作而言,LINQ等价与java的stream API。
对于数据库而言,LINQ等价于java的Hibernate。
所以说,C#用一套LINQ技术,归一化了java里好几个领域的技术。
JOIN查询
再来个join语句:
linq
from cp in _context.CollectPoints
join device in _context.Devices on cp.DeviceId equals device.Id
select new { CollectPoint = cp, DeviceName = device.Name };
该LINQ功能说明:
-
数据源定义:
from cp in _context.CollectPoints从CollectPoints表中获取所有记录,每个记录赋值给变量cp。
-
关联操作:
join device in _context.Devices on cp.DeviceId equals device.Id使用join关键字将CollectPoints表与Devices表关联,关联条件是cp.DeviceId等于device.Id。
这相当于 SQL 中的INNER JOIN操作,只返回两个表中匹配的记录。
-
结果投影:
select new { CollectPoint = cp, DeviceName = device.Name }使用select关键字定义查询结果的结构,创建一个匿名类型对象,包含两个属性:
CollectPoint:值为采集点对象cp(包含采集点的所有字段)。DeviceName:值为关联设备的Name属性(设备名称)。
技术原理:
-
LINQ 转换:这段代码会被 Entity Framework Core 转换为 SQL 语句,大致如下:
sqlSELECT cp.*, d.Name AS DeviceName FROM CollectPoints cp INNER JOIN Devices d ON cp.DeviceId = d.Id -
匿名类型 :
new { ... }创建的是一个编译时生成的匿名类型,用于临时存储查询结果,无需定义额外的实体类。
嵌套select
这是一个例子:
c#
await query
.OrderByDescending(p => p.CreateTime)
.Skip((page - 1) * size)
.Take(size)
.Select(p => new AlarmPushDto
{
Id = p.Id,
Name = p.Name,
PushType = p.PushType,
Enabled = p.Enabled,
Remark = p.Remark,
// 这里是嵌套select,注意,这里用的是同步ToList(),但整个LINQ还是ToListAsync(),这是没问题的
Receivers = p.Receivers.Select(r => r.Receiver).ToList()
})
.ToListAsync();
使用上述嵌套select,要求在DBContext里配置HasMany导航属性:
c#
modelBuilder.Entity<AlarmPush>()
// 配置1:N的映射关系
.HasMany(p => p.Receivers)
.WithOne()
// 配置关联字段,这里的ForeignKey并非物理数据库的外键,它只代表逻辑上的关联关系!
.HasForeignKey(r => r.PushId);
EF Core自动翻译的真实SQL为:
SQL
SELECT `t0`.`id`, `t0`.`name`, `t0`.`push_type`, `t0`.`enabled`, `t0`.`remark`, `t1`.`receiver`, `t1`.`push_id`
FROM (
SELECT `t`.`id`, `t`.`name`, `t`.`push_type`, `t`.`enabled`, `t`.`remark`, `t`.`create_time`
FROM `t_alarm_push` AS `t`
ORDER BY `t`.`create_time` DESC
LIMIT @__p_1 OFFSET @__p_0
) AS `t0`
LEFT JOIN `t_alarm_push_receiver` AS `t1` ON `t0`.`id` = `t1`.`push_id`
ORDER BY `t0`.`create_time` DESC, `t0`.`id`, `t1`.`push_id`
这里因为有分页,所以EF Core先做了一个含limit offset的子查询,然后用子查询结果跟子表做left join。
Include方法
配置了配置HasMany导航属性,也可用Include做表关联:
c#
var entity = await context.AlarmPushes
.Include(p => p.Receivers)
.FirstOrDefaultAsync(p => p.Id == dto.Id);
EF Core翻译为:
sql
SELECT `t0`.`id`, `t0`.`create_time`, `t0`.`enabled`, `t0`.`name`, `t0`.`push_type`, `t0`.`remark`, `t0`.`update_time`, `t1`.`push_id`, `t1`.`receiver`, `t1`.`create_time`, `t1`.`update_time`
FROM (
SELECT `t`.`id`, `t`.`create_time`, `t`.`enabled`, `t`.`name`, `t`.`push_type`, `t`.`remark`, `t`.`update_time`
FROM `t_alarm_push` AS `t`
WHERE `t`.`id` = @__dto_Id_0
LIMIT 1
) AS `t0`
LEFT JOIN `t_alarm_push_receiver` AS `t1` ON `t0`.`id` = `t1`.`push_id`
ORDER BY `t0`.`id`, `t1`.`push_id`
也是子查询left join子表,跟嵌套select的翻译结果差不多。与嵌套select不同的是,Include会把子表的所有属性选出来,而嵌套select是可以指定部分属性的,更加灵活。
另外,两者用法上有点区别:Include常用于修改场景,因为它能获得实体的全属性,方便后续SaveChanges,所以一般不配AsNoTracking。而嵌套Select则常用于查询场景(因为可以精确到部分属性),一般会配置AsNoTracking。
多路径嵌套
当LINQ里只有一个Include或一个嵌套Select,我们称之为单路径嵌套,即A和B是1:N的关系。
但如果A和B是1:N,A和C是1:M,在LINQ里写了两个Include或两个嵌套Select,这就是多路径嵌套,很显然,最终SQL会是:
A left join B left join C
这可能造成笛卡尔积爆炸,应尽量避免,可以将上述查询拆成两个查询执行,最后在内存里组装:
ids = A left join B
select C where ids in ()
string的Contains方法
等价于like %%。
SQL注入
LINQ里只要不使用动态特性,天然就能防御SQL注入,比如:
c#
var joinQry = from cp in context.CollectPoints
join device in context.Devices on cp.DeviceId equals device.Id
where device.Name == deviceName
select cp.Code;
上述的where部分,框架会自动帮我们做参数化,不用考虑SQL注入问题。
所谓动态特性包括:FromSqlRaw、动态LINQ字符串等,动态特性就需要我们自己处理SQL注入问题了。
SelectMany
等价与java stream的flatMap
Cast和OfType
Cast:对集合里的每个元素做Cast转型
OfType:对集合里的每个元素做类型过滤。