Al aprender LINQ, casi me derriba una dificultad, que es la operación de actualización de la base de datos que ves en el título. Ahora te llevaré paso a paso a este atolladero. Por favor, prepara tus ladrillos y tu saliva, sígueme.
Comencemos con el caso más simple. Tomemos la base de datos Northwind como ejemplo. Cuando necesite modificar el nombre del producto de un producto, puede escribir directamente el siguiente código en el cliente:
// Lista 0NorthwindDataContext db = new NorthwindDataContext();
Producto producto = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "Chai cambiado";
db.SubmitChanges();
Pruébelo y la actualización será exitosa. Sin embargo, creo que dicho código no aparecerá en sus proyectos porque es simplemente imposible de reutilizar. Bien, refactoricémoslo y extrayámoslo en un método. ¿Cuáles deberían ser los parámetros? es el nuevo nombre del producto y el ID del producto que se actualizará. Bueno, ese parece ser el caso.
public void UpdateProduct (int id, cadena nombre del producto)
{
NorthwindDataContext db = nuevo NorthwindDataContext();
Producto producto = db.Products.Single(p => p.ProductID == id);
producto.NombreProducto = NombreProducto;
db.SubmitChanges();
}En proyectos reales, no podemos simplemente modificar el nombre del producto. Otros campos del Producto también están sujetos a modificaciones. Entonces la firma del método UpdateProduct quedará de la siguiente manera:
Actualización de producto público vacío (int id,
cadena nombre del producto,
int proveedorId,
int ID de categoría,
cantidad de cadena por unidad,
unidad decimalprecio,
unidades cortas en stock,
unidades cortas en orden,
short reorderLevel) Por supuesto, esta es solo una base de datos simple. En proyectos reales, no es raro tener veinte, treinta o incluso cientos de campos. ¿Quién puede tolerar tales métodos? Si escribes así, ¿qué hace el objeto Producto?
Así es, use Producto como parámetro del método y lance la molesta operación de asignación al código del cliente. Al mismo tiempo, extrajimos el código para obtener la instancia del Producto para formar el método GetProduct y colocamos los métodos relacionados con las operaciones de la base de datos en una clase ProductRepository que es específicamente responsable de manejar la base de datos. ¡Ah, sí, SRP!
// Lista 1
// Repositorio de productos
Producto público GetProduct(int id)
{
NorthwindDataContext db = nuevo NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct(Producto producto)
{
NorthwindDataContext db = nuevo NorthwindDataContext();
db.Products.Attach(producto);
db.SubmitChanges();
}
//Código de cliente
Repositorio ProductRepository = nuevo ProductRepository();
Producto producto = repositorio.GetProduct(1);
product.ProductName = "Chai cambiado";
repositorio.UpdateProduct(producto);
Aquí utilizo el método Adjuntar para adjuntar una instancia de Producto a otro DataContext. Para la base de datos Northwind predeterminada, el resultado de esto es la siguiente excepción:
// Excepción 1 NotSupportException:
Se intentó adjuntar o agregar entidad; la entidad no es una entidad nueva y es posible que se haya cargado desde otro DataContext. Esta operación no es compatible.
Se ha intentado Adjuntar o Agregar una entidad que no es nueva,
Quizás haber sido cargado desde otro DataContext. Esto no es compatible. Al observar MSDN, sabemos que al serializar entidades para el cliente, estas entidades se separan de su DataContext original. El DataContext ya no rastrea los cambios en estas entidades o sus asociaciones con otros objetos. Si desea actualizar o eliminar datos en este momento, debe usar el método Adjuntar para adjuntar la entidad al nuevo DataContext antes de llamar a SubmitChanges; de lo contrario, se generará la excepción anterior.
En la base de datos Northwind, la clase Producto contiene tres clases relacionadas (es decir, asociaciones de clave externa): Detalle_Pedido, Categoría y Proveedor. En el ejemplo anterior, aunque Adjuntamos el Producto, no hay ninguna clase Adjuntar asociada a él, por lo que se genera una excepción NotSupportException.
Entonces, ¿cómo asociar clases relacionadas con el Producto? Esto puede parecer complicado, incluso para una base de datos sencilla como Northwind. Parece que primero debemos obtener las clases originales de Order_Detail, Category y Supplier relacionadas con el Producto original, y luego adjuntarlas al DataContext actual respectivamente, pero de hecho, incluso si hacemos esto, se generará NotSupportException.
Entonces, ¿cómo implementar la operación de actualización? Para simplificar, eliminamos otras clases de entidad en Northwind.dbml y solo conservamos el Producto. De esta forma, podemos comenzar el análisis desde el caso más simple.
Después de eliminar otras clases debido a problemas, ejecutamos nuevamente el código en la Lista 1, pero la base de datos no cambió el nombre del producto. Al observar la versión sobrecargada del método Adjuntar, podemos encontrar fácilmente el problema.
El método Adjuntar (entidad) llama a la sobrecarga Adjuntar (entidad, falso) de forma predeterminada, que adjuntará la entidad correspondiente en un estado sin modificar. Si el objeto Producto no se ha modificado, entonces deberíamos llamar a esta versión sobrecargada para adjuntar el objeto Producto al DataContext en un estado sin modificar para operaciones posteriores. En este momento, el estado del objeto Producto está "modificado" y solo podemos llamar al método Adjuntar (entidad, verdadero).
Entonces, cambiamos el código relevante en la Lista 1 a Adjuntar (producto, verdadero) y vemos qué sucedió.
// Excepción 2 InvalidOperationException:
Si una entidad declara un miembro de versión o no tiene una política de verificación de actualizaciones, solo se puede adjuntar como una entidad modificada sin el estado original.
Una entidad sólo se puede adjuntar como modificada sin estado original
si declara un miembro de versión o no tiene una política de verificación de actualizaciones.
LINQ to SQL utiliza la columna RowVersion para implementar la verificación de simultaneidad optimista predeterminada; de lo contrario, se producirá el error anterior al adjuntar entidades al DataContext en un estado modificado. Hay dos formas de implementar la columna RowVersion. Una es definir una columna de tipo de marca de tiempo para la tabla de la base de datos y la otra es definir el atributo IsVersion = true en el atributo de entidad correspondiente a la clave principal de la tabla. Tenga en cuenta que no puede tener la columna TimeStamp y el atributo IsVersion=true al mismo tiempo; de lo contrario, se generará una excepción InvalidOprationException: los miembros "System.Data.Linq.Binary TimeStamp" e "Int32 ProductID" están marcados como versiones de fila. En este artículo, utilizamos la columna de marca de tiempo como ejemplo.
Después de crear una columna denominada TimeStamp y escribir la marca de tiempo para la tabla Productos, arrástrela nuevamente al diseñador y luego ejecute el código en la Lista 1. Gracias a Dios finalmente funcionó.
Ahora, arrastramos la tabla Categorías al diseñador. Esta vez aprendí la lección y primero agregué la columna de marca de tiempo a la tabla Categorías. Después de probarlo, ¡resultó ser nuevamente el error en la Excepción 1! Después de eliminar la columna de marca de tiempo de Categorías, el problema persiste. Dios mío, ¿qué se hace exactamente en el terrible método Adjuntar?
Ah, por cierto, existe una versión sobrecargada del método Adjuntar, probémoslo.
public void UpdateProduct(Producto producto)
{
NorthwindDataContext db = nuevo NorthwindDataContext();
Producto oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Products.Attach(producto,productoantiguo);
db.SubmitChanges();
} ¡O error de excepción 1!
¡Me caeré! Adjunta, Adjunta, ¿qué te pasó?
Para explorar el código fuente de LINQ to SQL, utilizamos el complemento FileDisassembler de Reflector para descompilar System.Data.Linq.dll en código cs y generar archivos de proyecto, lo que nos ayuda a encontrarlo y ubicarlo en Visual Studio.
¿Cuándo se produce la excepción 1?
Primero encontramos la información descrita en la Excepción 1 de System.Data.Linq.resx y obtenemos la clave "CannotAttachAddNonNewEntities", luego buscamos el método System.Data.Linq.Error.CannotAttachAddNonNewEntities(), buscamos todas las referencias a este método y encontramos que hay dos Este método se utiliza en tres lugares, a saber, el método StandardChangeTracker.Track y el método InitializeDeferredLoader.
Abrimos el código de Table.Attach(entity, bool) nuevamente y, como era de esperar, encontramos que llama al método StandardChangeTracker.Track (lo mismo ocurre con el método Attach(entity,entity)):
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); en el método Track, el siguiente código genera la excepción 1:
si (objeto rastreado.HasDeferredLoaders)
{
tirar System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}Así que centramos nuestra atención en la propiedad StandardTrackedObject.HasDeferredLoaders:
bool de anulación interna HasDeferredLoaders
{
conseguir
{
foreach (asociación MetaAssociation en this.Type.Associations)
{
if (this.HasDeferredLoader(asociación.ThisMember))
{
devolver verdadero;
}
}
foreach (miembro MetaDataMember desde p en this.Type.PersistentDataMembers
donde p.IsDeferred &&!p.IsAssociation
seleccione p)
{
si (this.HasDeferredLoader(miembro))
{
devolver verdadero;
}
}
devolver falso;
}
} De esto podemos deducir aproximadamente que mientras haya elementos cargados de forma diferida en la entidad, la operación Adjuntar generará la Excepción 1. Esto coincide exactamente con el escenario en el que se produce la Excepción 1: la clase Producto contiene elementos cargados de forma diferida.
Entonces surgió una forma de evitar esta excepción: eliminar los elementos que deben demorarse en cargarse en el Producto. ¿Cómo eliminarlo? Puede usar DataLoadOptions para cargar inmediatamente o establecer elementos que requieren carga diferida en nulos. Pero el primer método no funcionó, así que tuve que utilizar el segundo método.
// Lista 2
clase Repositorio de productos
{
Producto público GetProduct(int id)
{
NorthwindDataContext db = nuevo NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
Producto público GetProductNoDeffered (int id)
{
NorthwindDataContext db = nuevo NorthwindDataContext();
//Opciones de DataLoadOptions = nuevas DataLoadOptions();
//opciones.LoadWith<Producto>(p => p.Categoría);
//db.LoadOptions = opciones;
var producto = db.Products.SingleOrDefault(p => p.ProductID == id);
producto.Categoría = nulo;
devolver producto;
}
public void UpdateProduct(Producto producto)
{
NorthwindDataContext db = nuevo NorthwindDataContext();
db.Products.Attach(producto, verdadero);
db.SubmitChanges();
}
}
//Código de cliente
Repositorio ProductRepository = nuevo ProductRepository();
Producto producto = repositorio.GetProductNoDeffered(1);
product.ProductName = "Chai cambiado";
repositorio.UpdateProduct(producto);
¿Cuándo se produce la excepción 2?
Siguiendo el método de la sección anterior, encontramos rápidamente el código que arrojó la Excepción 2. Afortunadamente, solo existía esta en todo el proyecto:
if (asModified && ((inheritanceType.VersionMember == null) && herenciaType.HasUpdateCheck))
{
tirar System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
Como puede ver, cuando el segundo parámetro asModified de Adjuntar es verdadero, no contiene la columna RowVersion (VersionMember=null) y contiene una columna de verificación de actualización (HasUpdateCheck), se generará la Excepción 2. El código de HasUpdateCheck es el siguiente:
bool de anulación pública HasUpdateCheck
{
conseguir
{
foreach (miembro MetaDataMember en this.PersistentDataMembers)
{
si (miembro.UpdateCheck! = UpdateCheck.Never)
{
devolver verdadero;
}
}
devolver falso;
}
}Esto también es consistente con nuestro escenario: la tabla Productos no tiene una columna RowVersion y en el código generado automáticamente por el diseñador, las propiedades UpdateCheck de todos los campos son las predeterminadas Siempre, es decir, la propiedad HasUpdateCheck es verdadera.
La forma de evitar la Excepción 2 es aún más sencilla: agregue una columna TimeStamp a todas las tablas o establezca el campo IsVersion=true en los campos de clave principal de todas las tablas. Dado que el último método modifica las clases generadas automáticamente y puede sobrescribirse con nuevos diseños en cualquier momento, recomiendo utilizar el primer método.
¿Cómo utilizar el método Adjuntar?
Después del análisis anterior, podemos descubrir dos condiciones relacionadas con el método Adjuntar: si hay una columna RowVersion y si hay una asociación de clave externa (es decir, elementos que deben cargarse de forma diferida). Resumí estas dos condiciones y el uso de varias sobrecargas de Adjuntar en una tabla. Cuando mires la tabla a continuación, debes estar completamente preparado mentalmente.
número de serie
Método de adjuntar
Si la columna RowVersion tiene una descripción asociada
1 Adjuntar(entidad) No No Sin modificación
2 Adjuntar(entidad) No Sí NotSupportException: se ha intentado adjuntar o agregar una entidad. La entidad no es una entidad nueva y se puede cargar desde otros contextos de datos. Esta operación no es compatible.
3 Adjuntar (entidad) Si no hay modificación
4 Adjuntar(entidad) no se modifica. Igual que 2 si el subconjunto no tiene una columna RowVersion.
5 Adjuntar (entidad, verdadero) No No InvalidOperationException: si la entidad declara un miembro de versión o no tiene una política de verificación de actualizaciones, solo se puede adjuntar como una entidad modificada sin estado original.
6 Adjuntar(entidad, verdadero) No Sí NotSupportException: se ha intentado adjuntar o agregar una entidad. La entidad no es una entidad nueva y se puede cargar desde otros contextos de datos. Esta operación no es compatible.
7 Adjuntar (entidad, verdadero) Si la modificación es normal (modificar por la fuerza la columna RowVersion informará un error)
8 Adjuntar(entidad, verdadero) Sí NotSupportException: se ha intentado adjuntar o agregar una entidad. La entidad no es una entidad nueva y se puede cargar desde otros contextos de datos. Esta operación no es compatible.
9 Adjuntar(entidad, entidad) No No DuplicateKeyException: no se puede agregar una entidad cuya clave ya esté en uso.
10 Adjuntar(entidad, entidad) No Sí NotSupportException: se ha intentado adjuntar o agregar entidad. La entidad no es una entidad nueva y se puede cargar desde otros contextos de datos. Esta operación no es compatible.
11 Adjuntar (entidad, entidad) DuplicateKeyException: no se puede agregar una entidad cuya clave ya esté en uso.
12 Adjuntar(entidad, entidad) Sí NotSupportException: se ha intentado adjuntar o agregar entidad. La entidad no es una entidad nueva y se puede cargar desde otros contextos de datos. Esta operación no es compatible.
¡Adjuntar solo se puede actualizar normalmente en la séptima situación (incluida la columna RowVersion y sin asociación de clave externa)! ¡Esta situación es casi imposible para un sistema basado en bases de datos! ¿Qué tipo de API es esta?
Resumen Calmémonos y empecemos a resumir.
Si escribe código LINQ to SQL directamente en la interfaz de usuario como la Lista 0, no sucederá nada desafortunado. Pero si intentas abstraer una capa separada de acceso a datos, ocurre un desastre. ¿Significa esto que LINQ to SQL no es adecuado para el desarrollo de arquitectura multicapa? Mucha gente dice que LINQ to SQL es adecuado para el desarrollo de sistemas pequeños, pero su tamaño pequeño no significa que no tenga capas. ¿Hay alguna manera de evitar tantas excepciones?
De hecho, este artículo ha dado algunas pistas. En el próximo ensayo de esta serie, intentaré ofrecer varias soluciones para que todos puedan elegir.