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

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.

Snazzy Sanoj
источник
1
Я не слежу за ходом мыслей здесь. Вы можете расширить? Обратите внимание, что внешний API полностью вне моего контроля.
Йерун Ванневел