Entités déconnectées

Une instance de DbContext suit automatiquement les entités retournées à partir de la base de données. Les modifications apportées à ces entités seront ensuite détectées lorsque SaveChanges est appelé, et la base de données sera mise à jour en fonction des besoins. Consultez Enregistrement de base et Données associées pour plus d’informations.

Toutefois, parfois, les entités sont interrogées par une instance de contexte, puis enregistrées à l’aide d’une autre instance. Cela se produit souvent dans les scénarios « déconnectés », par exemple une application web où les entités sont interrogées, envoyées au client, modifiées, envoyées sur le serveur dans une demande et puis enregistrées. Dans ce cas, la deuxième instance de contexte doit savoir si les entités sont nouvelles (doivent être insérées) ou existantes (doivent être mises à jour).

Conseil

Vous pouvez afficher cet exemple sur GitHub.

Conseil

EF Core peut suivre une seule instance d’une entité avec une valeur de clé primaire donnée. La meilleure façon d’éviter ce problème consiste à utiliser un contexte de courte durée de vie pour chaque unité de travail, de sorte que le contexte commence vide, ait des entités associées, enregistre ces entités, puis soit supprimé.

Identification des nouvelles entités

Le client identifie de nouvelles entités

Le cas le plus simple à gérer est lorsque le client indique au serveur si l’entité est nouvelle ou existante. Par exemple, la requête d’insertion d’une nouvelle entité diffère souvent de la requête pour mettre à jour une entité existante.

Le reste de cette section couvre les cas où il est nécessaire de déterminer, d’une autre manière, s’il faut insérer ou mettre à jour.

Avec des clés générées automatiquement

La valeur d’une clé générée automatiquement peut souvent être utilisée pour déterminer si une entité doit être insérée ou mise à jour. Si la clé n’a pas été définie (autrement dit si elle a toujours la valeur CLR par défaut de null, zéro, etc.), alors l’entité doit être nouvelle et a besoin d’être insérée. En revanche, si la valeur de clé a été définie, l’entité doit avoir déjà été précédemment enregistrée et doit être mise à jour. En d’autres termes, si la clé a une valeur, cette entité a été interrogée, envoyée au client et est maintenant revenue pour être mise à jour.

Il est facile de rechercher une clé non définie lorsque le type d’entité est connu :

public static bool IsItNew(Blog blog)
    => blog.BlogId == 0;

Toutefois, EF a également un moyen intégré de faire cela pour n’importe quel type d’entité et type de clé :

public static bool IsItNew(DbContext context, object entity)
    => !context.Entry(entity).IsKeySet;

Conseil

Les clés sont définies dès que les entités sont suivies par le contexte, même si l’entité est dans l’état Ajouté. Cela est utile lors du parcours d’un graphique d’entités pour décider quelles actions effectuer avec chacune, par exemple lors de l’utilisation de l’API TrackGraph. La valeur de clé doit uniquement être utilisée de la manière illustrée ici avant qu’un appel soit envoyé pour effectuer le suivi de l’entité.

Avec d’autres clés

Un autre mécanisme est nécessaire pour identifier les nouvelles entités lorsque les valeurs des clés ne sont pas générées automatiquement. Il existe deux approches générales pour cela :

  • Requête pour l'entité
  • Passez un indicateur à partir du client

Pour rechercher l’entité, utilisez simplement la méthode Find :

public static bool IsItNew(BloggingContext context, Blog blog)
    => context.Blogs.Find(blog.BlogId) == null;

L’affichage du code complet pour le passage d’un indicateur depuis un client n’entre pas dans la portée de ce document. Dans une application web, cela consiste généralement à effectuer des demandes différentes pour différentes actions, ou à passer un état dans la demande, puis l’extraire dans le contrôleur.

Enregistrement d’entités uniques

Si on sait si une insertion ou une mise à jour est nécessaire, vous pouvez ajouter Add ou Update de manière appropriée :

public static void Insert(DbContext context, object entity)
{
    context.Add(entity);
    context.SaveChanges();
}

public static void Update(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

Toutefois, si l’entité utilise les valeurs de clé générées automatiquement, la méthode Update peut être utilisée pour les deux cas :

public static void InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

Normalement, la méthode Update marque l’entité pour la mise à jour, et non l’insertion. Toutefois, si l’entité a une clé générée automatiquement, et qu’aucune valeur de clé n’a été définie, l’entité est automatiquement marquée pour insertion.

Si l’entité n’utilise pas les clés générées automatiquement, l’application doit décider si l’entité doit être insérée ou mise à jour. Par exemple :

public static void InsertOrUpdate(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs.Find(blog.BlogId);
    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
    }

    context.SaveChanges();
}

Les étapes ici sont :

  • Si Find retourne null, la base de données ne contient pas encore le blog avec cet ID, nous appelons donc Add pour le marquer pour insertion.
  • Si la recherche retourne une entité, il existe dans la base de données et le contexte suit maintenant l’entité existante
    • Ensuite, nous utilisons SetValues pour définir les valeurs de toutes les propriétés de cette entité sur celles envoyées par le client.
    • L’appel à SetValues marque l’entité à mettre à jour en fonction des besoins.

Conseil

SetValues marque uniquement comme modifiées les propriétés qui ont des valeurs différentes de celles de l’entité suivie. Cela signifie que lorsque la mise à jour est envoyée, seules les colonnes qui ont été modifiées seront mises à jour. (Et si rien n’a changé, alors aucune mise à jour ne sera envoyée du tout).

Travail avec les graphiques

Résolution de l'identité

Comme indiqué ci-dessus, EF Core peut suivre une seule instance d’une entité avec une valeur de clé primaire donnée. Lorsque vous travaillez avec des graphiques, le graphique doit idéalement être créé de sorte que cet invariant est géré, et le contexte doit être utilisé pour une seule unité de travail. Si le graphique contient des doublons, il sera nécessaire de traiter le graphique avant de l’envoyer à EF pour consolider les instances multiples en une seule. Cela peut être difficile lorsque les instances ont des valeurs et relations en conflit. La consolidation des doublons doit donc être effectuée dès que possible dans le pipeline de votre application afin d’éviter la résolution des conflits.

Toutes les entités nouvelles/toutes les entités existantes

Un exemple d’utilisation des graphiques est l’insertion ou la mise à jour d’un blog ainsi que de sa collection de billets associés. Si toutes les entités dans le graphique doivent être insérées, ou toutes doivent être mises à jour, le processus est identique à celui décrit ci-dessus pour les entités uniques. Par exemple, un graphique de blogs et publications créé comme suit :

var blog = new Blog
{
    Url = "http://sample.com", Posts = new List<Post> { new Post { Title = "Post 1" }, new Post { Title = "Post 2" }, }
};

peut être inséré comme suit :

public static void InsertGraph(DbContext context, object rootEntity)
{
    context.Add(rootEntity);
    context.SaveChanges();
}

L’appel à Add marque le blog et tous les billets à insérer.

De même, si toutes les entités dans un graphique doivent être mises à jour, alors Update peut être utilisé :

public static void UpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

Le blog et tous ses billets seront marqués pour être mis à jour.

Combinaison d’entités nouvelles et existantes

Avec les clés générées automatiquement, Update est de nouveau utilisable à la fois pour les insertions et pour les mises à jour, même si le graphique contient un mélange d’entités qui nécessitent l’insertion et d’autres qui nécessitent la mise à jour :

public static void InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

Update marque une entité dans le graphique, le blog ou un billet, pour insertion si elle ne dispose pas d’un ensemble clé-valeur, tandis que toutes les autres entités sont marquées pour mise à jour.

Comme précédemment, lorsque vous n’utilisez pas les clés générées automatiquement, vous pouvez utiliser une requête et un traitement :

public static void InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }
    }

    context.SaveChanges();
}

Gestion des suppressions

La suppression peut être compliquée à gérer, car souvent l’absence d’une entité signifie qu’elle droit être supprimée. Une façon de gérer cela consiste est d’utiliser des « suppressions récupérables » par exemple en marquant l’entité comme supprimée plutôt que la supprimer réellement. Les suppressions s’apparentent alors à des mises à jour. Les suppressions récupérables peuvent être implémentées à l’aide de filtres de requête.

Pour les vraies suppressions, il est courant d’utiliser une extension du modèle de requête pour effectuer ce qui est essentiellement une comparaison de graphique. Par exemple :

public static void InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }

        foreach (var post in existingBlog.Posts)
        {
            if (!blog.Posts.Any(p => p.PostId == post.PostId))
            {
                context.Remove(post);
            }
        }
    }

    context.SaveChanges();
}

TrackGraph

En interne, Add, Attach et Update utilisent la traversée de graphique en déterminant pour chaque entité si elle doit être marquée comme Added (à insérer), Modified (à mettre à jour), Unchanged (ne rien faire), ou Deleted (à supprimer). Ce mécanisme est exposé via l’API TrackGraph. Par exemple, supposons que, lorsque le client envoie un graphique d’entités, il définit certains indicateurs sur chaque entité indiquant comment elle doit être gérée. TrackGraph peut ensuite être utilisé pour traiter cet indicateur :

public static void SaveAnnotatedGraph(DbContext context, object rootEntity)
{
    context.ChangeTracker.TrackGraph(
        rootEntity,
        n =>
        {
            var entity = (EntityBase)n.Entry.Entity;
            n.Entry.State = entity.IsNew
                ? EntityState.Added
                : entity.IsChanged
                    ? EntityState.Modified
                    : entity.IsDeleted
                        ? EntityState.Deleted
                        : EntityState.Unchanged;
        });

    context.SaveChanges();
}

Les indicateurs sont uniquement affichés dans le cadre de l’entité pour simplifier l’exemple. En général, les indicateurs feraient partie d’un DTO ou d’un autre état inclus dans la demande.