Объединить свойства из таблицы сопоставления в один класс - PullRequest
1 голос
/ 16 апреля 2020

У меня есть веб-сайт, который использует 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, которые считывают объект StoryUserMapping, если он существует. Обычно это похоже на то, что EF, вероятно, обрабатывает более элегантно.

Варианты использования

Настройка: у меня 1 миллион историй, 1000 пользователей, сценарий наихудшего случая У меня есть StoryUserMapping для каждого: 1 миллиард записей.

Вариант использования 1: я хочу видеть все истории, которые я (вошедший в систему) пометил как «читать» с более чем 100 000 слов

Вариант использования 2: я хочу видеть все истории, в которых я НЕ пометил их NotInterested или WontRead

Меня не интересует запрос нескольких StoryUserMappings для одной истории, например, я не буду задавать вопрос: какие истории были отмечены как прочитано более чем n пользователей. Я бы предпочел не ограничивать это, если это изменится в будущем, но если мне нужно, это будет хорошо.

1 Ответ

2 голосов
/ 17 апреля 2020

Создайте себе объект модели агрегированного представления, который вы можете использовать для отображения данных в вашем представлении, аналогично тому, что вы получили в сущности 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; }
    ...
}

Эта модель представления касается только агрегирования данных для отображения в представлении. Таким образом, вам не нужно искажать существующие сущности, чтобы они соответствовали тому, как вы будете отображать данные в другом месте.

Модели сущностей вашей базы данных должны быть как можно ближе к "немым" объектам (кроме свойств навигации) - они выглядят очень разумно, поскольку они являются моментом.

В этом случае удалите ненужные [NotMapped] свойства из вашего существующего Story, который вы добавили ранее.

В вашем контроллере / сервис, вы можете запросить данные в соответствии с вашими случаями использования, которые вы упомянули. Получив результаты запроса, вы можете затем отобразить ваши результаты в вашу модель агрегированного представления для использования в представлении.

Вот пример для варианта использования получения всех 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();
    }
}

Затем можно создать аналогичный метод, чтобы получать только Story текущего пользователя, которые не отмечены NotInterested или WontRead .

Это практически то же самое, что и раньше, но с фильтром в 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 вместо вашей сущности.

Всегда хорошая практика поддерживать хороший уровень разделения между тем, что есть " домен "или код / ​​сущность базы данных из того, что будет использоваться по вашему мнению.

Я бы порекомендовал внимательно прочитать это и продолжать практиковаться, чтобы вы могли выработать правильные привычки и думать, как вы go вперед.


ПРИМЕЧАНИЕ:

Пока st вышеупомянутые предложения должны работать абсолютно нормально (я не проверял локально, поэтому вам, возможно, придется импровизировать / исправить, но вы получите общий смысл) - я бы также рекомендовал несколько других вещей, чтобы дополнить подход выше.

Я хотел бы взглянуть на введение свойства навигации для объекта UserStoryMapping (если у вас его уже нет; не могу сказать из кода вашего вопроса). Это исключит шаг сверху, когда мы .Select входим в анонимный объект и добавляем в запрос, чтобы получить Story s из базы данных, с помощью отображения StoryId. Вы сможете ссылаться на истории, относящиеся к отображению, просто являясь дочерним навигационным свойством.

Тогда вы также сможете просматривать какую-то библиотеку отображений, а не отображать каждое отдельное свойство себя за каждый звонок. Нечто подобное AutoMapper сделает свое дело (я уверен, что доступны другие картографы). Вы можете настроить сопоставления, чтобы выполнять всю тяжелую работу между объектами базы данных и просматривать модели. Есть отличная .ProjectTo<T>(), которая будет проецировать ваши запрашиваемые результаты на желаемый тип, используя те сопоставления, которые вы указали.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...