在日常开发中,你一定遇到过这样的场景:下单时需要同时扣减库存、生成订单、记录支付流水,只要其中一步出错,所有操作都得回滚。如果用传统的 DbContext 直接操作,很容易出现 “部分成功” 的烂摊子 —— 库存扣了但订单没生成,数据一致性直接崩盘。
今天就手把手教你用 UnitOfWork(工作单元)模式 解决这个问题,从代码实现到避坑指南,再到生活类比,让你彻底搞懂 “统一事务管理” 的核心逻辑。
在这里插入图片描述

一、先搞懂:UnitOfWork 到底是什么?(生活类比 + 核心定义)

小节:用 “超市购物” 理解 UnitOfWork
先抛掉技术术语,用生活场景类比:
你去超市购物时,不会拿一件商品就去结账(对应:单表操作),而是把牛奶、面包、水果都放进购物车(对应:多表操作),确认所有商品都选好后,再一次性去收银台结账(对应:事务提交)。如果中途发现牛奶过期了,你会把所有商品都放回货架(对应:事务回滚)—— 这就是 UnitOfWork 的核心思想:把一组相关操作封装成 “一个工作单元”,要么全部成功,要么全部失败。
核心定义
UnitOfWork(工作单元)是一种设计模式,核心作用是:
1.跟踪多个数据操作(新增 / 修改 / 删除)的状态;
2.统一管理事务,确保多表操作的原子性(要么全成,要么全败);
3.减少数据库连接次数,提升性能。

二、实战代码:ASP.NET中实现 UnitOfWork 模式

前置条件

  • 框架:ASP.NET Core 6.0+
  • ORM:Entity Framework Core
  • 数据库:SQL Server(其他数据库同理)
  • 场景:模拟 “用户下单”(涉及 3 张表:订单表、商品表、订单明细表)

步骤 1:定义实体类(Model)

// 商品表
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Stock { get; set; } // 库存
}

// 订单表
public class Order
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime CreateTime { get; set; } = DateTime.Now;
}

// 订单明细表
public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; } // 购买数量
    public decimal UnitPrice { get; set; }
    
    // 导航属性
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
}

步骤 2:定义仓储接口(IRepository)

仓储模式是 UnitOfWork 的基础,负责单表的 CRUD:

/// <summary>
/// 通用仓储接口
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public interface IRepository<TEntity, TKey> where TEntity : class
{
    // 根据主键查询
    Task<TEntity> GetByIdAsync(TKey id);
    // 添加实体
    void Add(TEntity entity);
    // 更新实体
    void Update(TEntity entity);
    // 删除实体
    void Delete(TEntity entity);
    // 批量添加
    void AddRange(IEnumerable<TEntity> entities);
}

// 商品仓储接口(可扩展专属方法)
public interface IProductRepository : IRepository<Product, int>
{
    // 扣减库存的专属方法
    Task<bool> ReduceStockAsync(int productId, int quantity);
}

// 订单仓储接口
public interface IOrderRepository : IRepository<Order, int> { }

// 订单明细仓储接口
public interface IOrderItemRepository : IRepository<OrderItem, int> { }

步骤 3:实现仓储类(Repository)

/// <summary>
/// 通用仓储实现
/// </summary>
public class Repository<TEntity, TKey> : IRepository<TEntity, TKey> where TEntity : class
{
    protected readonly DbContext _dbContext;
    protected readonly DbSet<TEntity> _dbSet;

    public Repository(DbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = dbContext.Set<TEntity>();
    }

    public async Task<TEntity> GetByIdAsync(TKey id)
    {
        return await _dbSet.FindAsync(id);
    }

    public void Add(TEntity entity)
    {
        _dbSet.Add(entity);
    }

    public void Update(TEntity entity)
    {
        _dbSet.Update(entity);
    }

    public void Delete(TEntity entity)
    {
        _dbSet.Remove(entity);
    }

    public void AddRange(IEnumerable<TEntity> entities)
    {
        _dbSet.AddRange(entities);
    }
}

// 商品仓储实现
public class ProductRepository : Repository<Product, int>, IProductRepository
{
    public ProductRepository(DbContext dbContext) : base(dbContext) { }

    public async Task<bool> ReduceStockAsync(int productId, int quantity)
    {
        // 乐观锁:防止并发扣库存(重要!)
        var sql = "UPDATE Products SET Stock = Stock - @Quantity WHERE Id = @ProductId AND Stock >= @Quantity";
        var rows = await _dbContext.Database.ExecuteSqlRawAsync(sql, 
            new SqlParameter("@ProductId", productId),
            new SqlParameter("@Quantity", quantity));
        return rows > 0; // 影响行数>0表示扣减成功
    }
}

// 订单仓储实现
public class OrderRepository : Repository<Order, int>, IOrderRepository
{
    public OrderRepository(DbContext dbContext) : base(dbContext) { }
}

// 订单明细仓储实现
public class OrderItemRepository : Repository<OrderItem, int>, IOrderItemRepository
{
    public OrderItemRepository(DbContext dbContext) : base(dbContext) { }
}

步骤 4:定义 UnitOfWork 接口和实现

/// <summary>
/// 工作单元接口
/// </summary>
public interface IUnitOfWork : IDisposable
{
    // 获取指定仓储
    IRepository<TEntity, TKey> GetRepository<TEntity, TKey>() where TEntity : class;
    
    // 专属仓储(简化调用)
    IProductRepository ProductRepository { get; }
    IOrderRepository OrderRepository { get; }
    IOrderItemRepository OrderItemRepository { get; }

    // 提交事务
    Task<int> SaveChangesAsync();
    
    // 开启事务
    Task BeginTransactionAsync();
    
    // 提交事务
    Task CommitTransactionAsync();
    
    // 回滚事务
    Task RollbackTransactionAsync();
}

/// <summary>
/// 工作单元实现
/// </summary>
public class UnitOfWork : IUnitOfWork
{
    private readonly DbContext _dbContext;
    private IDbContextTransaction _transaction;
    
    // 仓储缓存(避免重复创建)
    private readonly Dictionary<Type, object> _repositories = new Dictionary<Type, object>();

    // 专属仓储实例
    public IProductRepository ProductRepository { get; }
    public IOrderRepository OrderRepository { get; }
    public IOrderItemRepository OrderItemRepository { get; }

    public UnitOfWork(DbContext dbContext)
    {
        _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        
        // 初始化专属仓储
        ProductRepository = new ProductRepository(_dbContext);
        OrderRepository = new OrderRepository(_dbContext);
        OrderItemRepository = new OrderItemRepository(_dbContext);
    }

    // 获取通用仓储
    public IRepository<TEntity, TKey> GetRepository<TEntity, TKey>() where TEntity : class
    {
        var type = typeof(TEntity);
        if (!_repositories.ContainsKey(type))
        {
            var repositoryType = typeof(Repository<,>).MakeGenericType(type, typeof(TKey));
            _repositories[type] = Activator.CreateInstance(repositoryType, _dbContext);
        }
        return (IRepository<TEntity, TKey>)_repositories[type];
    }

    // 保存更改(无事务)
    public async Task<int> SaveChangesAsync()
    {
        return await _dbContext.SaveChangesAsync();
    }

    // 开启事务
    public async Task BeginTransactionAsync()
    {
        if (_transaction != null) return;
        _transaction = await _dbContext.Database.BeginTransactionAsync();
    }

    // 提交事务
    public async Task CommitTransactionAsync()
    {
        if (_transaction == null) throw new InvalidOperationException("未开启事务");
        try
        {
            await _dbContext.SaveChangesAsync();
            await _transaction.CommitAsync();
        }
        catch
        {
            await RollbackTransactionAsync();
            throw; // 抛出异常让上层处理
        }
        finally
        {
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    // 回滚事务
    public async Task RollbackTransactionAsync()
    {
        if (_transaction == null) return;
        await _transaction.RollbackAsync();
        await _transaction.DisposeAsync();
        _transaction = null;
    }

    // 释放资源
    public void Dispose()
    {
        _dbContext.Dispose();
    }
}

步骤 5:注册服务(Program.cs)

var builder = WebApplication.CreateBuilder(args);

// 1. 注册DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});

// 2. 注册UnitOfWork(生命周期:Scoped,每次请求创建一个实例)
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

// 3. 注册仓储(可选,因为UnitOfWork内部已创建)
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IOrderItemRepository, OrderItemRepository>();

步骤 6:业务层使用 UnitOfWork(下单场景)

/// <summary>
/// 订单业务服务
/// </summary>
public class OrderService
{
    private readonly IUnitOfWork _unitOfWork;

    public OrderService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    /// <summary>
    /// 创建订单(多表事务示例)
    /// </summary>
    /// <param name="userId">用户ID</param>
    /// <param name="productItems">商品列表(商品ID+数量)</param>
    /// <returns>订单ID</returns>
    public async Task<int> CreateOrderAsync(int userId, List<(int ProductId, int Quantity)> productItems)
    {
        if (productItems == null || productItems.Count == 0)
            throw new ArgumentException("商品列表不能为空");

        try
        {
            // 1. 开启事务
            await _unitOfWork.BeginTransactionAsync();

            // 2. 初始化订单
            var order = new Order
            {
                UserId = userId,
                TotalAmount = 0,
                CreateTime = DateTime.Now
            };
            _unitOfWork.OrderRepository.Add(order);

            // 3. 处理每个商品
            var orderItems = new List<OrderItem>();
            foreach (var item in productItems)
            {
                // 3.1 查询商品信息
                var product = await _unitOfWork.ProductRepository.GetByIdAsync(item.ProductId);
                if (product == null)
                    throw new Exception($"商品ID{item.ProductId}不存在");

                // 3.2 扣减库存(乐观锁保证并发安全)
                var reduceSuccess = await _unitOfWork.ProductRepository.ReduceStockAsync(item.ProductId, item.Quantity);
                if (!reduceSuccess)
                    throw new Exception($"商品{product.Name}库存不足");

                // 3.3 创建订单明细
                var orderItem = new OrderItem
                {
                    OrderId = order.Id, // 先添加订单,EF会自动生成ID
                    ProductId = item.ProductId,
                    Quantity = item.Quantity,
                    UnitPrice = product.Price
                };
                orderItems.Add(orderItem);

                // 3.4 累加订单总金额
                order.TotalAmount += product.Price * item.Quantity;
            }

            // 4. 批量添加订单明细
            _unitOfWork.OrderItemRepository.AddRange(orderItems);

            // 5. 提交事务(所有操作成功才提交)
            await _unitOfWork.CommitTransactionAsync();

            return order.Id;
        }
        catch (Exception ex)
        {
            // 6. 发生异常,回滚所有操作
            await _unitOfWork.RollbackTransactionAsync();
            throw new Exception($"创建订单失败:{ex.Message}", ex);
        }
    }
}

步骤 7:控制器调用

[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrderController(OrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpPost("create")]
    public async Task<IActionResult> CreateOrder(int userId, [FromBody] List<(int ProductId, int Quantity)> productItems)
    {
        try
        {
            var orderId = await _orderService.CreateOrderAsync(userId, productItems);
            return Ok(new { Code = 200, Message = "创建成功", OrderId = orderId });
        }
        catch (Exception ex)
        {
            return BadRequest(new { Code = 500, Message = ex.Message });
        }
    }
}

三、UnitOfWork 执行流程(流程图)

客户端发起下单请求

OrderService调用CreateOrderAsync

UnitOfWork开启事务BeginTransactionAsync

新增订单记录

遍历商品:查询商品+扣减库存+创建订单明细

所有操作是否成功?

提交事务CommitTransactionAsync

回滚事务RollbackTransactionAsync

返回订单ID,操作完成

抛出异常,告知用户失败

四、90% 的开发者会踩的坑(避坑指南)

小节:踩坑不可怕,关键是知道 “为什么错”

坑 1:UnitOfWork 生命周期错误(最常见)

  • 错误做法:将 UnitOfWork 注册为Singleton(单例)。
  • 问题:单例的 DbContext 会被多个请求共享,导致事务混乱、数据脏读 / 幻读。
  • 正确做法:注册为Scoped(每次请求一个实例),符合ASP.NET Core 的请求生命周期。

坑 2:嵌套事务未处理

  • 场景:在一个 UnitOfWork 事务中,调用另一个也用了 UnitOfWork 的方法。
  • 问题:EF Core 默认不支持嵌套事务,会抛出 “已存在活跃事务” 异常。
  • 解决方案
    1.统一由顶层方法管理事务;
    2.使用TransactionScope替代(但性能略差)。

坑 3:忽略并发问题(扣库存超卖)

  • 错误做法:先查询库存,再更新(if (product.Stock >= quantity) { product.Stock -= quantity; })。
  • 问题:高并发下,多个请求同时查询到 “库存充足”,导致超卖。
  • 解决方案:使用乐观锁(如代码中UPDATE … WHERE Stock >= @Quantity)或悲观锁。

坑 4:忘记释放资源

  • 错误做法:未实现IDisposable,导致 DbContext 和事务未释放,数据库连接泄漏。
  • 解决方案:UnitOfWork 实现IDisposable,释放 DbContext 和事务资源(如代码所示)。

坑 5:滥用 UnitOfWork(单表操作也用)

  • 错误场景:仅查询单表数据,也走 UnitOfWork + 事务。
  • 问题:增加不必要的性能开销。
  • 解决方案:只有多表操作需要事务时才用 UnitOfWork,单表 CRUD 直接用仓储。

五、与读者互动:

💬 互动问答
1.你在项目中是如何管理多表事务的?是用 UnitOfWork 还是其他方式?
2.除了扣库存,你还遇到过哪些需要保证数据一致性的场景?
3.如果你有更好的 UnitOfWork 实现方式,欢迎在评论区分享!

总结

1.UnitOfWork 模式的核心是统一事务管理,解决多表操作的原子性问题,可类比 “超市购物车结账” 理解;
2.实现 UnitOfWork 的关键:仓储模式基础 + 事务管理(Begin/Commit/Rollback) + 正确的生命周期(Scoped);
3.避坑重点:生命周期不能错、并发要加锁、避免嵌套事务、不滥用 UnitOfWork。

如果这篇文章帮你搞懂了 UnitOfWork 模式,欢迎点赞 + 收藏 + 关注!后续会更新更多ASP.NET设计模式实战内容~

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐