我正在编写一个 ASP.NET Core Web API 项目。 我从“用户”到“计划”有“一对多”关系,运行良好。 问题是,我想在“多”侧有一个“当前”条目,这意味着除了用户实体中存储的所有计划的列表之外 - 我想添加一个说明当前计划的单个计划 做这个的最好方式是什么? 我的计划课:
public class Plan
{
public Plan() { }
public Plan(string name, string description, User user)
{
this.Name = name;
this.Description = description;
this.Workouts = new HashSet<Workout>();
this.User = user;
this.UserId = user.Id;
this.status = PlanStatus.UnStarted;
}
[Key]
public int PlanId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string UserId { get; set; }
[ForeignKey("UserId")]
public virtual User User { get; set; }
public PlanStatus status;
public virtual ICollection<Workout> Workouts { get; set; }
}
PlanStatus 枚举:
public enum PlanStatus
{
Unstarted,
InProgress,
Completed
}
我的用户类别:
public class User : IdentityUser
{
public User() {
}
public User(string name, string email)
{
this.UserName = name;
this.Email = email;
this.Plans = new HashSet<Plan>();
this.Exercises = new HashSet<Exercise>();
}
public virtual ICollection<Plan> Plans { get; set; }
public virtual ICollection<Exercise> Exercises { get; set; }
}
我想到的一种方法是将“currentPlanId”添加到 User 类中,但这需要维护。 另一种方法是检查属于用户的所有计划并检查其“计划状态” - 如果是“进行中”,则为当前计划(一次应该只有一个具有该状态的计划)
哪个更好?或者还有其他方法吗?
您概述了一些选项,但我建议考虑当前计划是消费问题,而不是数据问题,因为数据已经容纳了确定它所需的一切。将 CurrentPlanId 添加到 User 的想法是一种非规范化形式,它会带来问题,因为无法从本质上强制 CurrentPlan 引用具有正确状态的计划,甚至根本无法强制执行与该用户关联的计划。就数据库和 FK 而言,它只关心它引用 Plan 表中的一行。另一种方法是将计划标记为“当前”计划,您可以通过状态有效地执行此操作。这种方法面临的问题是进行监管,以确保最多只有一个计划被视为当前计划。
在实体中,您可以有一个未映射的属性,例如:
[NotMapped]
public Plan? CurrentPlan
{
get => Plans
.FirstOrDefault(x => x.Status == PlanStatuses.InProgress); // I would generally recommend an OrderBy when using First vs. Single
}
这种方法的问题在于,为了有效地工作,需要预先加载计划。否则你会遇到延迟加载或
NullReferenceException
。它还会使用无法在 Linq2SQL 中使用的属性污染实体。例如,在具体化查询之前,您无法在查询中的 Where()
或 Select()
子句中引用 CurrentPlan,因为 EF 无法“查看”它并将其转换为 SQL。
这让我认为这是一个消费问题。当我们使用数据时,无论是从 API 发送回数据还是在 UI 上呈现数据,我们都应该考虑使用专用类来实现此目的。 DTO 或 ViewModel,而不是实体。通过这种方式,我们可以拥有一个包含 CurrentPlan 的视图模型,其中该视图模型的投影会根据规则填充该视图模型,以确定其当前计划是什么(如果有)。 viewModel 由
Select()
或使用可与 EF 的 IQueryable
实现配合使用的映射器填充。这样我们就可以得到:
var user = await _context.Users
.Where(x => /* insert criteria */)
.Select(x => new UserSummaryViewModel
{
UserId = x.UserId,
// Other fields the view is consuming...
CurrentPlan = x.Plans.FirstOrDefault(x => x.Status == PlanStatuses.InProgress);
}).ToListAsync();
像 Automapper 这样的映射器可以配置表达式来填充在
IQueryable
解析中工作的值,从而使生成的查询更整洁:
var user = await _context.Users
.Where(x => /* insert criteria */)
.ProjectTo<UserSummaryViewModel>(config)
.ToListAsync();
...其中“config”是映射器配置的实例,其中包含如何从实体构建视图模型的设置。
使用视图模型或 DTO 而不是实体的优点在于,它既使实体对数据关注保持纯粹,并且与急切加载整个对象图或触发延迟加载相比,它会导致更高效的查询。围绕投影进行设计以分离数据和消耗问题可以帮助解决此类提取规则而不会产生副作用,并带来性能更好的应用程序。