如何优化EF Core中含双用户引用的好友系统模型?
我正在用EF Core设计好友系统,参考了Stack Exchange上的数据库结构方案,但当前模型因双用户引用带来不少复杂度:
Friendship类同时关联请求方和接收方用户:
public sealed class Friendship : EntityBase { public User Requester { get; private set; } = null!; public User Addressee { get; private set; } = null!; // 其他无关属性、构造函数或方法... }
这导致User类必须维护两个导航属性才能让EF Core正确映射:
public sealed class User : IdentityUser<Guid> { private readonly List<Friendship> _friendshipsReceived = new(); public IReadOnlyCollection<Friendship> FriendshipsReceived => _friendshipsReceived.AsReadOnly(); private readonly List<Friendship> _friendshipsRequested = new(); public IReadOnlyCollection<Friendship> FriendshipsRequested => _friendshipsRequested.AsReadOnly(); // 其他无关属性、构造函数或方法... }
这种设计让业务逻辑变得繁琐,比如筛选已通过的好友时,必须合并两个列表再处理:
var result = _friendshipsReceived .Concat(FriendshipsRequested) .Where(f => f.GetLatestFriendshipStatus().Equals(FriendshipStatus.From(FriendshipStatusCode.Accepted)));
因为不管是发起的请求被对方接受,还是收到的请求被自己接受,都得合并两个列表才能准确获取已确认的好友关系,现在需要更简洁的替代实现方案。
方案一:在User类中封装合并后的逻辑
不用修改数据库结构,直接在User类里封装合并和筛选逻辑,避免重复编写Concat代码:
public sealed class User : IdentityUser<Guid> { // 原有的两个私有集合和导航属性... // 所有参与的好友关系 public IReadOnlyCollection<Friendship> AllFriendships => _friendshipsReceived.Concat(_friendshipsRequested).ToList().AsReadOnly(); // 直接获取已通过的好友关系 public IReadOnlyCollection<Friendship> AcceptedFriendships => AllFriendships.Where(f => f.GetLatestFriendshipStatus().StatusCode == FriendshipStatusCode.Accepted) .ToList().AsReadOnly(); }
业务层直接调用user.AcceptedFriendships就能拿到结果,不用每次手动合并列表。注意:如果是EF Core查询场景,这个属性仅做内存中处理,若要转为数据库查询,可在Repository层用投影实现类似逻辑。
方案二:用EF Core配置简化导航属性
调整EF Core的关系配置,让User类只保留一个Friendships导航属性,通过Fluent API指定双向外键关联:
首先修改User类:
public sealed class User : IdentityUser<Guid> { private readonly List<Friendship> _friendships = new(); public IReadOnlyCollection<Friendship> Friendships => _friendships.AsReadOnly(); }
然后在DbContext的OnModelCreating中配置关系:
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Friendship>() .HasOne(f => f.Requester) .WithMany(u => u.Friendships) .HasForeignKey("RequesterId") .OnDelete(DeleteBehavior.Restrict); modelBuilder.Entity<Friendship>() .HasOne(f => f.Addressee) .WithMany(u => u.Friendships) .HasForeignKey("AddresseeId") .OnDelete(DeleteBehavior.Restrict); // 可选:添加查询过滤,确保用户只能看到自己参与的好友关系 modelBuilder.Entity<Friendship>() .HasQueryFilter(f => f.RequesterId == CurrentUserId || f.AddresseeId == CurrentUserId); }
这里的CurrentUserId可从当前登录用户的上下文(比如Claim)中获取,配置后User的Friendships集合会自动包含所有自己发起或接收的好友关系,无需维护两个列表。
方案三:优化状态存储逻辑
如果核心痛点是筛选已接受的好友,可在Friendship类中直接存储最新状态,避免每次计算:
public sealed class Friendship : EntityBase { public User Requester { get; private set; } = null!; public User Addressee { get; private set; } = null!; public FriendshipStatusCode LatestStatus { get; private set; } // 状态更新时同步更新LatestStatus public void UpdateStatus(FriendshipStatusCode newStatus) { LatestStatus = newStatus; // 状态历史记录逻辑(如需要)... } }
筛选时直接用Where(f => f.LatestStatus == FriendshipStatusCode.Accepted),结合方案一或二,能进一步简化业务逻辑。
内容的提问来源于stack exchange,提问作者Maik Hasler

