Могу ли я обновить прикрепленный объект, используя отдельный, но равный объект?


10

Я получаю данные фильма из внешнего API. На первом этапе я буду чистить каждый фильм и вставлять его в свою базу данных. На втором этапе я буду периодически обновлять свою базу данных, используя API «Изменения» API, который я могу запросить, чтобы увидеть, какие фильмы изменили свою информацию.

Мой слой ORM - это Entity-Framework. Класс Movie выглядит следующим образом:

class Movie
{
    public virtual ICollection<Language> SpokenLanguages { get; set; }
    public virtual ICollection<Genre> Genres { get; set; }
    public virtual ICollection<Keyword> Keywords { get; set; }
}

Проблема возникает, когда у меня есть фильм, который необходимо обновить: моя база данных будет думать об отслеживаемом объекте и новом, который я получаю из вызова API обновления, как разные объекты, игнорируя .Equals().

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

Раньше у меня была эта проблема с языками, и мое решение состояло в том, чтобы искать присоединенные языковые объекты, отсоединять их от контекста, перемещать их PK в обновленный объект и прикреплять их к контексту. Когда SaveChanges()теперь выполняется, это по существу заменит его.

Это довольно вонючий подход, потому что, если я продолжу этот подход к своему Movieобъекту, это означает, что мне придется отделить фильм, языки, жанры и ключевые слова, найти каждый из них в базе данных, перенести их идентификаторы и вставить новые объекты.

Есть ли способ сделать это более элегантно? В идеале я просто хочу передать обновленный фильм в контекст, и он должен выбрать правильный фильм для обновления на основе Equals()метода, обновить все его поля и для каждого сложного объекта: снова использовать существующую запись на основе своего собственного Equals()метода и вставить, если это еще не существует.

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


Почему вы не можете просто обновить отслеживаемую сущность из данных API и сохранить изменения без замены / отсоединения / сопоставления / присоединения сущностей?
— Si-N

@ Si-N: вы можете расширить, как именно это пойдет тогда?
— Йерун Ванневел

Хорошо, теперь вы добавили, что последний абзац имеет больше смысла, вы пытаетесь избежать извлечения сущностей перед их обновлением? Нет ничего, что могло бы быть вашим ПК в вашем классе Кино? Как вы соотносите фильмы из внешнего интерфейса API с вашими сущностями? В любом случае, вы можете извлечь все сущности, которые нужно обновить, за один вызов БД. Разве это не будет более простым решением, которое не должно привести к значительному снижению производительности (если вы не говорите об огромном количестве фильмов для обновления)?
— Si-N

У моего класса Movie есть PK, idи фильмы из внешнего API сопоставляются с локальными с помощью поля tmdbid. Я не могу получить все объекты, которые необходимо обновить за один вызов, потому что они касаются фильмов, жанров, языков, ключевых слов и т. Д. Каждый из них имеет PK и может уже существовать в базе данных.
— Йерун Ванневел

Ответы:


8

Я не нашел того, на что надеялся, но нашел улучшение по сравнению с существующей последовательностью select-detach-update-attach.

Метод расширения AddOrUpdate(this DbSet)позволяет вам делать именно то, что я хочу: вставить, если его там нет, и обновить, если он нашел существующее значение. Я не осознавал, что использую это раньше, так как на самом деле видел только то, как оно используется в seed()методе в сочетании с миграциями. Если есть какая-то причина, по которой я не должен использовать это, дайте мне знать.

Что-то полезное для заметки: существует перегрузка, которая позволяет вам конкретно выбирать, как должно быть определено равенство. Здесь я мог бы использовать свой, TMDbIdно вместо этого я решил полностью игнорировать свой собственный идентификатор и вместо этого использовать PK в TMDbId в сочетании с DatabaseGeneratedOption.None. Я использую этот подход также в каждой подгруппе, где это уместно.

Интересная часть источника :

internalSet.InternalContext.Owner.Entry(existing).CurrentValues.SetValues(entity);

именно так данные на самом деле обновляются под капотом.

Все, что осталось, - это вызывать AddOrUpdateкаждый объект, на который я хочу повлиять:

public void InsertOrUpdate(Movie movie)
{
    _context.Movies.AddOrUpdate(movie);
    _context.Languages.AddOrUpdate(movie.SpokenLanguages.ToArray());
    // Other objects/collections
    _context.SaveChanges();
}

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

Связанное чтение: /programming/15336248/entity-framework-5-updating-a-record


Обновить:

Оказывается, мои тесты были недостаточно строгими. После использования этой техники я заметил, что хотя новый язык был добавлен, он не был связан с фильмом. в таблице «многие ко многим». Это известная, но, казалось бы, проблема с низким приоритетом, и, насколько я знаю, ее не удалось устранить.

В конце концов я решил пойти на подход, где у меня есть Update(T)методы для каждого типа и следовать этой последовательности событий:

  • Цикл над коллекциями в новом объекте
  • Для каждой записи в каждой коллекции найдите ее в базе данных
  • Если он существует, используйте Update()метод, чтобы обновить его новыми значениями
  • Если он не существует, добавьте его в соответствующий DbSet
  • Вернуть прикрепленные объекты и заменить коллекции в корневом объекте коллекциями прикрепленных объектов.
  • Найти и обновить корневой объект

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


После очистки я теперь использую этот метод:

private IEnumerable<T> InsertOrUpdate<T, TKey>(IEnumerable<T> entities, Func<T, TKey> idExpression) where T : class
{
    foreach (var entity in entities)
    {
        var existingEntity = _context.Set<T>().Find(idExpression(entity));
        if (existingEntity != null)
        {
            _context.Entry(existingEntity).CurrentValues.SetValues(entity);
            yield return existingEntity;
        }
        else
        {
            _context.Set<T>().Add(entity);
            yield return entity;
        }
    }
    _context.SaveChanges();
}

Это позволяет мне называть это так и вставлять / обновлять базовые коллекции:

movie.Genres = new List<Genre>(InsertOrUpdate(movie.Genres, x => x.TmdbId));

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

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}

Как вы относитесь к удалению в отношениях 1-М? например, 1-фильм может иметь несколько языков; если один из языков удален, ваш код удаляет его? Похоже, ваше решение только вставляет и / или обновляет (но не удаляет?)
— joedotnot

0

Так как вы имеете дело с различными полями, idи tmbidя предлагаю обновить API, чтобы создать единый и отдельный индекс всей информации, такой как жанры, языки, ключевые слова и т. Д., А затем сделать запрос индекса и проверить информацию, а не собирать вся информация о конкретном объекте в вашем классе Movie.


1
Я не слежу за ходом мыслей здесь. Вы можете расширить? Обратите внимание, что внешний API полностью вне моего контроля.
— Йерун Ванневел
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.