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(高效)。
相关推荐
p***s9133 分钟前
mysql用户名怎么看
数据库·mysql
5***g22935 分钟前
Ubuntu 系统下安装 Nginx
数据库·nginx·ubuntu
3***g20538 分钟前
SQL sever数据导入导出实验
数据库·sql·oracle
f***686040 分钟前
在Django中安装、配置、使用CKEditor5,并将CKEditor5录入的文章展现出来,实现一个简单博客网站的功能
数据库·django·sqlite
故事不长丨43 分钟前
C#委托的使用
c#·wpf·winfrom·委托·网站开发
必胜刻44 分钟前
Go连接Mysql数据库
数据库·mysql·golang
未来之窗软件服务1 小时前
幽冥大陆(三十八)P50酒店门锁SDK C#仙盟插件——东方仙盟筑基期
开发语言·单片机·c#·东方仙盟·东方仙盟sdk·东方仙盟vos智能浏览器
l***46681 小时前
使用mysql报Communications link failure异常解决
数据库·mysql
u***28471 小时前
如何在docker中的mysql容器内执行命令与执行SQL文件
sql·mysql·docker