Als ich LINQ lernte, wurde ich fast von einer Schwierigkeit überrascht, nämlich dem Vorgang zum Aktualisieren der Datenbank, den Sie im Titel sehen. Jetzt werde ich Sie Schritt für Schritt in diesen Sumpf führen. Bitte bereiten Sie Ihre Steine und Ihren Speichel vor, folgen Sie mir.
Beginnen wir mit dem einfachsten Fall. Nehmen wir die Northwind-Datenbank als Beispiel. Wenn Sie den Produktnamen eines Produkts ändern müssen, können Sie den folgenden Code direkt auf den Client schreiben.
// Liste 0NorthwindDataContext db = new NorthwindDataContext();
Produkt produkt = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "Chai geändert";
db.SubmitChanges();
Testen Sie es und das Update ist erfolgreich. Ich glaube jedoch, dass ein solcher Code in Ihren Projekten nicht auftauchen wird, weil er einfach nicht wiederverwendet werden kann. Okay, lass es uns umgestalten und in eine Methode extrahieren. Was sollen die Parameter sein? ist der neue Produktname und die zu aktualisierende Produkt-ID. Nun, das scheint der Fall zu sein.
public void UpdateProduct(int id, string productName)
{
NorthwindDataContext db = new NorthwindDataContext();
Produkt produkt = db.Products.Single(p => p.ProductID == id);
Produkt.Produktname = Produktname;
db.SubmitChanges();
}In tatsächlichen Projekten können wir nicht einfach den Produktnamen ändern. Auch in anderen Produktbereichen können Änderungen vorgenommen werden. Dann sieht die Signatur der UpdateProduct-Methode wie folgt aus:
public void UpdateProduct(int id,
Zeichenfolge Produktname,
int suplierId,
int Kategorie-ID,
ZeichenfolgenmengePerUnit,
DezimaleinheitPreis,
kurze EinheitenAuf Lager,
kurze EinheitenOnOrder,
(kurz reorderLevel) Natürlich ist dies nur eine einfache Datenbank. In tatsächlichen Projekten ist es nicht ungewöhnlich, zwanzig, dreißig oder sogar Hunderte von Feldern zu haben. Wer verträgt solche Methoden? Wenn Sie so schreiben, was macht das Produktobjekt?
Das ist richtig, verwenden Sie Product als Parameter der Methode und werfen Sie die lästige Zuweisungsoperation an den Client-Code. Gleichzeitig haben wir den Code zum Abrufen der Product-Instanz extrahiert, um die GetProduct-Methode zu bilden, und die Methoden für Datenbankoperationen in eine ProductRepository-Klasse eingefügt, die speziell für den Umgang mit der Datenbank zuständig ist. Oh ja, SRP!
// Liste 1
// ProductRepository
öffentliches Produkt GetProduct(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.id == id);
}
public void UpdateProduct(Produktprodukt)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(product);
db.SubmitChanges();
}
//Client-Code
ProductRepository-Repository = new ProductRepository();
Produkt Produkt = Repository.GetProduct(1);
product.ProductName = "Chai geändert";
Repository.UpdateProduct(Produkt);
Hier verwende ich die Attach-Methode, um eine Instanz von Product an einen anderen DataContext anzuhängen. Für die Standard-Northwind-Datenbank ist das Ergebnis die folgende Ausnahme:
// Ausnahme 1 NotSupportException:
Es wurde versucht, eine Entität anzuhängen oder hinzuzufügen. Die Entität ist keine neue Entität und wurde möglicherweise aus einem anderen DataContext geladen. Dieser Vorgang wird nicht unterstützt.
Es wurde versucht, eine Entität anzuhängen oder hinzuzufügen, die nicht neu ist.
Möglicherweise aus einem anderen DataContext geladen. Dies wird nicht unterstützt. Wenn wir uns MSDN ansehen, wissen wir, dass diese Entitäten bei der Serialisierung an den Client von ihrem ursprünglichen DataContext getrennt werden. Der DataContext verfolgt keine Änderungen an diesen Entitäten oder deren Zuordnungen zu anderen Objekten mehr. Wenn Sie zu diesem Zeitpunkt Daten aktualisieren oder löschen möchten, müssen Sie die Entität mithilfe der Attach-Methode an den neuen DataContext anhängen, bevor Sie SubmitChanges aufrufen. Andernfalls wird die obige Ausnahme ausgelöst.
In der Northwind-Datenbank enthält die Produktklasse drei verwandte Klassen (d. h. Fremdschlüsselzuordnungen): Order_Detail, Category und Supplier. Obwohl wir im obigen Beispiel das Produkt anhängen, ist ihm keine Attach-Klasse zugeordnet, sodass eine NotSupportException ausgelöst wird.
Wie ordnet man also Klassen zu, die sich auf das Produkt beziehen? Dies mag selbst für eine einfache Datenbank wie Northwind kompliziert erscheinen. Es scheint, dass wir zuerst die ursprünglichen Klassen „Order_Detail“, „Category“ und „Supplier“ in Bezug auf das ursprüngliche Produkt abrufen und sie dann jeweils an den aktuellen DataContext anhängen müssen, aber selbst wenn wir dies tun, wird tatsächlich eine NotSupportException ausgelöst.
Wie implementiert man den Aktualisierungsvorgang? Der Einfachheit halber löschen wir andere Entitätsklassen in Northwind.dbml und behalten nur Product bei. Auf diese Weise können wir die Analyse vom einfachsten Fall aus beginnen.
Nachdem wir aufgrund von Problemen andere Klassen gelöscht hatten, führten wir den Code in Liste 1 erneut aus, die Datenbank änderte jedoch den Namen des Produkts nicht. Wenn wir uns die überladene Version der Attach-Methode ansehen, können wir das Problem leicht finden.
Die Attach(entity)-Methode ruft standardmäßig die Attach(entity, false)-Überladung auf, die die entsprechende Entität in einem unveränderten Zustand anfügt. Wenn das Product-Objekt nicht geändert wurde, sollten wir diese überladene Version aufrufen, um das Product-Objekt in einem unveränderten Zustand für nachfolgende Vorgänge an den DataContext anzuhängen. Zu diesem Zeitpunkt ist der Status des Produktobjekts „geändert“ und wir können nur die Methode Attach(entity, true) aufrufen.
Also haben wir den relevanten Code in Liste 1 in Attach(product, true) geändert und sehen, was passiert ist?
// Ausnahme 2 InvalidOperationException:
Wenn eine Entität ein Versionsmitglied deklariert oder über keine Update-Prüfrichtlinie verfügt, kann sie nur als geänderte Entität ohne den ursprünglichen Status angehängt werden.
Eine Entität kann nur im geänderten Zustand ohne Originalzustand angehängt werden
wenn es ein Versionsmitglied deklariert oder keine Update-Prüfrichtlinie hat.
LINQ to SQL verwendet die RowVersion-Spalte, um die standardmäßige optimistische Parallelitätsprüfung zu implementieren. Andernfalls tritt der obige Fehler auf, wenn Entitäten in einem geänderten Zustand an den DataContext angehängt werden. Es gibt zwei Möglichkeiten, die RowVersion-Spalte zu implementieren. Eine besteht darin, eine Spalte vom Typ Zeitstempel für die Datenbanktabelle zu definieren, und die andere darin, das Attribut IsVersion=true für das Entitätsattribut zu definieren, das dem Primärschlüssel der Tabelle entspricht. Beachten Sie, dass Sie die TimeStamp-Spalte und das IsVersion=true-Attribut nicht gleichzeitig haben können, da andernfalls eine InvalidOprationException ausgelöst wird: Die Mitglieder „System.Data.Linq.Binary TimeStamp“ und „Int32 ProductID“ sind beide als Zeilenversionen markiert. In diesem Artikel verwenden wir die Zeitstempelspalte als Beispiel.
Nachdem Sie eine Spalte mit dem Namen „TimeStamp“ erstellt und „timestamp“ für die Tabelle „Products“ eingegeben haben, ziehen Sie sie zurück in den Designer und führen Sie dann den Code in Liste 1 aus. Gott sei Dank hat es endlich geklappt.
Jetzt ziehen wir die Kategorientabelle in den Designer. Diesmal habe ich die Lektion gelernt und zuerst die Zeitstempelspalte zur Kategorientabelle hinzugefügt. Nach dem Testen stellte sich heraus, dass es sich erneut um den Fehler in Ausnahme 1 handelte! Nach dem Löschen der Zeitstempelspalte der Kategorien bleibt das Problem bestehen. Oh mein Gott, was genau wird bei der schrecklichen Attach-Methode gemacht?
Übrigens gibt es eine überladene Version der Attach-Methode. Versuchen wir es mal.
public void UpdateProduct(Produktprodukt)
{
NorthwindDataContext db = new NorthwindDataContext();
Produkt oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Products.Attach(product, oldProduct);
db.SubmitChanges();
} Oder Ausnahme 1 Fehler!
Ich werde fallen! Attach, Attach, was ist mit dir passiert?
Um den LINQ to SQL-Quellcode zu erkunden, verwenden wir das FileDisassembler-Plug-in von Reflector, um System.Data.Linq.dll in CS-Code zu dekompilieren und Projektdateien zu generieren, was uns hilft, es in Visual Studio zu finden und zu lokalisieren.
Wann wird Ausnahme 1 ausgelöst?
Wir finden zuerst die in Ausnahme 1 beschriebenen Informationen aus System.Data.Linq.resx und erhalten den Schlüssel „CannotAttachAddNonNewEntities“, suchen dann die Methode System.Data.Linq.Error.CannotAttachAddNonNewEntities(), suchen alle Verweise auf diese Methode und finden dass es zwei gibt Diese Methode wird an drei Stellen verwendet, nämlich der StandardChangeTracker.Track-Methode und der InitializeDeferredLoader-Methode.
Wir öffnen den Code von Table.Attach(entity, bool) erneut und stellen wie erwartet fest, dass er die Methode StandardChangeTracker.Track aufruft (dasselbe gilt für die Methode Attach(entity, entity)):
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); In der Track-Methode löst der folgende Code Ausnahme 1 aus:
if (trackedObject.HasDeferredLoaders)
{
throw System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}Also wenden wir uns der Eigenschaft „StandardTrackedObject.HasDeferredLoaders“ zu:
Interner Override Bool HasDeferredLoaders
{
erhalten
{
foreach (MetaAssociation-Assoziation in this.Type.Associations)
{
if (this.HasDeferredLoader(association.ThisMember))
{
return true;
}
}
foreach (MetaDataMember-Mitglied von p in this.Type.PersistentDataMembers
wobei p.IsDeferred && !p.IsAssociation
wähle p)
{
if (this.HasDeferredLoader(member))
{
return true;
}
}
return false;
}
} Daraus können wir grob ableiten, dass der Attach-Vorgang Ausnahme 1 auslöst, solange es verzögert geladene Elemente in der Entität gibt. Dies entspricht genau dem Szenario, in dem Ausnahme 1 auftritt – die Produktklasse enthält verzögert geladene Elemente.
Dann wurde eine Möglichkeit gefunden, diese Ausnahme zu vermeiden: Entfernen Sie die Elemente, die verzögert in das Produkt geladen werden müssen. Wie entferne ich es? Sie können DataLoadOptions zum sofortigen Laden verwenden oder Elemente, die verzögertes Laden erfordern, auf null setzen. Da die erste Methode jedoch nicht funktionierte, musste ich die zweite Methode verwenden.
// Liste 2
Klasse ProductRepository
{
öffentliches Produkt GetProduct(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
öffentliches Produkt GetProductNoDeffered(int id)
{
NorthwindDataContext db = new NorthwindDataContext();
//DataLoadOptions options = new DataLoadOptions();
//options.LoadWith<Product>(p => p.Category);
//db.LoadOptions = Optionen;
var product = db.Products.SingleOrDefault(p => p.ProductID == id);
Produkt.Kategorie = null;
Produkt zurücksenden;
}
public void UpdateProduct(Produktprodukt)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(product, true);
db.SubmitChanges();
}
}
//Client-Code
ProductRepository-Repository = new ProductRepository();
Produkt Produkt = Repository.GetProductNoDeffered(1);
product.ProductName = "Chai geändert";
Repository.UpdateProduct(Produkt);
Wann wird Ausnahme 2 ausgelöst?
Indem wir der Methode im vorherigen Abschnitt folgten, fanden wir schnell den Code, der Ausnahme 2 auslöste. Glücklicherweise gab es im gesamten Projekt nur diesen:
if (asModified && ((inheritanceType.VersionMember == null) && inheritanceType.HasUpdateCheck))
{
throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
Wie Sie sehen können, wird Ausnahme 2 ausgelöst, wenn der zweite Parameter asModified von Attach wahr ist, die RowVersion-Spalte nicht enthält (VersionMember=null) und eine Aktualisierungsprüfungsspalte (HasUpdateCheck) enthält. Der Code von HasUpdateCheck lautet wie folgt:
public override bool HasUpdateCheck
{
erhalten
{
foreach (MetaDataMember-Mitglied in this.PersistentDataMembers)
{
if (member.UpdateCheck != UpdateCheck.Never)
{
return true;
}
}
return false;
}
}Dies stimmt auch mit unserem Szenario überein: Die Products-Tabelle hat keine RowVersion-Spalte und im vom Designer automatisch generierten Code sind die UpdateCheck-Eigenschaften aller Felder standardmäßig immer, d. h. die HasUpdateCheck-Eigenschaft ist true.
Die Möglichkeit, Ausnahme 2 zu vermeiden, ist noch einfacher: Fügen Sie allen Tabellen eine TimeStamp-Spalte hinzu oder legen Sie das Feld IsVersion=true für die Primärschlüsselfelder aller Tabellen fest. Da letztere Methode die automatisch generierten Klassen verändert und jederzeit durch neue Designs überschrieben werden kann, empfehle ich die Verwendung der ersteren Methode.
Wie verwende ich die Attach-Methode?
Nach der obigen Analyse können wir zwei Bedingungen im Zusammenhang mit der Attach-Methode herausfinden: ob eine RowVersion-Spalte vorhanden ist und ob eine Fremdschlüsselzuordnung vorliegt (d. h. Elemente, die verzögert geladen werden müssen). Ich habe diese beiden Bedingungen und die Verwendung mehrerer Attach-Überladungen in einer Tabelle zusammengefasst. Wenn Sie sich die folgende Tabelle ansehen, müssen Sie mental vollständig vorbereitet sein.
Seriennummer
Attach-Methode
Ob der RowVersion-Spalte eine Beschreibung zugeordnet ist
1 Anhängen (Entität) Nein Nein Keine Änderung
2 Attach(entity) Nein Ja NotSupportException: Es wurde versucht, eine Entität anzuhängen oder hinzuzufügen. Die Entität ist keine neue Entität und kann aus anderen DataContexts geladen werden. Dieser Vorgang wird nicht unterstützt.
3 Attach(entity) Gibt an, ob keine Änderung vorliegt
4 Attach(entity) wird nicht geändert. Identisch mit 2, wenn die Teilmenge keine RowVersion-Spalte hat.
5 Attach(entity, true) Nein Nein InvalidOperationException: Wenn die Entität ein Versionsmitglied deklariert oder über keine Aktualisierungsprüfungsrichtlinie verfügt, kann sie nur als geänderte Entität ohne Originalstatus angehängt werden.
6 Attach(entity, true) Nein Ja NotSupportException: Es wurde versucht, eine Entität anzuhängen oder hinzuzufügen. Die Entität ist keine neue Entität und kann aus anderen DataContexts geladen werden. Dieser Vorgang wird nicht unterstützt.
7 Attach(entity, true) Ob die Änderung normal ist (bei erzwungener Änderung der RowVersion-Spalte wird ein Fehler gemeldet)
8 Attach(entity, true) Ja NotSupportException: Es wurde versucht, eine Entität anzuhängen oder hinzuzufügen. Die Entität ist keine neue Entität und kann aus anderen DataContexts geladen werden. Dieser Vorgang wird nicht unterstützt.
9 Attach(Entity, Entity) Nein Nein DuplicateKeyException: Es kann keine Entität hinzugefügt werden, deren Schlüssel bereits verwendet wird.
10 Attach(Entity, Entity) Nein Ja NotSupportException: Es wurde versucht, eine Entität anzuhängen oder hinzuzufügen. Die Entität ist keine neue Entität und kann aus anderen DataContexts geladen werden. Dieser Vorgang wird nicht unterstützt.
11 Attach(Entity, Entity) DuplicateKeyException: Es kann keine Entität hinzugefügt werden, deren Schlüssel bereits verwendet wird.
12 Attach(Entity, Entity) Ja NotSupportException: Es wurde versucht, eine Entität anzuhängen oder hinzuzufügen. Die Entität ist keine neue Entität und kann aus anderen DataContexts geladen werden. Dieser Vorgang wird nicht unterstützt.
Attach kann nur in der 7. Situation normal aktualisiert werden (einschließlich der RowVersion-Spalte und ohne Fremdschlüsselzuordnung)! Diese Situation ist für ein datenbankbasiertes System nahezu unmöglich! Was für eine API ist das?
Zusammenfassung Lassen Sie uns zur Ruhe kommen und mit der Zusammenfassung beginnen.
Wenn Sie LINQ to SQL-Code direkt in der Benutzeroberfläche wie Liste 0 schreiben, wird nichts Unglückliches passieren. Wenn Sie jedoch versuchen, eine separate Datenzugriffsebene zu abstrahieren, kommt es zur Katastrophe. Bedeutet das, dass LINQ to SQL nicht für die Entwicklung einer mehrschichtigen Architektur geeignet ist? Viele Leute sagen, dass LINQ to SQL für die Entwicklung kleiner Systeme geeignet ist, aber seine geringe Größe bedeutet nicht, dass es nicht geschichtet ist. Gibt es eine Möglichkeit, so viele Ausnahmen zu vermeiden?
Dieser Artikel hat tatsächlich einige Hinweise gegeben. Im nächsten Aufsatz dieser Reihe werde ich versuchen, mehrere Lösungen anzubieten, aus denen jeder wählen kann.