EF Core动态sql

1.查询--- IQueryable 动态添加 Where 条件

最常用的方式是通过 IQueryable 动态添加 Where 条件,EF Core 会自动处理参数化和 SQL 生成,既安全又灵活。

场景示例:动态查询地址列表

假设需要实现一个 "地址查询接口",支持以下可选条件(用户可传可不传):

  • UserId 筛选(必传)
  • IsDefault 筛选(可选:1 = 默认,0 = 非默认)
  • Phone 模糊搜索(可选)
  • CreateTime 时间范围筛选(可选)
cs 复制代码
public async Task<List<Address>> QueryAddressesAsync(
        long userId,          // 必传条件
        byte? isDefault = null,  // 可选:是否默认地址
        string phone = null,    // 可选:手机号模糊搜索
        DateTime? startTime = null,  // 可选:创建开始时间
        DateTime? endTime = null     // 可选:创建结束时间
    )
    {
        // 1. 初始化查询(必传条件先加上)
        IQueryable<AddressBook> query = _waterDbContext.AddressBooks
            .Where(addr => addr.UserId == userId);  // 必传条件,先固定筛选

        // 2. 动态添加可选条件(只在参数有值时添加)
        if (isDefault.HasValue)
        {
            // 条件:IsDefault 等于传入的值(1或0)
            query = query.Where(addr => addr.IsDefault == isDefault.Value);
        }

        if (!string.IsNullOrEmpty(phone))
        {
            // 条件:Phone 包含传入的字符串(模糊搜索)
            query = query.Where(addr => addr.Phone.Contains(phone));
        }

        if (startTime.HasValue)
        {
            // 条件:CreateTime 大于等于开始时间
            query = query.Where(addr => addr.CreateTime >= startTime.Value);
        }

        if (endTime.HasValue)
        {
            // 条件:CreateTime 小于等于结束时间
            query = query.Where(addr => addr.CreateTime <= endTime.Value);
        }

        // 3. 执行查询并转换为领域对象(如果需要)
        var addressBooks = await query.ToListAsync();
        return _mapper.Map<List<Address>>(addressBooks);
    }

核心原理

  1. IQueryable 的 "延迟执行" 特性 :上述代码中,query 变量是 IQueryable<AddressBook> 类型,添加 Where 条件时不会立即执行查询 ,只会记录查询条件。直到调用 ToListAsync() 时,EF Core 才会根据所有条件生成最终的 SQL 并执行。

  2. 自动参数化 :EF Core 会将动态条件中的变量(如 isDefault.Valuephone)自动转换为 SQL 参数,避免 SQL 注入风险。例如,模糊搜索生成的 SQL 类似:

    sql

    复制代码
    SELECT * FROM AddressBooks 
    WHERE UserId = @p0 AND Phone LIKE '%' + @p1 + '%'
  3. 条件拼接的灵活性 :可以根据任意逻辑动态添加条件(比如 "如果是管理员,则不筛选 UserId"),甚至通过循环添加批量条件(如 Where(addr => ids.Contains(addr.Id)))。

进阶:动态排序

如果需要支持 "按不同字段排序"(如按 CreateTime 升序 / 降序),可以这样扩展:

cs 复制代码
// 新增排序参数
public async Task<List<Address>> QueryAddressesAsync(
    // ... 其他参数 ...
    string sortField = "CreateTime",  // 排序字段(默认按创建时间)
    bool isAscending = false         // 是否升序(默认降序)
)
{
    // ... 前面的条件拼接 ...

    // 动态排序
    query = sortField.ToLower() switch
    {
        "createtime" => isAscending ? query.OrderBy(addr => addr.CreateTime) : query.OrderByDescending(addr => addr.CreateTime),
        "name" => isAscending ? query.OrderBy(addr => addr.Name) : query.OrderByDescending(addr => addr.Name),
        _ => query.OrderByDescending(addr => addr.CreateTime)  // 默认排序
    };

    // 执行查询
    var addressBooks = await query.ToListAsync();
    return _mapper.Map<List<Address>>(addressBooks);
}

何时需要 "原始动态 SQL"?

如果查询逻辑极其复杂(如多表联合查询、自定义聚合函数),可以使用 EF Core 的 FromSqlRaw 方法拼接原始 SQL,但必须严格参数化

cs 复制代码
// 原始 SQL 示例(谨慎使用,必须参数化!)
var sql = @"
    SELECT * FROM AddressBooks 
    WHERE UserId = @UserId 
    {0}  -- 动态条件占位符
";
var dynamicWhere = isDefault.HasValue ? "AND IsDefault = @IsDefault" : "";
var finalSql = string.Format(sql, dynamicWhere);

var addressBooks = await _waterDbContext.AddressBooks
    .FromSqlRaw(finalSql, 
        new SqlParameter("@UserId", userId),
        new SqlParameter("@IsDefault", isDefault ?? (object)DBNull.Value)
    )
    .ToListAsync();

总结

  • 优先用 IQueryable 动态拼接条件:安全、类型友好、无需手动写 SQL,适合 90% 以上的场景。
  • 避免直接拼接 SQL 字符串:风险高,除非必须(如超复杂查询),且一定要用参数化。
  • 核心技巧 :利用 IQueryable 的延迟执行,按条件逐步添加 Where/OrderBy,最后用 ToListAsync 执行。

2.修改

方式 1:动态更新部分字段(适合需要加载实体的场景)
cs 复制代码
public async Task<bool> UpdateAddressAsync(AddressUpdateDto dto)
    {
        // 1. 查询要修改的实体
        var address = await _waterDbContext.AddressBooks
            .FirstOrDefaultAsync(addr => addr.Id == dto.AddressId);
        if (address == null)
        {
            throw new BusinessException("地址不存在");
        }

        // 2. 动态更新字段(仅更新传入了值的字段)
        if (!string.IsNullOrEmpty(dto.Name))
        {
            address.Name = dto.Name; // 姓名有值才更新
        }
        if (!string.IsNullOrEmpty(dto.Phone))
        {
            address.Phone = dto.Phone; // 手机号有值才更新
        }
        if (dto.IsDefault.HasValue)
        {
            address.IsDefault = dto.IsDefault.Value; // 是否默认有值才更新
        }

        // 3. 保存修改(EF 自动跟踪变化,只更新修改过的字段)
        return await _waterDbContext.SaveChangesAsync() > 0;
    }
方式 2:动态批量更新(无需加载实体,直接执行 SQL,EF Core 7.0+ 支持)

适合 "根据条件批量更新符合要求的记录",比如 "将用户所有非默认地址的 IsDeleted 设为 1"。

cs 复制代码
public async Task<int> BatchUpdateAddressesAsync(long userId, bool setDeleted)
    {
        // 1. 构建动态条件(比如只更新非默认地址)
        var query = _waterDbContext.AddressBooks
            .Where(addr => addr.UserId == userId 
                         && addr.IsDefault == 0); // 只处理非默认地址

        // 2. 动态执行更新(根据参数决定更新的值)
        int updatedCount = await query
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(addr => addr.IsDeleted, setDeleted ? 1 : 0) // 动态设置删除状态
                .SetProperty(addr => addr.UpdateTime, DateTime.Now) // 强制更新时间戳
            );

        return updatedCount; // 返回更新的行数
    }

3.删除

动态性体现在 "根据条件删除符合要求的记录",比如 "删除用户 30 天前的非默认地址"。

cs 复制代码
public async Task<bool> DeleteAddressAsync(long addressId, long currentUserId)
    {
        // 1. 查询实体并验证条件(比如不允许删除默认地址)
        var address = await _waterDbContext.AddressBooks
            .FirstOrDefaultAsync(addr => addr.Id == addressId);
        if (address == null)
        {
            throw new BusinessException("地址不存在");
        }

        // 2. 动态判断是否允许删除(例如:默认地址不允许删除)
        if (address.IsDefault == 1)
        {
            throw new BusinessException("默认地址不允许删除,请先取消默认");
        }

        // 3. 执行删除
        _waterDbContext.AddressBooks.Remove(address);
        return await _waterDbContext.SaveChangesAsync() > 0;
    }
方式 2:批量删除(根据动态条件,EF Core 7.0+ 支持)

适合 "一键清理符合条件的记录",比如 "删除用户所有已标记为删除的地址"。

cs 复制代码
public async Task<int> BatchDeleteAddressesAsync(long userId, bool deleteExpired)
    {
        // 1. 构建动态删除条件
        var query = _waterDbContext.AddressBooks
            .Where(addr => addr.UserId == userId);

        // 2. 动态添加额外条件(比如:如果需要删除过期地址,则加上时间条件)
        if (deleteExpired)
        {
            query = query.Where(addr => addr.CreateTime < DateTime.Now.AddDays(-30));
        }
        else
        {
            query = query.Where(addr => addr.IsDeleted == 1); // 否则只删除已标记删除的
        }

        // 3. 执行批量删除(直接在数据库执行,不加载实体到内存)
        int deletedCount = await query.ExecuteDeleteAsync();

        return deletedCount; // 返回删除的行数
    }

4.新增

动态性体现在 "根据条件决定实体的属性值",比如 "普通用户新增地址默认非默认,VIP 用户默认设为默认地址"。

cs 复制代码
public async Task<long> AddAddressAsync(AddressAddDto dto, bool isVipUser)
    {
        // 1. 创建实体并设置基础属性
        var address = new AddressBook
        {
            UserId = dto.UserId,
            Name = dto.Name,
            Phone = dto.Phone,
            AddressDetail = dto.AddressDetail,
            CreateTime = DateTime.Now
        };

        // 2. 动态设置条件属性(根据用户类型决定是否默认)
        if (isVipUser)
        {
            address.IsDefault = 1; // VIP 用户新增地址默认设为默认
        }
        else
        {
            address.IsDefault = 0; // 普通用户默认非默认
        }

        // 3. 动态添加额外属性(比如仅当地址在特定城市时才设置)
        if (dto.City == "北京" || dto.City == "上海")
        {
            address.IsBigCity = true; // 自定义字段:标记是否为大城市
        }

        // 4. 保存到数据库
        _waterDbContext.AddressBooks.Add(address);
        await _waterDbContext.SaveChangesAsync();

        return address.Id; // 返回新增的主键
    }

核心总结

  1. 动态新增 :通过条件判断给实体属性赋值,再调用 Add + SaveChanges
  2. 动态修改
    • 单实体:加载实体后,根据条件更新需要修改的字段,依赖 EF 自动跟踪。
    • 批量:用 Where 构建条件 + ExecuteUpdateAsync(高效,无需加载实体)。
  3. 动态删除
    • 单实体:查询后判断条件,调用 Remove + SaveChanges
    • 批量:用 Where 构建条件 + ExecuteDeleteAsync(高效)。
相关推荐
清水白石0087 小时前
解构异步编程的两种哲学:从 asyncio 到 Trio,理解 Nursery 的魔力
运维·服务器·数据库·python
资生算法程序员_畅想家_剑魔7 小时前
Mysql常见报错解决分享-01-Invalid escape character in string.
数据库·mysql
flysh057 小时前
如何利用 C# 内置的 Action 和 Func 委托
开发语言·c#
PyHaVolask8 小时前
SQL注入漏洞原理
数据库·sql
ptc学习者8 小时前
黑格尔时代后崩解的辩证法
数据库
代码游侠8 小时前
应用——智能配电箱监控系统
linux·服务器·数据库·笔记·算法·sqlite
!chen8 小时前
EF Core自定义映射PostgreSQL原生函数
数据库·postgresql
霖霖总总8 小时前
[小技巧14]MySQL 8.0 系统变量设置全解析:SET GLOBAL、SET PERSIST 与 SET PERSIST_ONLY 的区别与应用
数据库·mysql
逑之8 小时前
C语言笔记1:C语言常见概念
c语言·笔记·c#
马克学长8 小时前
SSM校园食堂订餐系统531p9(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·ssm 校园食堂订餐系统