Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Matthieu MEZIL

I love .Net

Abonnements

Actualités

Locations of visitors to this page English blog

Les relations avec l'Entity Framework - problématique d'ajout d'entité

J'ai eu une question qui peut en intéresser plus d'un. Aussi, j'en profite pour bloguer dessus.

Le code suivant :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    var p = new Products { ProductName = "test", Categories = c };

    Console.WriteLine(c.Products.Count);

}

attache automatiquement p au contexte. Pourquoi ?

Comme vous le savez probablement si vous avez un peu regardé l'Entity Framework, il n'y a pas de Lazy Loading. Donc quand vous faites p.Categories, il va vous retourner la catégorie présente dans le contexte. Si celle-ci n'existe pas dans le contexte, il va retourner null.

Par conséquent, quand vous affectez une catégorie (présente dans un contexte) à un nouveau produit, le produit sera automatiquement attaché à son tour.

Si vous ne voulez pas de ce fonctionnement, il vous faudra utiliser l'EntityReference :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    var p = new Products { ProductName = "test" };

    p.CategoriesReference.EntityKey = c.EntityKey;

}

Le fait d'être attaché au contexte fait que le code suivant :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    var p = new Products { ProductName = "test", Categories = c };

    Console.WriteLine(c.Products.Count);

    p.Categories = context.Categories.First(categ => categ.CategoryID == 2);

    Console.WriteLine(c.Products.Count);

}

retournera 1 puis 0.

Maintenant prenons le code suivant :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    var p = new Products { ProductName = "test", Categories = c };

    context.SaveChanges();

}

Ce qui peut quelque peu paraître surprenant c'est que le code suivant va ajouter notre nouveau produit en base alors que nous n'avons pas appelé de Add sur le contexte (ni context.AddToProduct, ni context.AddObject). En fait, en plus de l'attachement de l'entité au contexte, son EntityState va passer de Detached à Added, d'où l'ajout lors du SaveChanges.

Maintenant, creusons un peu cela.

Supposons que l'on supprime le ChangeTracker sur c avant de la lier à p :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    (c as IEntityWithChangeTracker).SetChangeTracker(null);

    var p = new Products { ProductName = "test"};

    p.Categories = c;

    context.SaveChanges();

}

Quand on supprime le ChangeTracker, cela signifie que l'on va pouvoir attacher c à un autre contexte mais tant qu'on ne l'a pas fait, l'EntityChangeTracker de l'entité reste l'ancienne :

private IEntityChangeTracker EntityChangeTracker

{

    get

    {

        if (this._entityChangeTracker == null)

        {

            this._entityChangeTracker = s_detachedEntityChangeTracker;

        }

        return this._entityChangeTracker;

    }

    set

    {

        this._entityChangeTracker = value;

    }

}

Donc dans le cas présent, il y aura un nouvel enregistrement en base.

Maintenant que se passe-t-il dans ce cas là :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    (c as IEntityWithChangeTracker).SetChangeTracker(null);

    var p = new Products { ProductName = "test"};

    using (var context2 = new NorthwindEntities())

    {

        context2.Attach(c);

        p.Categories = c;

    }

    var c2 = context.Categories.First(categ => categ.CategoryID == 1);

    Console.WriteLine(c2.Products.Count);

    Console.WriteLine(c.Products.Count);

    context.SaveChanges();

}

p n'est pas sauvegardé en base car il est lié à context2 et non à context cependant, la console affichera 1 et 1 car avec l'utilisation du cache, c2 sera égale à c.

Du coup, même le code suivante :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    (c as IEntityWithChangeTracker).SetChangeTracker(null);

    var p = new Products { ProductName = "test"};

    using (var context2 = new NorthwindEntities())

    {

        context2.Attach(c);

    }

    var c2 = context.Categories.First(categ => categ.CategoryID == 1);

    p.Categories = c2;

    Console.WriteLine(c2.Products.Count);

    Console.WriteLine(c.Products.Count);

    context.SaveChanges();

}

n'enregistrera pas p en base.

Maintenant, que se passe-t-il dans le cas d'un Detach.

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    context.Detach(c);

    var p = new Products { ProductName = "test"};

    p.Categories = c;

    Console.WriteLine(c.Products.Count);

    context.SaveChanges();

}

Bien entendu, p ne sera pas enregistré en base mais il est intéressant de constater que la console affichera quand même 1. En effet, lorsque l'on détache une entité, celle-ci est désormais rattachée à un DetachedEntityChangeTracker qui va également inclure p.

Dernier point. Le code suivant :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    context.Detach(c);

    var p = new Products { ProductName = "test"};

    p.Categories = c;

    context.AddToProducts(p);

}

génèrera une exception InvalidOperationException "The object cannot be added to the ObjectStateManager because it already has an EntityKey. Use ObjectContext.Attach to attach an object that has an existing key." sur le Add.

En revanche, le code suivant

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    var p = new Products { ProductName = "test"};

    p.Categories = c;

    context.AddToProducts(p);

}

fonctionnera sans problème.

Pourquoi ?

Il y a deux points. Quand on détache une entité, son EntityState passe à Detached. Ensuite, quand on fait un Add sur le contexte, on ajoute son graphe entier. Par conséquent, on ajoute notre nouveau produit + notre catégorie qui va passer de l'EntityState Detached à Added ce qui va poser un problème de conflit car la clé est déjà utilisée. En revanche, dans le deuxième cas, rien ne se passe réellement lors du Add. En effet, si la même entité est déjà présente dans le contexte avec un état Added (ce qui est notre cas), le Add ne fait rien et le Add ne va pas rajouter la catégorie vu que celle-ci est déjà attachée au contexte.

Soit dit en passant, le code suivant :

using (var context = new NorthwindEntities())

{

    var p = new Products { ProductName = "test"};

    context.AddToProducts(p);

    context.AddToProducts(p);

    context.SaveChanges();

}

ne va rajouter qu'un seul enregistrement en base.

Dernière petite précision (qui coule de source au vu de ce qui précède), le code suivant :

using (var context = new NorthwindEntities())

{

    var c = new Categories { CategoryName = "test" };

    var p = new Products { ProductName = "test" };

    p.Categories = c;

    context.AddToProducts(p);

}

ajoutera sans problème une nouvelle catégorie et un nouveau produit au contexte.

Le code suivant :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    context.Detach(c);

    using (var context2 = new NorthwindEntities())

    {

        var p = new Products { ProductName = "test" };

        p.Categories = c;

        context2.AddToProducts(p);

    }

}

génèrera la même exception que tout à l'heure pour les mêmes raisons mais le code suivant :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    context.Detach(c);

    using (var context2 = new NorthwindEntities())

    {

        context2.AttachTo("Categories", c);

        var p = new Products { ProductName = "test" };

        p.Categories = c;

        context2.AddToProducts(p);

    }

}

fonctionnera sans problème car l'attachement de la catégorie fera passer son état de Detached à Unchanged. Notons également que dans ce cas le AddToProducts ne sert à rien car la catégorie étant attachée au moment de l'affectation de la catégorie de p, cela l'inclut automatiquement dans le contexte.

En revanche, le code suivant :

using (var context = new NorthwindEntities())

{

    var c = context.Categories.First(categ => categ.CategoryID == 1);

    context.Detach(c);

    using (var context2 = new NorthwindEntities())

    {

        var p = new Products { ProductName = "test" };

        p.Categories = c;

        context2.AttachTo("Categories", c);

        context2.AddToProducts(p);

    }

}

va générer une exception de type InvalidOperationException "An object with the same key already exists in the ObjectStateManager. The existing object is in the Unchanged state. An object can only be added to the ObjectStateManager again if it is in the added state." car le fait d'attacher la catégorie attache tout le graphe et donc le produit en lui affectant l'EntityState à Unchanged.

Je suis conscient que cela fait beaucoup de notions à assimiler mais j'espère que cela vous permettra d'améliorer votre vision des choses.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Publié jeudi 5 juin 2008 02:32 par Matthieu MEZIL

Classé sous : , ,

Commentaires

# re: Les relations avec l'Entity Framework - problématique d'ajout d'entité @ mardi 2 septembre 2008 12:13

Bonjour,

J'avoue ne pas avoir tout saisie sur vos explications et comme un exemple vaut mieux qu'une longue explication ...

j'essai un peu de voir les possibilités des EF et j'ai essayé ça :

Products product = null;

Categories c = null;

using (NorthwindEFEntities context = new NorthwindEFEntities())

{

   c = context.Categories.First(categ => categ.CategoryID == 2);

   context.Detach(c);

}

using (NorthwindEFEntities context2 = new NorthwindEFEntities())

{

    context2.AttachTo("Categories", c);

    product = context2.Products.First(p1 => p1.ProductID == 1);

    product.Categories = c;

    context2.Detach(product);

}

using (NorthwindEFEntities context3 = new NorthwindEFEntities())

{

     context3.Attach(product);

     context3.SaveChanges();

}

et je suis assez surpris que le produit d'identifiant 1 n'ai pas eu sa catégorie modifiée.

De plus je remarque que la categories de product passe à null après le detach, pourquoi ?

Dadv

# re: Les relations avec l'Entity Framework - problématique d'ajout d'entité @ mardi 2 septembre 2008 22:52

Effectivement, lorsque l'on détache une entité, elle n'est plus liée à un context. Donc elle n'a plus d'entité attachée. Seule reste la CategoryReference. C'est l'intérêt de

(c as IEntityWithChangeTracker).SetChangeTracker(null);

Lorsqu'on attache le produit à un nouveau contexte, on n'a pas de category vu que le contexte n'a pas déjà chargé la Category.

De plus, lors d'un attach, l'état est toujours à Unchanged donc le SaveChanges ne fera rien. Enfin, même en utilisant le ApplyPropertyChanges, les relations ne sont pas mises à jour.

La seule solution que je vois consiste à ne pas faire de Attach avec context3 mais de faire un First ou un GetObjectByKey avec la clé de Products. Ensuite, il faudrait réaffecter les relations manuellement (ou automatiquement par reflection).

Matthieu MEZIL

# re: Les relations avec l'Entity Framework - problématique d'ajout d'entité @ lundi 15 septembre 2008 20:02

Bonjour,

Je reviens vers vous car je n'arrive vraiment pas à faire ce que je veux avec les EF :)

J'aimerais récupérer un objet (dans l'exemple une ligne de la table Customers de la base Northwind)

qui possède une liste d'objets liée à lui (ici des Orders). Je sauvegarde cette objet Customers dans un ViewState d'un contexte web.

Je voudrais ajouter un order vide à cette objet et que quand je sauvegarde mon contexte , cet Orders soit inséré en base (seul OrderID est obligatoire et c'est une rubrique auto calculé).

Je met mon code ci-desosus :

- CustomersBusiness.cs

public class CustomersBusiness

   {

       public Customers GetCustomerById(string customerId)

       {

           Customers customer;

           using (NORTHWINDEntities context = new NORTHWINDEntities())

           {

               customer = context.Customers.Include("Orders").First(c => c.CustomerID == customerId);

           }

           return customer;

       }

       public void UpdateCustomer(Customers newCustomer, Customers oldCustomer)

       {

           using (NORTHWINDEntities context = new NORTHWINDEntities())

           {

               context.Attach(newCustomer);

               context.ApplyPropertyChanges("Customers", newCustomer);                

               context.SaveChanges();                

           }

       }

   }

------------

Default.aspx.cs

public partial class _Default : System.Web.UI.Page

   {

       public Customers Customer

       {

           get { return ViewState["Customer"] as Customers; }

           set

           {

               if (ViewState["Customer"] == null)

               {

                   ViewState["Customer"] = value;

               }

           }

       }

       public Customers CustomerOld

       {

           get { return ViewState["CustomerOld"] as Customers; }

           set

           {

               if (ViewState["CustomerOld"] == null)

               {

                   ViewState["CustomerOld"] = value;

               }

           }

       }

       private Business.CustomersBusiness business = new Business.CustomersBusiness();

       protected void Page_Load(object sender, EventArgs e)

       {          

           this.CustomerOld = business.GetCustomerById("ALFKI");

           this.Customer = this.CustomerOld;

       }

       protected void Button1_Click(object sender, EventArgs e)

       {

           this.Customer.CompanyName = "Test";

           Orders order = new Orders();

           this.Customer.Orders.Add(order);

           business.UpdateCustomer(this.Customer, this.CustomerOld);

       }

   }

dans cette exemple le CompanyName est bien modifié mais l'Orders n'est pas ajouté.

Y a t il une manipulation particuliere à faire pour permmetre d'insérer cette Orders directement ?

Dadv

# re: Les relations avec l'Entity Framework - problématique d'ajout d'entité @ lundi 15 septembre 2008 20:05

J'ai fait une petite erreur à la ligne :

context.Attach(newCustomer);

il fallait biensure lire

context.Attach(oldCustomer);

Dadv

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- SQL Server 2008 : Un livre en cours de préparation ! par SQL Server vu par Christian Robert le il y a 4 heures et 11 minutes

- IIS7 : à quel pool d'application correspond le processus w3wp.exe par Atteint de JavaScriptite Aiguë [Cyril Durand] le il y a 5 heures et 8 minutes

- PDC 2008 - J-14 ! par Nix's Blog le il y a 6 heures et 53 minutes

- [Silverlight] La version finale de Silverlight 2 sera disponible en téléchargement demain ! par Thomas Lebrun le il y a 8 heures et 46 minutes

- SharePoint 2007 : Professional Developers Conference 2008 par Philippe Sentenac [MVP SharePoint] le il y a 14 heures et 39 minutes

- [Silverlight] En attendant Silverlight 2 RTW par Blog Technique d'Audrey PETIT le 10-11-2008, 21:55

- Le nouveau Gojira, c’est pour lundi… par CoqBlog le 10-11-2008, 01:18

- SharePoint : nouvel article sur la mise en place des Scopes dans MOSS Searchs par Blog Technique de Romelard Fabrice le 10-10-2008, 17:52

- Hello CS par Le Blog de julz le 10-10-2008, 12:26

- MSDN/TechNet/Microsoft Days Tour 2008 à Lille les 13 et 14 Octobre ! par RedoBlog - The .NET Gentleman !!! le 10-10-2008, 09:35