5

Estoy utilizando Entity Framework 4.3.1 en un proyecto, usando primero el código y la API DbContext. Mi aplicación es una aplicación de n niveles donde pueden aparecer objetos desconectados de un cliente. Estoy usando SQL Server 2008 R2, pero pronto cambiaré a SQL Azure. Me encuentro con un problema que parece que no puedo resolver.Guardar objetos individuales con el código de Entity Framework primero

Imagínese que tengo unas cuantas clases:

class A { 
    // Random stuff here 
} 
class B { 
    // Random stuff here 
    public A MyA { get; set; } 
} 
class C { 
    // Random stuff here 
    public A MyA { get; set; } 
} 

Por defecto, EF funciona con gráficos de objetos. Por ejemplo, si tengo una instancia de B que encapsula una instancia de A y llamo a myDbSet.Add(myB);, también marcará la instancia de A como agregada (suponiendo que aún no se esté rastreando).

Tengo un escenario en mi aplicación donde tengo que ser explícito sobre qué objetos persisten en la base de datos, en lugar de hacer un seguimiento de gráficos de objetos enteros. El orden de las operaciones es la siguiente:

A myA = new A(); // Represents something already in DB that doesn't need to be udpated. 
C myC = new C() { // Represents something already in DB that DOES need to be updated. 
    A = myA; 
} 
B myB0 = new B() { // Not yet in DB. 
    A = myA; 
} 
B myB1 = new B() { // Not yet in DB. 
    A = myA; 
} 

myDbSetC.Attach(myC); 
context.Entry(myC).State = Modified; 

myDbSetB.Add(myB0); // Tries to track myA with a state of Added 
myDbSetB.Add(myB1); 

context.SaveChanges(); 

En este punto consigo diciendo un error AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges. Creo que esto se debe a que llamar a poner en myB0 marca la instancia de A como está añadiendo, que entra en conflicto con la instancia de A ya siendo rastreado

Idealmente podría hacer algo como llamar al myDbSet.AddOnly(myB), pero obviamente no tenemos esa opción.

He intentado varias soluciones:

Intento # 1: En primer lugar, intentaron crear un método de ayuda para evitar Mya se añada una segunda vez.

private void MarkGraphAsUnchanged<TEntity>(TEntity entity) where TEntity : class { 
     DbEntityEntry entryForThis = this.context.Entry<TEntity>(entity); 
     IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct(); 

     foreach (DbEntityEntry entry in entriesItWantsToChange) { 
      if (!entryForThis.Equals(entry)) { 
       entry.State = System.Data.EntityState.Unchanged; 
      } 
     } 
    } 

... 

myDbSetB.Add(myB0); 
MarkGraphAsUnchanged(myB0); 

Aunque esto resuelve el problema de que se trata de añadir Mya, todavía provoca violaciónes clave dentro de la ObjectStateManager.

Intento # 2: Intenté hacer lo mismo que el anterior, pero estableciendo el estado en Independiente en lugar de Sin cambios. Esto funciona para guardar, pero insiste en establecer myB0.A = null, que tiene otros efectos adversos en mi código.

Intento n. ° 3: Utilicé un TransactionScope alrededor de todo el DbContext. Sin embargo, incluso al llamar al SaveChanges() entre cada Attach() y Add(), el rastreador de cambios no vacía sus entradas rastreadas, así que tengo el mismo problema que en el intento # 1.

Intento # 4: continué con el TransactionScope, excepto utilicé un patrón repositorio/DAO e internamente crear un nuevo DbContext y llamar SaveChanges() para cada operación distinta que hago. En este caso, recibí el error 'La actualización de la tienda, la inserción o la instrucción de eliminación afectó a un número inesperado de filas'. Cuando se utiliza el Analizador de SQL, encuentro que cuando se llama a SaveChanges() en la segunda operación que hice (la primera Add()), lo que realmente envía el SQL UPDATE a la base de datos de la primera operación una segunda vez - pero no lo hace cambiar cualquier fila Esto se siente como un error en Entity Framework para mí.

Intento n. ° 5: En lugar de utilizar TransactionScope, decidí utilizar solo DbTransaction. Todavía creo contextos múltiples, pero paso una EntityConnection precompilada a cada nuevo contexto a medida que se crea (mediante el almacenamiento en caché y la apertura manual de la EntityConnection creada por el primer contexto). Sin embargo, cuando hago esto, el segundo contexto ejecuta un inicializador que he definido, aunque ya se hubiera ejecutado cuando la aplicación se inició por primera vez. En un entorno de desarrollo tengo este seeding algunos datos de prueba, y en realidad agota el tiempo para un bloqueo de base de datos en una tabla mi primer Attach() modificado (pero todavía está bloqueado debido a que la transacción aún está abierta).

¡Ayuda! Intenté todo lo que se me ocurrió y, salvo por la refacturación completa de mi aplicación para no utilizar las propiedades de navegación o el uso de DAO construidos manualmente para hacer las instrucciones INSERTAR, ACTUALIZAR y ELIMINAR, me siento perdido. ¡Parece que debe haber una forma de obtener los beneficios de Entity Framework para el mapeo O/R pero aún así controlar manualmente las operaciones dentro de una transacción!

+0

¿Has intentado solo conectar myA? Tendrás que hacer esto antes de adjuntar cualquier otra cosa. – cadrell0

Respuesta

2

Debe haber algo más que no se muestra porque no hay ningún problema con la forma de adjuntar y agregar entidades. El siguiente código adjuntará myA, myC, myB0 y myB1 al contexto sin cambios y establecerá el estado de myC en modificado.

myDbSetC.Attach(myC); 
context.Entry(myC).State = Modified; 

el siguiente código detectará correctamente que todas las entidades que ya están conectados y en lugar de lanzar una excepción (como lo haría en la API ObjectContext) o la inserción de todas las entidades de nuevo (como se espera) que acaba de cambiar myB0 y myB1 al estado agregó:

myDbSetB.Add(myB0); 
myDbSetB.Add(myB1); 

Si su myA y myC se inicializan correctamente con las teclas de las entidades existentes código entero se ejecutará correctamente y ahorrar, excepto el único problema:

C myC = new C() { 
    A = myA; 
} 

Esto se ve como independent association y la asociación independiente tiene su propio estado, pero API para establecer su estado es not available in DbContext API. Si esta es una nueva relación que desea guardar, no se guardará porque todavía se realiza un seguimiento como sin cambios. Debe utilizar la asociación de clave externa o debe convertir su contexto a ObjectContext:

ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; 

y utilizar ObjectStateManager a change state of the relation.

+0

Tiene razón, estas son asociaciones independientes. Usted dijo "Si su myA y myC están inicializados correctamente con las claves de las entidades existentes". Mi suposición es que el rastreador de cambios/administrador de estado está usando esas claves para identificar las diversas entradas. Después de más análisis (mi gráfico de objetos real es mucho más complejo), hay un caso en el que hay dos objetos, llamémoslos As. Tienen las mismas claves y contenidos pero son instancias diferentes en la memoria. Eso causa problemas: el duplicado A se agrega junto con el B a pesar de que A ya está siendo rastreado con las mismas teclas. –

+0

No debe tener dos instancias para la misma entidad. Siempre dará como resultado problemas o excepciones. –

+0

¡Gracias por el consejo! Revisaré y aseguraré que las instancias de objeto sean iguales y veremos si todavía tengo problemas. Es desafortunado que se aplique esta restricción, y que no hay forma de eludir el seguimiento de cambios y realizar cada operación manualmente. También creo que hay algunos problemas relacionados con los contextos múltiples dentro de la misma transacción ... aunque es probable que sean diferentes de este problema de varias instancias. De todos modos, si asegurar que las instancias son las mismas soluciona el problema, aceptaré la respuesta. ¡Gracias de nuevo! –

0

Como sugirió Ladislav, obtuve las instancias del objeto coherentes, lo que resolvió el problema al intentar agregar As redundante.

Como resultado, tanto B0 como B1 en realidad encapsulan otros objetos (D0 y D1, respectivamente) que a su vez encapsulan A. Tanto D0 como D1 ya estaban en la base de datos pero Entity no los rastreaba.

Al agregar B0/B1, también se insertó D0/D1, erróneamente. Terminé usando la API de contexto de objetos. Ladislav sugirió marcar ObjectStateEntry para D0/D1 como Sin cambios, y las relaciones entre D0/D1 y A como Sin cambios. Esto parece hacer lo que necesito: actualice C e inserte B0/B1 solamente.

A continuación se muestra mi código para hacer esto, al que llamo justo antes de SaveChanges.Tenga en cuenta que estoy seguro de que todavía hay algunos casos extremos que no se manejan, y esto no se ha probado a fondo, pero debería dar una idea aproximada de que debe hacerse.

// Entries are put in here when they are explicitly added, modified, or deleted. 
private ISet<DbEntityEntry> trackedEntries = new HashSet<DbEntityEntry>(); 
private void MarkGraphAsUnchanged() 
{ 
    IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct(); 
    foreach (DbEntityEntry entry in entriesItWantsToChange) 
    { 
     if (!this.trackedEntries.Contains(entry)) 
     { 
      entry.State = System.Data.EntityState.Unchanged; 
     } 
    } 

    IEnumerable<ObjectStateEntry> allEntries = 
      this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added) 
      .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)) 
      .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified)); 

     foreach (ObjectStateEntry entry in allEntries) 
     { 
      if (entry.IsRelationship) 
      { 
       /* We can't mark relationships are being unchanged if we are truly adding or deleting the entity. 
       * To determine this, we need to first lookup the entity keys, then state entries themselves. 
       */ 
       EntityKey key1 = null; 
       EntityKey key2 = null; 
       if (entry.State == EntityState.Deleted) 
       { 
        key1 = (EntityKey)entry.OriginalValues[0]; 
        key2 = (EntityKey)entry.OriginalValues[1]; 
       } 
       else if (entry.State == EntityState.Added) 
       { 
        key1 = (EntityKey)entry.CurrentValues[0]; 
        key2 = (EntityKey)entry.CurrentValues[1]; 
       } 

       ObjectStateEntry entry1 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key1); 
       ObjectStateEntry entry2 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key2); 

       if ((entry1.State != EntityState.Added) && (entry1.State != EntityState.Deleted) && (entry2.State != EntityState.Added) && (entry2.State != EntityState.Deleted)) 
       { 
        entry.ChangeState(EntityState.Unchanged); 
       } 
      } 
     } 
    } 

Whew !!! El patrón básico es:

  1. Realice un seguimiento explícito de los cambios a medida que se realizan.
  2. Regrese y limpie todo lo que Entity cree que necesita hacer, pero realmente no lo hace.
  3. En realidad guarde los cambios en la base de datos.

Este método para "retroceder y limpiar" obviamente no es óptimo, pero parece ser la mejor opción por el momento, sin tener que conectar manualmente entidades periféricas (como D0/D1) antes Intento cualquier operación de salvar. Tener toda esta lógica en un repositorio genérico ayuda; la lógica solo necesita escribirse una vez. Espero que en una versión futura, Entity pueda agregar esta capacidad directamente (y eliminar la restricción de tener múltiples instancias de un objeto en el montón pero con la misma clave).

Cuestiones relacionadas