今日目标
- 掌握 EF Core 标准增删改(异步)
- 掌握事务(单个 SaveChanges / 手动事务)
- 掌握批量操作
- 完成完整用户业务 CRUD + 事务练习
- 背会高频面试题
一、核心知识点(极简速记)
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 列表
- 校验:
- 账号不能为空
- 账号不能重复
- 业务逻辑:
- 新增用户
- 给用户分配角色(批量插入 SysUserRole)
- 两步必须在同一个事务中
- 返回:统一格式 R<T>
2. 根据 ID 查询用户(带角色)
- 参数:用户 ID
- 返回:用户信息 + 角色列表
- 要求:AsNoTracking
3. 更新用户
- 参数:用户 ID、姓名、状态、角色 Id 列表
- 逻辑:
- 查询用户是否存在
- 更新用户基础信息
- 删除该用户原有角色关系
- 新增新的角色关系
- 全部操作必须事务保证
4. 删除用户
- 参数:用户 ID
- 逻辑:
- 判断用户是否存在
- 开启事务
- 删除用户角色关联记录
- 删除用户
- 提交事务
5. 分页查询用户列表
- 参数:页码、页大小、账号、姓名(可选)
- 逻辑:
- 动态条件拼接
- 分页
- 排序(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; }
}