我正在努力提出数据库设计。
场景:我正在创建一个非常基本的摔跤模拟器游戏。
我有以下模型类:
摔跤手
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
的多对多表,其中列出了Id
中的Wrestler
和它们中的Match
重新分配参加比赛。
但是,我想知道如何确定比赛的赢家和输家?
是否有另一个表需要解决此问题,例如:
(下面我的描述可能不正确)
答案 0 :(得分:1)
一种选择是向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可能会出现的问题是,假设两者始终保持同步。要更改节目的促销,您可以替换促销参考和/或更新PromotionId。正确的方法是更新导航属性,但是,直到调用SaveChanges
之后,PromotionId才会自动更新。假设PromotionId始终有效并且使用show.PromotionId与show.Promotion.PromotionId。
EF完全支持双向导航属性(Wrestler具有Matches和Match具有Wrestlers),尽管通常更容易管理单向参考,并将双向参考保存在您真正需要的地方。您始终可以从顶级实体(如Match)中查询/过滤数据,并在该匹配的上下文中向下钻取至Wrestler,而无需Wrestler上的“ Matches”。
例如,如果我有一个摔角手,一个包含角球的比赛并分配了一个摔角手,则我的DbContext可能有摔角手,这样我就可以管理我的摔角手池,但是在审查比赛或查看我的摔角手时摔跤手的比赛表现,摔跤手不需要角球等。我可以通过比赛来获取该信息:
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();
处理双向引用的问题是,在编辑双向关系时,您需要同时更新双方。例如,用“ Ruggy Rugged”替换“ Iggy the Ugly”的比赛,您需要从Wrestler Iggy中删除“ Corner”并将其添加到Randy,然后从该Corner Wrestlers集合中删除Iggy,并添加Randy。忘记更新双向关系的一侧会导致更新错误或最后出现意外的数据状态。通常,尽可能多地依赖于1向引用会更简单。
编辑:使用从“匹配”到“角落”的单向引用将匹配项映射到“角落”:
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()
确保角落需要MatchID,但不引用Match实体。 Map(x => x.MapKey("MatchId"))
告诉映射在Corners表中查找MatchId列以链接到Match。
代码逻辑仍然需要防止将多个角最终设置为IsWinner = True的任何可能性。 IMO避免此类问题的最佳实践是采用DDD方法对实体采取行动,而不是直接在实体中访问设置程序。如果实体具有受保护的/内部的setter,而是使用用于更新状态的操作的方法(例如,在“匹配”级别具有AssignWinner(cornerId)
方法,则该方法将成为设置IsWinner的唯一位置,并且可以验证Corner是该方法的一部分)该匹配项以及IsWinner的所有其他角都是错误的。只是为了避免数据状态问题/ w EF或其他ORM而需要考虑的事情。
编辑#2:不具有双向参考的Match,Corner,Wrestler(以及Shadow CornerWrestler连接表)
实体:
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);
}
请注意,CornerWrestler表中没有双向引用,实体中也没有FK属性,CornerWrestler实体也没有。从一个角落,您正在与摔跤手的集合打交道。 EF管理着后台的多对多表。当CornerWrestler表仅包含CornerId和WrestlerId作为复合PK时,这是可能的。这类似于WrestlerMatch联接表的工作方式,不同之处在于,如果要支持在该表/实体中跟踪IsWinner,则需要映射WrestlerMatch实体并在集合中使用该实体(而不是Wrestlers)。如果该联接表仅包含参考FK,则EF可以映射该联接表。 (AFAIK仅在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知道数据结构在幕后的工作方式,而不是依靠模仿关系数据结构的实体结构。 (利用“ M”中的“映射器”)