EntityFramework Core实现一对一、一对多、多对多关系
准备工作
根据教程前几节,已经建立好了三个实体类,并且生成了数据库。三个实体类分别是:
联赛League:
public class League
{
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
[Required, MaxLength(50)]
public string Country { get; set; }
}
球员Player:
public class Player
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
}
俱乐部Club:
public class Club
{
public Club()
{
Players = new List<Player>();
}
public int Id { get; set; }
public string Name { get; set; }
public string City { get; set; }
[Column(TypeName = "date")]
public DateTime DateOfEstablished { get; set; }
public string History { get; set; }
public League League { get; set; } // 导航属性
public List<Player> Players { get; set; } // 导航属性
}
一对多的关系
Club有一个导航属性League,导航到单个League上,一个Club都对应一个League,但可能有多个Club对应到同一个League上,所以Club对League就是多对一的关系。所以Club表中应该存在一个外键对应League,在代码中看不出,可以在数据库中看一下:
确实存在。这种关系可以在代码中指定,不指定也会自动生成。
Club的Players属性也是另一种形式的导航属性,这个导航属性的类型是一个集合,相当于另一个方向的导航。这个时候Club是主表,Player是子表。Players表中也有一个ClubId外键。
多对多的关系
计划再创建一个比赛Game实体模型,它与球员Player之间是m:n的关系。这种关系使用EF Core无法直接实现,可以加一个中间表GamePlayer。比如一个队员,这个赛季参加了5场比赛,它就应该对应5哥GamePlayer,一对多的关系。而每场比赛,又有多个队员参加,每个队员又相当于这场比赛的一个GamePlayer,所以Game和GamePlayer也是一对多的关系。这样Player和Game之间就相当于间接形成了多对多的关系。
比赛Game:
public class Game
{
public int Id { get; set; }
public int Round { get; set; }
public DateTimeOffset? StartTime { get; set; }
}
因为StartTime是具体的时间,所以用DateTimeOffset类型。
再建一个GamePlayer类:
该类是Games和Players的中间表,只需要包含它们两个的主键就行了,作为GamePlayer的外键。而GamePlayer不再需要多余的主键了,可以用两个外键做联合主键。
先去Player类中添加一个导航属性,因为是List类型,所以初始化一下,以免出现空指针空引用异常NullReferenceExcepiton:
public class Player
{
public Player()
{
GamePlayers = new List<GamePlayer>();
}
public int Id { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public List<GamePlayer> GamePlayers { get; set; }
}
Game类中也是一样:
public class Game
{
public Game()
{
GamePlayers = new List<GamePlayer>();
}
public int Id { get; set; }
public int Round { get; set; }
public DateTimeOffset? StartTime { get; set; }
public List<GamePlayer> GamePlayers { get; set; }
}
现在在Game和Player类中都体现了一对多的关系,可以回GamePlayer类中再体现一下一对多的关系:
public class GamePlayer
{
public int GameId { get; set; }
public int PlayerId { get; set; }
public Game Game { get; set; }
public Player Player { get; set; }
}
这种一对多的关系可以两端同时体现。然后必须手动设定联合主键。可以去数据库上下文DbContext中,重写一下OnModelCreating方法,在里面使用Fluent API来设定:
public class DemoDbContext: DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=YangDemo;Integrated Security=True");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<GamePlayer>().HasKey(x => new
{
x.PlayerId,
x.GameId
});
}
public DbSet<League> Leagues { get; set; }
public DbSet<Club> Clubs { get; set; }
public DbSet<Player> Players { get; set; }
public DbSet<Game> Games { get; set; }
public DbSet<GamePlayer> GamePlayers { get; set; }
}
使用modelBuilder,选择GamePlayer实体,使用HasKey方法来设定主键。联合主键就是后面的匿名类中的两个属性。然后添加迁移并更新数据库。
可以看到GamePlayers表中的联合主键:
一对一的关系
假设每个队员Player对应一份简历Resume,一份简历也只属于一个队员。
建立简历Resume类:
public class Resume
{
public int Id { get; set; }
public string Description { get; set; }
public int PlayerId { get; set; }
public Player Player { get; set; }
}
除了简历自己的属性之外,需要建立一个与Player类的主键相同类型的属性,在这里是int类型,并命名为PlayerId,然后建立到Player类的导航属性。
在Player表中做同样操作:
public int ResumeId { get; set; }
public Resume Resume { get; set; }
现在Player和Resume就是一对一的关系,EF Core会选择其中的一个类作为主体,但是EF Core很可能选错,所以还是要使用Fluent API来手动指定一下。
回到数据库上下文中。这次Entity选择哪一项都可以。但我们主体应该是Player,也就是Resume应该有一个外键是Player的Id。
modelBuilder.Entity<Resume>()
.HasOne(x => x.Player)
.WithOne(x => x.Resume)
.HasForeignKey<Resume>(x => x.PlayerId);
大意是Resume实体有一个Player,一个Player也有一个Resume,Resume有外键PlayerId。然后就可以添加迁移并更新数据库了。