Day17:EF Core 增删改 + 事务

今日目标

  1. 掌握 EF Core 标准增删改(异步)
  2. 掌握事务(单个 SaveChanges / 手动事务)
  3. 掌握批量操作
  4. 完成完整用户业务 CRUD + 事务练习
  5. 背会高频面试题

一、核心知识点(极简速记)

1. 增删改核心 API

  • 增加AddAsync(entity) / AddRangeAsync
  • 删除Remove(entity) / RemoveRange
  • 修改Update(entity) / UpdateRange
  • 提交SaveChangesAsync() 【关键】 只有调用 SaveChanges 才会真正执行 SQL

2. 事务(超重点)

  • 默认:1 次 SaveChanges = 1 个隐式事务

  • 手动事务:多个操作一起成功 / 一起回滚

    using var transaction = await _db.Database.BeginTransactionAsync();
    try {
    // 多次增删改
    await transaction.CommitAsync();
    } catch {
    await transaction.RollbackAsync();
    }

3. 批量操作(高效)

  • 批量增:AddRange
  • 批量删:RemoveRange
  • 批量更新 / 删除(推荐):ExecuteUpdateAsync / ExecuteDeleteAsync(直接 SQL,不加载实体)

4. 重要规范

  • 必须用异步:All Async
  • 查询必须 AsNoTracking
  • 修改必须先查再改(企业最稳)
  • 事务必须包裹多步业务

二、高频面试题 + 标准答案(直接背)

1. EF Core 增删改一定要调用 SaveChanges 吗?为什么?

答案 :必须。EF Core 采用工作单元模式 ,Add/Update/Remove 只是状态跟踪,不会执行数据库操作,只有调用 SaveChangesAsync () 才会批量生成 SQL 执行。

2. SaveChangesAsync 自带事务吗?多个操作一起执行会回滚吗?

答案 :自带隐式事务 。一次 SaveChanges 中的所有操作要么全部成功,要么全部失败回滚,保证原子性。

3. 修改数据时,直接 new 一个带 Id 的实体 Update 可以吗?有什么坑?

答案 :可以,但不推荐 。如果实体未被跟踪,直接 Update 会覆盖所有字段,没赋值的字段会被更新为默认值 ,导致数据丢失。企业规范:先查询 → 再修改属性 → SaveChanges

4. EF Core 怎么实现手动事务?应用场景是什么?

答案 :用 BeginTransactionAsync 手动开启。场景:一个接口包含多次 SaveChanges,例如:新增用户 + 新增日志 + 分配角色,必须同时成功 / 失败。

5. Remove 和 ExecuteDeleteAsync 区别?性能谁更好?

答案

  • Remove:先查询实体到内存 → 再删除(安全,有触发器 / 审计生效)
  • ExecuteDeleteAsync:直接生成 DELETE SQL(性能极高,不加载实体)大数据量批量删除优先用 ExecuteDeleteAsync

6. 什么是 EF Core 实体跟踪?增删改查对跟踪有什么要求?

答案:跟踪是 EF Core 记录实体状态(Unchanged/Added/Modified/Deleted)。

  • 查询:AsNoTracking 关闭,提升性能
  • 增删改:必须开启跟踪,才能正常提交

7. 批量更新数据推荐用什么方式?

答案 :优先 ExecuteUpdateAsync,直接生成 UPDATE 语句,无需查询实体,性能提升 10~100 倍。


三、今日实战练习(完整业务需求・无提示)

练习名称:用户管理 CRUD 业务(带角色分配 + 事务)

项目结构要求

  • 接口层:UserController
  • 业务层:UserService(注入 AppDbContext)
  • 数据层:EF Core + 你已有的表结构
    • SysUser
    • SysRole
    • SysUserRole

需求文档(详细版)

1. 添加用户

  • 接收参数:账号、密码、姓名、状态、角色 Id 列表
  • 校验:
    1. 账号不能为空
    2. 账号不能重复
  • 业务逻辑:
    1. 新增用户
    2. 给用户分配角色(批量插入 SysUserRole)
    3. 两步必须在同一个事务中
  • 返回:统一格式 R<T>

2. 根据 ID 查询用户(带角色)

  • 参数:用户 ID
  • 返回:用户信息 + 角色列表
  • 要求:AsNoTracking

3. 更新用户

  • 参数:用户 ID、姓名、状态、角色 Id 列表
  • 逻辑:
    1. 查询用户是否存在
    2. 更新用户基础信息
    3. 删除该用户原有角色关系
    4. 新增新的角色关系
    5. 全部操作必须事务保证

4. 删除用户

  • 参数:用户 ID
  • 逻辑:
    1. 判断用户是否存在
    2. 开启事务
    3. 删除用户角色关联记录
    4. 删除用户
    5. 提交事务

5. 分页查询用户列表

  • 参数:页码、页大小、账号、姓名(可选)
  • 逻辑:
    1. 动态条件拼接
    2. 分页
    3. 排序(CreateTime 倒序)
  • 返回:总条数 + 列表

6. 批量禁用用户

  • 参数:用户 ID 数组
  • 要求:使用 ExecuteUpdateAsync
  • 逻辑:批量更新 Status=0

四、实战练习代码

UserService.cs

cs 复制代码
public class UserService 
{
    private readonly AppDbContext _dbContext;

    // 构造函数注入
    public UserService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

   
    public async Task<dynamic> GetUserWithRolesAsync(long userId)
    {
        var user = await _dbContext.Users
        .AsNoTracking()
        .Include(u => u.UserRoles)
            .ThenInclude(ur => ur.Role )
        .Where(u => u.Id == userId)
        .Select(u => new
        {
            u.Id,
            u.Account,
            u.Name,
            Roles = u.UserRoles.Select(ur => new
            {
                ur.Role.Id,
                ur.Role.RoleName,
                ur.Role.RoleCode
            }).ToList()
        })
        .FirstOrDefaultAsync();

        return user;
    }

    /// <summary>
    /// 添加用户
    /// 校验:账号不能为空,账号不能重复
    /// 业务逻辑:新增用户,给用户分配角色(批量插入 SysUserRole),两步必须在同一个事务中
    /// </summary>
    /// <param name="dto">接收参数:账号、密码、姓名、状态、角色 Id 列表</param>
    /// <returns>统一格式 R<T></returns>
    public async Task<R<dynamic>> AddUserAsync(UserCreateDto dto) 
    {
        //先判断账号是否唯一
        // 【重要】不能用 AsNoTracking,因为要判断存在
        var exists = await _dbContext.Users.AnyAsync(u => u.Account == dto.Account);
        if (exists)
            return R<dynamic>.Fail("账号已存在");

        //事务
        using (var transaction = await _dbContext.Database.BeginTransactionAsync())
        {
            try
            {
                // 执行第一个数据库操作:新增用户
                User user = new User();
                user.Account = dto.Account;
                user.Name = dto.Name;
                user.Password = dto.Password;// 未来必须加密,这里先保留
                user.Status = dto.Status;

                await _dbContext.Users.AddAsync(user);
                await _dbContext.SaveChangesAsync(); // 获取主键


                // 执行第二个数据库操作: 给用户分配角色(批量插入 SysUserRole)
                // 批量分配角色
                var userRoles = dto.RoleId.Select(roleId => new UserRole
                {
                    UserId = user.Id,
                    RoleId = roleId
                }).ToList();

                await _dbContext.UserRoles.AddRangeAsync(userRoles);
                // 保存更改
                await _dbContext.SaveChangesAsync();

                // 提交事务
                await transaction.CommitAsync();

                return R<dynamic>.Sucess(new { user.Id, user.Account, user.Name });
            }
            catch (Exception ex)
            {
                // 发生异常时回滚事务
                await transaction.RollbackAsync();
                // 可以选择抛出异常或进行其他错误处理
                throw; // 或者处理异常,例如:throw new Exception("Transaction failed", ex);
            }
        }

    }


    /// <summary>
    /// 更新用户 
    /// 逻辑:查询用户是否存在,更新用户基础信息,删除该用户原有角色关系,新增新的角色关系,全部操作必须事务保证
    /// </summary>
    /// <param name="dto">参数:用户 ID、姓名、状态、角色 Id 列表</param>
    /// <returns></returns>
    public async Task<R<dynamic>> UpdUserAsync(UserUpdateDto dto) 
    {
        //查询用户是否存在
        // 【关键】修改必须 TRACKING,不能 AsNoTracking
        var user = await _dbContext.Users
            .FirstOrDefaultAsync(u => u.Id == dto.Id);

        if (user == null)
            return R<dynamic>.Fail("用户不存在");
        else 
        {
            using (var transaction = await _dbContext.Database.BeginTransactionAsync())
            {
                try
                {
                    // 执行第一个数据库操作:更新用户基础信息
                    user.Name = dto.Name ?? user.Name;
                    user.Status = dto.Status ?? user.Status;

                    // 【高性能】直接删,不加载数据
                    await _dbContext.UserRoles
                        .Where(ur => ur.UserId == user.Id)
                        .ExecuteDeleteAsync();

                    // 新增新角色
                    var userRoles = dto.RoleId.Select(roleId => new UserRole
                    {
                        UserId = user.Id,
                        RoleId = roleId
                    }).ToList();
                    await _dbContext.UserRoles.AddRangeAsync(userRoles);

                    // 不需要 Update(user),自动跟踪
                    await _dbContext.SaveChangesAsync();

                    await transaction.CommitAsync();
                    return R<dynamic>.Sucess("修改成功");
                }
                catch (Exception ex)
                {
                    // 发生异常时回滚事务
                    await transaction.RollbackAsync();
                    // 可以选择抛出异常或进行其他错误处理
                    throw; // 或者处理异常,例如:throw new Exception("Transaction failed", ex);
                }
            }
        }
    }


    /// <summary>
    /// 删除用户
    /// 逻辑:判断用户是否存在,开启事务,删除用户角色关联记录,删除用户,提交事务
    /// </summary>
    /// <param name="userId">用户 ID</param>
    /// <returns></returns>
    public async Task<R<dynamic>> DelUserAsync(int userId) 
    {
        var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId);
        if (user == null)
            return R<dynamic>.Fail("用户不存在");

        using (var transaction = await _dbContext.Database.BeginTransactionAsync()) 
        {
            try 
            {
                // 高性能删除关联
                await _dbContext.UserRoles
                    .Where(ur => ur.UserId == userId)
                    .ExecuteDeleteAsync();

                _dbContext.Users.Remove(user);

                await _dbContext.SaveChangesAsync();

                await transaction.CommitAsync();

                return R<dynamic>.Sucess("删除成功");

            } catch (Exception ex) 
            {
                await transaction.RollbackAsync();
                throw new Exception("Transaction failed", ex);
            }

        }
    }


    /// <summary>
    /// 分页查询用户列表
    /// </summary>
    /// <param name="pageNum">页码</param>
    /// <param name="pageSize">页大小</param>
    /// <param name="account">账号(可选)</param>
    /// <param name="name">姓名(可选)</param>
    /// <returns>总数+用户列表</returns>
    public async Task<(long total, List<User> list)> GetUsers(int pageNum, int pageSize, string? account, string? name) 
    {
        var query = _dbContext.Users.AsNoTracking()
                    .Where(u => string.IsNullOrEmpty(account) || u.Account.Contains(account))
                    .Where(u => string.IsNullOrEmpty(name) || u.Name.Contains(name));

        //取得总数
        var total = await query.CountAsync();

        //用户列表
        var list = await query
            .OrderByDescending(u => u.Id)
            .Skip((pageNum - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();

         return (total, list);

    }

    /// <summary>
    /// 6. 批量禁用用户
    /// 要求:使用 ExecuteUpdateAsync
    /// 逻辑:批量更新 Status = 0
    /// </summary>
    /// <param name="userIds">用户 ID 数组</param>
    /// <returns></returns>
    public async Task<R<dynamic>> ChangeStatus(long[] userIds)
    {
        await _dbContext.Users
            .Where(u => userIds.Contains(u.Id))
            .ExecuteUpdateAsync(x => x.SetProperty(u => u.Status, 0));

        return R<dynamic>.Sucess("批量禁用成功");
    }
}

UserController.cs

cs 复制代码
    [ApiController]
    [Route("api/[controller]")]
    public class UserController:ControllerBase
    {

        private readonly UserService _userService;

        public UserController(UserService userService) 
        {
            _userService = userService;
        }

        /// <summary>
        /// 根据 ID 查询用户(带角色)
        /// 要求:AsNoTracking
        /// </summary>
        /// <param name="userId">用户 ID</param>
        /// <returns>:用户信息 + 角色列表</returns>
        [HttpGet("user-roles/{userId}")]
        public async Task<ActionResult<R<object>>> GetUserRoles(long userId)
        {
            var data = await _userService.GetUserWithRolesAsync(userId);
            return Ok(R<object>.Sucess(data));
        }

        /// <summary>
        /// 新增用户
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        [HttpPost]
        public async Task<ActionResult<R<User>>> AddUser(UserCreateDto dto) 
        {
            //数据校验
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var data = await _userService.AddUserAsync(dto);

            return Ok(data);
        }


        /// <summary>
        /// 修改用户
        /// </summary>
        /// <param name="dto"></param>
        /// <returns></returns>
        [HttpPut]
        public async Task<ActionResult<R<User>>> UpdUser(UserUpdateDto dto)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var data = await _userService.UpdUserAsync(dto);

            if (data == null)
                return NotFound();

            return Ok(data);
        }

        
        /// <summary>
        /// 删除用户
        /// </summary>
        /// <param name="Id"></param>
        /// <returns></returns>
        [HttpDelete("{Id}")]
        public async Task<ActionResult<R<User>>> DelUser([FromRoute]int Id)
        {
            var data = await _userService.DelUserAsync(Id);

            if (data == null)
                return NotFound();

            return Ok(data);
        }

        /// <summary>
        /// 分页查询
        /// </summary>
        /// <param name="pageNum"></param>
        /// <param name="pageSize"></param>
        /// <param name="account"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        [HttpGet]
        public async Task<ActionResult<R<object>>> GetUsers(
            [FromQuery] int pageNum = 1,
            [FromQuery] int pageSize = 10,
            [FromQuery] string? account = null,
            [FromQuery] string? name = null)
        {
            var (total, list) = await _userService.GetUsers(pageNum, pageSize, account, name);
            return Ok(R<object>.Sucess(new { total, list }));
        }


        /// <summary>
        /// 批量修改状态
        /// 
        /// 
        /// postman 访问测试时,
        /// 参数位置:Body → raw → JSON
        /// 参数格式(数组格式!):[10005,10006,10007,10008]
        /// </summary>
        /// <param name="ids"></param>
        /// <returns></returns>
        [HttpPut("batch-disable")]
        public async Task<ActionResult<R<dynamic>>> BatchDisable([FromBody] long[] ids)
        {
            var res = await _userService.ChangeStatus(ids);
            return Ok(res);
        }
    }

UserCreateDto.cs

cs 复制代码
public class UserCreateDto
{
    /// <summary>
    /// 账号,不能为空且唯一
    /// </summary>
    [Required(ErrorMessage ="账号不能为空")]
    public string Account { get; set; }

    /// <summary>
    /// 密码
    /// </summary>
    public string Password { get; set; }

    /// <summary>
    /// 姓名
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 状态 0=禁用 1=正常
    /// </summary>
    public int Status { get; set; } = 1;


    /// <summary>
    /// 角色 Id 列表
    /// </summary>
    public int[] RoleId { get; set; }
}

UserUpdateDto.cs

cs 复制代码
/// <summary>
/// 参数:用户 ID、姓名、状态、角色 Id 列表
/// </summary>
public class UserUpdateDto
{
    [Required]
    public long Id { get; set; }

    /// <summary>
    /// 姓名
    /// </summary>
    public string? Name { get; set; }

    /// <summary>
    /// 状态 0=禁用 1=正常
    /// </summary>
    public int? Status { get; set; } 


    /// <summary>
    /// 角色 Id 列表
    /// </summary>
    public int[]? RoleId { get; set; }
}
相关推荐
MoFe11 天前
【.net core】【watercloud】处理rabbitmq类初始化时获取系统已注入的数据库连接问题(调用已注入服务)
数据库·rabbitmq·.netcore
MoFe15 天前
【.net core】【RabbitMq】rabbitmq在.net core中的简单使用
分布式·rabbitmq·.netcore
van久6 天前
Day15:EF Core 入门 + CodeFirst(就业核心版)
.netcore
叫我黎大侠6 天前
.NET 实战:调用千问视觉模型实现 OCR(车票识别完整教程)
阿里云·ai·c#·ocr·asp.net·.net·.netcore
van久6 天前
Day15-4:【日志】中间件和过滤器 的对比选择
.netcore
van久7 天前
Day14: 搭建企业标准的DDD 简洁版四层架构
架构·.netcore
van久7 天前
ADay15-1: 安装 EF Core 包 到 对应DDD(领域驱动设计)四层架构‌
.netcore
时光追逐者8 天前
C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12)
c#·.net·.netcore
willhuo10 天前
基于Playwright的抖音网页自动化浏览器项目使用指南
爬虫·c#·.netcore·webview