C# LINQ开发心得

LINQ

LINQ(Language Integrated Query,语言集成查询)是 .NET 框架中的一种查询技术,它允许开发者使用统一的语法 查询不同类型的数据源(如集合、数据库、XML 等,像List这样的集合类也是可以用LINQ的!)。

查询构建

  • 查询表达式 :使用类似 SQL 的语法(如 fromwhereselect
  • 方法语法 :使用 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功能说明:

  1. 数据源定义

    复制代码
      from cp in _context.CollectPoints

    从CollectPoints表中获取所有记录,每个记录赋值给变量cp。

  2. 关联操作

    复制代码
      join device in _context.Devices on cp.DeviceId equals device.Id

    使用join关键字将CollectPoints表与Devices表关联,关联条件是cp.DeviceId等于device.Id

    这相当于 SQL 中的INNER JOIN操作,只返回两个表中匹配的记录。

  3. 结果投影

    复制代码
      select new { CollectPoint = cp, DeviceName = device.Name }

    使用select关键字定义查询结果的结构,创建一个匿名类型对象,包含两个属性:

    • CollectPoint:值为采集点对象 cp(包含采集点的所有字段)。
    • DeviceName:值为关联设备的 Name 属性(设备名称)。

技术原理:

  • LINQ 转换:这段代码会被 Entity Framework Core 转换为 SQL 语句,大致如下:

    sql 复制代码
    SELECT 
        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:对集合里的每个元素做类型过滤。

相关推荐
小白不白1112 小时前
Invoke的用法
开发语言·人工智能·数码相机·计算机视觉·c#
FuckPatience3 小时前
C# 链表元素的引用地址分析
链表·c#
hoiii1873 小时前
C# Txt/Excel/Access 导入导出工具
开发语言·c#·excel
专注VB编程开发20年3 小时前
TFTP 与FTP核心区别:UDP和TCP
c#·tftp
yi念zhi间4 小时前
C#实现控制台多区域输出
开发语言·c#
2501_930707784 小时前
使用 C# 在 Excel 中合并并居中单元格
开发语言·c#·excel
feifeigo1234 小时前
C# ADB 安卓设备数据传输工具
android·adb·c#
xiaoshuaishuai84 小时前
C# 逆向分析Privazer
数据库·microsoft·c#