Kann ich ein angehängtes Objekt mit einem getrennten, aber gleichwertigen Objekt aktualisieren?

10

Ich rufe Filmdaten von einer externen API ab. In einer ersten Phase werde ich jeden Film kratzen und in meine eigene Datenbank einfügen. In einer zweiten Phase werde ich meine Datenbank regelmäßig aktualisieren, indem ich die API "Änderungen" der API verwende, die ich abfragen kann, um festzustellen, bei welchen Filmen die Informationen geändert wurden.

Meine ORM-Schicht ist Entity-Framework. Die Movie-Klasse sieht folgendermaßen aus:

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

Das Problem tritt auf, wenn ich einen Film habe, der aktualisiert werden muss: In meiner Datenbank wird das verfolgte Objekt und das neue, das ich vom Update-API-Aufruf erhalte, als unterschiedliche Objekte betrachtet, unabhängig davon .Equals().

Dies verursacht ein Problem, da beim Versuch, die Datenbank mit dem aktualisierten Film zu aktualisieren, diese eingefügt wird, anstatt den vorhandenen Film zu aktualisieren.

Ich hatte dieses Problem zuvor mit den Sprachen und meine Lösung bestand darin, nach den angehängten Sprachobjekten zu suchen, sie vom Kontext zu trennen, ihre PK in das aktualisierte Objekt zu verschieben und diese an den Kontext anzuhängen. Wenn SaveChanges()es jetzt ausgeführt wird, wird es im Wesentlichen ersetzt.

Dies ist ein ziemlich stinkender Ansatz, denn wenn ich diesen Ansatz für mein MovieObjekt fortsetze, bedeutet dies, dass ich den Film, die Sprachen, die Genres und die Schlüsselwörter trennen, jeden in der Datenbank nachschlagen, ihre IDs übertragen und die einfügen muss neue Objekte.

Gibt es eine Möglichkeit, dies eleganter zu tun? Im Idealfall möchte ich nur den aktualisierten Film an den Kontext übergeben und den richtigen Film auswählen, der basierend auf der Equals()Methode aktualisiert werden soll, alle Felder aktualisieren und für jedes komplexe Objekt: Verwenden Sie den vorhandenen Datensatz erneut basierend auf seiner eigenen Equals()Methode und fügen Sie if ein es existiert noch nicht.

Ich kann das Trennen / Anhängen überspringen, indem ich .Update()Methoden für jedes komplexe Objekt bereitstelle , die ich in Kombination zum Abrufen aller angehängten Objekte verwenden kann. Dazu muss ich jedoch jedes einzelne vorhandene Objekt abrufen, um es dann zu aktualisieren.

Jeroen Vannevel
quelle
Warum können Sie die verfolgte Entität nicht einfach aus Ihren API-Daten aktualisieren und Änderungen speichern, ohne Entitäten zu ersetzen / zu trennen / abzugleichen / anzuhängen?
Si-N
@ Si-N: kannst du erweitern wie genau das dann gehen würde?
Jeroen Vannevel
Ok, jetzt haben Sie den letzten Absatz hinzugefügt. Es ist sinnvoller, das Abrufen der Entitäten zu vermeiden, bevor Sie sie aktualisieren. Es gibt nichts, was Ihre PK in Ihrer Filmklasse sein könnte? Wie passen Sie die Filme von der externen API an Ihre Entitäten an? Sie können ohnehin alle Entitäten, die Sie aktualisieren müssen, in einem Datenbankaufruf abrufen. Wäre dies nicht die einfachere Lösung, die keinen großen Leistungseinbruch verursachen sollte (es sei denn, Sie sprechen über eine große Anzahl von zu aktualisierenden Filmen)?
Si-N
Meine Filmklasse verfügt über eine PK, idund die Filme aus der externen API werden mithilfe des Felds mit den lokalen Filmen abgeglichen tmdbid. Ich kann nicht alle Entitäten abrufen, die in einem Aufruf aktualisiert werden müssen, da es sich um Filme, Genres, Sprachen, Schlüsselwörter usw. handelt. Jede dieser Entitäten hat eine PK und ist möglicherweise bereits in der Datenbank vorhanden.
Jeroen Vannevel

Antworten:

8

Ich habe nicht gefunden, was ich mir erhofft hatte, aber ich habe eine Verbesserung gegenüber der vorhandenen Sequenz zum Auswählen, Entfernen, Aktualisieren und Anhängen gefunden.

Mit der Erweiterungsmethode AddOrUpdate(this DbSet)können Sie genau das tun, was ich tun möchte: Einfügen, wenn es nicht vorhanden ist, und aktualisieren, wenn ein vorhandener Wert gefunden wurde. Ich habe es nicht früher bemerkt, da ich wirklich nur gesehen habe, dass es in der seed()Methode in Kombination mit Migrationen verwendet wird. Wenn es einen Grund gibt, warum ich dies nicht verwenden sollte, lassen Sie es mich wissen.

Beachten Sie Folgendes: Es ist eine Überlastung verfügbar, mit der Sie gezielt auswählen können, wie die Gleichheit bestimmt werden soll. Hier hätte ich meine verwenden können, TMDbIdaber ich habe mich stattdessen dafür entschieden, meine eigene ID einfach ganz zu ignorieren und stattdessen eine PK auf TMDbId in Kombination mit zu verwenden DatabaseGeneratedOption.None. Gegebenenfalls verwende ich diesen Ansatz auch für jede Untersammlung.

Interessanter Teil der Quelle :

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

Auf diese Weise werden die Daten tatsächlich unter der Haube aktualisiert.

Alles, was übrig bleibt, ist, AddOrUpdatejedes Objekt aufzurufen , von dem ich betroffen sein möchte:

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

Es ist nicht so sauber wie ich gehofft habe, da ich jedes Teil meines Objekts, das aktualisiert werden muss, manuell angeben muss, aber es ist ungefähr so ​​nah wie es nur geht.

Verwandte Lektüre: /programming/15336248/entity-framework-5-updating-a-record


Aktualisieren:

Es stellte sich heraus, dass meine Tests nicht streng genug waren. Nachdem ich diese Technik angewendet hatte, bemerkte ich, dass die neue Sprache zwar hinzugefügt wurde, aber nicht mit dem Film verbunden war. in der Viele-zu-Viele-Tabelle. Dies ist ein bekanntes Problem mit scheinbar niedriger Priorität, das meines Wissens nicht behoben wurde.

Am Ende habe ich mich für den Ansatz entschieden, bei dem ich Update(T)Methoden für jeden Typ habe und diese Abfolge von Ereignissen befolge:

  • Durchlaufen Sie Sammlungen in neuem Objekt
  • Suchen Sie für jeden Eintrag in jeder Sammlung in der Datenbank nach
  • Wenn es existiert, verwenden Sie die Update()Methode, um es mit den neuen Werten zu aktualisieren
  • Wenn es nicht vorhanden ist, fügen Sie es dem entsprechenden DbSet hinzu
  • Geben Sie die angehängten Objekte zurück und ersetzen Sie die Sammlungen im Stammobjekt durch die Sammlungen der angehängten Objekte
  • Suchen und aktualisieren Sie das Stammobjekt

Es ist viel manuelle Arbeit und es ist hässlich, so dass es einige weitere Umgestaltungen durchlaufen wird, aber jetzt zeigen meine Tests, dass es für strengere Szenarien funktionieren sollte.


Nachdem ich es weiter aufgeräumt habe, verwende ich jetzt diese Methode:

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

Dadurch kann ich es so aufrufen und die zugrunde liegenden Sammlungen einfügen / aktualisieren:

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

Beachten Sie, wie ich den abgerufenen Wert dem ursprünglichen Stammobjekt neu zuordne: Jetzt ist er mit jedem angehängten Objekt verbunden. Das Aktualisieren des Stammobjekts (des Films) erfolgt auf folgende Weise:

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
quelle
Wie gehen Sie mit Löschungen in der 1-M-Beziehung um? Beispielsweise kann 1-Movie mehrere Sprachen haben. Wenn eine der Sprachen entfernt wird, entfernt Ihr Code sie dann? Es scheint, dass Ihre Lösung nur Einfügungen und / oder Aktualisierungen (aber nicht entfernen?)
joedotnot
0

Da Sie sich mit verschiedenen Feldern befassen idund tmbid, schlage ich vor, die API zu aktualisieren, um einen einzigen und separaten Index aller Informationen wie Genres, Sprachen, Schlüsselwörter usw. zu erstellen. Rufen Sie dann auf, um Informationen zu indizieren und zu suchen, anstatt sie zu sammeln die gesamten Informationen zu einem bestimmten Objekt in Ihrer Movie-Klasse.

Snazzy Sanoj
quelle
1
Ich folge hier nicht dem Gedankengang. Können Sie erweitern? Beachten Sie, dass die externe API völlig außerhalb meiner Kontrolle liegt.
Jeroen Vannevel