摔跤模拟器Entity Framework/数据库设计
Wrestling simulator Entity Framework / database design
我正在努力想出一个数据库设计。
场景:我正在创建一个非常基本的摔跤模拟器游戏。
我有以下型号类:
摔跤手
public class Wrestler
{
public int WrestlerId { get; set; }
public string Name { get; set; }
public int Overall { get; set; }
public string Finisher { get; set; }
public virtual ICollection<Match> Matches { get; set; }
}
晋升
public class Promotion
{
public int PromotionId { get; set; }
public string Name { get; set; }
public decimal Budget { get; set; }
public string Size { get; set; }
}
显示
public class Show
{
public int ShowId { get; set; }
public string Name { get; set; }
public int PromotionId {get; set;}
public virtual Promotion Promotion { get; set; }
}
匹配
public class Match
{
public int MatchId { get; set; }
public string MatchType { get; set; }
public int ShowId { get; set; }
public virtual Show Show { get; set; }
public virtual ICollection<Wrestler> Wrestlers { get; set; }
}
摔跤比赛
public class WrestlerMatch
{
public virtual int WrestlerId { get; set; }
public virtual int MatchId { get; set; }
public virtual Wrestler Wrestler { get; set; }
public virtual Match Match { get; set; }
}
对于匹配,我创建了一个名为 WrestlerMatch
的多对多 table,其中列出了 Wrestler
的 Id
和 Match
他们被分配参加比赛。
但是,我想知道如何确定比赛的赢家和输家?
有没有其他table我需要解决这个问题,例如:
(以下我的描述可能不正确)
一种选择是向 WrestlerMatch 添加类似 bool IsWinner { get; set; }
的内容。
此结构适用于 1 对 1 和团队比赛,但管理团队比赛的 IsWinner 有点手动,因为 IsWinner 将设置在 2 个或更多条目上。
或者,您可以在比赛中引入“边”或“角”之类的东西,以跟踪比赛中哪一方获胜,然后将一名或多名摔跤手关联到每一方。
那么你会:
匹配
-> 角落(与 IsWinner)
-> 角落摔跤手
-> 摔跤手
业务逻辑需要强制规定一场比赛中可以有多少个角球,以及一个角球可以有多少名摔跤手,(确保相等的计数,一场比赛中摔跤手不会加倍等)。支持1v1、2v2、4v4、2v2v2、2v2v2v2等
关于 EF 和导航属性的一些快速提示,可帮助避免一些麻烦:
使用导航属性时,我建议不要在实体中声明 FK 字段,而是使用 Map(x => x.MapKey())
(EF6) 或影子属性 (EF Core)。例如:
public class Show
{
public int ShowId { get; set; }
public string Name { get; set; }
public virtual Promotion Promotion { get; set; }
}
public class ShowConfiguration : EntityTypeConfiguration<Show>
{
public ShowConfiguration()
{
ToTable("Shows");
HasKey(x => x.ShowId)
.Property(x => x.ShowId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.Promotion)
.WithMany()
.Map(x => x.MapKey("PromotionId");
}
}
同时拥有 Promotion 和 PromotionId 可能会出现的问题是假设两者始终同步。要更改节目的促销,您可以替换促销参考 and/or 更新 PromotionId。正确的方法是更新导航 属性,但是 PromotionId 直到调用 SaveChanges
后才会自动更新。假设 PromotionId 始终有效并使用 show.PromotionId 与 show.Promotion.PromotionId.
,这可能会为任何代码留下漏洞漏洞
EF 完全支持双向导航属性(Wrestler 有 Matches,Match 有 Wrestlers),但通常更容易管理单向引用并在您真正需要的地方保存双向引用。您始终可以 query/filter 来自比赛等顶级实体的数据,并在该比赛的上下文中向下钻取到摔跤手,而无需摔跤手上的“比赛”。
例如,如果我有一个 Wrestler,一个包含 Corners 的 Match 并且 Wrestler 被分配给一个角,我的 DbContext 可能有 Wrestlers 以便我可以管理我的 Wrestler pool,但是当涉及到查看比赛时,或者查看我的 Wrestler 的比赛表现,Wrestler 不需要角球等。我可以通过比赛访问该信息:
var wrestlerWinCount = context.Matches
.Where(m => m.Corners
.Where(c=> c.IsWinner)
.Any(c => c.Wrestlers.Any(w => w.WrestlerId == wrestlerId)))
.Count();
双向引用将允许:
var wrestlerWinCount = context.Wrestlers
.Where(w => w.WrestlerId == wrestlerId)
.SelectMany(w => w.Corners)
.Where(c => c.IsWinner)
.Count();
处理双向引用的问题是,当编辑双向关系时,您需要更新双方。例如,对于将“Iggy the Ugly”替换为“Randy the Rugged”的比赛,您需要从 Wrestler Iggy 中删除“Corner”并将其添加到 Randy,然后从该 Corner Wrestlers 集合中删除 Iggy,并添加 Randy。忘记更新双向关系的一侧可能会导致更新错误或最终出现意外的数据状态。尽可能依赖单向引用通常更简单。
编辑:使用单向参考将匹配映射到角,从匹配到角:
public MatchConfiguration()
{
ToTable("Matches");
HasKey(x => x.MatchId)
.Property(x => x.MatchId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.Corners)
.WithRequired()
.Map(x => x.MapKey("MatchId"));
}
一场比赛有多个角球,代码逻辑需要强制执行有效的最小值和最大值。 WithRequired()
确保 Corner 需要 MatchId,但不引用 Match 实体。 Map(x => x.MapKey("MatchId"))
告诉映射在角落 table 到 link 到匹配项中查找 MatchId 列。
代码逻辑仍然需要防止多个角最终设置为 IsWinner = True 的任何可能性。 IMO 帮助避免此类问题的最佳实践是采用 DDD 方法对实体采取行动,而不是直接在实体中代码访问 setter。如果实体有 protected/internal 设置器,而是使用方法来更新状态(即在匹配级别有一个 AssignWinner(cornerId)
方法,该方法成为唯一设置 IsWinner 的地方,并且可以验证 Corner 是比赛的一部分,所有其他角落 IsWinner 都是错误的。只是希望避免数据状态问题 /w EF 或其他 ORM 的一些考虑。
编辑#2:没有双向参考的比赛、角球、摔跤手(以及影子角球手加入 table)
实体:
public class Match
{
public int MatchId { get; set; }
// other match related fields.
public virtual ICollection<Corner> Corners { get; set; }
}
public class Corner
{
public int CornerId { get; set; }
public bool IsWinner { get; set; }
public string Name { get; set; }
public virtual ICollection<Wrestler> Wrestlers { get; set; }
}
public class Wrestler
{
public int WrestlerId { get; set; }
public string Name { get; set; }
// other wrestler specific fields...
}
对于配置,让 EF 知道它们之间的关系:
public MatchConfiguration()
{
ToTable("Matches");
HasKey(x => x.MatchId)
.Property(x => x.MatchId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.Corners)
.WithRequired()
.Map(x => x.MapKey("MatchId"));
}
public CornerConfiguration()
{
ToTable("Corners");
HasKey(x => x.CornerId)
.Property(x => x.CornerId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.Wrestlers)
.WithMany()
.Map(x =>
{
x.MapLeftKey("CornerId");
x.MapRightKey("WrestlerId");
x.ToTable("CornerWrestlers");
});
}
public WrestlerConfiguration()
{
ToTable("Wrestlers");
HasKey(x => x.WrestlerId)
.Property(x => x.WrestlerId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
请注意,没有双向引用,实体中没有 FK 属性,也没有 CornerWrestler table 的 CornerWrestler 实体。在一个角落里,您正在与一群摔跤手打交道。 EF 在幕后管理多对多 table。当 CornerWrestler table 仅包含 CornerId 和 WrestlerId 作为复合 PK 时,这是可能的。这类似于 WrestlerMatch 连接 table 的工作方式,除了需要映射 WrestlerMatch 实体并在集合中使用它(而不是 Wrestlers)如果你想支持跟踪 IsWinner table/entity.如果 table 仅包含引用 FK,则 EF 可以映射连接 table。 (据我所知,这仅在 EF6 中受支持,EF Core 仍未实现此功能,并且需要加入实体。)因此角落实体直接与摔跤手打交道的映射使访问摔跤手变得简单直观。
让所有摔跤手参加一场比赛:
var wrestlers = match.Corners.SelectMany(c => c.Wrestlers);
如果您绘制出 CornerWrestler 实体,那么从比赛中访问摔跤手就有点迂回了...
var wrestlers = match.Corners
.SelectMany(c => c.CornerWrestlers.Select(cw => cw.Wrestler));
即您总是必须通过 CornerWrestler(或 WrestlerMatch)导航才能找到摔跤手。
无论如何,它可能看起来与您开始时的设置有点不同,但请仔细阅读 EF 的一对多与多对多关系配置以及不同的配置选项。它可以让您以更直观的方式安排事物,让 EF 计算出数据结构在幕后如何工作,而不是依赖于模仿关系数据结构的实体结构。 (利用 OR“M”中的“映射器”)
我正在努力想出一个数据库设计。
场景:我正在创建一个非常基本的摔跤模拟器游戏。
我有以下型号类:
摔跤手
public class Wrestler
{
public int WrestlerId { get; set; }
public string Name { get; set; }
public int Overall { get; set; }
public string Finisher { get; set; }
public virtual ICollection<Match> Matches { get; set; }
}
晋升
public class Promotion
{
public int PromotionId { get; set; }
public string Name { get; set; }
public decimal Budget { get; set; }
public string Size { get; set; }
}
显示
public class Show
{
public int ShowId { get; set; }
public string Name { get; set; }
public int PromotionId {get; set;}
public virtual Promotion Promotion { get; set; }
}
匹配
public class Match
{
public int MatchId { get; set; }
public string MatchType { get; set; }
public int ShowId { get; set; }
public virtual Show Show { get; set; }
public virtual ICollection<Wrestler> Wrestlers { get; set; }
}
摔跤比赛
public class WrestlerMatch
{
public virtual int WrestlerId { get; set; }
public virtual int MatchId { get; set; }
public virtual Wrestler Wrestler { get; set; }
public virtual Match Match { get; set; }
}
对于匹配,我创建了一个名为 WrestlerMatch
的多对多 table,其中列出了 Wrestler
的 Id
和 Match
他们被分配参加比赛。
但是,我想知道如何确定比赛的赢家和输家?
有没有其他table我需要解决这个问题,例如:
(以下我的描述可能不正确)
一种选择是向 WrestlerMatch 添加类似 bool IsWinner { get; set; }
的内容。
此结构适用于 1 对 1 和团队比赛,但管理团队比赛的 IsWinner 有点手动,因为 IsWinner 将设置在 2 个或更多条目上。
或者,您可以在比赛中引入“边”或“角”之类的东西,以跟踪比赛中哪一方获胜,然后将一名或多名摔跤手关联到每一方。
那么你会:
匹配 -> 角落(与 IsWinner) -> 角落摔跤手 -> 摔跤手
业务逻辑需要强制规定一场比赛中可以有多少个角球,以及一个角球可以有多少名摔跤手,(确保相等的计数,一场比赛中摔跤手不会加倍等)。支持1v1、2v2、4v4、2v2v2、2v2v2v2等
关于 EF 和导航属性的一些快速提示,可帮助避免一些麻烦:
使用导航属性时,我建议不要在实体中声明 FK 字段,而是使用 Map(x => x.MapKey())
(EF6) 或影子属性 (EF Core)。例如:
public class Show
{
public int ShowId { get; set; }
public string Name { get; set; }
public virtual Promotion Promotion { get; set; }
}
public class ShowConfiguration : EntityTypeConfiguration<Show>
{
public ShowConfiguration()
{
ToTable("Shows");
HasKey(x => x.ShowId)
.Property(x => x.ShowId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.Promotion)
.WithMany()
.Map(x => x.MapKey("PromotionId");
}
}
同时拥有 Promotion 和 PromotionId 可能会出现的问题是假设两者始终同步。要更改节目的促销,您可以替换促销参考 and/or 更新 PromotionId。正确的方法是更新导航 属性,但是 PromotionId 直到调用 SaveChanges
后才会自动更新。假设 PromotionId 始终有效并使用 show.PromotionId 与 show.Promotion.PromotionId.
EF 完全支持双向导航属性(Wrestler 有 Matches,Match 有 Wrestlers),但通常更容易管理单向引用并在您真正需要的地方保存双向引用。您始终可以 query/filter 来自比赛等顶级实体的数据,并在该比赛的上下文中向下钻取到摔跤手,而无需摔跤手上的“比赛”。
例如,如果我有一个 Wrestler,一个包含 Corners 的 Match 并且 Wrestler 被分配给一个角,我的 DbContext 可能有 Wrestlers 以便我可以管理我的 Wrestler pool,但是当涉及到查看比赛时,或者查看我的 Wrestler 的比赛表现,Wrestler 不需要角球等。我可以通过比赛访问该信息:
var wrestlerWinCount = context.Matches
.Where(m => m.Corners
.Where(c=> c.IsWinner)
.Any(c => c.Wrestlers.Any(w => w.WrestlerId == wrestlerId)))
.Count();
双向引用将允许:
var wrestlerWinCount = context.Wrestlers
.Where(w => w.WrestlerId == wrestlerId)
.SelectMany(w => w.Corners)
.Where(c => c.IsWinner)
.Count();
处理双向引用的问题是,当编辑双向关系时,您需要更新双方。例如,对于将“Iggy the Ugly”替换为“Randy the Rugged”的比赛,您需要从 Wrestler Iggy 中删除“Corner”并将其添加到 Randy,然后从该 Corner Wrestlers 集合中删除 Iggy,并添加 Randy。忘记更新双向关系的一侧可能会导致更新错误或最终出现意外的数据状态。尽可能依赖单向引用通常更简单。
编辑:使用单向参考将匹配映射到角,从匹配到角:
public MatchConfiguration()
{
ToTable("Matches");
HasKey(x => x.MatchId)
.Property(x => x.MatchId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.Corners)
.WithRequired()
.Map(x => x.MapKey("MatchId"));
}
一场比赛有多个角球,代码逻辑需要强制执行有效的最小值和最大值。 WithRequired()
确保 Corner 需要 MatchId,但不引用 Match 实体。 Map(x => x.MapKey("MatchId"))
告诉映射在角落 table 到 link 到匹配项中查找 MatchId 列。
代码逻辑仍然需要防止多个角最终设置为 IsWinner = True 的任何可能性。 IMO 帮助避免此类问题的最佳实践是采用 DDD 方法对实体采取行动,而不是直接在实体中代码访问 setter。如果实体有 protected/internal 设置器,而是使用方法来更新状态(即在匹配级别有一个 AssignWinner(cornerId)
方法,该方法成为唯一设置 IsWinner 的地方,并且可以验证 Corner 是比赛的一部分,所有其他角落 IsWinner 都是错误的。只是希望避免数据状态问题 /w EF 或其他 ORM 的一些考虑。
编辑#2:没有双向参考的比赛、角球、摔跤手(以及影子角球手加入 table)
实体:
public class Match
{
public int MatchId { get; set; }
// other match related fields.
public virtual ICollection<Corner> Corners { get; set; }
}
public class Corner
{
public int CornerId { get; set; }
public bool IsWinner { get; set; }
public string Name { get; set; }
public virtual ICollection<Wrestler> Wrestlers { get; set; }
}
public class Wrestler
{
public int WrestlerId { get; set; }
public string Name { get; set; }
// other wrestler specific fields...
}
对于配置,让 EF 知道它们之间的关系:
public MatchConfiguration()
{
ToTable("Matches");
HasKey(x => x.MatchId)
.Property(x => x.MatchId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.Corners)
.WithRequired()
.Map(x => x.MapKey("MatchId"));
}
public CornerConfiguration()
{
ToTable("Corners");
HasKey(x => x.CornerId)
.Property(x => x.CornerId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(x => x.Wrestlers)
.WithMany()
.Map(x =>
{
x.MapLeftKey("CornerId");
x.MapRightKey("WrestlerId");
x.ToTable("CornerWrestlers");
});
}
public WrestlerConfiguration()
{
ToTable("Wrestlers");
HasKey(x => x.WrestlerId)
.Property(x => x.WrestlerId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
请注意,没有双向引用,实体中没有 FK 属性,也没有 CornerWrestler table 的 CornerWrestler 实体。在一个角落里,您正在与一群摔跤手打交道。 EF 在幕后管理多对多 table。当 CornerWrestler table 仅包含 CornerId 和 WrestlerId 作为复合 PK 时,这是可能的。这类似于 WrestlerMatch 连接 table 的工作方式,除了需要映射 WrestlerMatch 实体并在集合中使用它(而不是 Wrestlers)如果你想支持跟踪 IsWinner table/entity.如果 table 仅包含引用 FK,则 EF 可以映射连接 table。 (据我所知,这仅在 EF6 中受支持,EF Core 仍未实现此功能,并且需要加入实体。)因此角落实体直接与摔跤手打交道的映射使访问摔跤手变得简单直观。
让所有摔跤手参加一场比赛:
var wrestlers = match.Corners.SelectMany(c => c.Wrestlers);
如果您绘制出 CornerWrestler 实体,那么从比赛中访问摔跤手就有点迂回了...
var wrestlers = match.Corners
.SelectMany(c => c.CornerWrestlers.Select(cw => cw.Wrestler));
即您总是必须通过 CornerWrestler(或 WrestlerMatch)导航才能找到摔跤手。
无论如何,它可能看起来与您开始时的设置有点不同,但请仔细阅读 EF 的一对多与多对多关系配置以及不同的配置选项。它可以让您以更直观的方式安排事物,让 EF 计算出数据结构在幕后如何工作,而不是依赖于模仿关系数据结构的实体结构。 (利用 OR“M”中的“映射器”)