将属性从映射 table 合并到单个 class

Merge properties from mapping table to single class

我有一个使用 EF Core 3.1 访问其数据的网站。它使用的主要 table 是 [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 对象上使用非映射 属性 访问器,如果 StoryUserMapping 对象存在,则读取该对象。这通常感觉 EF 可能处理得更优雅。

用例

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

用例 1:我想查看我(已登录用户)标记为 "to read" 且字数超过 100,000 的所有故事

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

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

自己创建一个聚合视图模型对象,您可以使用该对象在视图中显示数据,类似于您目前在 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; }
    ...
}

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

您的数据库实体模型应尽可能接近 "dumb" 对象(导航属性除外)- 它们看起来非常明智。

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

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

这是为当前用户获取所有 Story 的用例示例:

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();
    }
}

然后您可以创建一个类似的方法来仅获取当前用户未标记为 NotInterestedWontRead.

Story

它实际上与以前相同,但在 Where 中使用过滤器以确保您不会检索 NotInterestedWontRead:

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 而不是您的实体。

将 "domain" 或数据库 code/entities 与您将在视图中使用的内容保持良好的分离度始终是一种很好的做法。

我建议您仔细阅读并继续练习,这样您就可以在前进的过程中养成正确的习惯和想法。


注意:

虽然上面的建议应该绝对有效(我没有在本地测试过,所以你可能需要 improvise/fix,但你得到了一般的要点) - 我还推荐了一些其他的东西补充上面的方法。

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

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