【问题标题】:Merge properties from mapping table to single class将映射表中的属性合并到单个类
【发布时间】:2020-04-16 14:42:06
【问题描述】:

我有一个使用 EF Core 3.1 访问其数据的网站。它使用的主表是 [Story] ​​每个用户可以存储有关每个故事的一些元数据 [StoryUserMapping]。我想做的是当我读入 Story 对象时,EF 会自动加载该故事的元数据(如果存在)。

类:

public class Story
{
    [Key]
    public int StoryId { get; set; }
    public long Words { get; set; }
    ...
}

public class StoryUserMapping
{
    public string UserId { get; set; }
    public int StoryId { get; set; }
    public bool ToRead { get; set; }
    public bool Read { get; set; }
    public bool WontRead { get; set; }
    public bool NotInterested { get; set; }
    public byte Rating { get; set; }
}

public class User
{
    [Key]
    public string UserId { get; set; }
    ...
}

StoryUserMapping 具有复合键 ([UserId], [StoryId])。

我想看到的是:

public class Story
{
    [Key]
    public int StoryId { get; set; }
    public bool ToRead { get; set; } //From user mapping table for currently logged in user
    public bool Read { get; set; } //From user mapping table for currently logged in user
    public bool WontRead { get; set; } //From user mapping table for currently logged in user
    public bool NotInterested { get; set; } //From user mapping table for currently logged in user
    public byte Rating { get; set; } //From user mapping table for currently logged in user
    ...
}

有没有办法在 EF Core 中做到这一点?我当前的系统是将 StoryUserMapping 对象作为 Story 对象的属性加载,然后在 Story 对象上使用 Non-Mapped 属性访问器,如果 StoryUserMapping 对象存在,则读取该对象。这通常感觉像是 EF 可能更优雅地处理的事情。

用例

设置:我有 100 万个故事,1000 个用户,最坏的情况下,我有一个 StoryUserMapping 每个故事:10 亿条记录。

用例 1:我想查看我(登录用户)标记为“阅读”且超过 100,000 字的所有故事

用例 2:我想查看我没有标记为 NotInterested 或 WontRead 的所有故事

我不关心每个故事查询多个 StoryUserMappings,例如我不会问这个问题:哪些故事被超过 n 个用户标记为已读。如果将来发生变化,我宁愿不限制这一点,但如果我需要这样做就可以了。

【问题讨论】:

  • 我要问的第一个问题是为什么你需要根Story对象中的这些数据吗? Story 及其导航属性是否不够?您是否通过 API 返回这些数据?我不会将带有 [NotMapped] 属性的聚合属性添加到 Story 类中,因为这表明当有更好的方法时,您会在实体模型中硬塞数据。
  • 我同意这是代码异味。我想要 Story 对象中的属性的主要原因是为了方便。我将这些字段用作 Web 应用程序的 cshtml 页面上的过滤器。如果效果更好,我很乐意将它们留在子对象中。更重要的是,我不能仅仅获得 Story 与 StoryUserMapping 的正常关系,因为我无法传入 Current UserId(或者我不知道如何)
  • 在这种情况下,我建议您考虑使用视图模型/dto/aggregate 对象来传输您的视图数据。您不希望扭曲实体以适应它们在视图中的外观 - 您的控制器/服务逻辑将需要映射/按摩实体数据以适应输出/视图。阅读创建 MVC Web 应用程序时的最佳实践,以及如何最好地将您的关注点分离到应用程序中的责任“层”:)
  • 我在原始问题中添加了一些用例。我在建议的方法中看到的问题是,如果我尝试满足用例 1,我查询所有超过 100,000 个单词的故事,得到 1,000 条记录 * n 个故事。然后必须手动过滤那些我标记为“待读”的故事。这是一个潜在的大量数据传输,最终会导致很少的记录。
  • 感谢您添加一些用例 - 当然有助于了解您想要实现的目标。不过,我关于使用聚合 viewmodel/dto 对象的原始评论仍然有效。您可以利用 EF/LINQ 的强大功能使用现有模型查询正确的数据,然后然后将结果映射到并显示它们的聚合对象。我很快会添加一个答案,应该可以帮助你:)

标签: c# entity-framework-core entity-framework-core-3.1


【解决方案1】:

为自己创建一个聚合视图模型对象,您可以使用它在视图中显示数据,类似于您目前在 Story 实体下得到的结果:

public class UserStoryViewModel
{
    public int StoryId { get; set; }
    public bool ToRead { get; set; } 
    public bool Read { get; set; }
    public bool WontRead { get; set; }
    public bool NotInterested { get; set; }
    public byte Rating { get; set; }
    ...
}

此视图模型只关心聚合数据以显示在视图中。这样,您无需调整现有实体以适应在其他地方显示数据的方式。

您的数据库实体模型应该尽可能接近“哑”对象(除了导航属性)——它们看起来非常明智。

在这种情况下,请从您之前添加的现有 Story 中删除不必要的 [NotMapped] 属性。

在您的控制器/服务中,您可以根据您提到的用例查询您的数据。获得查询结果后,您可以然后将结果映射到您的聚合视图模型以在视图中使用。

以下是获取当前用户的所有Storys 的用例示例:

public class UserStoryService
{
    private readonly YourDbContext _dbContext;

    public UserStoryService(YourDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public Task<IEnumerable<UserStoryViewModel>> GetAllForUser(string currentUserId)
    {
        // at this point you're not executing any queries, you're just creating a query to execute later
        var allUserStoriesForUser = _dbContext.StoryUserMappings
            .Where(mapping => mapping.UserId == currentUserId)
            .Select(mapping => new
            { 
                story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
                mapping
            })
            .Select(x => new UserStoryViewModel
            {
                // use the projected properties from previous to map to your UserStoryViewModel aggregate
                ...
            });

        // calling .ToList()/.ToListAsync() will then execute the query and return the results
        return allUserStoriesForUser.ToListAsync();
    }
}

然后您可以创建一个类似的方法来仅获取当前用户的未标记为NotInterested 或WontRead 的Storys。

它几乎和以前一样,但在Where 中使用过滤器以确保您不会检索到NotInterested 或WontRead:

public Task<IEnumerable<UserStoryViewModel>> GetForUserThatMightRead(string currentUserId)
{
    var storiesUserMightRead = _dbContext.StoryUserMappings
        .Where(mapping => mapping.UserId == currentUserId && !mapping.NotInterested && !mapping.WontRead)
        .Select(mapping => new
        { 
            story = _dbContext.Stories.Single(story => story.StoryId == mapping.StoryId),
            mapping
        })
        .Select(x => new UserStoryViewModel
        {
            // use the projected properties from previous to map to your UserStoryViewModel aggregate
            ...
        });

    return storiesUserMightRead.ToListAsync();
}

那么您需要做的就是更新您的视图的@model 以使用您的新聚合UserStoryViewModel 而不是您的实体。

最好将“域”或数据库代码/实体与您的视图中将使用的内容保持良好的分离度。

我建议您仔细阅读此内容并继续练习,这样您就可以在前进的过程中养成正确的习惯和思考方式。


注意:

虽然上面的建议应该可以正常工作(我没有在本地测试过,所以你可能需要即兴创作/修复,但你得到了一般的要点)——我还推荐了一些其他的东西来补充上面的方法.

我会考虑在 UserStoryMapping 实体上引入导航属性(除非您已经拥有它;无法从您的问题代码中分辨出来)。这将消除上面我们将.Selecting 到匿名对象并添加到查询以通过映射的StoryId 从数据库中获取Storys 的步骤。您只需将其作为子导航属性即可引用属于该映射的故事。

然后,您还应该能够查看某种映射库,而不是为每次调用自己映射每个单独的属性。像AutoMapper 这样的东西可以解决问题(我确信其他映射器可用)。您可以设置映射来完成数据库实体和视图模型之间的所有繁重工作。有一个漂亮的 .ProjectTo&lt;T&gt;() 会使用您指定的映射将您的查询结果投影到所需的类型。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-14
    相关资源
    最近更新 更多