EF Core 中的一对多关系实现
在现实世界的应用程序中,我们经常需要处理不同实体之间的关系。例如,在博客系统中,文章 (Article) 和评论 (Comment) 之间存在一对多的关系。这篇文章将介绍如何在 Entity Framework Core (EF Core) 中实现一对多关系,并通过具体的代码示例来演示不同的操作。
实体定义
首先,我们需要定义两个实体:Article 和 Comment。Article 表示一篇文章,而 Comment 则表示针对某篇文章的一个评论。由于一个文章可以有多个评论,因此我们定义了一个一对多的关系。
public class Article
{
public int Id { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public ICollection<Comment> ArtComments { get; set; }
}
public class Comment
{
public int Id { get; set; }
public int ComArticleId { get; set; }
public string ComMessage { get; set; }
public Article ComArticle { get; set; }
}
执行Add-Migration xx 和Update-Database 后生成数据表
配置一对多关系
在 EF Core 中配置一对多关系可以通过两种方式完成:使用流利 API 或者使用数据注解。在这个例子中,我们将使用流利 API 来配置关系,在DbContext中配置也可以
// DbContext中配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Article>()
.HasMany(a => a.ArtComments)
.WithOne(c => c.ComArticle)
.HasForeignKey(c => c.ComArticleId);
}
// 在CommentConfig中配置也可以
internal class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comment");
builder.Property(p => p.ComMessage).IsRequired().HasMaxLength(500).IsUnicode();
// 配置一对多关系
builder.HasOne<Article>(p => p.ComArticle).WithMany(p => p.ArtComments).HasForeignKey(p => p.ComArticleId);
}
}
上述配置定义了 Article 实体和 Comment 实体之间的一对多关系。每个 Comment 实体都有一个外键 ComArticleId,它指向了 Article 实体的主键 Id。
操作一对多关系
接下来,我们来看看如何使用一对多关系进行一些基本的操作。
插入数据
要插入一条包含多个评论的文章,我们可以这样做:
static void InsertData()
{
using (var myDb = new MyDbContext())
{
var article = new Article
{
Title = "国足提赢日本了",
Message = "20年前,xxxxxxxxx\r\nxx",
};
var comment = new Comment
{
ComMessage = "操,怎么拿20年前的新闻出来糊弄人"
};
var comment1 = new Comment
{
ComMessage = "标题党"
};
article.ArtComments.Add(comment1);
article.ArtComments.Add(comment);
myDb.Articles.Add(article);
myDb.SaveChanges();
}
}
查询关联表数据
为了获取文章及其相关评论,我们可以使用 Include 方法或者Select 来加载相关的评论数据:
/// <summary>
/// 查询关联表数据
/// 1、可以使用Include();
/// 2、可以使用Select();
/// </summary>
static void SelectData1()
{
using (var myDb = new MyDbContext())
{
// Include()把关联的表带出来
// 也可以用Select() 把关联表查询出来
// var comment = myDb.Comments.Include(p => p.ComArticle).Single(p => p.Id == 1);
var comment = myDb.Comments.Select(p => new { p.Id, p.ComMessage, artId = p.ComArticle.Id, p.ComArticle.Title, p.ComArticle.Message }).First(p => p.Id == 1);
Console.WriteLine($"commId={comment.Id};commMsg={comment.ComMessage}");
Console.WriteLine($"artId={comment.artId};artTitle={comment.Title};artCom={comment.Message}");
var sql = myDb.Comments.Include(p => p.ComArticle).ToQueryString();
// SELECT [t].[Id], [t].[ComArticleId], [t].[ComMessage], [t0].[Id], [t0].[Message], [t0].[Title]
// FROM[T_Comment] AS[t]
//LEFT JOIN[T_Article] AS[t0] ON[t].[ComArticleId] = [t0].[Id]
Console.WriteLine(sql);
}
}
/// <summary>
/// 单向导航属性 HasOne() .WithMany()(配置一对多,配置在从表配置文件中) = HasMany() .WithOne(配置多对一,配置在主表配置文件中) 关系配置在任意一方都可以
/// 对于一个表被多个表引用时,建议使用单向导航属性,比如员工信息被系统多个地方使用,就使用单向导航属性
/// 案例请看 User与Leave实体直接的关联关系,详细配置看LeaveConfig
/// </summary>
static void SingleForeignKey()
{
using (var mydb = new MyDbContext())
{
var user = new User { Name = "小杨同学" };
var leave = new Leave() { Requester = user, Remark = "请假去玩" };
mydb.Leaves.Add(leave);
mydb.SaveChanges();
var leaveObj = mydb.Leaves.First(p => p.LeaveId == 1);
Console.WriteLine(leaveObj.LeaveId + ";RequesterName=" + leaveObj.Requester.Name);
}
}
internal class User
{
public int UserId { get; set; }
public string Name { get; set; }
}
internal class UserConfig : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.ToTable("T_User");
}
}
internal class Leave
{
public int LeaveId { get; set; }
public User Requester { get; set; } // 请求人
public User Approver { get; set; } // 审批人
public string Remark { get; set; }
}
internal class LeaveConfig : IEntityTypeConfiguration<Leave>
{
public void Configure(EntityTypeBuilder<Leave> builder)
{
builder.ToTable("T_Leave");
builder.HasOne<User>(p => p.Requester).WithMany().IsRequired();
builder.HasOne<User>(p => p.Approver).WithMany();
}
}
/// <summary>
/// 双向导航属性
/// 对于主从表使用双向导航属性
/// 比如单据头与单据明细
/// 案例请看Comment与Article 之间的关联关系 详细配置看CommentConfig
/// 给Comment表,冗余外键;避免关联查询消耗性能(双向导航属性)
/// 外键名称必须与数据库中的ID名称一致,否则会生成列ID字段。
/// </summary>
static void MuliHasForeignKey()
{
using (var mydb = new MyDbContext())
{
var comment = mydb.Comments.Select(p => new { p.ComMessage, p.Id, p.ComArticleId }).Single(p => p.Id == 2);
Console.WriteLine(comment.Id + ";" + comment.ComArticleId);
}
}
// 文章配置类
public class ArticleConfig : IEntityTypeConfiguration<Article>
{
public void Configure(EntityTypeBuilder<Article> builder)
{
builder.ToTable("T_Article");
builder.Property(p => p.Title).IsRequired().IsUnicode().HasMaxLength(100);
builder.Property(p => p.Message).IsRequired().IsUnicode().HasMaxLength(500);
}
}
// 评论配置类
internal class CommentConfig : IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comment");
builder.Property(p => p.ComMessage).IsRequired().HasMaxLength(500).IsUnicode();
// 配置一对多关系
builder.HasOne<Article>(p => p.ComArticle).WithMany(p => p.ArtComments).HasForeignKey(p => p.ComArticleId);
}
}
internal class MyDbContext : DbContext
{
public DbSet<Article> Articles { get; set; }
public DbSet<Comment> Comments { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<Leave> Leaves { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
string conStr = "Server=.;Database=Demo2;uid=sa;pwd=sasa;TrustServerCertificate=True;";
optionsBuilder.UseSqlServer(conStr);
//optionsBuilder.UseLoggerFactory(loggerFactory);
//optionsBuilder.LogTo(sql =>
//{
// if (!sql.Contains("CommandExecuted")) return;
// Console.WriteLine(sql);
//}); // 简单查询sql日志
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//modelBuilder.Entity<User>().ToTable("T_User"); 这样也可以设置表名称
var assembly = this.GetType().Assembly;
modelBuilder.ApplyConfigurationsFromAssembly(assembly);
}
}