Czy mogę zaktualizować dołączony obiekt za pomocą odłączonego, ale równego obiektu?

10

Pobieram dane filmu z zewnętrznego interfejsu API. W pierwszej fazie zeskrobuję każdy film i wstawię go do własnej bazy danych. W drugiej fazie będę okresowo aktualizować moją bazę danych za pomocą interfejsu API „Zmiany” interfejsu API, który mogę zapytać, aby zobaczyć, które filmy zostały zmienione.

Moja warstwa ORM to Entity-Framework. Klasa Movie wygląda następująco:

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

Problem pojawia się, gdy mam film, który wymaga aktualizacji: moja baza danych będzie myślała o śledzonym obiekcie, a nowy, który otrzymam z wywołania API aktualizacji, to różne obiekty, niezależnie od tego .Equals().

Powoduje to problem, ponieważ gdy teraz spróbuję zaktualizować bazę danych o zaktualizowany film, wstawi go zamiast aktualizować istniejący film.

Miałem już ten problem z językami i moim rozwiązaniem było wyszukiwanie załączonych obiektów językowych, odłączenie ich od kontekstu, przeniesienie ich PK do zaktualizowanego obiektu i dołączenie go do kontekstu. Kiedy SaveChanges()jest teraz wykonywany, zasadniczo go zastąpi.

Jest to dość śmierdzące podejście, ponieważ jeśli będę kontynuować to podejście do mojego Movieobiektu, oznacza to, że będę musiał odłączyć film, języki, gatunki i słowa kluczowe, wyszukać każdy z nich w bazie danych, przenieść ich identyfikatory i wstawić nowe obiekty.

Czy można to zrobić bardziej elegancko? Idealnie chcę po prostu przekazać zaktualizowany film do kontekstu i wybrać ten właściwy film do aktualizacji na podstawie Equals()metody, zaktualizować wszystkie jego pola i dla każdego złożonego obiektu: ponownie użyć istniejącego rekordu na podstawie własnej Equals()metody i wstawić, jeśli jeszcze nie istnieje.

Mogę pominąć odłączanie / dołączanie, podając .Update()metody dla każdego złożonego obiektu, których mogę użyć w połączeniu z odzyskiwaniem wszystkich dołączonych obiektów, ale nadal będzie to wymagało ode mnie pobrania każdego istniejącego obiektu w celu jego aktualizacji.

Jeroen Vannevel
źródło
Dlaczego nie możesz po prostu zaktualizować śledzonego obiektu na podstawie danych API i zapisać zmian bez zastępowania / odłączania / dopasowywania / dołączania jednostek?
Si-N
@ Si-N: czy możesz rozwinąć, jak dokładnie by to poszło?
Jeroen Vannevel
Ok, teraz dodałeś ten ostatni akapit, który ma większy sens, starasz się unikać pobierania encji przed ich aktualizacją? Nie ma nic, co mogłoby być twoim PK na zajęciach z filmu? Jak dopasowujesz filmy z zewnętrznego interfejsu API do swoich bytów? W każdym razie możesz pobrać wszystkie elementy, które musisz zaktualizować w jednym wywołaniu db, czy nie byłoby to prostsze rozwiązanie, które nie powinno powodować dużego spadku wydajności (chyba że mówisz o dużej liczbie filmów do zaktualizowania)?
Si-N
Moja klasa filmów ma PK, ida filmy z zewnętrznego interfejsu API są dopasowywane do lokalnych za pomocą pola tmdbid. Nie mogę pobrać wszystkich elementów, które wymagają aktualizacji w jednym połączeniu, ponieważ dotyczą one filmów, gatunków, języków, słów kluczowych itp. Każdy z nich ma PK i może już istnieć w bazie danych.
Jeroen Vannevel,

Odpowiedzi:

8

Nie znalazłem tego, na co liczyłem, ale znalazłem poprawę w stosunku do istniejącej sekwencji select-detach-update-attach.

Metoda rozszerzenia AddOrUpdate(this DbSet)pozwala robić dokładnie to, co chcę: wstawić, jeśli go nie ma, i zaktualizować, jeśli znalazła istniejącą wartość. Nie zdawałem sobie sprawy z używania tego wcześniej, ponieważ tak naprawdę widziałem, że można go stosować tylko w seed()metodzie w połączeniu z Migracjami. Jeśli jest jakiś powód, dla którego nie powinienem tego używać, daj mi znać.

Coś użytecznego do odnotowania: Dostępne jest przeciążenie, które pozwala dokładnie wybrać sposób ustalenia równości. Tutaj mogłem użyć mojego, TMDbIdale zamiast tego zdecydowałem się po prostu całkowicie zignorować mój własny identyfikator i zamiast tego użyć PK na TMDbId w połączeniu z DatabaseGeneratedOption.None. W stosownych przypadkach stosuję to podejście do każdej podkolekcji.

Ciekawa część źródła :

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

tak właśnie dane są aktualizowane pod maską.

Pozostaje tylko wywoływanie AddOrUpdatekażdego obiektu, na który chcę mieć wpływ:

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

Nie jest tak czysty, jak się spodziewałem, ponieważ muszę ręcznie określić każdy element mojego obiektu, który należy zaktualizować, ale jest tak blisko, jak to możliwe.

Powiązana lektura: /programming/15336248/entity-framework-5-updating-a-record


Aktualizacja:

Okazuje się, że moje testy nie były wystarczająco rygorystyczne. Po użyciu tej techniki zauważyłem, że podczas dodawania nowego języka nie był on połączony z filmem. w tabeli wiele do wielu. Jest to znany, ale pozornie niski priorytet, o ile mi wiadomo.

W końcu zdecydowałem się na podejście, w którym mam Update(T)metody dla każdego typu i śledzę następującą sekwencję zdarzeń:

  • Pętla nad kolekcjami w nowym obiekcie
  • Dla każdego wpisu w każdej kolekcji wyszukaj go w bazie danych
  • Jeśli istnieje, użyj Update()metody, aby zaktualizować go o nowe wartości
  • Jeśli nie istnieje, dodaj go do odpowiedniego DbSet
  • Zwróć dołączone obiekty i zastąp kolekcje w obiekcie głównym kolekcjami dołączonych obiektów
  • Znajdź i zaktualizuj obiekt główny

To dużo pracy ręcznej i jest brzydkie, więc przejdzie jeszcze kilka refaktoryzacji, ale teraz moje testy wskazują, że powinno działać w bardziej rygorystycznych scenariuszach.


Po dalszym oczyszczeniu używam teraz tej metody:

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();
}

To pozwala mi tak to nazwać i wstawić / zaktualizować podstawowe kolekcje:

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

Zauważ, jak ponownie przypisuję odzyskaną wartość do oryginalnego obiektu głównego: teraz jest on połączony z każdym dołączonym obiektem. Aktualizacja obiektu głównego (filmu) odbywa się w ten sam sposób:

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}
Jeroen Vannevel
źródło
Jak sobie radzisz z usuwaniem w relacji 1-M? na przykład 1-Movie może mieć wiele języków; jeśli jeden z języków zostanie usunięty, czy kod go usuwa? Wygląda na to, że Twoje rozwiązanie wprowadza i aktualizuje (ale nie usuwa?)
joedotnot 14.09.17
0

Ponieważ mamy do czynienia z różnych dziedzin idi tmbiduważam, że aktualizowanie API do jednolitego i oddzielny indeks wszystkich informacji jak gatunków, języków, słowa kluczowe itp ... A potem zatelefonować do indeksu i sprawdzić, zamiast gromadzenia informacji wszystkie informacje o konkretnym obiekcie w klasie filmu.

Snazzy Sanoj
źródło
1
Nie podążam tutaj tokiem myśli. Czy potrafisz rozwinąć? Pamiętaj, że zewnętrzny interfejs API jest całkowicie poza moją kontrolą.
Jeroen Vannevel