You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Entity Framework Core中按父实体限制子实体数量不超MaxSize?

问题场景

当2名及以上学生同时注册某课堂课程的最后剩余座位时,他们会同时进入RegisterStudent()控制器方法。目前的逻辑是先获取父实体YogabandEvent,检查已注册人数< MaxSize时再添加新注册者,以此确保父实体的MaxSize属性被遵守。

但存在并发问题:若2名及以上学生同时获取YogabandEvent实体(此时MaxSize对应的数值为9),之后各自添加新注册者,最终注册人数可能超过MaxSize的限制(例如达到11人以上)。

目前有两种实现方式:

  • 获取包含子实体Registrants的父实体,添加新Registrant后保存
  • 直接向Registrant表插入新实体

但两种方式都可能出现注册人数超过MaxSize的情况。

问题

如何使用Entity Framework Core限制父实体YogabandEvent的子实体Registrant数量不超过其MaxSize值?

代码示例(简化后)
public class YogabandEvent : BaseEntity
{
    public YogabandEvent() 
    {
        Registrants = new List<Registrant>();
    }

    public DateTime Date { get; set; }
    public DateTime DateUtc { get; set; }
    public MaxSize MaxSize { get; set; }

    public ICollection<Registrant> Registrants { get; set; }
 }

 public class Registrant : BaseEntity
 {
     public RegistrantType RegistrantType { get; set; }
     public int UserId { get; set; }
     public User User { get; set; }
     public int EventId { get; set; }
     public YogabandEvent Event { get; set; }
 }

 public enum MaxSize
 {
     Five = 5,
     Ten = 10,
     Fifteen = 15,
     Twenty = 20,
     ThirtyFive = 35
 }
解决方案

要解决EF Core中的这个并发超限问题,需要结合原子性操作和多层校验,从根源上避免并发冲突,以下是几种可行方案:

1. 数据库事务+行级锁(推荐)

在查询YogabandEvent时通过数据库行锁,确保同一时间只有一个请求能读取并修改该实体的注册信息,彻底避免并发读取旧数据的问题。

示例代码:

using var transaction = _dbContext.Database.BeginTransaction();
try
{
    // 用FOR UPDATE锁定目标活动行,阻止其他事务读取
    var yogabandEvent = await _dbContext.YogabandEvents
        .Include(e => e.Registrants)
        .FromSqlRaw("SELECT * FROM YogabandEvents WHERE Id = {0} FOR UPDATE", eventId)
        .FirstOrDefaultAsync();

    if (yogabandEvent == null)
    {
        throw new Exception("活动不存在");
    }

    int maxSize = (int)yogabandEvent.MaxSize;
    if (yogabandEvent.Registrants.Count >= maxSize)
    {
        throw new Exception("活动已满");
    }

    // 添加新注册者
    var newRegistrant = new Registrant
    {
        EventId = eventId,
        UserId = userId,
        RegistrantType = RegistrantType.Student
    };
    _dbContext.Registrants.Add(newRegistrant);
    await _dbContext.SaveChangesAsync();

    transaction.Commit();
}
catch (Exception ex)
{
    transaction.Rollback();
    // 抛出异常或返回友好提示
    throw;
}

2. 乐观并发控制

给YogabandEvent添加行版本属性,EF Core会自动用它检测并发冲突,当冲突发生时重试操作,适合并发量中等的场景。

首先修改实体:

public class YogabandEvent : BaseEntity
{
    // ... 原有属性
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

注册逻辑中处理并发异常:

int retryCount = 3;
while (retryCount > 0)
{
    try
    {
        var yogabandEvent = await _dbContext.YogabandEvents
            .Include(e => e.Registrants)
            .FirstOrDefaultAsync(e => e.Id == eventId);

        if (yogabandEvent == null)
            throw new Exception("活动不存在");

        int maxSize = (int)yogabandEvent.MaxSize;
        if (yogabandEvent.Registrants.Count >= maxSize)
            throw new Exception("活动已满");

        var newRegistrant = new Registrant
        {
            EventId = eventId,
            UserId = userId,
            RegistrantType = RegistrantType.Student
        };
        _dbContext.Registrants.Add(newRegistrant);
        await _dbContext.SaveChangesAsync();
        return;
    }
    catch (DbUpdateConcurrencyException)
    {
        retryCount--;
        if (retryCount == 0)
            throw new Exception("活动注册人数已达上限,请稍后重试");
        // 清空跟踪器,重新获取最新数据
        _dbContext.ChangeTracker.Clear();
    }
}

3. 数据库触发器校验

直接在数据库层面创建触发器,插入Registrant记录前自动检查对应活动的注册人数是否超限,即使绕过EF Core操作数据库也能保证限制生效。

示例SQL(以SQL Server为例):

CREATE TRIGGER trg_CheckRegistrantLimit
ON Registrants
INSTEAD OF INSERT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @EventId INT, @MaxSize INT, @CurrentCount INT;

    SELECT @EventId = EventId FROM inserted;

    SELECT @MaxSize = CAST(MaxSize AS INT) FROM YogabandEvents WHERE Id = @EventId;
    SELECT @CurrentCount = COUNT(*) FROM Registrants WHERE EventId = @EventId;

    IF @CurrentCount < @MaxSize
    BEGIN
        INSERT INTO Registrants (RegistrantType, UserId, EventId)
        SELECT RegistrantType, UserId, EventId FROM inserted;
    END
    ELSE
    BEGIN
        RAISERROR('活动注册人数已达上限', 16, 1);
        ROLLBACK TRANSACTION;
    END
END

内容的提问来源于stack exchange,提问作者chuckd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:24:56