首页 > 解决方案 > EF Core 错误的连接语句

问题描述

我在 EF Core 2.2 中有以下模型类

public class User
{
    [Key]
    public long Id { get; set; }
    [ForeignKey("Post")]
    public long? PostId { get; set; }
    public virtual Post Post { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    [Key]
    public long Id { get; set; }
    [ForeignKey("User")]
    public long UserId { get; set; }
    public virtual User User { get; set; }
}

我已经检查了与 SSMS 的关系,它们很好。

但是当我使用

 dbContext.Posts.Include(p => p.User);

EF Core 生成以下连接语句

FROM Posts [p] 
LEFT JOIN Users [p.Users] ON [p].[Id] = [p.Users].[PostId]

我包括来自 Post 的用户,并希望它如下所示

FROM Posts [p]   
LEFT JOIN Users [p.Users] ON [p].[UserId] = [p.Users].[Id]

模型有什么问题?

假设我想最后保存PostId在用户模型中。

是否有一个属性可以告诉 ef core 在加入模型时使用哪个属性?

标签: sql-serverentity-frameworkasp.net-coreef-core-2.2

解决方案


从对其他响应的讨论来看,您似乎希望用户包含帖子,但随后还要让用户跟踪对最新帖子的引用。EF 可以对此进行映射,但是您可能需要对这些关系进行一些明确的说明。

例如:

[Table("Users")]
public class User
{
    [Key]
    public int UserId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
    public virtual Post LatestPost { get; set; }
}

[Table("Posts")]
public class Post
{
    [Key]
    public int PostId { get; set; }
    public string PostText { get; set; }
    public DateTime PostedAt { get; set; }
    public virtual User User { get; set; }
} 

然后是一个配置,以确保 EF 正确连接用户和帖子之间的关系:

// EF6
public class UserConfiguration : EntityTypeConfiguration<User>
{
    public UserConfiguration()
    {
        HasMany(x => x.Posts)
            .WithRequired(x => x.User)
            .Map(x=>x.MapKey("UserId"));
        HasOptional(x => x.LatestPost)
            .WithMany()
            .Map(x=>x.MapKey("LatestPostId"));
    }
}

// EFCore
public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasMany(x => x.Posts)
            .WithOne(x => x.User)
            .HasForeignKey("UserId");
        HasOne(x => x.LatestPost)
            .WithMany()
            .IsRequired(false)
            .HasForeignKey("LatestPostId");
    }
}

您也可以在 OnModelCreating 事件中使用 modelBuilder 引用来完成此操作。请注意,我没有在我的实体中声明 FK 属性。这也是一个选项,但我通常建议不要声明 FK 以避免引用与 FK 更新问题。我将 LatestPost FK 命名为 LatestPostId 只是为了更准确地揭示它的用途。如果您愿意,它可以映射到“PostId”。

现在假设我要添加一个新帖子,我想将它与用户相关联,并将其分配为该用户的最新帖子:

using (var context = new SomethingDbContext())
{
    var user = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
        .Single(x => x.UserId == 1);

    var newPost = new Post { PostText = "Test", User = user };
    user.Posts.Add(newPost);
    user.LatestPost = newPost;
    context.SaveChanges();
}

您可以通过加载用户并将 LatestPost 引用设置为所需的帖子来更新“最新”帖子引用。

但是,您应该考虑这种结构的风险。问题是没有办法可靠地强制(在数据级别)用户中的最新帖子引用实际上引用了与该用户关联的帖子。例如,如果我有一个指向特定帖子的最新帖子,那么我会从用户的帖子集合中删除该帖子引用,这可能会导致 FK 约束错误,或者只是解除帖子与用户的关联,但用户的最新帖子仍然指向该记录。我还可以将另一个用户的帖子分配给该用户的最新帖子参考。IE

using (var context = new SomethingDbContext())
{
    var user1 = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
        .Single(x => x.UserId == 1);
    var user1 = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
        .Single(x => x.UserId == 2);

    var newPost = new Post { PostText = "Test", User = user1 };
    user1.Posts.Add(newPost);
    user1.LatestPost = newPost;
    user2.LatestPost = newPost;
    context.SaveChanges();
}

那会很好。用户 2 的“LatestPostId”将设置为此新帖子,即使此帖子的 UserId 仅引用 User1。

在处理最新帖子之类的内容时,更好的解决方案是不对模式进行非规范化以适应它。相反,在实体中为最新帖子使用未映射的属性,或者更好的是,在需要时依靠投影来检索此数据。在这两种情况下,您都会从 User 表中删除 LatestPostId

未映射的属性:

[Table("Users")]
public class User
{
    [Key]
    public int UserId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
    [NotMapped]
    public Post LatestPost 
    {
        get { return Posts.OrderByDescending(x => x.PostedAt).FirstOrDefault(); }
    }
}

未映射属性方法的警告是,如果您想访问此属性,您需要记住在 User 上预先加载 Posts,否则您将触发延迟加载。您也不能在发送到 EF (EF6) 的 Linq 表达式中使用此属性,尽管它们可能与 EFCore 一起使用,但如果表达式提前转换到内存中,则可能会出现性能问题。EF 将无法将 LatestPost 转换为 SQL,因为架构中没有键。

投影:

[Table("Users")]
public class User
{
    [Key]
    public int UserId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}

然后,如果您想检索用户并且它是最新帖子:

var userAndPost = context.Users.Where(x => x.UserId == userId)
    .Select(x => new { User = x, LatestPost = x.Posts.OrderByDescending(PostedAt).FirstOrDefault()} ).Single();

投影Select可以检索感兴趣的实体,或者更好的是,只需将这些实体中的字段返回到平面视图模型或 DTO 以发送到 UI 等。这导致对数据库的更有效查询。Select用于检索您无需担心通过 急切加载的详细信息,Include并且如果正确完成,将避免延迟加载的陷阱。


推荐阅读