[ADO.Net EF] Insercion de registros relacionados

Una de las operaciones que se puede volver un dolor de cabeza (cuando empezamos a usar EF) es la inserción o actualización de tablas que están relacionadas. Miremos a Northwind:

Entity Framework

Como se pueden ver si queremos insertar un registro en la tabla Products, necesitamos los CategoryID y SupplierID que están relacionados con otras dos tablas.

El sentido común te dice que el insertar debería ser algo así:

   1: public Int32 InsertarProduct(Products objProd)

   2: {

   3:   Int32 prodId = 0;

   4:  

   5:   using (NorthwindEntities edmNorth = new NorthwindEntities())

   6:   {        

   7:     edmNorth.AddToProducts(objProd);        

   8:     edmNorth.SaveChanges();

   9:     prodId = objProd.ProductID;

  10:    }

  11:   return prodId;

  12: }

Y la llamada al método sería la siguiente:

   1: [TestMethod()]

   2:  public void InsertarProductTest()

   3:  {

   4:    RepositoryOrders target = new RepositoryOrders(); 

   5:    Products objProd = new Products(); 

   6:    objProd.ProductName = "My Product 2";

   7:    objProd.Suppliers = new Suppliers() { SupplierID = 18 };

   8:    objProd.Categories = new Categories() { CategoryID = 1 };          

   9:    objProd.QuantityPerUnit = "24 - 50 g pkgs.";

  10:    objProd.UnitPrice = 34.45m;

  11:    objProd.UnitsInStock = 34;

  12:    objProd.UnitsOnOrder = 3;

  13:    objProd.ReorderLevel = 3;

  14:    objProd.Discontinued = false;

  15:  

  16:    int expected = 0; // TODO: Initialize to an appropriate value

  17:    int actual;

  18:    actual = target.InsertarProduct(objProd);

  19:    Assert.AreNotEqual(expected, actual);     

  20:  }

Pero al ejecutar el código te dará un error como el siguiente: “An error occurred while updating the entries”. Debido a que el EF va intentar actualizar el registro SupplierID y CategoryID, pero el nombre no permite nulos, como estos objetos han sido creados manualmente, EF piensa que quieres actualizar ellos también, cuando sólo quieres insertar un registro de Product.

Una de las cosas que estaba haciendo era remover las relaciones manualmente (editando el modelo Xml) y el modelo no sabía que estaban relacionadas así que sólo pasaba los datos. Pero actualizar el modelo era una tarea muy trabajosa, y llegas a esos momentos en que dices: “creo que ya es hora, de buscar una forma más simple de hacer esto”.

En este foro: How to insert a row using Entity Framework?, te dan una luz de la solución, y con unos adicionales puede ser así:

1. Vamos a crear una clase parcial de Product con los campos de relación a las otras tablas. Clase parcial, para que cuando se actualice el modelo esta no se actualice.

   1: public partial class Products

   2: {

   3:   public Int32 SupplierID { get; set; }

   4:   public Int32 CategoryID { get; set; }

   5: }

2. Vamos a cambiar el código de inserción. En el foro, se habla de dos opciones, pero la primera involucra hacer una consulta a la base datos, lo cual no es necesario en una inserción simple, por eso optamos por el segundo.

   1: public Int32 InsertarProduct(Products objProd)

   2: {

   3:    Int32 prodId = 0;

   4:  

   5:    using (NorthwindEntities edmNorth = new NorthwindEntities())

   6:    {

   7:      objProd.SuppliersReference.EntityKey = 

   8:              new EntityKey("NorthwindEntities.Suppliers",

   9:                "SupplierID", objProd.SupplierID);

  10:      objProd.CategoriesReference.EntityKey = 

  11:              new EntityKey("NorthwindEntities.Categories",

  12:                "CategoryID", objProd.CategoryID);  

  13:      

  14:      edmNorth.AddToProducts(objProd);        

  15:      edmNorth.SaveChanges();

  16:      prodId = objProd.ProductID;

  17:    }

  18:  

  19:    return prodId;

  20:  }

3. Y finalmente la llamada al método será así:

   1: [TestMethod()]

   2: public void InsertarProductTest()

   3: {

   4:    RepositoryOrders target = new RepositoryOrders(); 

   5:    Products objProd = new Products(); 

   6:    objProd.ProductName = "My Product 2";   

   7:    //usando clase parcial

   8:    objProd.SupplierID = 18;

   9:    objProd.CategoryID = 1;    

  10:    objProd.QuantityPerUnit = "24 - 50 g pkgs.";

  11:    objProd.UnitPrice = 34.45m;

  12:    objProd.UnitsInStock = 34;

  13:    objProd.UnitsOnOrder = 3;

  14:    objProd.ReorderLevel = 3;

  15:    objProd.Discontinued = false;

  16:  

  17:    int expected = 0; // TODO: Initialize to an appropriate value

  18:    int actual;

  19:    actual = target.InsertarProduct(objProd);

  20:    Assert.AreNotEqual(expected, actual);     

  21: }

Se intento pasar los valores en los mismos objetos del modelo, pero hubo algunos errores porque se cruzaba la entidad enviada y la referencia que creamos antes de insertar el producto. Y esto de usar clases parciales, es forma transparente y simple de hacerlo, a propósito si van exponer su modelo en un servicio WCF, tiene que serializar estos atributos de la clase parcial para que también puedan ser expuestos en los contratos.

Se pudo mostrar otros detalles y los errores de otras pruebas que hice, pero si hacía eso nunca terminaba el artículo, así que mejor enviarlo masticado :D.

P.D.: Si tienen otras formas de hacer un Insert o Update con tablas relacionadas, lo pueden enviar en los comentarios.

Saludos,

6 comentarios en “[ADO.Net EF] Insercion de registros relacionados”

  1. Sergio, me parece una forma muy enrevesada, la verdad, de resolver esta casuística. Por regla general el problema con este tipo de cosas biene de pensar en como manejar las relaciones, y en V1 la mejor manera es siempre pensar en trabajar con padres y despues con los hijos, ejemplo que te resolvería más fácilmente este problema. De todas formas, por si no lo sabes en la siguiente versión de EF, el contexto de trabajo nos aportará nuevos métodos para establecer el estado correcto de las ObjectStateEntry asociadas a las entidades y relaciones, de hecho en estos métodos se apoyan la plantilla de Selft Tracking Entities precisamente!!

    Saludos
    Unai

  2. Hola! Armando la arquitectura para trabajar con el EFx llegamos a los problemas que estas exponiendo, y en un primer termino lo resolvimos de la manera que explicas.
    Luego, encontramos las EF Extensions que permite hacer lo mismo de una manera mas transparente. Ademas tiene un par de cosas de lo mas interesante. El link: http://code.msdn.microsoft.com/EFExtensions
    Saludos

    Pablo Mayo

  3. Unai, entonces en V1, sugieres que para insertar un hijo, teniendo los datos del mismo, lo que se debe hacer es recuperar el padre, y sobre el padre insertar los hijos, algo así:

    public Int32 Insertar2Product(Products objProd)
    {
    Int32 prodId = 0;

    using (NorthwindEntities edmNorth = new NorthwindEntities())
    {
    var sup = (from s in edmNorth.Suppliers
    where s.SupplierID == objProd.SupplierID
    select s).First();

    sup.Products.Add(objProd);
    //edmNorth.AddToProducts(objProd);

    edmNorth.SaveChanges();
    prodId = objProd.ProductID;
    }

    return prodId;
    }

    Saludos,

  4. No tienes porque hacer la consulta para recuperar el padre. Basta con que el ObjectStateManager tenga su ObjectStateEntry asociada y esto lo puedes conseguir con un attach ( el estado de esa entry por defecto es Unchanged)

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *