发生这种情况是因为与连接表的 SQL 查询一样,您正在为所有组合构建笛卡尔坐标。作为一个带有 2 个赞的帖子的简单示例,我们在其中运行带有内部连接的 SQL 语句:
SELECT p.PostId, l.LikeId FROM Posts p INNER JOIN Likes l ON p.PostId == l.PostId WHERE p.PostId = 1
我们会回来的:
PostId LikeId
1 1
1 2
显然,从 Post 和 Like 返回更多数据以填充有意义的视图,但原始数据看起来会有很多重复。这与更多的一对多关系复合,因此您需要手动解释数据以解决您收到的帖子有两个赞。
当您正确利用导航属性来映射实体之间的关系时,EF 将完成繁重的工作,将这些笛卡尔组合重新转换为有意义的模型结构。
以如下模型结构为例:
public class Post
{
public int PostId { get; set; }
public DateTime Date { get; set; }
// ... Other Post-related data fields.
public virtual User Patient { get; set; }
public virtual ICollection<Like> Likes { get; set; } = new List<Like>();
}
public class Like
{
public int LikeId { get; set; }
public virtual Post Post { get; set; }
public virtual User User { get; set; }
}
public class User
{
public int UserId { get; set; }
public string UserName { get; set; }
}
EF 可能会通过约定自动解决所有这些关系,但我建议您熟悉显式配置,因为在您想要或需要使用不同命名约定的特定情况下,约定总是会失败。 EF 中有四种解决关系的选项:约定、属性、EntityTypeConfiguration 和使用OnModelCreating 的模型构建器。下面的示例通过 EF6 概述了 EntityTypeConfiguration。 EF 代码使用一个名为 IEntityTypeConfiguration 的接口,它以类似的方式工作,但您实现了一个配置方法,而不是在构造函数中调用基类方法。属性和约定通常是最少的工作量,但我通常会遇到它们没有映射的情况。 (使用 EF Core 似乎更可靠)通过 modelBuilder 进行配置是较小应用程序的一种选择,但它很快就会变得混乱。
public class PostConfiguration : EntityTypeConfiguration<Post>
{
public PostConfiguration()
{
ToTable("Posts");
HasKey(x => x.PostId)
.Property(x => x.PostId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.Patient)
.WithMany()
.Map(x => x.MapKey("PatientUserId"));
HasMany(x => x.Likes)
.WithRequired(x => x.Post)
.Map(x => x.MapKey("PostId"));
}
}
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
ToTable("Users");
HasKey(x => x.UserId)
.Property(x => x.UserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
public class LikeConfiguration : EntityTypeConfiguration<Like>
{
public LikeConfiguration()
{
ToTable("Likes");
HasKey(x => x.LikeId)
.Property(x => x.LikeId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasRequired(x => x.User)
.WithMany()
.Map(x => x.MapKey("UserId"));
}
}
// In your DbContext
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.AddFromAssembly(GetType().Assembly);
}
现在,当您要查询数据时,EF 将管理 FK 关系并解析任何联接和生成的笛卡尔积。问题就变成了你想从领域模型中得到什么数据?
var viewData = _context.Posts
.Select(p => new PostDTO
{
ID = p.PostID,
UserName = p.Patient.UserName,
Text = p.Text,
Likes = p.Likes
.Select(l => new LikeDTO
{
ID = l.LikeId
UserName = l.User.UserName
}).ToList(),
Liked = p.Likes.Any(l => l.User.UserId == userModel.UserId)
}).OrderByDescending(p => p.Date)
.ToList();
这是基于您的原始查询的粗略猜测,您希望在哪里发布帖子、患者姓名、喜欢的内容以及当前用户是否喜欢帖子的指示符。请注意,没有显式连接,甚至没有急切加载。 (Include) EF 将仅使用填充 DTO 所需的列和关联数据所需的 ID 构建必要的语句。
您还应避免在返回数据时将 DTO 与实体混合。在上面的示例中,我为 Like 和 Post 引入了 DTO,因为我们想从我们的域中返回有关 Likes 的一些详细信息。我们不会传回对实体的引用,因为当它们被序列化时,序列化程序会尝试触摸每个可能导致延迟加载被触发的属性,并且总体上会返回比我们的消费者需要或应该看到的更多信息。通过导航属性映射和表达关系后,EF 将自动构建具有所需连接的查询,并处理返回的数据以填充您希望看到的内容。