【问题标题】:Entity Framework Core one to many mapping from SQLEntity Framework Core 从 SQL 的一对多映射
【发布时间】:2019-08-11 21:15:52
【问题描述】:

我有一个包含 4 个表的简单博客数据库结构:

每个表中的一些示例数据如下所示:

博客表:

帖子表:

标签表:

PostTags 表:

我有这个 SQL 脚本。

SELECT b.Id, 
       b.Title, 
       p.Id, 
       p.Title, 
       p.PostContent, 
       t.Name
FROM dbo.Blogs b
     JOIN Posts p ON p.BlogId = b.Id
     LEFT JOIN PostTags pt ON pt.PostId = p.Id
     LEFT JOIN Tags t ON t.Id = pt.TagId
WHERE b.Id = 1
      AND p.IsDeleted = 0;

有几种方法可以使用 EF Core 执行此脚本。一种是直接从代码中调用这个 SQL 脚本。另一种创建存储过程或视图并从代码中调用它的方法。

假设我有以下类来映射 EF Core 执行的 SQL 脚本的结果。

public partial class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slogan { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

public partial class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }
    public string Title { get; set; }
    public string PostContent { get; set; }

    public virtual ICollection<PostTag> PostTags { get; set; }
}

public partial class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<PostTag> PostTags { get; set; }
}   

public partial class PostTag
{
    public int Id { get; set; }
    public int PostId { get; set; }
    public int TagId { get; set; }

    public virtual Post Post { get; set; }
    public virtual Tag Tag { get; set; }
}     

这是控制器中的一个方法:

[Route("posts/{blogId}")]
[HttpGet]
public async Task<IActionResult> GetBlogPosts(int blogId)
{
        string sql = @"
                        SELECT b.Id, 
                            b.Title, 
                            p.Id, 
                            p.Title, 
                            p.PostContent, 
                            t.Id,
                            t.Name
                        FROM dbo.Blogs b
                            JOIN Posts p ON p.BlogId = b.Id
                            LEFT JOIN PostTags pt ON pt.PostId = p.Id
                            LEFT JOIN Tags t ON t.Id = pt.TagId
                        WHERE b.Id = 1
                            AND p.IsDeleted = 0;
                ";

    // this is not working
    var result = db.Blogs.FromSql(sql).ToList().FirstOrDefault(); 

    return Ok(result);
}

如何将 SQL 脚本的结果映射到 Blog 对象,以便获得以下结果?

{
    "Blog": [
        {
            "Id": 1,
            "Title": "Another .NET Core Guy",
            "Posts": [
                {
                    "Id": 1,
                    "Title": "Post 1",
                    "PostContent": "Content 1 is about EF Core and Razor page",
                    "Tags": [
                        {
                            "Id": 1,
                            "Name": "Razor Page"
                        },
                        {
                            "Id": 2,
                            "Name": "EF Core"
                        }
                    ]
                },
                {
                    "Id": 2,
                    "Title": "Post 2",
                    "PostContent": "Content 2 is about Dapper",
                    "Tags": [
                        {
                            "Id": 3,
                            "Name": "Dapper"
                        }
                    ]
                },
                {
                    "Id": 4,
                    "Title": "Post 4",
                    "PostContent": "Content 4",
                    "Tags": [
                        {
                            "Id": 5,
                            "Name": "SqlKata"
                        }
                    ]
                }
            ]
        }
    ]
}

更新 2019 年 8 月 13 日:

EF Core 尚不支持此类功能,因为它已在 EF Core Github 页面 https://github.com/aspnet/EntityFrameworkCore/issues/14525 上声明了这里

【问题讨论】:

  • 您必须使用原始 SQL 查询而不是 linq-to-entity 的任何特殊原因?
  • @Lowkey,没有特别的原因。这是因为“Inlucde”或“ThenInclude”中没有选项有过滤器。见这里github.com/aspnet/EntityFrameworkCore/issues/1833

标签: c# entity-framework-core asp.net-core-2.0


【解决方案1】:

Fromsql得到的结果是平面关系,不是嵌套关系。如果你坚持用这个来获取嵌套关系的数据,有两种方法: 1.你可以自定义一个sql脚本来实现; 2.您可以使用Include方法在EF Core中加载相关数据 并选择您想要使用嵌套关系的属性并使用查询结果填充它。

这是一个使用Include方法在EF Core中加载相关数据的工作演示,您可以参考:

Post model 和 Tag model 之间存在多对多关系,您应该像下面这样定义它们:

 public class Post
{
    public int Id { get; set; }
    public int BlogId { get; set; }
    public string Title { get; set; }
    public string PostContent { get; set; }
    public bool IsDeleted { get; set; }


    public  ICollection<PostTag> PostTags { get; set; }
}

public class PostTag
{
    //public int Id { get; set; }

    public int PostId { get; set; }
    public int TagId { get; set; }

    public virtual Post Post { get; set; }
    public virtual Tag Tag { get; set; }
}

 public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }

    public bool IsDeleted { get; set; }

    public virtual ICollection<PostTag> PostTags { get; set; }
}

数据库上下文:

public class TestDbContext:DbContext
{
    public TestDbContext (DbContextOptions<TestDbContext> options):base(options)
    { }


    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    public DbSet<Tag> Tags { get; set; }


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PostTag>()
        .HasKey(pt => new { pt.PostId, pt.TagId });

        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Post)
            .WithMany(p => p.PostTags)
            .HasForeignKey(pt => pt.PostId);

        modelBuilder.Entity<PostTag>()
            .HasOne(pt => pt.Tag)
            .WithMany(t => t.PostTags)
            .HasForeignKey(pt => pt.TagId);
    }
}

控制器:

[Route("posts/{blogId}")]
    [HttpGet]
    public async Task<IActionResult> GetBlogPosts(int blogId)
    {
        var blogs = db.Blogs
            .Where(b => b.Id == blogId)
            .Include(b => b.Posts)
                .ThenInclude(p => p.PostTags).ThenInclude(pt => pt.Tag)
            .Select(b=>new {
                Id=b.Id,
                Title=b.Title,
                Posts= b.Posts.Select(p => new {
                    Id=p.Id,
                    Title=p.Title,
                    PostContent=p.PostContent,
                    Tags =p.PostTags.Select(pt=> new {
                        Id=pt.Tag.Id,
                        Name=pt.Tag.Name,
                    })
                })
            });

        return Json(blogs);
    }

参考:

https://docs.microsoft.com/en-us/ef/core/modeling/relationships#many-to-many

https://docs.microsoft.com/en-us/ef/core/querying/related-data

【讨论】:

  • 你是对的,sql脚本的结果是一个平面数据集。我一直在尝试一些选择。我认为这对 Include 过滤器或 ThenInclude 过滤器也有一些限制。如果我需要过滤掉某些列,假设我不需要已删除的帖子和已删除的标签。此外,这有 N+1 个问题,而不是一个带有一些关节的简单 Select 语句。在我的测试中,它产生了 7 个选择语句,因为我有 5 个帖子、6 个标签。如果我有更多的选择语句将成倍增加。
【解决方案2】:

如果 EF Core 的版本可以是 2.1 或更高,则可以在 DbContext 类中使用 DbQuery&lt;ClassName&gt; ClassName。然后您可以调用var result = db.ClassName.FromSql(sql).ToList().FirstOrDefault(); 或在数据库中创建视图,然后您可以在OnModelCreating 方法中分配给视图。

您创建的课程应该代表您拥有的视图。 我不知道 ef core 如何使用包含列表和下一个列表的示例模型解析您的 SQL 查询。可能您必须在 SQL 查询中使用别名,例如 Blogs as BPosts as b.Posts,但您必须尝试并试验。

有关 DbQuey 的更多信息,您可以在 https://docs.microsoft.com/en-us/ef/core/modeling/query-types 上的 Microsoft 文档中阅读

【讨论】:

  • 是的,我在发布问题之前检查了查询类型,但文档在展示如何编写我需要的内容方面非常有限。
【解决方案3】:

想把这个写成评论,但是因为我是新来的,所以我不能这样做:

编辑:我写这个答案只是因为您说由于 .Include() 和 .ThenInclude() 的行为而必须用普通 SQL 编写它

如果您选择所有内容并且在进一步的过程中不需要它,则不需要 .Include() 或 .ThenInclude() 语句(就像您所做的那样,选择并且不再对它做任何事情)。

我看到的一种方法是在没有 linq 的情况下进行两次选择,方法是将实体添加为 navigationproperty 并像这样执行两个 db 请求:

public partial class Post
{

    public int Id { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; }
    public string Title { get; set; }
    public string PostContent { get; set; }

    public virtual ICollection<PostTag> PostTags { get; set; }
}

var result = await db.Posts.Where(x => x.BlogId == blogId && x.Blog. ... whatever you may also need).Select(x => whatever you need from here).ToArrayAsync()

对上面的其他实体进行另一个查询,然后加入一个新实体

或者使用ASP.NET Core & EntityFramework Core: Left (Outer) Join in Linq中所示的 linq 进行操作

希望这看起来合理

【讨论】:

  • 感谢您的建议。是的,它可以通过多种方式完成,一种方式是先从 Post 进行反向选择。我想知道是否有一种解决方案可以直接从 EF Core 中的 sql 脚本映射结果。我知道这可以通过 Dapper + Slapper.Mapper 完成,但它需要我更改我无法完全控制的 SQL 脚本 stackoverflow.com/questions/9350467/…
【解决方案4】:

如果您想要一个优化的查询,并且如果需要还需要执行额外的where,您可能不得不依赖 linq-to-sql 样式来构建您的查询,并映射到所需的结果你自己:

var query = (
from b in db.Blogs
join p in db.Posts on b.Id equals p.BlogId
from pt in db.PostTag.Where(posttag => posttag.PostId == p.Id).DefaultIfEmpty() //LEFT OUTER JOIN
from t in db.Tags.Where(tag => tag.Id == pt.TagId).DefaultIfEmpty() //LEFT OUTER JOIN
where (...) //Your additional conditions
select new 
{
    BlogId = b.Id,
    BlogTitle = b.Title,
    PostId = p.Id,
    PostTitle = p.Title,
    p.PostContent,
    TagId = (int?) t.Id,
    TagName = t.Name
}).ToList();

从这里开始,您可以自己编写GroupBy 语句,也可以使用一些插件。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-01
  • 2018-08-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-04
相关资源
最近更新 更多