Чи можу я оновити доданий об’єкт за допомогою відокремленого, але рівного об'єкта?


10

Я отримую дані про фільми із зовнішнього API. На першому етапі я скреблю кожен фільм і вставляю його у власну базу даних. На другому етапі я періодично оновлюватиму свою базу даних, використовуючи API "Зміни" API, який я можу запитати, щоб побачити, які фільми змінили свою інформацію.

Мій рівень ORM - це Entity-Framework. Клас фільму виглядає приблизно так:

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().

Це спричиняє проблеми, оскільки коли я зараз намагаюся оновити базу даних оновленим фільмом, вона вставить її замість оновлення існуючого фільму.

У мене виникло це питання раніше з мовами, і моє рішення полягало в пошуку приєднаних мовних об’єктів, від'єднанні їх від контексту, переміщенні їх ПК до оновленого об'єкта та приєднанні його до контексту. Коли SaveChanges()він зараз виконується, він по суті замінить його.

Це досить смердючий підхід, оскільки якщо я продовжую цей підхід до свого Movieоб'єкта, це означає, що мені доведеться відірвати фільм, мови, жанри та ключові слова, шукати кожного в базі даних, переносити їхні ідентифікатори та вставляти нові об’єкти.

Чи є спосіб зробити це більш елегантно? В ідеалі я просто хочу передати оновлений фільм до контексту, який повинен вибрати правильний фільм для оновлення на основі Equals()методу, оновити всі його поля та для кожного складного об'єкта: знову використати існуючий запис за власним Equals()методом та вставити, якщо вона ще не існує.

Я можу пропустити від'єднання / приєднання, надаючи .Update()методи для кожного складного об'єкта, який я можу використовувати в поєднанні з отриманням усіх приєднаних об'єктів, але це все одно вимагатиме від мене кожного кожного існуючого об'єкта, а потім оновити його.


Чому ви не можете просто оновити відстежувану сутність зі своїх даних API та зберегти зміни, не замінюючи / від'єднуючи / співставляючи / приєднуючи об'єкти?
Si-N

@ Si-N: чи можете ви розширити, як саме це піде тоді?
Jeroen Vannevel

Ок, тепер ви додали, що останній абзац має більше сенсу, ви намагаєтесь уникати пошуку об'єктів перед їх оновленням? Немає нічого, що могло б стати вашим ПК у вашому класі фільму? Як ви співставляєте фільми із зовнішніх програм до ваших сутностей? Ви можете все-таки отримати всі об'єкти, необхідні для оновлення за один db-виклик, чи не буде це простішим рішенням, яке не повинно спричинити велику ефективність (якщо ви не говорите про велику кількість фільмів для оновлення)?
Si-N

У моєму класі фільму є ПК, idа фільми із зовнішнього API узгоджуються з локальними, використовуючи поле tmdbid. Я не можу отримати всі об'єкти, які потрібно оновити за один дзвінок, оскільки мова йде про фільми, жанри, мови, ключові слова тощо. Кожен з них має ПК та може вже існувати в базі даних.
Jeroen Vannevel

Відповіді:


8

Я не знайшов те, на що сподівався, але я знайшов поліпшення існуючої послідовності select-detach-update-update-attach.

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

Щось корисне для зауваження: доступна перевантаження, яка дозволяє спеціально вибрати спосіб визначення рівності. Тут я міг би використовувати свій, TMDbIdале натомість я просто вирішив просто нехтувати власним ідентифікатором і замість цього використовувати ПК в 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-Movie може мати кілька мов; якщо одну з мов видалено, чи видаляється ваш код? Здається, ваше рішення лише вставляє та / або оновлює (але не видаляє?)
joedotnot

0

Оскільки ви маєте справу з різними полями, idі tmbidя пропоную оновити API, щоб зробити єдиний та окремий індекс усієї інформації, наприклад, жанри, мови, ключові слова тощо. всю інформацію про конкретний об'єкт у вашому класі фільму.


1
Я не слідкую за думкою думки. Чи можете ви розширити? Зауважте, що зовнішній API повністю перебуває під моїм контролем.
Jeroen Vannevel
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.