Lors de l'apprentissage de LINQ, j'ai presque été frappé par une difficulté, à savoir l'opération de mise à jour de la base de données que vous voyez dans le titre. Maintenant, je vais vous emmener étape par étape dans ce bourbier. S'il vous plaît, préparez vos briques et votre salive, suivez-moi.
Commençons par le cas le plus simple. Prenons comme exemple la base de données Northwind Lorsque vous devez modifier le ProductName d'un produit, vous pouvez directement écrire le code suivant sur le client :
// Liste 0NorthwindDataContext db = new NorthwindDataContext();
Produit produit = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "Chai modifié" ;
db.SubmitChanges();
Testez-le et la mise à jour est réussie. Cependant, je pense qu'un tel code n'apparaîtra pas dans vos projets, car il est tout simplement impossible à réutiliser. D'accord, refactorisons-le et extrayons-le dans une méthode. Quels doivent être les paramètres ? est le nouveau nom du produit et l'ID du produit à mettre à jour. Eh bien, cela semble être le cas.
public void UpdateProduct (int id, chaîne productName)
{
NorthwindDataContext db = new NorthwindDataContext();
Produit produit = db.Products.Single(p => p.ProductID == id);
produit.ProductName = productName;
db.SubmitChanges();
}Dans les projets réels, nous ne pouvons pas simplement modifier le nom du produit. D'autres domaines du Produit sont également sujets à modification. Alors la signature de la méthode UpdateProduct deviendra la suivante :
public void UpdateProduct (identifiant int,
chaîne NomProduit,
int identifiant fournisseur,
int identifiant de catégorie,
chaîne quantitéParUnit,
unité décimalePrix,
unités courtesEnStock,
unités courtesSur Commande,
short reorderLevel) Bien sûr, il ne s'agit que d'une simple base de données. Dans les projets réels, il n'est pas rare d'avoir vingt, trente voire des centaines de champs. Qui peut tolérer de telles méthodes ? Si vous écrivez ainsi, que fait l’objet Product ?
C'est vrai, utilisez Product comme paramètre de la méthode et lancez l'opération d'affectation ennuyeuse au code client. Dans le même temps, nous avons extrait le code permettant d'obtenir l'instance Product pour former la méthode GetProduct et placé les méthodes liées aux opérations de base de données dans une classe ProductRepository spécifiquement responsable du traitement de la base de données. Ah ouais, PDS !
// Liste 1
// Référentiel Produit
Produit public GetProduct (identifiant int)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct (Produit produit)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(produit);
db.SubmitChanges();
}
//Code client
Dépôt ProductRepository = new ProductRepository();
Produit produit = référentiel.GetProduct(1);
product.ProductName = "Chai modifié" ;
référentiel.UpdateProduct(produit);
Ici, j'utilise la méthode Attach pour attacher une instance de Product à un autre DataContext. Pour la base de données Northwind par défaut, le résultat est l'exception suivante :
// Exception 1 NotSupportException :
Tentative d'attachement ou d'ajout d'entité, l'entité n'est pas une nouvelle entité et peut avoir été chargée à partir d'un autre DataContext. Cette opération n'est pas prise en charge.
Une tentative a été faite pour attacher ou ajouter une entité qui n'est pas nouvelle,
Peut-être avoir été chargé à partir d'un autre DataContext. Ceci n'est pas pris en charge. En regardant MSDN, nous savons que lors de la sérialisation des entités sur le client, ces entités sont détachées de leur DataContext d'origine. Le DataContext ne suit plus les modifications apportées à ces entités ou leurs associations avec d'autres objets. Si vous souhaitez mettre à jour ou supprimer des données à ce stade, vous devez utiliser la méthode Attach pour attacher l'entité au nouveau DataContext avant d'appeler SubmitChanges, sinon l'exception ci-dessus sera levée.
Dans la base de données Northwind, la classe Product contient trois classes liées (c'est-à-dire des associations de clés étrangères) : Order_Detail, Category et Supplier. Dans l'exemple ci-dessus, bien que nous attachions le produit, aucune classe Attach n'y est associée, donc une NotSupportException est levée.
Alors comment associer des classes liées au Produit ? Cela peut paraître compliqué, même pour une simple base de données comme Northwind. Il semble que nous devions d'abord obtenir les classes originales de Order_Detail, Category et Supplier liées au produit d'origine, puis les attacher respectivement au DataContext actuel, mais en fait, même si nous faisons cela, NotSupportException sera levée.
Alors comment mettre en œuvre l’opération de mise à jour ? Pour plus de simplicité, nous supprimons les autres classes d’entités dans Northwind.dbml et ne conservons que Product. De cette façon, nous pouvons commencer l’analyse à partir du cas le plus simple.
Après avoir supprimé d'autres classes en raison de problèmes, nous avons réexécuté le code de la liste 1, mais la base de données n'a pas modifié le nom du produit. En regardant la version surchargée de la méthode Attach, nous pouvons facilement trouver le problème.
La méthode Attach(entity) appelle la surcharge Attach(entity, false) par défaut, qui attachera l'entité correspondante dans un état non modifié. Si l'objet Product n'a pas été modifié, nous devons alors appeler cette version surchargée pour attacher l'objet Product au DataContext dans un état non modifié pour les opérations ultérieures. A ce stade, le statut de l'objet Product est "modifié", et nous ne pouvons appeler que la méthode Attach(entity, true).
Nous avons donc modifié le code correspondant dans la liste 1 en Attach(product, true) et voyons ce qui s'est passé ?
// Exception 2 InvalidOperationException :
Si une entité déclare un membre de version ou n'a pas de politique de vérification des mises à jour, elle ne peut être attachée qu'en tant qu'entité modifiée sans l'état d'origine.
Une entité ne peut être attachée que telle que modifiée sans son état d'origine
s'il déclare un membre de version ou n'a pas de politique de vérification des mises à jour.
LINQ to SQL utilise la colonne RowVersion pour implémenter la vérification de concurrence optimiste par défaut, sinon l'erreur ci-dessus se produira lors de l'attachement d'entités au DataContext dans un état modifié. Il existe deux manières d'implémenter la colonne RowVersion. L'une consiste à définir une colonne de type horodatage pour la table de base de données et l'autre consiste à définir l'attribut IsVersion=true sur l'attribut d'entité correspondant à la clé primaire de la table. Notez que vous ne pouvez pas avoir la colonne TimeStamp et l'attribut IsVersion=true en même temps, sinon une InvalidOprationException sera levée : les membres "System.Data.Linq.Binary TimeStamp" et "Int32 ProductID" sont tous deux marqués comme versions de ligne. Dans cet article, nous utilisons la colonne timestamp comme exemple.
Après avoir créé une colonne nommée TimeStamp et saisi timestamp pour la table Products, faites-la glisser vers le concepteur, puis exécutez le code dans la liste 1. Dieu merci, ça a finalement fonctionné.
Maintenant, nous faisons glisser la table Catégories dans le concepteur. J'ai appris la leçon cette fois et j'ai d'abord ajouté la colonne d'horodatage à la table Catégories. Après l'avoir testé, il s'est avéré qu'il s'agissait à nouveau de l'erreur de l'exception 1 ! Après avoir supprimé la colonne d'horodatage des catégories, le problème persiste. Oh mon Dieu, que fait exactement la terrible méthode Attach ?
Oh, au fait, il existe une version surchargée de la méthode Attach, essayons-la.
public void UpdateProduct (Produit produit)
{
NorthwindDataContext db = new NorthwindDataContext();
Produit oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Products.Attach(produit, ancienProduit);
db.SubmitChanges();
} Ou erreur d'exception 1 !
Je vais tomber ! Attachez, Attachez, que vous est-il arrivé ?
Pour explorer le code source LINQ to SQL, nous utilisons le plug-in FileDisassembler de Reflector pour décompiler System.Data.Linq.dll en code CS et générer des fichiers de projet, ce qui nous aide à le trouver et à le localiser dans Visual Studio.
Quand l’exception 1 est-elle levée ?
Nous trouvons d'abord les informations décrites dans l'exception 1 à partir de System.Data.Linq.resx et obtenons la clé "CannotAttachAddNonNewEntities", puis trouvons la méthode System.Data.Linq.Error.CannotAttachAddNonNewEntities(), trouvons toutes les références à cette méthode et trouvons qu'il y en a deux. Cette méthode est utilisée à trois endroits, à savoir la méthode StandardChangeTracker.Track et la méthode InitializeDeferredLoader.
Nous ouvrons à nouveau le code de Table.Attach(entity, bool) et comme prévu nous constatons qu'il appelle la méthode StandardChangeTracker.Track (il en va de même pour la méthode Attach(entity,entity)) :
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); Dans la méthode Track, le code suivant renvoie l'exception 1 :
si (trackedObject.HasDeferredLoaders)
{
lancer System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}Nous tournons donc notre attention vers la propriété StandardTrackedObject.HasDeferredLoaders :
remplacement interne bool HasDeferredLoaders
{
obtenir
{
foreach (association MetaAssociation dans this.Type.Associations)
{
si (this.HasDeferredLoader(association.ThisMember))
{
renvoie vrai ;
}
}
foreach (membre MetaDataMember de p dans this.Type.PersistentDataMembers
où p.IsDeferred && !p.IsAssociation
sélectionnez p)
{
if (this.HasDeferredLoader(membre))
{
renvoie vrai ;
}
}
renvoie faux ;
}
} De cela, nous pouvons déduire grossièrement que tant qu'il y a des éléments chargés paresseux dans l'entité, l'opération Attach lancera l'exception 1. Cela correspond exactement au scénario dans lequel l'exception 1 se produit : la classe Product contient des éléments chargés paresseusement.
Ensuite, un moyen d'éviter cette exception est apparu : supprimez les éléments dont le chargement dans le produit doit être retardé. Comment le supprimer ? Vous pouvez utiliser DataLoadOptions pour charger immédiatement ou définir les éléments nécessitant un chargement différé sur null. Mais la première méthode n’a pas fonctionné, j’ai donc dû utiliser la deuxième méthode.
// Liste 2
classe ProductRepository
{
Produit public GetProduct (identifiant int)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
produit public GetProductNoDeffered (identifiant int)
{
NorthwindDataContext db = new NorthwindDataContext();
//Options DataLoadOptions = new DataLoadOptions();
//options.LoadWith<Product>(p => p.Category);
//db.LoadOptions = options ;
var product = db.Products.SingleOrDefault(p => p.ProductID == id);
produit.Catégorie = null ;
retourner le produit ;
}
public void UpdateProduct (Produit produit)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(produit, vrai);
db.SubmitChanges();
}
}
//Code client
Dépôt ProductRepository = new ProductRepository();
Produit produit = référentiel.GetProductNoDeffered(1);
product.ProductName = "Chai modifié" ;
référentiel.UpdateProduct(produit);
Quand l’exception 2 est-elle levée ?
En suivant la méthode de la section précédente, nous avons rapidement trouvé le code qui lançait l'exception 2. Heureusement, il n'y avait que celui-ci dans tout le projet :
if (asModified && ((inheritanceType.VersionMember == null) && successorType.HasUpdateCheck))
{
lancer System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
Comme vous pouvez le voir, lorsque le deuxième paramètre asModified de Attach est vrai, ne contient pas la colonne RowVersion (VersionMember=null) et contient une colonne de vérification de mise à jour (HasUpdateCheck), l'exception 2 sera levée. Le code de HasUpdateCheck est le suivant :
remplacement public bool HasUpdateCheck
{
obtenir
{
foreach (membre MetaDataMember dans this.PersistentDataMembers)
{
if (member.UpdateCheck != UpdateCheck.Never)
{
renvoie vrai ;
}
}
renvoie faux ;
}
}Cela est également cohérent avec notre scénario : la table Products n'a pas de colonne RowVersion et dans le code généré automatiquement par le concepteur, les propriétés UpdateCheck de tous les champs sont toujours par défaut, c'est-à-dire que la propriété HasUpdateCheck est vraie.
La façon d'éviter l'exception 2 est encore plus simple : ajoutez une colonne TimeStamp à toutes les tables ou définissez le champ IsVersion=true sur les champs de clé primaire de toutes les tables. Étant donné que cette dernière méthode modifie les classes générées automatiquement et peut être écrasée par de nouvelles conceptions à tout moment, je recommande d'utiliser la première méthode.
Comment utiliser la méthode Attach ?
Après l'analyse ci-dessus, nous pouvons découvrir deux conditions liées à la méthode Attach : s'il existe une colonne RowVersion et s'il existe une association de clé étrangère (c'est-à-dire des éléments qui doivent être chargés paresseux). J'ai résumé ces deux conditions et l'utilisation de plusieurs surcharges d'Attach dans un tableau. Lorsque vous regardez le tableau ci-dessous, vous devez être pleinement préparé mentalement.
numéro de série
Méthode de fixation
Si la colonne RowVersion a une description associée
1 Attacher(entité) Non Non Aucune modification
2 Attach(entity) Non Oui NotSupportException : une tentative d'attachement ou d'ajout d'entité a été tentée. L'entité n'est pas une nouvelle entité et peut être chargée à partir d'autres DataContexts. Cette opération n'est pas prise en charge.
3 Attach(entity) S'il n'y a pas de modification
4 Attach(entity) n’est pas modifié. Identique à 2 si le sous-ensemble n’a pas de colonne RowVersion.
5 Attach(entity, true) Non Non InvalidOperationException : Si l'entité déclare un membre de version ou n'a pas de politique de vérification des mises à jour, elle ne peut être attachée qu'en tant qu'entité modifiée sans état d'origine.
6 Attach(entity, true) Non Oui NotSupportException : une tentative d'attachement ou d'ajout d'entité a été tentée. L'entité n'est pas une nouvelle entité et peut être chargée à partir d'autres DataContexts. Cette opération n'est pas prise en charge.
7 Attach(entity, true) Si la modification est normale (la modification forcée de la colonne RowVersion signalera une erreur)
8 Attach(entity, true) Oui NotSupportException : une tentative d'attachement ou d'ajout d'entité a été tentée. L'entité n'est pas une nouvelle entité et peut être chargée à partir d'autres DataContexts. Cette opération n'est pas prise en charge.
9 Attach(entité, entité) Non Non DuplicateKeyException : Impossible d'ajouter une entité dont la clé est déjà utilisée.
10 Attach(entité, entité) Non Oui NotSupportException : une tentative d'attachement ou d'ajout d'entité a été tentée. L'entité n'est pas une nouvelle entité et peut être chargée à partir d'autres DataContexts. Cette opération n'est pas prise en charge.
11 Attach (entité, entité) DuplicateKeyException : impossible d'ajouter une entité dont la clé est déjà utilisée.
12 Attach(entité, entité) Oui NotSupportException : une tentative d'attachement ou d'ajout d'entité a été tentée. L'entité n'est pas une nouvelle entité et peut être chargée à partir d'autres DataContexts. Cette opération n'est pas prise en charge.
Attach ne peut être mis à jour normalement que dans la 7ème situation (y compris la colonne RowVersion et aucune association de clé étrangère) ! Cette situation est presque impossible pour un système basé sur une base de données ! De quel type d'API s'agit-il ?
Résumé Calmons-nous et commençons à résumer.
Si vous écrivez du code LINQ to SQL directement dans l'interface utilisateur comme la liste 0, rien de malheureux ne se produira. Mais si vous essayez de supprimer une couche d’accès aux données distincte, le désastre survient. Cela signifie-t-il que LINQ to SQL n'est pas adapté au développement d'une architecture multicouche ? Beaucoup de gens disent que LINQ to SQL convient au développement de petits systèmes, mais la petite taille ne signifie pas qu'il n'y a pas de couches. Existe-t-il un moyen d’éviter autant d’exceptions ?
Cet article a en fait donné quelques indices. Dans le prochain essai de cette série, j'essaierai de proposer plusieurs solutions parmi lesquelles chacun pourra choisir.