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 :