عندما تعلمت LINQ، كنت على وشك أن أواجه صعوبة، وهي عملية تحديث قاعدة البيانات التي تراها في العنوان. الآن سوف آخذك خطوة بخطوة إلى هذا المستنقع، من فضلك قم بإعداد الطوب الخاص بك ولعابك، اتبعني.
لنبدأ بأبسط الحالات، ولنأخذ قاعدة بيانات Northwind كمثال. عندما تحتاج إلى تعديل اسم المنتج لأحد المنتجات، يمكنك كتابة التعليمات البرمجية التالية مباشرة على العميل:
// القائمة 0NorthwindDataContext db = new NorthwindDataContext();
منتج المنتج = db.Products.Single(p => p.ProductID == 1);
Product.ProductName = "تم تغيير الشاي";
db.SubmitChanges();
اختبره وكان التحديث ناجحًا. ومع ذلك، أعتقد أن هذا الرمز لن يظهر في مشاريعك، لأنه من المستحيل إعادة استخدامه. حسنًا، فلنعيد تشكيلها واستخراجها إلى طريقة. ماذا يجب أن تكون المعلمات؟ هو اسم المنتج الجديد ومعرف المنتج المراد تحديثه. حسنا، يبدو أن هذا هو الحال.
UpdateProduct الفراغ العام (معرف كثافة العمليات، اسم المنتج سلسلة)
{
NorthwindDataContext db = new NorthwindDataContext();
منتج المنتج = db.Products.Single(p => p.ProductID == id);
اسم المنتج = اسم المنتج؛
db.SubmitChanges();
}في المشاريع الفعلية، لا يمكننا تعديل اسم المنتج فقط. تخضع مجالات المنتج الأخرى للتعديل أيضًا. ثم سيصبح توقيع طريقة UpdateProduct كما يلي:
UpdateProduct (معرف كثافة العمليات) الفراغ العام،
اسم منتج السلسلة,
معرف المورد int,
معرف الفئة الدولية,
كمية السلسلة لكل وحدة,
سعر الوحدة العشرية,
وحدات قصيرة في المخزون,
وحدات قصيرة عند الطلب,
Short reorderLevel) بالطبع، هذه مجرد قاعدة بيانات بسيطة. في المشاريع الفعلية، ليس من غير المألوف أن يكون لديك عشرين أو ثلاثين أو حتى مئات الحقول. من يستطيع تحمل مثل هذه الأساليب؟ إذا كتبت بهذه الطريقة، فماذا يفعل كائن المنتج؟
هذا صحيح، استخدم المنتج كمعلمة للطريقة وقم بإلقاء عملية التعيين المزعجة إلى رمز العميل. في الوقت نفسه، قمنا باستخراج التعليمات البرمجية للحصول على مثيل المنتج لتشكيل طريقة GetProduct، ووضع الأساليب المتعلقة بعمليات قاعدة البيانات في فئة ProductRepository المسؤولة بشكل خاص عن التعامل مع قاعدة البيانات. أوه نعم، SRP!
// القائمة 1
// مستودع المنتج
المنتج العام GetProduct(معرف كثافة العمليات)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.id == id);
}
UpdateProduct الفراغ العام (منتج المنتج)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(product);
db.SubmitChanges();
}
// رمز العميل
ProductRepository repository = new ProductRepository();
منتج المنتج = repository.GetProduct(1);
Product.ProductName = "تم تغيير الشاي";
repository.UpdateProduct(product);
أستخدم هنا طريقة الإرفاق لإرفاق مثيل المنتج بـ DataContext آخر. بالنسبة لقاعدة بيانات Northwind الافتراضية، تكون نتيجة ذلك الاستثناء التالي:
// الاستثناء 1 NotSupportException:
تمت محاولة إرفاق كيان أو إضافته، الكيان ليس كيانًا جديدًا وربما تم تحميله من DataContext آخر. هذه العملية غير مدعومة.
جرت محاولة لإرفاق أو إضافة كيان ليس جديدًا،
ربما تم تحميله من DataContext آخر، وهذا غير مدعوم. بالنظر إلى MSDN، نعلم أنه عند إجراء تسلسل للكيانات إلى العميل، يتم فصل هذه الكيانات عن DataContext الأصلي. لم يعد DataContext يتتبع التغييرات التي تطرأ على هذه الكيانات أو ارتباطاتها بكائنات أخرى. إذا كنت تريد تحديث البيانات أو حذفها في هذا الوقت، فيجب عليك استخدام الأسلوب Attach لإرفاق الكيان بـ DataContext الجديد قبل استدعاء SubmitChanges، وإلا فسيتم طرح الاستثناء أعلاه.
في قاعدة بيانات Northwind، تحتوي فئة المنتج على ثلاث فئات ذات صلة (على سبيل المثال، اقترانات المفاتيح الخارجية): Order_Detail والفئة والمورد. في المثال أعلاه، على الرغم من أننا قمنا بإرفاق المنتج، إلا أنه لا توجد فئة إرفاق مرتبطة به، لذلك يتم طرح NotSupportException.
فكيف يتم ربط الفئات المتعلقة بالمنتج؟ قد يبدو هذا معقدًا، حتى بالنسبة لقاعدة بيانات بسيطة مثل Northwind. يبدو أنه يجب علينا أولاً الحصول على فئات Order_Detail والفئة والمورد الأصلية المتعلقة بالمنتج الأصلي، ثم إرفاقها بـ DataContext الحالي على التوالي، ولكن في الواقع، حتى لو فعلنا ذلك، فسيتم طرح NotSupportException.
فكيف يتم تنفيذ عملية التحديث؟ للتبسيط، نقوم بحذف فئات الكيانات الأخرى في Northwind.dbml ونحتفظ بالمنتج فقط. وبهذه الطريقة يمكننا أن نبدأ التحليل من أبسط الحالات.
بعد حذف الفئات الأخرى بسبب حدوث مشكلات، قمنا بتنفيذ الكود الموجود في القائمة 1 مرة أخرى، لكن قاعدة البيانات لم تغير اسم المنتج. من خلال النظر إلى الإصدار المثقل من طريقة Attach، يمكننا العثور على المشكلة بسهولة.
تستدعي الطريقة Attach(entity) التحميل الزائد Attach(entity, false) بشكل افتراضي، والذي سيؤدي إلى إرفاق الكيان المقابل في حالة غير معدلة. إذا لم يتم تعديل كائن المنتج، فيجب علينا استدعاء هذا الإصدار المحمل بشكل زائد لإرفاق كائن المنتج بـ DataContext في حالة غير معدلة للعمليات اللاحقة. في هذا الوقت، تم "تعديل" حالة كائن المنتج، ويمكننا فقط استدعاء الأسلوب Attach(entity, true).
لذلك قمنا بتغيير الكود ذي الصلة في القائمة 1 إلى Attach(product, true) ونرى ماذا حدث؟
// الاستثناء 2 InvalidOperationException:
إذا أعلن أحد الكيانات أنه عضو في الإصدار أو لم يكن لديه سياسة التحقق من التحديث، فيمكن إرفاقه فقط ككيان معدل بدون الحالة الأصلية.
لا يمكن إرفاق الكيان إلا بصيغته المعدلة دون الحالة الأصلية
إذا أعلن عضوًا في الإصدار أو لم يكن لديه سياسة التحقق من التحديث.
يستخدم LINQ to SQL عمود RowVersion لتنفيذ فحص التزامن المتفائل الافتراضي، وإلا سيحدث الخطأ أعلاه عند إرفاق الكيانات بـ DataContext في حالة معدلة. هناك طريقتان لتنفيذ عمود RowVersion. إحداهما هي تحديد عمود نوع الطابع الزمني لجدول قاعدة البيانات، والأخرى هي تحديد السمة IsVersion=true على سمة الكيان المقابلة للمفتاح الأساسي للجدول. لاحظ أنه لا يمكن أن يكون لديك عمود TimeStamp والسمة IsVersion=true في نفس الوقت، وإلا فسيتم طرح InvalidOprationException: يتم وضع علامة على العضوين "System.Data.Linq.Binary TimeStamp" و"Int32 ProductID" كإصدارات صف. في هذه المقالة، نستخدم عمود الطابع الزمني كمثال.
بعد إنشاء عمود باسم TimeStamp واكتب الطابع الزمني لجدول المنتجات، اسحبه مرة أخرى إلى المصمم، ثم قم بتنفيذ التعليمات البرمجية في القائمة 1. والحمد لله، وأخيرا عملت.
الآن، نقوم بسحب جدول الفئات إلى المصمم. لقد تعلمت الدرس هذه المرة وقمت أولاً بإضافة عمود الطابع الزمني إلى جدول الفئات. وبعد اختباره، تبين أنه الخطأ في الاستثناء 1 مرة أخرى! بعد حذف عمود الطابع الزمني للفئات، تظل المشكلة قائمة. يا إلهي، ما الذي يتم بالضبط في طريقة الإرفاق الرهيبة؟
أوه، بالمناسبة، هناك نسخة مثقلة من طريقة الإرفاق، فلنجربها.
UpdateProduct الفراغ العام (منتج المنتج)
{
NorthwindDataContext db = new NorthwindDataContext();
المنتج oldProduct = db.Products.SingleOrDefault(p => p.ProductID == Product.ProductID);
db.Products.Attach(product, oldProduct);
db.SubmitChanges();
} أو خطأ الاستثناء 1!
سوف أسقط! إرفاق، إرفاق، ماذا حدث لك؟
لاستكشاف التعليمات البرمجية المصدرية من LINQ إلى SQL، نستخدم المكون الإضافي FileDisassembler الخاص بـ Reflector لفك ترجمة System.Data.Linq.dll إلى كود cs وإنشاء ملفات المشروع، مما يساعدنا في العثور عليه وتحديد موقعه في Visual Studio.
متى يتم طرح الاستثناء 1؟
نجد أولاً المعلومات الموضحة في الاستثناء 1 من System.Data.Linq.resx ونحصل على المفتاح "CannotAttachAddNonNewEntities"، ثم نجد طريقة System.Data.Linq.Error.CannotAttachAddNonNewEntities()، ونبحث عن جميع المراجع إلى هذه الطريقة، ونبحث عن أن هناك طريقتين. يتم استخدام هذه الطريقة في ثلاثة أماكن، وهي طريقة StandardChangeTracker.Track وطريقة InitializeDeferredLoader.
نفتح كود Table.Attach(entity, bool) مرة أخرى، وكما هو متوقع نجد أنه يستدعي الأسلوب StandardChangeTracker.Track (وينطبق الشيء نفسه على أسلوب Attach(entity, الكيان)):
TrackedObject = this.context.Services.ChangeTracker.Track(entity, true); في أسلوب التعقب، يطرح التعليمة البرمجية التالية الاستثناء 1:
إذا (trackedObject.HasDeferredLoaders)
{
throw System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}لذلك نوجه اهتمامنا إلى الخاصية StandardTrackedObject.HasDeferredLoaders:
منطقي التجاوز الداخلي HasDeferredLoaders
{
يحصل
{
foreach (ارتباط MetaAssociation في هذا.Type.Associations)
{
إذا (this.HasDeferredLoader(association.ThisMember))
{
عودة صحيحة؛
}
}
foreach (عضو MetaDataMember من p في this.Type.PersistentDataMembers
حيث p.IsDeferred && !p.IsAssociation
حدد ع)
{
إذا (this.HasDeferredLoader(عضو))
{
عودة صحيحة؛
}
}
عودة كاذبة.
}
} من هذا يمكننا أن نستنتج تقريبًا أنه طالما كانت هناك عناصر محملة كسولة في الكيان، فإن عملية الإرفاق ستؤدي إلى الاستثناء 1. يتماشى هذا تمامًا مع السيناريو الذي يحدث فيه الاستثناء 1 - تحتوي فئة المنتج على عناصر تم تحميلها ببطء.
ثم ظهرت طريقة لتجنب هذا الاستثناء - وهي إزالة العناصر التي يجب تأخير تحميلها في المنتج. كيفية إزالته؟ يمكنك استخدام DataLoadOptions للتحميل على الفور، أو تعيين العناصر التي تتطلب التحميل البطيء إلى قيمة خالية. لكن الطريقة الأولى لم تنجح، لذلك اضطررت إلى استخدام الطريقة الثانية.
// القائمة 2
مستودع المنتجات فئة
{
المنتج العام GetProduct(معرف كثافة العمليات)
{
NorthwindDataContext db = new NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
المنتج العام GetProductNoDeffered(معرف كثافة العمليات)
{
NorthwindDataContext db = new NorthwindDataContext();
//DataLoadOptions options = new DataLoadOptions();
//options.LoadWith<Product>(p => p.Category);
//db.LoadOptions = options;
فار المنتج = db.Products.SingleOrDefault(p => p.ProductID == id);
فئة المنتج = فارغة؛
عودة المنتج؛
}
UpdateProduct الفراغ العام (منتج المنتج)
{
NorthwindDataContext db = new NorthwindDataContext();
db.Products.Attach(product, true);
db.SubmitChanges();
}
}
// رمز العميل
ProductRepository repository = new ProductRepository();
منتج المنتج = repository.GetProductNoDeffered(1);
Product.ProductName = "تم تغيير الشاي";
repository.UpdateProduct(product);
متى يتم طرح الاستثناء 2؟
باتباع الطريقة الموضحة في القسم السابق، وجدنا بسرعة الكود الذي أطلق الاستثناء 2. ولحسن الحظ، لم يكن هناك سوى هذا الاستثناء في المشروع بأكمله:
إذا (asModified && ((inheritanceType.VersionMember == null) && inheritanceType.HasUpdateCheck))
{
throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
كما ترون، عندما تكون المعلمة الثانية asModified of Attach صحيحة، ولا تحتوي على عمود RowVersion (VersionMember=null)، وتحتوي على عمود التحقق من التحديث (HasUpdateCheck)، سيتم طرح الاستثناء 2. رمز HasUpdateCheck هو كما يلي:
منطقي التجاوز العام HasUpdateCheck
{
يحصل
{
foreach (عضو MetaDataMember في this.PersistentDataMembers)
{
إذا (عضو.UpdateCheck!= UpdateCheck.Never)
{
عودة صحيحة؛
}
}
عودة كاذبة.
}
}يتوافق هذا أيضًا مع السيناريو الخاص بنا - لا يحتوي جدول المنتجات على عمود RowVersion، وفي التعليمات البرمجية التي تم إنشاؤها تلقائيًا بواسطة المصمم، تكون خصائص UpdateCheck لجميع الحقول هي الافتراضية دائمًا، أي أن خاصية HasUpdateCheck صحيحة.
تعتبر طريقة تجنب الاستثناء 2 أبسط، قم بإضافة عمود TimeStamp إلى كافة الجداول أو قم بتعيين الحقل IsVersion=true في حقول المفتاح الأساسي لجميع الجداول. نظرًا لأن الطريقة الأخيرة تعدل الفئات التي تم إنشاؤها تلقائيًا ويمكن استبدالها بتصميمات جديدة في أي وقت، فإنني أوصي باستخدام الطريقة الأولى.
كيفية استخدام طريقة إرفاق؟
بعد التحليل أعلاه، يمكننا معرفة شرطين متعلقين بطريقة الإرفاق: ما إذا كان هناك عمود RowVersion وما إذا كان هناك اقتران مفتاح خارجي (أي العناصر التي تحتاج إلى التحميل البطيء). لقد قمت بتلخيص هذين الشرطين واستخدام العديد من التحميلات الزائدة للإرفاق في الجدول. عند النظر إلى الجدول أدناه، يجب أن تكون مستعدًا ذهنيًا بالكامل.
رقم سري
طريقة إرفاق
ما إذا كان عمود RowVersion يحتوي على وصف مرتبط أم لا
1 إرفاق (كيان) لا لا لا تعديل
2 إرفاق (كيان) لا نعم NotSupportException: تمت محاولة إرفاق أو إضافة كيان. الكيان ليس كيانًا جديدًا ويمكن تحميله من DataContexts أخرى. هذه العملية غير مدعومة.
3 إرفاق (الكيان) ما إذا كان هناك أي تعديل
4 لم يتم تعديل إرفاق (الكيان). نفس 2 إذا كانت المجموعة الفرعية لا تحتوي على عمود RowVersion.
5 Attach(entity, true) No No InvalidOperationException: إذا أعلن الكيان عن عضو إصدار أو ليس لديه سياسة التحقق من التحديث، فلا يمكن إرفاقه إلا ككيان معدل بدون الحالة الأصلية.
6 إرفاق (كيان، صحيح) لا نعم NotSupportException: تمت محاولة إرفاق أو إضافة كيان الكيان ليس كيانًا جديدًا ويمكن تحميله من DataContexts أخرى. هذه العملية غير مدعومة.
7 إرفاق (كيان، صحيح) ما إذا كان التعديل طبيعيًا (سيؤدي تعديل عمود RowVersion بالقوة إلى الإبلاغ عن خطأ)
8 إرفاق (كيان، صحيح) نعم NotSupportException: تمت محاولة إرفاق أو إضافة كيان الكيان ليس كيانًا جديدًا ويمكن تحميله من DataContexts أخرى. هذه العملية غير مدعومة.
9 إرفاق (كيان، كيان) لا لا DuplicateKeyException: لا يمكن إضافة كيان مفتاحه قيد الاستخدام بالفعل.
10 إرفاق (كيان، كيان) لا نعم NotSupportException: تمت محاولة إرفاق أو إضافة كيان الكيان ليس كيانًا جديدًا ويمكن تحميله من DataContexts أخرى. هذه العملية غير مدعومة.
11 إرفاق (كيان، كيان) DuplicateKeyException: لا يمكن إضافة كيان مفتاحه قيد الاستخدام بالفعل.
12 إرفاق (كيان، كيان) نعم NotSupportException: تمت محاولة إرفاق أو إضافة كيان الكيان ليس كيانًا جديدًا ويمكن تحميله من DataContexts أخرى. هذه العملية غير مدعومة.
لا يمكن تحديث المرفق بشكل طبيعي إلا في الحالة السابعة (بما في ذلك عمود RowVersion وعدم وجود اقتران مفتاح خارجي)! هذا الوضع يكاد يكون مستحيلاً بالنسبة لنظام قائم على قاعدة البيانات! ما هو نوع API هذا؟
ملخص دعونا نهدأ ونبدأ بالتلخيص.
إذا قمت بكتابة LINQ إلى كود SQL مباشرة في واجهة المستخدم مثل القائمة 0، فلن يحدث أي شيء مؤسف. ولكن إذا حاولت تجريد طبقة وصول منفصلة إلى البيانات، فستحدث كارثة. هل هذا يعني أن LINQ to SQL غير مناسب لتطوير بنية متعددة الطبقات؟ يقول الكثير من الناس أن LINQ to SQL مناسب لتطوير الأنظمة الصغيرة، لكن الحجم الصغير لا يعني أنه ليس متعدد الطبقات. هل هناك أي طريقة لتجنب الكثير من الاستثناءات؟
لقد أعطت هذه المقالة بالفعل بعض الأدلة في المقالة التالية من هذه السلسلة، وسأحاول تقديم العديد من الحلول ليختار منها الجميع.