如何在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
相关产品推荐
相关产品推荐

