Лучший способ загрузить свойства навигации в новом объекте

1

Я пытаюсь добавить новую запись в базу данных SQL с помощью EF. Код выглядит так:

    public void Add(QueueItem queueItem)
    {
        var entity = queueItem.ApiEntity;            


        var statistic = new Statistic
        {
            Ip = entity.Ip,
            Process = entity.ProcessId,
            ApiId = entity.ApiId,
            Result = entity.Result,
            Error = entity.Error,
            Source = entity.Source,
            DateStamp = DateTime.UtcNow,
            UserId = int.Parse(entity.ApiKey),
        };

        _statisticRepository.Add(statistic);
        unitOfWork.Commit();

    }    

В объекте Statistic есть свойства навигации Api и User которые я хочу загрузить в новый объект Statistic. Я попытался загрузить свойства навигации с использованием кода ниже, но он создает большие запросы и снижает производительность. Любое предложение о том, как загрузить свойства навигации другим способом?

    public Statistic Add(Statistic statistic)
    {
        _context.Statistic.Include(p => p.Api).Load();
        _context.Statistic.Include(w => w.User).Load();
        _context.Statistic.Add(statistic);
        return statistic;
    }

У некоторых из вас может возникнуть вопрос, почему я хочу загрузить свойства навигации при добавлении нового объекта, потому что я выполняю некоторые вычисления в DbContext.SaveChanges() перед перемещением объекта в базу данных. Код выглядит так:

public override int SaveChanges()
        {

            var addedStatistics = ChangeTracker.Entries<Statistic>().Where(e => e.State == EntityState.Added).ToList().Select(p => p.Entity).ToList();

            var userCreditsGroup = addedStatistics
                .Where(w => w.User != null)
                .GroupBy(g =>  g.User )
                .Select(s => new
                {
                    User = s.Key,
                    Count = s.Sum(p=>p.Api.CreditCost)
                })
                .ToList();      

//Skip code

}

Таким образом, Linq выше не будет работать без загрузки свойств навигации, поскольку он их использует.

Я также добавляю статистический объект для полного просмотра

  public class Statistic : Entity
    {
        public Statistic()
        {
            DateStamp = DateTime.UtcNow;

        }

        public int Id { get; set; }
        public string Process { get; set; }
        public bool Result { get; set; }
        [Required]
        public DateTime DateStamp { get; set; }

        [MaxLength(39)]
        public string Ip { get; set; }        
        [MaxLength(2083)]
        public string Source { get; set; }
        [MaxLength(250)]
        public string Error { get; set; }
        public int UserId { get; set; }
        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int ApiId { get; set; }
        [ForeignKey("ApiId")]
        public virtual Api Api { get; set; }

    }
Теги:
linq
entity-framework
entity-framework-4

3 ответа

2
Лучший ответ

Как вы говорите, следующие операции против вашего контекста будут генерировать большие запросы:

_context.Statistic.Include(p => p.Api).Load();
_context.Statistic.Include(w => w.User).Load();

Они материализуют графы объектов для всех статистических данных и связанных с ними объектов api, а затем все статистические данные и ассоциированные пользователи в контексте статистики

Просто заменив это одним вызовом следующим образом, это уменьшит это до одной поездки в оба конца:

_context.Statistic.Include(p => p.Api).Include(w => w.User).Load();

Как только они будут загружены, трекер отслеживания фреймворка сущности будет фиксировать отношения для новых объектов статистики и, следовательно, заполнять свойства навигации для api и пользователя для всех новых статистических данных за один раз.

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

Однако, глядя на метод SaveChanges, похоже, что исправление отношения происходит один раз за новую статистику. Т.е. каждый раз, когда добавляется новая статистика, вы запрашиваете базу данных для всех статистических данных и связанных с ними атрибутов api и user, чтобы инициировать фиксацию отношений для новой статистики.

В этом случае я был бы более склонен к следующему:

_context.Statistics.Add(statistic);
_context.Entry(statistic).Reference(s => s.Api).Load();
_context.Entry(statistic).Reference(s => s.User).Load();

Это будет запрашивать только Api и User для новой статистики, а не для всех статистических данных. Т.е. вы будете генерировать 2 запроса на одну строку базы данных для каждой новой статистики.

Кроме того, если вы добавляете большое количество статистических данных в одну партию, вы можете использовать локальный кеш в контексте, предварительно загружая всех пользователей и объекты api. Т.е. занять предел для предварительного кэширования всех объектов user и api в виде 2 больших запросов.

// preload all api and user entities
_context.Apis.Load();
_context.Users.Load();

// batch add new statistics
foreach(new statistic in statisticsToAdd)
{
    statistic.User = _context.Users.Local.Single(x => x.Id == statistic.UserId);
    statistic.Api = _context.Api.Local.Single(x => x.Id == statistic.ApiId);
    _context.Statistics.Add(statistic);
}

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

_context.ChangeTracker.DetectChanges();

Отказ от ответственности: весь код вводится непосредственно в браузер, поэтому остерегайтесь опечаток.

  • 0
    Я пытался использовать _context.Entry (статистика) .Reference (s => s.User) .Load (); Метод и получили исключение при отправке объекта в базу данных: System.InvalidOperationException: Элемент «Load» не может быть вызван для свойства «Пользователь», так как объект типа «Статистика» не существует в контексте. Чтобы добавить объект в контекст, вызовите метод Add или Attach из DbSet <Statistic>.
  • 0
    Хорошая точка зрения! Я обновлю код соответственно. Если у меня будет шанс, я соберу тестовый проект, чтобы проверить его - это все из памяти, я боюсь.
0

Как насчет поиска и загрузки только необходимых столбцов.

private readonly Dictionary<int, UserKeyType> _userKeyLookup = new Dictionary<int, UserKeyType>();

Я не уверен, как вы создаете репозиторий, вам может понадобиться очистить поиск после завершения изменений сохранения или в начале транзакции.

_userKeyLookup.Clean();

Сначала найдите в поиске, если не нашли, то загрузите из контекста.

public Statistic Add(Statistic statistic)
{
    // _context.Statistic.Include(w => w.User).Load();
    UserKeyType key;
    if (_userKeyLookup.Contains(statistic.UserId))
    {
        key = _userKeyLookup[statistic.UserId];
    }
    else
    {
        key = _context.Users.Where(u => u.Id == statistic.UserId).Select(u => u.Key).FirstOrDefault();
        _userKeyLookup.Add(statistic.UserId, key);
    }

    statistic.User = new User { Id = statistic.UserId, Key = key };

    // similar code for api..
    // _context.Statistic.Include(p => p.Api).Load();

    _context.Statistic.Add(statistic);
    return statistic;
}

Затем немного измените группировку.

var userCreditsGroup = addedStatistics
    .Where(w => w.User != null)
    .GroupBy(g => g.User.Id)
    .Select(s => new
    {
        User = s.Value.First().User,
        Count = s.Sum(p=>p.Api.CreditCost)
    })
    .ToList();
0

Извините, у меня нет времени проверить это, но EF отображает объекты на объекты. Поэтому не следует просто назначать работу объекта:

public void Add(QueueItem queueItem)
{
    var entity = queueItem.ApiEntity;            


    var statistic = new Statistic
    {
        Ip = entity.Ip,
        Process = entity.ProcessId,
        //ApiId = entity.ApiId,
        Api = _context.Apis.Single(a => a.Id == entity.ApiId),
        Result = entity.Result,
        Error = entity.Error,
        Source = entity.Source,
        DateStamp = DateTime.UtcNow,
        //UserId = int.Parse(entity.ApiKey),
        User = _context.Users.Single(u => u.Id == int.Parse(entity.ApiKey)
    };

    _statisticRepository.Add(statistic);
    unitOfWork.Commit();

}    

Я немного угадал ваши имена, вы должны настроить его перед тестированием

Ещё вопросы

Сообщество Overcoder
Наверх
Меню