3. دراسة حالة مع SQL Server Express 2012
3.1. Introduction
معظم الأمثلة المتوفرة على الإنترنت حول Entity Framework هي أمثلة تستخدم خادم SQL. وهذا أمر طبيعي إلى حد ما. فمن المرجح أن يكون خادم SGBD هو الأكثر انتشارًا في عالم الشركات. وسنتبع هذا الاتجاه. ثم سيتم توسيع نطاق الأمثلة لتشمل جميع قواعد البيانات المذكورة في الفقرة 1.2.
3.2. تثبيت الأدوات
لن نصف عملية تثبيت الأدوات. في الواقع، سيتطلب ذلك عددًا كبيرًا من لقطات الشاشة التي تصبح قديمة بسرعة. إنها مهمة (ليست سهلة دائمًا، صحيح) نتركها للقارئ.
علينا تثبيت الأدوات التالية:
- SGBD SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. قم بتنزيل الإصدار "With Tools" الذي يأتي مع أداة إدارة SGBD:
بمجرد تثبيت SGBD، نقوم بتشغيله:
![]() |
![]() |
- [1]: في قائمة "ابدأ"، قم بتشغيل "مدير تكوين خادم SQL"؛
- [2]: في هذا المدير، قم بتشغيل الخادم؛
- [3]: تم تشغيله.
نقوم الآن بتشغيل أداة إدارة خادم SQL:
![]() |
- [1]: في قائمة "ابدأ"، قم بتشغيل "SQL Server Management Studio"؛
- [2]: أداة الإدارة.
سنقوم بالاتصال بالخادم:
![]() |
- في [1]، نقوم بتوصيل مستكشف الكائنات؛
- في [2]، نحدد معلمات الاتصال:
- [3]: الخادم (المحلي) (انتبه إلى الأقواس الضرورية) يشير إلى الخادم المثبت على الجهاز،
- [4]: نختار مصادقة Windows. يجب أن تكون مسؤولاً عن جهازك حتى ينجح هذا الاتصال،
- [5]: يتم الاتصال؛
![]() |
- [6]: تم الاتصال؛
- [7]: نريد تعديل بعض خصائص الخادم؛
![]() |
- [8]: نطلب وجود وضعين للمصادقة:
- مصادقة Windows كما تم استخدامها للتو. يمكن لمستخدم Windows يتمتع بالحقوق المناسبة عندئذٍ الاتصال،
- مصادقة خادم SQL. يجب أن يكون المستخدم ضمن المستخدمين المسجلين في SGBD؛
بعد ذلك، يمكننا التحقق من خصائص الخادم؛
- [9]: نقوم بتحرير خصائص المستخدم (مسؤول النظام)؛
![]() |
- في [10]، يتم تعيين كلمة مرور له. في بقية الوثيقة، تكون كلمة المرور هي sqlserver2012؛
![]() |
- في [10]، نمنحه إذنًا للاتصال؛
- في [11]، يتم تنشيط الاتصال. وبذلك يمكن المصادقة على المساعد؛
- في [12]، يتم قطع الاتصال بالخادم.
الآن، نعيد الاتصال باستخدام اسم المستخدم sa/sqlserver2012:
![]() |
- في [1]، يتم إعادة الاتصال؛
- في [2]، في المصادقة SQL Server؛
- في [3]، المستخدم هو sa؛
- في [4]، كلمة المرور هي sqlserver2012؛
- في [5]، يتم الاتصال؛
![]() |
- في [6]، نكون قد سجلنا الدخول.
سنقوم الآن بإنشاء قاعدة بيانات تجريبية:
![]() |
- في [1]، نقوم بإنشاء BD جديدة؛
- في [2]، ستسمى demo؛
- في [3]، نقوم بالتحقق؛
![]() |
- في [4]، يتم إنشاء قاعدة البيانات؛
- في [5]، يتم إنشاء جدول جديد في قاعدة البيانات demo؛
![]() |
![]() |
![]() |
![]() |
- في [6]، يتم تعريف جدول من عمودين ID و NOM؛
- في [7]، يتم تعيين العمود [ID] كمفتاح أساسي؛
- في [8]، يُرمز للمفتاح الأساسي برمز مفتاح؛
- في [9]، يتم حفظ الجدول؛
- في [10]، نسميها؛
- في [11]، لكي تظهر الجدولة في قاعدة البيانات [demo]، يجب تحديث قاعدة البيانات؛
- في [12]، تم إنشاء الجدول [PERSONNES] بنجاح.
نحن نعرف ما يكفي في الوقت الحالي عن استخدام أداة إدارة خادم SQL.
3.3. الخادم المدمج (localdb)\v11.0
يأتي VS Express 2012 مزودًا بخادم مدمج SQL. نفترض هنا أن VS Express 2012 قد تم تثبيته [http://www.microsoft.com/visualstudio/fra/downloads]. نقوم بتشغيل VS 2012 [1]:
![]() |
يتم تشغيل أداة إدارة SQL Server 2012 [2] ويتم الاتصال بـ [3].
![]() |
- في [4]، قم بتسجيل الدخول إلى الخادم (localdb)\v11.0؛
- في [5]، باستخدام مصادقة Windows؛
- في [6]، يعرض الاتصال الناجح قواعد بيانات الخادم. يمكننا، كما في السابق، إنشاء قاعدة بيانات جديدة.
لن نستخدم هذا الخادم المدمج في VS 2012.
3.4. إنشاء قاعدة البيانات من الكيانات
يتيح Entity Framework 5 Code First إنشاء قاعدة بيانات من الكيانات. وهذا ما سنراه الآن. باستخدام VS Express 2012، نقوم بإنشاء أول مشروع وحدة تحكم بلغة C#:
![]() |
![]() |
- في [1]، تعريف المشروع؛
- في [2]، المشروع الذي تم إنشاؤه.
ستحتاج جميع مشاريعنا إلى DLL من Entity Framework 5. نقوم بإضافته:
![]() |
- في [1]، تتيح أداة NuGet تنزيل التبعيات؛
![]() |
- إلى [2]، يتم تنزيل التبعية Entity Framework؛
- في [3]، تمت إضافة المرجع إلى المشروع.
يمكن معرفة المزيد من خلال الاطلاع على خصائص المرجع المضاف:
![]() |
- في [1]، إصدار DLL. يلزم الإصدار 5؛
- في [2]، مكانها في نظام الملفات: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll حيث <solution> هو مجلد الحل VS. ستنتقل جميع الحزم التي تمت إضافتها بواسطة NuGet إلى مجلد <solution>/packages؛
- في [3]، تم إنشاء ملف [packages.config]. ومحتواه كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>
وهو يسرد الحزم التي تم استيرادها بواسطة NuGet.
لنعد إلى المشروع VS وننشئ مجلدًا باسم [Models] داخل المشروع:
![]() |
- في [1]، إضافة مجلد إلى المشروع؛
- في [2]، سيُسمى [Models].
سنحافظ على هذه العادة لاحقًا بوضع تعريف كياناتنا في المجلد [Models].
لبناء كياناتنا، سنستعين بتعريف قاعدة البيانات MySQL 5 المستخدمة في المشروع NHibernate. لنتذكر دور الكيانات EF:
![]() |
يجب أن تعكس الكيانات جداول قاعدة البيانات. تستخدم طبقة الوصول إلى البيانات هذه الكيانات بدلاً من العمل مباشرة مع الجداول. لنبدأ بالجدول [MEDECINS]:
3.4.1. الكيان [Medecin]
تحتوي على معلومات عن الأطباء الذين تديرهم التطبيق [RdvMedecins].
![]() | ![]() |
- ID: رقم تعريف الطبيب - المفتاح الأساسي للجدول
- VERSION: رقم تعريف إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- NOM: اسم الطبيب
- PRENOM: اسمه الأول
- TITRE: لقبه (الآنسة، السيدة، السيد)
يمكننا البدء بالفئة [Medecin] التالية:
using System;
[Table("MEDECINS", Schema = "dbo")]
namespace RdvMedecins.Entites
{
public class Medecin
{
// data
public int Id { get; set; }
public string Titre { get; set; }
public string Nom { get; set; }
public string Prenom { get; set; }
}
- السطر 3: الفئة [Medecin] مرتبطة بالجدول [MEDECINS] في قاعدة البيانات. وسيكون هذا الجدول موجودًا في مخطط يسمى "dbo".
نضع هذه الفئة في ملف [Entites.cs] [1]. وهنا سنضع جميع كياناتنا.
![]() |
في المجلد [Models] نفسه، نقوم بإنشاء الملف [Context.cs] التالي:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// السياق
public class RdvMedecinsContext : DbContext
{
// الأطباء
public DbSet<Medecin> Medecins { get; set; }
}
// تهيئة قاعدة البيانات
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
}
}
- السطر 8: ستمثل الفئة [RdvMedecinsContext] سياق الاستمرارية، c.-à-d. مجموعة الكيانات التي تديرها ORM. يجب أن تكون مشتقة من الفئة [System.Data.Entity.DbContext]؛
- السطر 11: سيمثل الحقل [Medecins] الكيانات من النوع [Medecin] في سياق الاستمرارية. وهي من النوع DbSet<Medecin>. عادةً ما يكون عدد [DbSet] مساوياً لعدد الجداول في قاعدة البيانات، بواقع واحد لكل جدول؛
- السطر 15: يتم تعريف فئة [RdvMedecinsInitializer] لتهيئة قاعدة البيانات التي تم إنشاؤها. هنا، وهي مشتقة من فئة [DropCreateDataBaseAlways] التي، كما يشير اسمها، تحذف قاعدة البيانات إذا كانت موجودة بالفعل ثم تعيد إنشائها. وهذا مفيد في مرحلة تطوير BD. معلمة فئة [DropCreateDataBaseAlways] هي نوع سياق الاستمرارية المرتبط بالقاعدة. يمكن استخدام فئات أم أخرى غير [DropCreateDataBaseAlways] لفئة التهيئة:
- [DropCreateDatabaseIfModelChanges]: يعيد إنشاء قاعدة البيانات إذا تغيرت الكيانات،
- [CreateDatabaseIfNotExists]: تنشئ قاعدة البيانات إذا لم تكن موجودة؛
يبقى لنا إنشاء برنامج رئيسي. سيكون كما يلي [CreateDB_01.cs]:
using System;
using System.Data.Entity;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class CreateDB_01
{
static void Main(string[] args)
{
// إنشاء قاعدة البيانات
Database.SetInitializer(new RdvMedecinsInitializer());
using (var context = new RdvMedecinsContext())
{
context.Database.Initialize(false);
}
}
}
}
- السطر 12: [System.Data.Entity.DataBase] هي فئة توفر طرقًا ثابتة لإدارة قاعدة البيانات المرتبطة بسياق الاستمرارية. تسمح الطريقة الثابتة [SetInitializer] بتحديد فئة تهيئة قاعدة البيانات. هذا لا يطلق عملية التهيئة؛
- السطر 13: للعمل مع سياق ثبات، يجب إنشاء مثيل له. وهذا ما يتم هنا. نستخدم جملة using بحيث يتم إغلاق السياق تلقائيًا عند الخروج من الجملة. لذا، في السطر 17، يتم إغلاق السياق؛
- السطر 15: يتم تشغيل إنشاء قاعدة البيانات المرتبطة بسياق الاستمرارية [RdvMedecinsContext] بشكل صريح. يشير المعامل false إلى أنه لا يجب إجراء هذه العملية إذا كانت قد أجريت بالفعل لهذا السياق. هنا، كان من الممكن أيضًا استخدام true.
عند العمل مع قاعدة بيانات، يتم عادةً تسجيل معلمات الاتصال في الملف [App.config]. نلاحظ أنها غير موجودة فيه في الوقت الحالي:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- لمزيد من المعلومات حول تكوين Entity Framework، تفضل بزيارة http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
</configuration>
تم تسجيل العناصر المذكورة أعلاه في [App.config] عند إضافة تبعية Entity Framework إلى مراجع المشروع.
دعونا نقوم بتشغيل المشروع (Ctrl-F5) بعد تشغيل SQL Server Express (هذا أمر مهم):
![]() | ![]() |
يجب أن ينتهي التشغيل دون أخطاء. لنفتح الآن أداة إدارة SQL Server ونقوم بتحديث العرض:
![]() |
نلاحظ أنه تم إنشاء قاعدة بيانات تحمل الاسم الكامل للفئة [RdvMedecinsContext] وأنها تحتوي على جدول [dbo.MEDECINS] (وهذا هو الاسم الذي أطلقنا عليه) مع أعمدة تتضمن أسماء حقول الكيان [Medecin]. إذا تم تنفيذ الكود بشكل صحيح ولم تظهر قاعدة البيانات المذكورة أعلاه، فيجب التحقق من الخادم المدمج (localdb)\v11.0 (انظر الصفحة 19). مع VS 2012 pro، يتم استخدام هذا الخادم إذا لم يكن خادم SQL نشطًا عند تنفيذ الكود. أما مع VS 2012 Express، فلا.
دعونا نلقي نظرة على بنية الجدول [MEDECINS]:
- تتضمن أسماء حقول الكيان [Medecin]؛
- العمود [Id] هو المفتاح الأساسي. هذه هي قاعدة EF: إذا كان للكيان E حقل Id أو Eid (MedecinId)، فإن هذا العمود يكون المفتاح الأساسي في الجدول المرتبط؛
- أنواع أعمدة الجدول هي أنواع حقول الكيان؛
- بالنسبة للأعمدة العنوان، الاسم، الاسم الأول، تم استخدام نوع [nvarchar(max)]. يمكن أن نكون أكثر دقة، 5 أحرف للعنوان، 30 حرفًا للاسم والاسم الأول؛
- يمكن أن يكون للعمود "العنوان" و"الاسم" و"الاسم الأول" القيمة NULL. سنقوم بتغيير ذلك.
لنلقِ نظرة على خصائص المفتاح الأساسي [Id]:
![]() |
في [1]، نرى أن المفتاح الأساسي من النوع [Identité]، مما يعني أن قيمته يتم إنشاؤها تلقائيًا بواسطة خادم SQL. سنعتمد هذه الاستراتيجية مع جميع SGBD.
سنقلل من الاعتماد على قواعد EF باستخدام التعليقات التوضيحية. يصبح رمز الكيان في [Entites.cs] كما يلي:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// البيانات
[Key]
[Column("ID")]
public int Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Required]
[Column("VERSION")]
public int Version { get; set; }
}
}
- السطران 2 و 3: توجد التعليقات التوضيحية في مساحات الأسماء [System.ComponentModel.DataAnnotations] (Key، Required، MaxLength] و [System.ComponentModel.DataAnnotations.Schema] (Column). توجد تعليقات أخرى في URL و [http://msdn.microsoft.com/en-us/data/gg193958.aspx]؛
- السطر 11: [Key] يشير إلى المفتاح الأساسي؛
- السطر 12: [Column] يحدد اسم العمود المطابق للحقل؛
- السطر 14: [Required] يشير إلى أن الحقل إلزامي (SQL NOT NULL)؛
- السطر 15: [MaxLength] يحدد الحجم الأقصى لسلسلة الأحرف، و [MinLength] يحدد الحجم الأدنى لها؛
لنقم بتنفيذ المشروع باستخدام هذا التعريف الجديد للكيان [Medecin]. تكون القاعدة التي تم إنشاؤها عندئذٍ كما يلي:
![]() |
- الأعمدة تحمل الأسماء التي حددناها لها؛
- تمت ترجمة التعليق التوضيحي [Required] إلى SQL NOT NULL؛
- تمت ترجمة التعليق التوضيحي [MaxLength(N)] إلى نوع SQL nvarchar(N).
في التطبيق NHibernate، كان العمود [VERSION] موجودًا لمنع الوصول المتزامن إلى نفس السطر في الجدول. والمبدأ هو كما يلي:
- تقوم عملية P1 بقراءة سطر L من الجدول [MEDECINS] في الوقت T1. السطر له الإصدار V1؛
- تقوم العملية P2 بقراءة نفس السطر L من الجدول [MEDECINS] في الوقت T2. السطر له الإصدار V1 لأن العملية P1 لم تقم بعد بالتحقق من صحة تعديلها؛
- تقوم العملية P1 بالتحقق من صحة تعديلها للسطر L. ثم تتغير إصدارة السطر L إلى V2=V1+1؛
- تقوم العملية P2 بالتحقق من صحة تعديلها للسطر L. ثم تطلق العملية ORM استثناءً لأن العملية P2 لديها إصدار V1 للسطر L يختلف عن الإصدار V2 الموجود في قاعدة البيانات.
يُطلق على ذلك اسم الإدارة المتفائلة للوصول المتزامن. مع EF 5، يجب أن يكون للحقل الذي يؤدي هذا الدور أحد السمتين [Timestamp] أو [ConcurrencyCheck]. SQL Server له نوع [timestamp]. يتم إنشاء قيمة العمود الذي له هذا النوع تلقائيًا بواسطة SQL Server عند أي إدراج/تعديل لسطر. يمكن عندئذٍ استخدام هذا العمود لإدارة التنافس على الوصول. لاستئناف المثال السابق، ستجد العملية P2 timestamp مختلفًا عن الذي قرأته، لأن التعديل الذي أجرته العملية P1 في غضون ذلك سيكون قد غيّره.
تتطور كياننا [Medecin] على النحو التالي:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// بيانات
[Key]
[Column("ID")]
public int Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
}
- السطور 26-28: العمود الجديد مع السمة [Timestamp] في السطر 27. يجب أن يكون نوع الحقل byte[] (السطر 28). يمكن أن يكون اسم الحقل أي اسم. لا نضع له السمة [Required] لأن التطبيق لن يوفر هذه القيمة بل SGBD نفسه.
إذا تم تنفيذ المشروع باستخدام هذه الكيان الجديد، تتطور قاعدة البيانات على النحو التالي:
![]() |
يبقى أمامنا نقطة أخيرة يجب تسويتها. «يعرف» سياق الاستمرارية أن الكيان يجب إدراجه في قاعدة البيانات لأن مفتاحه الأساسي يساوي «null» في تلك المرحلة. وعملية الإدراج في قاعدة البيانات هي التي ستعطي قيمة للمفتاح الأساسي. هنا، النوع int الممنوح للمفتاح الأساسي [Id] غير مناسب لأن هذا النوع لا يقبل القيمة null. لذلك، يتم إعطاؤه النوع int؟ الذي يقبل القيم int بالإضافة إلى المؤشر null. وبالتالي، ستكون الكيان [Medecin] المستخدم كما يلي:
public class Medecin
{
// بيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
...
يبقى لنا أن نرى كيف يمكن تمثيل مفهوم المفتاح الأجنبي بين الجداول في كيان.
3.4.2. الكيان [Creneau]
تسرد الجدول [CRENEAUX] الفترات الزمنية التي يمكن فيها استخدام RV:
![]() |
![]() |
- ID: رقم يحدد الفترة الزمنية - المفتاح الأساسي للجدول
- VERSION: الرقم الذي يحدد إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- ID_MEDECIN: رقم يحدد الطبيب الذي ينتمي إليه هذا الموعد – مفتاح خارجي في العمود MEDECINS(ID).
- HDEBUT: وقت بدء الفترة الزمنية
- MDEBUT: دقائق بداية الفترة
- HFIN: ساعة انتهاء الفترة الزمنية
- MFIN: دقائق نهاية الفترة الزمنية
تشير السطر الثاني من الجدول [CRENEAUX] (انظر [1] أعلاه) ، على سبيل المثال ، إلى أن الفترة رقم 2 تبدأ في الساعة 8:20 وتنتهي في الساعة 8:40 وتخص الطبيب رقم 1 (السيدة ماري PELISSIER).
بناءً على ما نعرفه، يمكننا تعريف الكيان [Creneau] على النحو التالي في [Entites.cs]:
[Table("CRENEAUX", Schema = "dbo")]
public class Creneau
{
// بيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[Column("HDEBUT")]
public int Hdebut { get; set; }
[Required]
[Column("MDEBUT")]
public int Mdebut { get; set; }
[Required]
[Column("HFIN")]
public int Hfin { get; set; }
[Required]
[Column("MFIN")]
public int Mfin { get; set; }
[Required]
public virtual Medecin Medecin { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
يكمن التغيير الوحيد في السطرين 20-21. إن حقيقة أن الجدول [CRENEAUX] يحتوي على مفتاح خارجي في الجدول [MEDECINS] تنعكس في الكيان [Creneau] من خلال وجود مرجع إلى الكيان [Medecin]، السطر 21. لا يهم اسم الحقل، بل النوع هو المهم فقط. يجب إعلان الخاصية على أنها افتراضية باستخدام الكلمة الرئيسية virtual. في الواقع، سيقوم EF بإعادة تعريف جميع الخصائص التي تسمى خصائص التنقل، أي تلك التي تتوافق مع مفتاح خارجي وتسمح بالانتقال من جدول إلى آخر.
لاختبار الكيان الجديد، يتعين علينا إجراء بعض التعديلات في [Context.cs]:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// السياق
public class RdvMedecinsContext : DbContext
{
// الكيانات
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
}
// تهيئة قاعدة البيانات
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
}
}
يعكس السطر 12 حقيقة أن السياق يحتوي على كيان إضافي يجب إدارته. عند تنفيذ المشروع، نحصل على قاعدة البيانات الجديدة التالية:
![]() |
تم إنشاء الجدول [CRENEAUX] بالفعل، والجديد هو وجود مفتاحين خارجيين [1] و [2]. تم إنشاء اسمها من اسم الحقل المقابل في الكيان (Medecin) مع إضافة اللاحقة "_Id". لمعرفة خصائص هذا المفتاح الأجنبي، نحاول تعديله إلى [3].
![]() |
تُظهر لقطة الشاشة أعلاه أن [Medecin_Id] هو مفتاح أجنبي للجدول [CRENEAUX] وأنه يشير إلى المفتاح الأساسي [ID] للجدول [MEDECINS].
إذا تم إنشاء الكيانات لقاعدة بيانات موجودة، فلن يُسمى عمود المفتاح الأجنبي بالضرورة [Medecin_Id]. بالنسبة للأعمدة الأخرى، رأينا أن التعليق التوضيحي [Column] يحل هذه المشكلة. الغريب أن الأمر أكثر تعقيدًا بالنسبة للمفتاح الأجنبي. يجب اتباع الخطوات التالية:
public class Creneau
{
// البيانات
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
- الأسطر 5-7: ننشئ حقلًا من نوع المفتاح الأجنبي (int). باستخدام السمة [Column]، نحدد اسم العمود الذي سيكون المفتاح الأجنبي في الجدول المرتبط بالكيان؛
- السطر 9: نضيف التعليق التوضيحي [ForeignKey] إلى الحقل من النوع [Medecin]. وسيكون حجة هذا التعليق التوضيحي هو اسم الحقل (وليس اسم العمود) المرتبط بعمود المفتاح الأجنبي في الجدول.
يؤدي تنفيذ المشروع هذه المرة إلى إنشاء الجدول التالي:
![]() |
فيما سبق، تحمل عمود المفتاح الأجنبي الاسم الذي أطلقنا عليه. تجدر الإشارة إلى أن الحقول:
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
لم ينتج عنها سوى عمود واحد، وهو العمود [MEDECIN_ID]. ومع ذلك، فإن وجود الحقل [MedecinId] مهم. عند قراءة سطر من الجدول [CRENEAUX]، سيتلقى قيمة العمود [MEDECIN_ID]، أي قيمة المفتاح الأجنبي في الجدول [MEDECINS]. وهذا مفيد في كثير من الأحيان.
يعكس الحقل [Medecin] أعلاه العلاقة متعددة إلى واحد التي تربط الكيان [Creneau] بالكيان [Medecin]. ترتبط عدة كائنات [Creneau] بنفس الكائن [Medecin]. يمكن تمثيل العلاقة العكسية، حيث يرتبط كائن [Medecin] بعدة كائنات [Creneau]، بواسطة حقل إضافي في الكيان [Medecin]:
public class Medecin
{
// البيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
...
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
في السطر 8، تمت إضافة الحقل [Creneaux] الذي يمثل مجموعة من الكائنات [Creneau]. سيتيح لنا هذا الحقل الوصول إلى جميع المواعيد المتاحة للطبيب.
عند إعادة تشغيل المشروع، نلاحظ أن الجدول [MEDECINS] لم يتغير:
![]() |
لم تتم إضافة أي عمود. العلاقة بين المفتاح الأجنبي الموجودة بين الجدول [CRENEAUX] والجدول [MEDECINS] كافية لكي يتمكن EF من إنشاء الحقول المرتبطة بها:
public class Medecin
{
...
public ICollection<Creneau> Creneaux { get; set; }
...
}
public class Creneau
{
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
نحن نعرف الأساسيات. يمكننا الانتهاء من إنشاء الكيانين الآخرين.
3.4.3. الكيانات [Client] و [Rv]
بناءً على ما تعلمناه، يمكننا كتابة الكيانين [Client] و [Rv]. يحتوي الكيان [Client] على معلومات حول العملاء الذين تديرهم التطبيق [RdvMedecins].
![]() | ![]() |
- ID: رقم تعريف العميل - المفتاح الأساسي للجدول
- VERSION: رقم تعريف إصدار السطر في الجدول. يتم زيادة هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تعديل على السطر.
- NOM: اسم العميل
- PRENOM: اسمه الأول
- TITRE: لقبه (آنسة، سيدة، سيد)
قد تكون الكيان [Client] كما يلي:
[Table("CLIENTS", Schema = "dbo")]
public class Client
{
// البيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// مواعيد العميل
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
الفئة [Client] تكاد تكون مطابقة للفئة [Medecin]. يمكن اشتقاقهما من نفس الفئة الأم. الجديد هو السطر 21. وهو يعكس حقيقة أن العميل قد يكون لديه عدة مواعيد ويشتق من وجود مفتاح خارجي من الجدول [RVS] إلى الجدول [CLIENTS].
تمثل الكيان [Rv] موعدًا:
![]() |
- ID: رقم يحدد RV بشكل فريد – مفتاح أساسي
- JOUR: يوم RV
- ID_CRENEAU: الفترة الزمنية لـ RV - مفتاح خارجي في العمود [ID] من الجدول [CRENEAUX] – يحدد في آن واحد الفترة الزمنية والطبيب المعني.
- ID_CLIENT: رقم العميل الذي تم الحجز له – مفتاح خارجي في العمود [ID] من الجدول [CLIENTS]
قد تكون الكيان [Rv] كما يلي:
[Table("MEDECINS", Schema = "dbo")]
public class Rv
{
// البيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[Column("JOUR")]
public DateTime Jour { get; set; }
[Column("CLIENT_ID")]
public int ClientId { get; set; }
[ForeignKey("ClientId")]
[Required]
public virtual Client Client { get; set; }
[Column("CRENEAU_ID")]
public int CreneauId { get; set; }
[ForeignKey("CreneauId")]
[Required]
public virtual Creneau Creneau { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
- السطور 5-7: المفتاح الأساسي؛
- السطور 8-10: تاريخ الموعد؛
- السطور 11-12: المفتاح الخارجي من الجدول [RVS] إلى الجدول [CLIENTS]؛
- الأسطر 13-15: العميل الذي لديه موعد؛
- السطور 16-17: المفتاح الأجنبي من الجدول [RVS] إلى الجدول [CRENEAUX]؛
- الأسطر 18-20: الفترة الزمنية للموعد؛
- السطور 21-23: حقل إدارة الوصول المتزامن.
في السطر 17، نرى علاقة متعددة إلى واحد: يمكن أن يتوافق مع فترة زمنية واحدة عدة مواعيد (ليست في نفس اليوم). يمكن أن تنعكس العلاقة العكسية في الكيان [Creneau]:
public class Creneau
{
// مواعيد الفئة
public ICollection<Rv> Rvs { get; set; }
...
}
السطر 4، مجموعة المواعيد المحددة في هذا النطاق الزمني.
عند تنفيذ المشروع، تكون قاعدة البيانات التي تم إنشاؤها كما يلي:
![]() |
لم تتغير الجداول [MEDECINS] و [CRENEAUX]. الجداول [CLIENTS] و [RVS] هي كما يلي:
![]() | ![]() |
هذا ما كان متوقعًا. لا يزال لدينا بعض التفاصيل التي يجب تسويتها:
- إدارة اسم قاعدة البيانات. هنا تم إنشاؤه بواسطة EF؛
- ملء قاعدة البيانات بالبيانات.
3.4.4. تحديد اسم قاعدة البيانات
لتحديد اسم قاعدة البيانات التي تم إنشاؤها بواسطة EF، سنستخدم سلسلة اتصال محددة في [App.config]. يتطور ملف التكوين هذا على النحو التالي:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- لمزيد من المعلومات حول تكوين Entity Framework، تفضل بزيارة http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
<!-- سلسلة الاتصال بالقاعدة -->
<connectionStrings>
<add name="RdvMedecinsContext"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- مزود المصنع -->
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider"
invariant="System.Data.SqlClient"
description=".Net Framework Data Provider for SqlServer"
type="System.Data.SqlClient.SqlClientFactory, System.Data,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/>
</DbProviderFactories>
</system.data>
</configuration>
- الأسطر 15-19: سلسلة الاتصال بالقاعدة؛
- السطر 16: السمة [name] تستعيد اسم الفئة [RdvMedecinsContext] المستخدمة لسياق الاستمرارية. من المهم تذكر ذلك. يمكن تجاوز هذا القيد في منشئ السياق:
// المنشئ
public RdvMedecinsContext()
: base("monContexte")
{
}
في هذه الحالة، يمكن أن يكون name= "monContexte ". وهذا ما سنحصل عليه في بقية المستند.
- السطر 17: سلسلة الاتصال. [Data Source]: اسم الخادم الذي يوجد عليه SGBD، [Initial Catalog]: اسم قاعدة البيانات، أي هنا [rdvmedecins-ef]، [User Id]: مالك الاتصال، [Password]: كلمة مروره. سيقوم القارئ بتكييف هذه السلسلة مع بيئته؛
- السطور 21-29: تحدد [DbProviderFactory]. لا أعرف ما هو. إذا صدقت الاسم، فقد تكون فئة تسمح بإنشاء الطبقة [ADO.NET] التي تفصل EF عن SGBD:
![]() |
في الواقع، هذه الأسطر غير ضرورية لـ SQL Server لكنني اضطررت لإضافتها من أجل SGBD الأخرى. لذا أضعها هنا للتذكير فقط. فهي لا تسبب أي إزعاج. النقطة المهمة الوحيدة هي إصدار السطر 27. إنه إصدار DLL [System.Data] الموجود في مراجع المشروع:
![]() |
ها نحن ذا. نحن جاهزون. نقوم بتنفيذ المشروع ونحصل على قاعدة [rdvmedecins-ef] التالية:
![]() |
ستكون هذه قاعدتنا النهائية. يبقى لنا إدخال البيانات فيها.
3.4.5. ملء قاعدة البيانات
يمكن استخدام فئة تهيئة قاعدة البيانات لإدخال البيانات فيها:
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
// تهيئة قاعدة البيانات
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
protected override void Seed(RdvMedecinsContext context)
{
base.Seed(context);
// يتم تهيئة قاعدة البيانات
// العملاء
Client[] clients ={
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
foreach (Client client in clients)
{
context.Clients.Add(client);
}
// الأطباء
Medecin[] medecins ={
new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
};
foreach (Medecin medecin in medecins)
{
context.Medecins.Add(medecin);
}
// المواعيد
Creneau[] creneaux ={
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
};
foreach (Creneau creneau in creneaux)
{
context.Creneaux.Add(creneau);
}
// المواعيد
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
}
}
}
- السطر 6: تتم التهيئة في الطريقة [Seed]. هذه الطريقة موجودة في الفئة الأم. وهي معاد تعريفها هنا. الحجة هي سياق الاستمرارية [RdvMedecinsContext] للتطبيق؛
- السطر 8: يتم تمرير الحجة إلى الفئة الأم؛ ومن المحتمل أن تفتح هذه الفئة سياق الاستمرارية الذي تم تمريره إليها لأن هذا الفتح لم يعد ضروريًا بعد ذلك؛
- الأسطر 11-16: إنشاء 4 عملاء؛
- الأسطر 17-20: تتم إضافة هؤلاء إلى سياق الاستمرارية، وبشكل أكثر دقة إلى الأطباء التابعين له. تجدر الإشارة إلى الطريقة [Add] التي تسمح بذلك. يجب هنا تذكر تعريف السياق:
public class RdvMedecinsContext : DbContext
{
// الكيانات
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...
يُقال أيضًا إن العملاء قد تم ربطهم بالسياق، أي أنهم يُدارون الآن بواسطة EF. في السابق، كانوا غير مرتبطين به. كانوا موجودين ككائنات ولكن لم تكن تُدار بواسطة EF؛
- الأسطر 21-27: إنشاء 4 أطباء؛
- الأسطر 28-31: يتم وضعهم في سياق الاستمرارية؛
- الأسطر 33-70: إنشاء فترات زمنية. الأسطر 34-57، للطبيب medecins[0]، الأسطر 58-69، للطبيب medecins[1]. الأطباء الآخرون ليس لديهم فترات زمنية؛
- السطور 71-74: يتم وضع هذه الفترات الزمنية في سياق الاستمرارية؛
- السطر 76: إنشاء موعد للعميل الأول مع الموعد الأول ووضعه في سياق الاستمرارية.
عند تنفيذ المشروع، نحصل على القاعدة التالية:
![]() | ![]() |
فيما سبق، نرى الجدول [CLIENTS] مملوءًا.
3.4.6. تعديل الكيانات
حاليًا، الفئتان [Medecin] و [Client] متطابقتان تقريبًا. في الواقع، إذا أزلنا الحقول المضافة لإدارة الاستمرارية مع EF 5، فستكونان متطابقتين. سنقوم بجعلها مشتقة من فئة [Personne]. وبذلك تصبح هاتان الكيانات كما يلي:
// شخص
public abstract class Personne
{
// البيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// التوقيع
public override string ToString()
{
return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// التوقيع المختصر
public string ShortIdentity()
{
...
}
// الأداة
private string dump(byte[] timestamp)
{
...
}
}
[Table("MEDECINS", Schema = "dbo")]
public class Medecin : Personne
{
// مواعيد الطبيب
public ICollection<Creneau> Creneaux { get; set; }
// التوقيع
public override string ToString()
{
return String.Format("Medecin {0}", base.ToString());
}
}
[Table("CLIENTS", Schema = "dbo")]
public class Client : Personne
{
// مواعيد العميل
public ICollection<Rv> Rvs { get; set; }
// التوقيع
public override string ToString()
{
return String.Format("Client {0}", base.ToString());
}
}
عند تنفيذ المشروع، نحصل على نفس القاعدة. قامت EF 5 بتعيين الفئات الأدنى في التسلسل الهرمي لكل منها في جدول. في الواقع، لدى EF 5 استراتيجيات مختلفة لتوليد الجداول لتمثيل وراثة الكيانات. لن نعرضها هنا. يمكن قراءة " Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type" على سبيل المثال، في URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].
سنستخدم من الآن فصاعدًا هذه النسخة من الكيانات.
3.4.7. إضافة قيود إلى قاعدة البيانات
لا يزال هناك تفصيل واحد يتعين علينا تسويته. الجدول [RVS] الخاص بالمواعيد هو كما يلي:
![]() |
يجب أن يكون لهذه الجدولة قيد فريدية: في يوم معين، لا يمكن حجز فترة زمنية لطبيب إلا مرة واحدة لموعد واحد. من حيث الجدولة، هذا يعني أن الزوج (JOUR,CRENEAU_ID) يجب أن يكون فريداً. لا أعرف ما إذا كان يمكن التعبير عن هذا القيد مباشرة في الكود، سواء على الكيانات أو على السياق. هذا محتمل لكنني لم أتحقق من ذلك. سنتبع نهجًا آخر. سنستخدم عميل إدارة خادم SQL لإضافة هذا القيد.
باستخدام "SQL Server Management Studio"، لم أجد طريقة بسيطة لإضافة هذا القيد سوى تنفيذ الأمر SQL الذي ينشئه:
![]() |
- في [1]، يتم إنشاء استعلام SQL لقاعدة البيانات [rdvmedecins-ef]؛
- في [2]، الاستعلام SQL الذي ينشئ قيد التفرد؛
- في [3]، أدى تنفيذ هذا الاستعلام إلى إنشاء فهرس جديد في الجدول [RVS].
توجد أدوات أخرى لإدارة خادم SQL. سنستخدم هنا أداة EMS SQL Manager for SQL Server Freeware [http://www.sqlmanager.net/fr/products/mssql/manager/download]. بمجرد تثبيتها، نقوم بتشغيلها:
![]() |
- في [1]، نقوم بتسجيل قاعدة بيانات؛
- في [2]، نقوم بالاتصال بالخادم (المحلي)؛
- في [3]، باستخدام مصادقة SQL Server؛
- في [4]، تحت الهوية sa؛
- في [5]، وكلمة المرور sqlserver2012؛
- في [6]، ننتقل إلى الخطوة التالية؛
![]() |
- في [7]، نختار قاعدة البيانات [rdvmedecins-ef]؛
- في [8]، ننهي المعالج؛
- في [9]، تظهر قاعدة البيانات في شجرة قواعد البيانات. نقوم بالاتصال بها [10]؛
- في [11]، يتم الاتصال.
يتيح "SQL Manager Lite for SQL Server" إنشاء قيد التفرد على الجدول [RVS].
![]() |
- في [1]، نرى قيد التفرد الذي أنشأناه سابقًا؛
- في [2]، نقوم بحذفها؛
- في [3]، اختفى الفهرس المطابق لشرط التفرد هذا.
نعيد إنشاء القيد المحذوف:
![]() |
- في [1]، نقوم بإنشاء فهرس جديد للجدول [RVS]؛
- في [2]، نمنحه اسمًا؛
- في [3]، وهو قيد تفرد؛
- في [4]، على الأعمدة JOUR و CRENEAU_ID؛
تعطينا علامة التبويب DDL الرمز SQL الذي سيتم تنفيذه:
![]() |
- في [6]، يتم تجميع الأمر SQL؛
![]() |
- إلى [7]، يتم التأكيد؛
- إلى [8]، ظهر المؤشر الجديد.
الواجهة التي يوفرها "SQL Manager Lite for SQL server" مشابهة لتلك التي يوفرها "SQL Server Management Studio". يمكن العثور على واجهات مشابهة لـ SGBD Oracle و PostgreSQL و Firebird و MySQL. لذلك سنواصل من الآن فصاعدًا مع هذه المجموعة من أدوات إدارة SGBD.
للوصول إلى المعلومات الموجودة في جدول ما، ما عليك سوى النقر عليه مرتين:
![]() |
تتوفر المعلومات المتعلقة بالجدول المحدد في علامات التبويب. في الأعلى، نرى علامة التبويب [Fields] الخاصة بالجدول [CLIENTS]. تعرض علامة التبويب [Data] محتوى الجدول:

3.4.8. القاعدة النهائية
لدينا قاعدتنا النهائية. نقوم بتصدير البرنامج النصي الخاص بها SQL حتى نتمكن من إعادة إنشائها إذا لزم الأمر.
![]() |
- إلى [1]، بداية المساعد؛
- إلى [2]، الخادم؛
- إلى [3]، قاعدة البيانات التي سيتم تصديرها؛
![]() |
- في [4]، حدد اسم الملف الذي سيتم فيه حفظ البرنامج النصي SQL؛
- في [5]، حدد الترميز؛
- في [6]، حدد ما تريد استخراجه (الجداول، القيود، البيانات)؛
![]() |
- في [7]، يمكنك تحسين البرنامج النصي الذي سيتم إنشاؤه؛
- في [8]، أكمل المساعد.
تم إنشاء البرنامج النصي وتحميله في محرر البرامج النصية. يمكنك الاطلاع على الكود SQL الذي تم إنشاؤه. سنقوم بإعادة بناء قاعدة البيانات باستخدام هذا البرنامج النصي.
![]() |
- في [1]، يتم حذف قاعدة البيانات؛
- في [2] و [3]، نعيد إنشائها؛
![]() |
- في [4]، يتم المصادقة؛
- في [5]، يتم تنفيذ البرنامج النصي SQL لإنشاء قاعدة البيانات؛
![]() |
- في [6]، يتم تسجيلها في "SQL Manager"؛
- في [7]، يتم الاتصال بالقاعدة التي تم إنشاؤها للتو؛
![]() |
- في [8]، لا تحتوي قاعدة البيانات في الوقت الحالي على أي جداول؛
- في [9a]، نفتح محرر نصوص SQL؛
![]() |
- في [9b]، يتم فتح البرنامج النصي SQL الذي تم إنشاؤه مسبقًا؛
- في [10]، نقوم بتنفيذه؛
![]() |
- في [11]، تم إنشاء الجداول؛
- في [12]، تم ملؤها؛
![]() |
- في [14]، نجد قيد التفرد الذي أنشأناه للجدول [RVS].
سنعمل الآن مع قاعدة البيانات الحالية هذه. إذا تم إتلافها أو تلفها، فإننا نعرف كيفية إعادة إنشائها.
3.5. استخدام قاعدة البيانات مع Entity Framework
سنقوم بما يلي:
- إضافة عناصر إلى قاعدة البيانات وحذفها وتعديلها؛
- الاستعلام عن قاعدة البيانات باستخدام LINQ to Entities؛
- إدارة الوصول المتزامن إلى نفس عنصر في قاعدة البيانات؛
- فهم مفاهيم Lazy Loading / Eager Loading؛
- نكتشف أن تحديث قاعدة البيانات من خلال سياق الاستمرارية يتم في معاملة واحدة.
3.5.1. حذف عناصر من سياق الاستمرارية
لدينا قاعدة بيانات ممتلئة. سنقوم بإفراغها. نقوم بإنشاء فئة جديدة [Erase.cs] في المشروع الحالي [1]:
![]() |
الفئة [Erase] هي كما يلي:
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Erase
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// إفراغ قاعدة البيانات الحالية
// العملاء
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
// الأطباء
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// حفظ سياق الاستمرارية
context.SaveChanges();
}
}
}
}
- السطر 9: تتم العمليات في سياق الاستمرارية دائمًا في جملة [using]. وهذا يضمن إغلاق السياق عند الخروج من [using]؛
- السطر 13: يتم استعراض سياق العملاء [context.Clients]. سيتم وضع جميع عملاء قاعدة البيانات في سياق الاستمرارية؛
- السطر 15: لكل منهم، يتم تنفيذ العملية [Remove] التي تحذفهم من السياق. في الواقع، لا يزالون موجودين في السياق ولكن في حالة "محذوف"؛
- الأسطر 18-21: نقوم بنفس الشيء بالنسبة للأطباء؛
- السطر 23: يتم حفظ سياق الاستمرارية في قاعدة البيانات.
عند حفظ السياق في قاعدة البيانات، فإن كيانات السياق التي:
- لديها مفتاح أساسي فارغ تخضع لعملية SQL INSERT؛
- في حالة "محذوفة" تخضع لعملية SQL DELETE؛
- في حالة "تم تعديلها" تخضع لعملية SQL UPDATE؛
كما سنلاحظ لاحقًا، تتم هذه العمليات SQL داخل معاملة واحدة. إذا فشلت إحداها، يتم التراجع عن كل ما تم إنجازه سابقًا.
لنجعل البرنامج [Erase] الكائن الجديد لبدء المشروع [1] ثم نقوم بتنفيذ المشروع.
![]() |
دعونا نتحقق من قاعدة البيانات. سنلاحظ أن جميع الجداول فارغة [2]. هذا أمر مثير للدهشة، لأننا طلبنا ببساطة حذف الأطباء والعملاء. من خلال تفاعل المفاتيح الخارجية، تم إفراغ الجداول الأخرى بشكل متسلسل.
تم تعريف المفتاح الأجنبي للجدول [CRENEAUX] إلى الجدول [MEDECINS] على النحو التالي بواسطة مزود EF 5:
![]() |
- في [1]، يتم تحديد الجدول [CRENEAUX]؛
- في [2]، يتم تحديد علامة تبويب المفاتيح الخارجية؛
- في [3]، يتم تحرير المفتاح الأجنبي الوحيد؛
![]() |
- في [4]، في علامة التبويب DDL، تعريف SQL لقيد المفتاح الأجنبي؛
- في [5]، تؤدي الشروط ON وDELETE وCASCADE إلى أن حذف طبيب ما يؤدي إلى حذف المواعيد المرتبطة به.
يتم تعريف قيود المفاتيح الخارجية للجدول [RVS] بطريقة مماثلة:
- السطور 1-6: سيؤدي حذف عميل إلى حذف المواعيد المرتبطة به أيضًا؛
- السطور 1-6: سيؤدي حذف فترة زمنية إلى حذف جميع المواعيد المرتبطة بها أيضًا.
3.5.2. إضافة عناصر إلى سياق الاستمرارية
الآن بعد أن قمنا بإفراغ قاعدة البيانات، سنقوم بملئها من جديد. نضيف إلى المشروع البرنامج [Fill.cs] [1].
![]() |
البرنامج [Fill.cs] هو التالي:
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Fill
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// إفراغ قاعدة البيانات الحالية
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// إعادة تهيئتها
// العملاء
Client[] clients ={
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
foreach (Client client in clients)
{
context.Clients.Add(client);
}
// الأطباء
Medecin[] medecins ={
new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
};
foreach (Medecin medecin in medecins)
{
context.Medecins.Add(medecin);
}
// المواعيد
Creneau[] creneaux ={
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
};
foreach (Creneau creneau in creneaux)
{
context.Creneaux.Add(creneau);
}
// المواعيد
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// نحفظ سياق الاستمرارية
context.SaveChanges();
}
}
}
}
- السطر 10: نفتح سياق الاستمرارية؛
- الأسطر 13-20: يتم وضع أسطر الجداول [CLIENTS] و [MEDECINS] في السياق ثم حذفها منه. لقد رأينا للتو أن هذا أدى إلى إفراغ قاعدة البيانات بالكامل؛
- الأسطر 22-88: تتم إضافة عناصر إلى سياق الاستمرارية. جميعها تحتوي على مفتاح أساسي بقيمة null. وبالتالي سيتم إدراجها في قاعدة البيانات؛
- السطر 90: تتم مزامنة التغييرات التي تم إجراؤها على السياق مع قاعدة البيانات. وستخضع هذه القاعدة لسلسلة من العمليات SQL DELETE تليها سلسلة من العمليات SQL INSERT؛
نجعل البرنامج [Fill] هو الكائن الجديد لبدء تشغيل المشروع [1] ثم نقوم بتنفيذ هذا الأخير.
![]() |
نلاحظ في [2] أن الجداول قد تم ملؤها.
3.5.3. عرض محتوى قاعدة البيانات
سنقوم الآن بعرض محتوى قاعدة البيانات باستخدام استعلامات LINQ to Entity. ظهر LINQ (Language INtegrated Query) مع إطار العمل .NET 3.5 في عام 2007. ويظهر كامتداد للغات .NET و c.a.d حيث يتم دمجه في اللغة ويتم التحقق من صياغته بواسطة المُجمِّع. وهي تسمح بالاستعلام عن مجموعات مختلفة باستخدام بناء جملة يشبه لغة SQL (لغة الاستعلام الهيكلية) الخاصة بالاستعلام عن قواعد البيانات. توجد إصدارات مختلفة من LINQ:
- LINQ to Object، لاستعلام المجموعات الموجودة في الذاكرة؛
- LINQ to XML، لاستعلام XML؛
- LINQ إلى Entity، لاستعلام قواعد البيانات؛
لكي يوجد، يعتمد LINQ على العديد من الامتدادات التي تمت إضافتها إلى لغات .NET. يمكن استخدام هذه الامتدادات خارج LINQ. لن نقوم بعرضها هنا، بل سنكتفي بإعطاء مرجعين حيث سيجد القارئ وصفًا مفصلاً لـ LINQ:
- LINQ in Action، فابريس مارغري، ستيف إيشرت، جيم وولي، منشورات مانينغ؛
- LINQ pocket reference، جوزيف وبن ألباهاري، من دار نشر أورايلي.
لقد قرأت الكتاب الأول ووجدته ممتازًا. لم أقرأ الكتاب الثاني، لكنني قرأت لكتابيه "C# 3.0 in a nutshell" عند صدور LINQ. لقد وجدت هذا الكتاب أعلى بكثير من متوسط الكتب التي اعتدت قراءتها. يبدو أن الكتب الأخرى لهذين المؤلفين هي من نفس المستوى. سنستخدم أيضًا LINQPad، وهو أداة تعليمية لـ LINQ كتبها جوزيف ألباهاري.
سنقوم بعرض الكيانات الموجودة في قاعدة البيانات. وللقيام بذلك، نضيف إلى فئاتها طريقتين للعرض. لنبدأ بالكيان [Medecin]:
// طبيب
public class Medecin
{
// البيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// المواعيد المتاحة للطبيب
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// التوقيع
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// توقيع مختصر
public string ShortIdentity()
{
return ToString();
}
// أداة
private string dump(byte[] timestamp){
string str = "";
foreach (byte b in timestamp)
{
str += b;
}
return str;
}
}
- الأسطر 27-30: طريقة ToString للفئة. لاحظ أنها لا تعرض المجموعة الموجودة في السطر 21؛
- الأسطر 32-37: الطريقة ShortIdentity التي تقوم بنفس الشيء.
علينا هنا شرح مفهومي التحميل المتأخر (Lazy Loading) والتحميل الفوري (Eager Loading) لتقييم تأثير الطريقتين السابقتين. لقد رأينا أن كيانًا ما قد يكون له تبعيات على كيان آخر. وهذه التبعيات من نوعين:
- من واحد إلى عدة، كما هو موضح أعلاه حيث يرتبط طبيب بعدة فترات زمنية؛
- من عدة إلى واحد، كما في الكيان [Creneau] أدناه حيث ترتبط عدة فترات زمنية بنفس الطبيب؛
public class Creneau
{
// البيانات
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
عندما يتم تحميل التبعيات في نفس الوقت مع الكيانات المرتبطة بها، نسمي ذلك Eager Loading. وإلا، فإننا نتحدث عن التحميل المتأخر (Lazy Loading): لا يتم تحميل التبعيات إلا عند الإشارة إليها لأول مرة. بشكل افتراضي، يستخدم EF 5 التحميل المتأخر: لا يتم تحميل التبعيات في نفس الوقت مع الكيان.
لنلقِ نظرة على طريقتنا [ToString] أعلاه:
// مواعيد الطبيب
public ICollection<Creneau> Creneaux { get; set; }
// التوقيع
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// توقيع مختصر
public string ShortIdentity()
{
return ToString();
}
لا تعرض الطريقة [ToString] التبعية [Creneaux] في السطر 2. لو فعلت ذلك، لكان ذلك قد فرض تحميل جميع مواعيد الطبيب قبل تنفيذها. ولتجنب هذا التحميل المكلف، لم يتم تضمين التبعية في توقيع الكيان. بشكل عام، سنقوم بتضمين توقيعين في كل كيان:
- طريقة ToString التي ستعرض الكيان وتبعياته المحتملة متعددة إلى واحد. وكما تم شرحه للتو، سيؤدي ذلك إلى تحميل التبعية؛
- طريقة ShortIdentity التي لن تشير إلى أي تبعيات. وبالتالي لن يتم تحميل أي تبعيات؛
وستكون طرق عرض الكيانات الأخرى كما يلي:
الكيان [Client]:
public class Client
{
// البيانات
...
// مواعيد العميل
public ICollection<Rv> Rvs { get; set; }
// التوقيع
public override string ToString()
{
return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// توقيع مختصر
public string ShortIdentity()
{
return ToString();
}
}
- الأسطر 9-12: لا تعرض الطريقة [ToString] التبعية الموجودة في السطر 6؛
الكيان [Creneau]:
public class Creneau
{
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
// مواعيد الموعد
public ICollection<Rv> Rvs { get; set; }
// التوقيع
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// التوقيع المختصر
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
}
}
- السطر 16: تشير الطريقة [ToString] إلى التبعية الموجودة في السطر 9. سيؤدي ذلك إلى تحميلها؛
- السطر 11: التبعية [Rvs] غير مشار إليها. ولن يتم تحميلها؛
- السطران 21-22: لم تعد الطريقة [ShortIdentity] تشير إلى المرجع [Medecin] في السطر 9. وبالتالي لن يتم تحميل هذا المرجع.
الكيان [Rv]:
public class Rv
{
// البيانات
...
[Column("CLIENT_ID")]
public int ClientId { get; set; }
[ForeignKey("ClientId")]
[Required]
public virtual Client Client { get; set; }
[Column("CRENEAU_ID")]
public int CreneauId { get; set; }
[ForeignKey("CreneauId")]
[Required]
public virtual Creneau Creneau { get; set; }
// توقيع
public override string ToString()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
}
// توقيع قصير
public string ShortIdentity()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
}
}
- السطور 17-20: تشير الطريقة [ToString] إلى التبعيات في السطرين 9 و14. وهذا سيؤدي إلى تحميلها؛
- الأسطر 17-20: الطريقة [ShortIdentity] تتجنب ذلك، وبالتالي لن يتم تحميل التبعيات.
في الختام، يجب الانتباه إلى طرق [ToString] للكيانات. إذا لم يتم الانتباه إلى ذلك، فإن عرض جدول ما قد يؤدي إلى تحميل نصف قاعدة البيانات إذا كان الجدول يحتوي على العديد من التبعيات.
بعد توضيح ذلك، نكتب الكود الجديد [Dump.cs] التالي:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class Dump
{
static void Main(string[] args)
{
// تفريغ قاعدة البيانات
using (var context = new RdvMedecinsContext())
{
// العملاء
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// الأطباء
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// المواعيد
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// المواعيد
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
سنشرح الأسطر 17-21 التي تعرض الكيانات [Client]. وينطبق التفسير المقدم على الكيانات الأخرى.
// العملاء
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
- السطر 3: تم إدخال الكلمة الرئيسية var مع C# 3.0. وهي تسمح بتجنب تحديد النوع الدقيق للمتغير. يقوم المُجمِّع عندئذٍ باستنتاجه من نوع التعبير المُعيَّن للمتغير؛
- السطر 3: التعبير المخصص للمتغير clients هو استعلام LINQ to Entity. يمكن التعرف فيه على كلمات رئيسية من لغة SQL المنقولة إلى LINQ. الصيغة المستخدمة هنا هي التالية:
from variable in DbSet select variable
البناء النحوي الأكثر عمومية لـ LINQ هو
from variable in collection select variable
سيتم تصفح المجموعة، وسيتم تقييم المتغير لكل عنصر فيها. لا يتم ذلك إلا عندما يتم تعداد المتغير [clients] في السطر 3 بواسطة for / each في الأسطر 4-7. وطالما لم يتم ذلك، فإن المتغير [clients] يظل مجرد استعلام غير مُقيَّم؛
- السطر 4: يتم تعداد الطلب [clients]. سيؤدي ذلك إلى تقييم الطلب. سيتم نقل أسطر الجدول [CLIENTS] واحدة تلو الأخرى إلى سياق الاستمرارية؛
- السطر 6: يتم استخدام الطريقة [ToString] للكيان [Client] للعرض. لا يوجد تحميل للتبعيات؛
لننتقل إلى الأسطر التالية من الكود:
- الأسطر 24-28: يتم إدخال أسطر الجدول [MEDECINS] في سياق الاستمرارية وعرضها. لا يوجد تحميل للتبعيات؛
- الأسطر 31-35: يتم جلب أسطر الجدول [CRENEAUX] إلى سياق الاستمرارية وعرضها. لقد رأينا أن الطريقة [ToString] لهذه الكيان تعرض التبعية [Medecin]. لكن هذه التبعية محملة بالفعل. لذلك لن يكون هناك تحميل جديد؛
- السطور 38-42: يتم جلب سطور الجدول [RVS] إلى سياق الاستمرارية وعرضها. لقد رأينا أن الطريقة [ToString] لهذه الكيان تعرض التبعيات [Client] و [Creneau]. لكن هاتين التبعيتين محملتان بالفعل. ولذلك لن يكون هناك تحميل جديد.
تجدر الإشارة إلى أن ترتيب العرض ليس محايدًا. لو أردنا عرض الكيانات [Rv] أولاً، لكانت الطريقة [ToString] الخاصة بها قد تسببت في تحميل الكيانات [Client] و [Creneau] المرتبطة بهذه المواعيد. ولم تكن الكيانات الأخرى لتُحمَّل. بل كانت ستُحمَّل لاحقًا في عرض آخر. وهذا يؤثر على الأداء. يحتاج الكود السابق إلى أربعة أوامر SQL لعرض جميع الكيانات. لنفترض الآن أننا نستخدم أولاً الجدول [RVS] الخاص بالمواعيد. يلزم إجراء استعلام أول SQL للجدول [RVS]. بعد ذلك، ستؤدي الطريقة [ToString] للكيان [Rv] إلى تحميل الكيانات المرتبطة [Client] و [Creneau]. يلزم طلب SQL لكل منها. بافتراض وجود N2 عميل و N3 فترات زمنية وأن جميع هذه الكيانات مشار إليها في الجدول [RVS]، فسيتطلب عرضها 1+N2+N3+SQL استعلامات. وبالتالي، فإن الأداء أقل من الإصدار الذي تمت دراسته. لعرض الجدول [RVS] مع تبعياته، سيكون من الضروري إجراء ربط بين الجداول. يمكن تحقيق ذلك باستخدام LINQ. سنعود إلى هذا الأمر في مثال لاحق. في الوقت الحالي، سنذكر أنه يجب الانتباه إلى الاستعلامات SQL الكامنة وراء كودنا LINQ.
نقوم بضبط إعدادات المشروع لتنفيذ هذا الكود الجديد [1] و [2] ثم نقوم بتنفيذه:
![]() |
يكون عرض وحدة التحكم كما يلي:
3.5.4. تعلم LINQ مع LINQPad
استخدمنا أعلاه استعلامات LINQ to Entity لعرض محتوى جداول قاعدة البيانات. كتب جوزيف ألباهاري برنامجًا لتعلم الأشكال المختلفة لـ LINQ. نقدمه الآن.
LINQPad متاح في URL التالي [http://www.linqpad.net/]. بمجرد تثبيته، نقوم بتشغيله [1]:
![]() |
يمكن للمبتدئين في LINQ التعرف على البرنامج من خلال الأمثلة الموجودة في علامة التبويب [Samples] [2] التي تعرض العديد من الأمثلة. لنختر المثال [3] الذي سيظهر بعد ذلك في نافذة أخرى [4]. الرمز الكامل للمثال هو التالي:
// الآن لننتقل إلى تعبير استعلام بسيط باستخدام LINQ-to-objects (لاحظ عدم وجود فاصلة منقوطة):
from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word
// لا تتردد في تعديل هذا... (لا أحد يراقبك!) سيُطلب منك حفظ أي
// التغييرات في ملف منفصل.
//
// نصيحة : يمكنك تنفيذ جزء من الاستعلام عن طريق تمييزه، ثم الضغط على F5.
السطور 3-5 هي مثال على استعلام LINQ to Object. يتبع الاستعلام LINQ الصيغة التالية:
from variable in collection orderby élément1 select élément2
- تشير المتغير إلى العنصر الحالي في المجموعة. في مثالنا، هذه المجموعة هي قائمة الكلمات الناتجة عن السلسلة المقسمة؛
- يتم ترتيب المجموعة وفقًا لمعلمة élément1 من orderby. في مثالنا، سيتم ترتيب مجموعة الكلمات وفقًا لطولها؛
- تشير الكلمة الرئيسية select إلى ما نريد استخراجه من العنصر الحالي variable من المجموعة. في مثالنا، سيكون ذلك هو الكلمة.
لنقم بتنفيذ هذا الاستعلام LINQ:
![]() |
- في [1]: يتم تنفيذ تعبير LINQ بواسطة [F5] أو عبر زر التنفيذ؛
- في [2]: العرض. يتم عرض الكلمات حسب طولها. يوضح هذا المثال البسيط قوة LINQ؛
- في [3]، يمكن تنزيل أمثلة أخرى، لا سيما تلك الموجودة في كتاب "LINQ in action" [4]؛
![]() |
- في [5]، نختار مثالاً من الكتاب؛
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
// تجميع الكلمات حسب الطول
var groups =
from word in words
orderby word ascending
group word by word.Length into lengthGroups
orderby lengthGroups.Key descending
select new { Length = lengthGroups.Key, Words = lengthGroups };
// اطبع كل مجموعة
foreach (var group in groups)
{
Console.WriteLine("Words of length " + group.Length);
foreach (string word in group.Words)
Console.WriteLine(" " + word);
}
- السطر 4: طلب جديد LINQ مع كلمات رئيسية جديدة؛
- السطر 5: المجموعة المطلوبة هي جدول الكلمات في السطر 1؛
- السطر 6: يتم فرز المجموعة حسب الترتيب الأبجدي للكلمات؛
- السطر 7: يتم تجميع المجموعة في (الكلمة الرئيسية into) مجموعة جديدة lengthGroups. يمثل lengthGroups.Key عامل التجميع (الكلمة الرئيسية by)، وهو هنا طول الكلمات. تجمع lengthGroups الكلمات التي لها نفس عامل التجميع، أي نفس الطول؛
- السطر 8: يتم ترتيب المجموعة lengthGroups حسب مفتاح التجميع التنازلي، أي هنا حسب الحجم التنازلي للكلمات؛
- السطر 9: من هذه المجموعة، يتم إنتاج كائنات جديدة (فئات مجهولة) لها حقلان:
- Length: طول الكلمات،
- Words: الكلمات التي لها هذا الطول؛
هنا، نرى بشكل خاص فائدة الكلمة الرئيسية var في السطر 4. نظرًا لاستخدامنا لفئة مجهولة في السطر 9، لا يمكننا تحديد نوع المتغير groups. أما المُجمِّع، فسيعطي اسمًا داخليًا للفئة المجهولة وسيصنف المتغير groups باستخدامه. وسيكون قادرًا بعد ذلك على تحديد ما إذا كان المتغير groups مستخدمًا بشكل صحيح
- السطر 12: مسار الاستعلام في السطر 4. ولا يتم تقييمه إلا في هذه اللحظة. نتذكر أن تنفيذه سينتج مجموعة من الكائنات، المحددة في السطر 9؛
- السطر 14: يتم عرض الخاصية Length للعنصر الحالي، أي طول الكلمات؛
- الأسطر 15-17: يتم عرض كل عنصر من مجموعة الخاصية Words، أي مجموعة الكلمات التي لها الطول المعروض سابقًا.
عند تنفيذ هذا الاستعلام، نحصل على النتيجة التالية في LINQPad:
![]() |
الآن بعد أن رأينا بعض أمثلة الاستعلامات [LINQ to Object]، دعونا نلقي نظرة على استعلامات [LINQ to Entity] التي ستسمح لنا بالاستعلام عن قواعد البيانات. سنقوم أولاً بالاتصال بقاعدة البيانات SQL Server التي أنشأناها وملأناها:
![]() |
- في [1]، نضيف اتصالاً بقاعدة بيانات؛
- في [2]، نحدد وسائل الوصول إلى مصدر البيانات. للوصول إلى قاعدة البيانات SQL Server، سنستخدم [LINQPad Driver]؛
- في [3]، من الممكن أيضًا استرداد سياق استمرارية [DbContext] محدد في ملف .exe أو .dll (الخيار 3). لسوء الحظ، حتى تاريخه (8 أكتوبر 2012)، لا يتم دعم Entity Framework 5؛
- في [4]، يمكن تنزيل برامج تشغيل لملفات SGBD أخرى غير SQL Server؛
- في [5]، يمكن تنزيل برنامج التشغيل لـ SGBD و MySQL و Oracle؛
![]() |
- في [6]، برنامج التشغيل الذي تم تنزيله؛
- في [7]، نقوم بالاتصال بقاعدة بيانات SQL Server؛
![]() |
- في [8]، توجد قاعدة البيانات على خادم الأسماء (المحلي)؛
- في [9]، يتم الاتصال باستخدام المصادقة sa / sqlserver2012؛
- في [10]، إلى قاعدة البيانات [rdvmedecins-ef] التي أنشأناها؛
- في [11]، يمكننا اختبار الاتصال؛
- في [12]، ننهي المعالج؛
- في [13]، يظهر الاتصال في LINQPad.
تم إنشاء الكيانات من الجدول [rdvmedecins-ef]. وهي كما يلي:
![]() |
- في [1]، يمثل [CLIENTS] مجموعة الكيانات [Client]. لكل كيان:
- الخصائص (ID، TITRE، NOM، PRENOM، TIMESTAMP)،
- علاقة 1 إلى عدة [CLIENTRVS]؛
- حيث يمثل [2] و [CRENEAUXes] مجموعة الكيانات [Creneau]. لكل كيان:
- الخصائص (ID، HDEBUT، MDEBUT، HFIN، MFIN، MEDECIN_ID، TIMESTAMP)،
- علاقة واحد إلى عدة [CRENEAURVS]،
- علاقة من عدة إلى واحد [MEDECIN]؛
- في [3]، تمثل الكيان [MEDECINS] مجموعة الكيانات [Medecin]. لكل كيان:
- الخصائص (ID، TITRE، NOM، PRENOM، TIMESTAMP)،
- علاقة 1 إلى عدة [MEDECINCRENEAUXes]؛
- في [4]، تمثل الكيان [RVS] مجموعة الكيانات [Rv]. لكل كيان:
- الخصائص (ID، JOUR، CLIET_ID، CRENEAU_ID، TIMESTAMP)،
- علاقة متعددة إلى واحد [CLIENT]،
- علاقة متعددة إلى واحد [CRENEAU].
تجدر الإشارة إلى أن أسماء الخصائص المذكورة أعلاه تختلف عن الأسماء التي استخدمناها حتى الآن. لا يهم ذلك. نريد فقط تعلم المبادئ الأساسية للاستعلام عن قاعدة البيانات.
لنرى كيف يمكننا الاستعلام عن قاعدة الكيانات هذه. على سبيل المثال، نريد قائمة بالأطباء مرتبة حسب TITRE و NOM:
![]() |
- في [1]، نقوم بإنشاء استعلام جديد؛
- في [2]، نص الاستعلام؛
![]() |
- في [3]، نتيجة الاستعلام؛
- في [4]، نفس الاستعلام مع تعبيرات لامدا. الاستعلام الذي يحتوي على تعبيرات لامدا أقل قابلية للقراءة من الاستعلام النصي وقد نرغب في الاستغناء عنه. ومع ذلك، فهي ضرورية في بعض الأحيان لأنها تسمح بأشياء معينة لا تسمح بها الاستعلامات النصية. يشير تعبير لامدا إلى دالة ذات معلمة إدخال a ومعلمة إخراج b، بالصيغة a=>b. تقبل الطريقة OrderBy المذكورة أعلاه دالة لامدا كمعلمة وحيدة. تزودها هذه الدالة بالمعلمة التي يجب ترتيب المجموعة وفقًا لها. وبالتالي، فإن MEDECINS.OrderBy(m=>m.TITRE) هي قائمة الأطباء مرتبة حسب الألقاب. يجب قراءة التعليمات على أنها خط أنابيب على مجموعة. يتم توفير مجموعة الأطباء كمدخل للطريقة OrderBy. ستقوم هذه الطريقة باستغلال الكيانات [Medecin] واحدًا تلو الآخر. في تعبير لامدا m=>m.TITRE، يمثل m مدخل دالة لامدا. يمكن تسميته كما نشاء. هنا، سيكون مدخل دالة لامدا كيانًا [Medecin]. تُقرأ الدالة m=>m.TITRE على النحو التالي: إذا سميت مدخلتي m (كيان [Medecin])، فإن مخرجي هو m.TITRE، أي لقب الطبيب. MEDECINS.OrderBy(m=>m.TITRE) هي بدورها مجموعة، وهي مجموعة الأطباء مرتبة حسب الألقاب. يمكن لهذه المجموعة الجديدة أن تغذي طريقة أخرى، في المثال الطريقة ThenBy. تعمل هذه الطريقة على نفس المبدأ. وهي تستخدم للإشارة إلى معلمات إضافية لفرز المجموعة.
قراءة كود لامدا المكافئ للنص الذي نكتبه عادةً هي طريقة جيدة لتعلمه؛
![]() |
- في [5]، الأمر SQL الصادر على القاعدة. هنا أيضًا، سنقرأ هذا الرمز بعناية. فهو يسمح بتقييم التكلفة الفعلية لطلب LINQ.
فيما يلي، نقدم بعض الأمثلة على الاستعلام LINQ. في كل مرة، نعرض النتائج المعروضة والرموز lambda و SQL المكافئة. لفهم هذه الطلبات، يجب تذكر العلاقات متعددة إلى واحد التي تربط الكيانات ببعضها البعض. فمن خلالها ننتقل من كيان إلى آخر. نسميها خصائص التنقل.
![]() |
// العملاء الذين يحملون لقب "السيد" مرتبة حسب ترتيب الأسماء التنازلي
النتائج:
![]() |
LINQ | |
Lambda | |
SQL | |
// جميع المواعيد مع الطبيب المعني
النتائج (الجزئية):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// جميع المواعيد مع العميل والطبيب المرتبطين
النتائج:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// الأطباء الذين ليس لديهم مواعيد
النتائج:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
لا يوجد استعلام LINQ لهذا الطلب. يجب استخدام تعبيرات لامدا. يُقرأ هذا التعبيرات على النحو التالي: أخذ مجموعة الأطباء (MEDECINS) وأحتفظ (Where) فقط بالأطباء (m) الذين لا أستطيع العثور في مجموعة المواعيد (RVS) على موعد (rv) مع هذا الطبيب (m).
// المواعيد المتاحة للسيدة بيليسييه
النتائج (الجزئية):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// عدد مواعيد السيدة بيليسييه في 08/10/2012
النتائج:
![]() |
LINQ | |
Lambda | |
SQL | |
// قائمة العملاء الذين حددوا موعدًا مع السيدة بيليسييه في 08/10/2012
النتائج:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// عدد المواعيد لكل طبيب
النتائج:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
3.5.5. تعديل كيان مرتبط بسياق الاستمرارية
لقد رأينا العمليات التالية على سياق الاستمرارية:
- إضافة عنصر إلى السياق ([dbContext].[DbSet].Add)؛
- حذف عنصر من السياق ([dbContext].[DbSet].Remove)؛
- استعلام عن سياق باستخدام استعلامات LINQ.
عندما نريد مزامنة السياق مع قاعدة البيانات، نكتب [dbContext].SaveChanges().
![]() | ![]() |
يوضح الرمز [ModifyAttachedEntity] تعديل كيان مرتبط بالسياق:
using System;
using System.Data;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class ModifyAttachedEntity
{
static void Main(string[] args)
{
Client client1, client2, client3;
// السياق الأول
using (var context = new RdvMedecinsContext())
{
// إفراغ قاعدة البيانات الحالية
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// إضافة عميل
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// المتابعة
Console.WriteLine("client1--avant");
Console.WriteLine(client1);
// حفظ السياق
context.SaveChanges();
// متابعة
Console.WriteLine("client1--après");
Console.WriteLine(client1);
}
// السياق الثاني
using (var context = new RdvMedecinsContext())
{
// استرداد العميل 1 في العميل 2
client2 = context.Clients.Find(client1.Id);
// متابعة
Console.WriteLine("client2");
Console.WriteLine(client2);
// يتم تعديل العميل 2
client2.Nom = "yy";
// حفظ السياق
context.SaveChanges();
}
// السياق الثالث
using (var context = new RdvMedecinsContext())
{
// استرداد العميل 2 في العميل 3
client3 = context.Clients.Find(client2.Id);
// متابعة
Console.WriteLine("client3");
Console.WriteLine(client3);
}
}
}
}
- السطر 15: فتح سياق التطبيق؛
- الأسطر 18-25: يتم إفراغ السياق. وبشكل أكثر دقة، يتم نقل جميع الكيانات إلى السياق من قاعدة البيانات ثم تنتقل إلى حالة "محذوفة". تجدر الإشارة إلى أن قاعدة البيانات لم تتغير في هذه المرحلة. طالما أن السياق غير متزامن مع قاعدة البيانات، فإن هذه الأخيرة لا تتغير. نتذكر أن حذف الكيانات [Medecin] و [Client] يكفي لتفريغ قاعدة البيانات من خلال سلسلة من عمليات الحذف المتتالية؛
- السطران 27-28: تمت إضافة عميل جديد إلى قاعدة البيانات؛
- السطران 30-31: يتم عرضه قبل حفظه في قاعدة البيانات؛
- السطر 33: تتم مزامنة السياق مع قاعدة البيانات. ستخضع الكيانات التي تم وضع علامة "محذوف" عليها لعملية SQL DELETE، والكيان المضاف لعملية SQL INSERT؛
- السطران 35-36: يتم عرض العميل بعد المزامنة مع قاعدة البيانات؛
والنتيجة التي تم الحصول عليها في وحدة التحكم هي كما يلي:
تجدر الإشارة إلى النقاط التالية:
- قبل المزامنة مع قاعدة البيانات، لا يمتلك العميل مفتاحًا أساسيًا، ولا timestamp,
- بعد المزامنة، يصبح لديه كلاهما. نذكر هنا أن المفتاح الأساسي تم تكوينه ليتم إنشاؤه بواسطة SQL Server. وبالمثل، يقوم هذا SGBD بإنشاء الطابع الزمني تلقائيًا؛
- السطر 37: يتم إغلاق سياق الاستمرارية. تصبح الكيانات التي كان يحتوي عليها "منفصلة". وهي موجودة ككائنات ولكن ليس ككيانات مرتبطة بسياق استمرارية؛
- السطر 39: يتم إعادة تشغيل سياق جديد فارغ؛
- السطر 42: يتم استرداد العميل مباشرة من قاعدة البيانات عبر مفتاحه الأساسي. ثم يتم إحضاره إلى السياق. إذا لم يتم العثور عليه، فإن طريقة Find تُرجع المؤشر null؛
- السطران 48-49: يتم عرضه؛
وينتج عن ذلك النتيجة التالية:
- السطر 47: يتم تعديله؛
- السطر 49: يتم مزامنة السياق مع قاعدة البيانات. سيكتشف EF أن بعض عناصر السياق قد تم تعديلها منذ أن تم إحضارها إليه. بالنسبة لهذه العناصر، سيقوم بإنشاء أوامر SQL UPDATE في قاعدة البيانات. لذا، هنا، ستتكون المزامنة من أمر واحد UPDATE؛
- السطر 50: يتم إغلاق السياق الثاني. الكيان client2 الذي كان مرتبطًا بالسياق يصبح الآن منفصلاً عنه؛
- السطر 52: يتم فتح سياق ثالث فارغ؛
- السطر 55: يتم إحضار العميل الوحيد للقاعدة إليه مرة أخرى. نريد أن نرى ما إذا كان التعديل الذي تم إجراؤه عليه في السياق السابق قد انعكس في القاعدة؛
- السطران 57-58: يتم عرض العميل. وهذا يعطي النتيجة التالية:
تم تعديل اسم العميل في قاعدة البيانات. تجدر الإشارة إلى أن timestamp الخاص به قد تم تحديثه.
- السطر 59: يتم إغلاق السياق. بالمناسبة، تجدر الإشارة إلى أنه على عكس المرتين السابقتين، لم تكن هناك حاجة مسبقًا لمزامنة السياق مع قاعدة البيانات (SaveChanges) لأن السياق لم يتم تعديله.
3.5.6. إدارة الكيانات المنفصلة
لنعد إلى بنية الطبقات لتطبيق مثل ذلك الموجود في دراسة الحالة:
![]() |
تستخدم الطبقة [DAO] الطبقتين ORM و EF5 للوصول إلى البيانات. لدينا المكونات الأساسية لهذه الطبقة. ستقوم كل طريقة بفتح سياق استمرارية، وإجراء العمليات اللازمة عليه (إدراج، تعديل، حذف، استعلام)، ثم إغلاقه. ستنتقل الكيانات التي تديرها الطبقة [DAO] إلى طبقة الويب ASP.NET. في هذه الطبقة، تكون خارج سياق الاستمرارية وبالتالي منفصلة. في طبقة الويب، يمكن للمستخدم تعديل هذه الكيانات (إضافة، تعديل، حذف). وعندما تعود إلى الطبقة [DAO]، تظل منفصلة. لكن الطبقة [DAO] ستضطر إلى ترحيل التعديلات التي أجراها المستخدم إلى قاعدة البيانات. لذا، سيتعين عليها العمل مع كيانات منفصلة. لنلقِ نظرة على الحالات الثلاث المحتملة:
إضافة كيان منفصل
هذه هي الحالة العادية للإضافة. يكفي إضافة (Add) الكيان المنفصل إلى السياق مع التأكد من أن له مفتاحًا أساسيًا يساوي null.
تعديل كيان منفصل
يمكن استخدام الكود التالي:
- ستقوم الطريقة [DbContext].Entry(entité-détachée) بوضع الكيان في السياق؛
- يتم تعيين حالة هذه الكيان إلى "معدلة" حتى تصبح موضوعًا لأمر SQL UPDATE.
حذف كيان منفصل
يمكن استخدام الكود التالي:
- السطر 1: نضع في السياق الكيان الذي له نفس المفتاح الأساسي للكيان المنفصل؛
- السطر 2: يتم حذفها:
تجدر الإشارة إلى أن هذا يتطلب في الأساس استخدام SELECT متبوعًا بـ DELETE، في حين أن DELETE وحده يكفي عادةً. يمكن أيضًا اتباع المثال الخاص بتعديل كيان منفصل وكتابة:
نظرًا لأنني لم أتمكن من تنفيذ سجلات على عمليات SQL التي تم إجراؤها على قاعدة البيانات، لا أعرف ما إذا كان هناك طريقة يُفضل استخدامها على الأخرى.
فيما يلي مثال:
![]() | ![]() |
رمز البرنامج [ModifyDetachedEntities] هو التالي:
using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class ModifyDetachedEntities
{
static void Main(string[] args)
{
Client client1;
// إفراغ قاعدة البيانات الحالية
Erase();
// إضافة عميل
using (var context = new RdvMedecinsContext())
{
// إنشاء عميل
client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
// إضافة العميل إلى السياق
context.Clients.Add(client1);
// حفظ السياق
context.SaveChanges();
}
// عرض قاعدة البيانات
Dump("1-----------------------------");
// العميل 1 غير موجود في السياق - يتم تعديله
client1.Nom = "y";
// سياق جديد
using (var context = new RdvMedecinsContext())
{
// هنا، لدينا سياق فارغ
// نضع العميل 1 في السياق في حالة معدلة
context.Entry(client1).State = EntityState.Modified;
// نحفظ السياق
context.SaveChanges();
}
// عرض قاعدة البيانات
Dump("2-----------------------------");
// حذف الكيان خارج السياق
using (var context = new RdvMedecinsContext())
{
// هنا، لدينا سياق جديد فارغ
// نضع client1 في السياق في حالة محذوفة
context.Entry(client1).State = EntityState.Deleted;
// نحفظ السياق
context.SaveChanges();
}
// عرض قاعدة البيانات
Dump("3-----------------------------");
}
static void Erase()
{
// إفراغ القاعدة
using (var context = new RdvMedecinsContext())
{
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// يتم حفظ السياق
context.SaveChanges();
}
}
static void Dump(string str)
{
Console.WriteLine(str);
// عرض القاعدة
using (var context = new RdvMedecinsContext())
{
foreach (var rv in context.Rvs)
{
Console.WriteLine(rv);
}
foreach (var creneau in context.Creneaux)
{
Console.WriteLine(creneau);
}
foreach (var client in context.Clients)
{
Console.WriteLine(client);
}
foreach (var medecin in context.Medecins)
{
Console.WriteLine(medecin);
}
}
}
}
}
- السطر 15: يتم مسح قاعدة البيانات؛
- الأسطر 17-25: يتم إضافة عميل إلى قاعدة البيانات؛
- السطر 27: يعرض محتوى قاعدة البيانات؛
- بعد السطر 25، لم يعد سياق الاستمرارية موجودًا. وبالتالي، لم تعد هناك كيانات مرتبطة. انتقل الكيان client1 إلى الحالة "غير مرتبط"؛
- السطر 29: يتم تعديل اسم الكيان المنفصل؛
- السطر 31: يتم فتح سياق جديد فارغ؛
- السطر 35: يتم وضع الكيان المنفصل client1 في السياق في حالة "معدل"؛
- السطر 37: تتم مزامنة السياق مع قاعدة البيانات؛
- السطر 38: يتم إغلاقه؛
- السطر 40: يتم عرض قاعدة البيانات؛
تم تعديل اسم العميل في قاعدة البيانات. تجدر الإشارة إلى أنه تم تحديث timestamp؛
- السطر 42: فتح سياق جديد فارغ؛
- السطر 46: يتم وضع الكيان المنفصل client1 في السياق في حالة "محذوف"؛
- السطر 48: تمت مزامنة السياق مع قاعدة البيانات؛
- السطر 49: يتم إغلاقه؛
- السطر 51: يتم عرض قاعدة البيانات؛
تم حذف الكيان بالفعل من قاعدة البيانات.
الآن، نرى طريقتين لتحميل تبعيات الكيان: التحميل المتأخر (Lazy) والتحميل الفوري (Eager).
3.5.7. التحميل المتأخر (Lazy) والتحميل الفوري (Eager)
لنعد إلى مخطط التبعيات المتعددة إلى واحد لأحد كياناتنا الأربعة:
![]() |
فيما سبق، تمتلك الكيان [Creneau] خاصية تنقلية [Creneau.Medecin] نحو الكيان [Medecin]. يُطلق على ذلك اسم التبعية. وقد رأينا أنه توجد أيضًا تبعية من واحد إلى عدة كيانات. وينطبق المبدأ الذي سيتم شرحه عليها أيضًا.
بشكل افتراضي، تعمل EF 5 في وضع التحميل المتأخر (Lazy Loading): عندما تجلب كيانًا إلى سياق الاستمرارية من قاعدة البيانات، فإنها لا تجلب تبعياته. سيتم جلب هذه التبعيات عند استخدامها لأول مرة. وهذا إجراء منطقي. لو لم يكن الأمر كذلك، فإن جلب المواعيد إلى السياق سيؤدي، وفقًا للتبعيات المذكورة أعلاه، إلى جلب:
- الكيانات [Creneau] المرتبطة بالمواعيد؛
- الكيانات [Medecin] المرتبطة بهذه الفترات الزمنية؛
- الكيانات [Clients] المرتبطة بالمواعيد.
لكن في بعض الأحيان، نحتاج إلى كيان وتبعياته. سنوضح طريقتي التحميل.
![]() | ![]() |
رمز [LazyEagerLoading] هو التالي:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class LazyEagerLoading
{
// الكيانات
static Medecin[] medecins;
static Client[] clients;
static Creneau[] creneaux;
static void Main(string[] args)
{
// تتم تهيئة قاعدة البيانات
InitBase();
Console.WriteLine("Initialisation terminée");
// التحميل المسبق
Creneau creneau;
int idCreneau = (int)creneaux[0].Id;
using (var context = new RdvMedecinsContext())
{
// الفتحة رقم 0
creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// عرض التبعية
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
}
// التحميل المتأخر - الوضع الافتراضي
using (var context = new RdvMedecinsContext())
{
// الفتحة رقم 0
creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// عرض التبعية
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 2 suivante s'est produite : {0}", e);
}
}
static void InitBase()
{
// يتم تهيئة قاعدة البيانات
using (var context = new RdvMedecinsContext())
{
// إفراغ قاعدة البيانات الحالية
...
// يتم تهيئة قاعدة البيانات
// العملاء
clients = new Client[] {
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
...
// المواعيد
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// يتم حفظ سياق الاستمرارية
context.SaveChanges();
}
}
}
}
- السطر 18: نبدأ من قاعدة معروفة، وهي تلك المستخدمة حتى الآن. بعد هذه العملية، يتم ملء الجداول في الأسطر 11-13 بالكيانات المنفصلة؛
- السطران 21-22: نركز على الفترة الزمنية الأولى والطبيب المرتبط بها؛
- السطر 23: سياق جديد؛
- السطر 26: نضع الفترة الزمنية في السياق مع تبعيتها (التحميل المتحمس). ولأن هذا ليس الوضع الافتراضي، يجب طلب هذه التبعية صراحةً. وتسمح طريقة Include بذلك. ومعلمتها هي اسم التبعية في الكيان الذي تم إحضاره إلى السياق. يستخدم الاستعلام الذي يجلب الكيان إلى السياق تعبيرات لامدا. تسمح الطريقة Single بتحديد شرط يسمح بإحضار كيان واحد فقط. هنا، نبحث في قاعدة البيانات عن الكيان [Creneau] الذي يحتوي على المفتاح الأساسي للفترة رقم 0؛
- السطر 27: يتم عرض الكيان الذي تم إرجاعه. دعونا نذكر طريقتي الكتابة المستخدمتين في الكيانات:
// التوقيع
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// التوقيع المختصر
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
}
- الأسطر 2-5: تعرض الطريقة [ToString] التبعية [Medecin]. إذا لم تكن هذه التبعية موجودة بالفعل في السياق، فسيتم البحث عنها في قاعدة البيانات لإضافتها؛
- الأسطر 8-11: لا تعرض الطريقة [ShortIdentity] التبعية [Medecin]. وبالتالي لن يتم البحث عنها في قاعدة البيانات إذا لم تكن موجودة في السياق؛
في هذه المرحلة، يكون عرض وحدة التحكم كما يلي:
- السطر 28: يتم إغلاق السياق؛
- الأسطر 30-37: نحاول كتابة التبعية [Medecin] للكيان. نذكر طريقة عمل Lazy Loading: يتم تحميل التبعية عند استخدامها لأول مرة إذا لم تكن موجودة. هنا، عادةً ما تكون موجودة. يكون العرض كما يلي:
- الأسطر 39-44: في سياق جديد، يتم البحث عن الفتحة رقم 0 مرة أخرى في قاعدة البيانات وإدخالها في السياق. هنا، لا يُطلب التبعية [Medecin] بشكل صريح. وبالتالي لن يتم إدخالها (التحميل المتأخر)؛
- السطر 43: عرض الهوية المختصرة للفتحة هو كما يلي:
هنا، من المهم استخدام ShortIdentity بدلاً من ToString لعرض الكيان. إذا استخدمنا ToString، فسيتم عرض التبعية [Medecin]، ولهذا الغرض سيتم البحث عنها في قاعدة البيانات. لكننا لا نريد ذلك.
- السطر 44: يتم إغلاق السياق؛
- الأسطر 46-53: نحاول عرض تبعية الكيان. من المهم القيام بذلك خارج السياق وإلا سيتم البحث عنها في قاعدة البيانات والعثور عليها. هنا نحن خارج السياق. الكيان [Creneau] منفصل وتبعيته [Medecin] غير موجودة (Lazy Loading). ماذا سيحدث؟ العرض على الشاشة هو التالي:
وجدت EF أن التبعية [Medecin] غائبة. حاولت تحميلها ولكن بما أن السياق مغلق، لم تعد هذه العملية ممكنة. سنحتفظ بهذا الاستثناء [System.ObjectDisposedException] لأنه مميز لتحميل تابع خارج سياق مفتوح.
الآن دعونا ندرس التنافس على الوصول إلى الكيانات.
3.5.8. تنافس الوصول إلى الكيانات
لنعد إلى تعريف الكيان [Client]:
public class Client
{
// البيانات
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// مواعيد العميل
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// التوقيع
...
}
سنركز على الحقل [Timestamp] في السطر 23. نعلم أن قيمته يتم إنشاؤها بواسطة SGBD. قلنا أيضًا أن التعليق التوضيحي [Timestamp] في السطر 22 يجعل EF 5 يستخدم الحقل المُعلَّق عليه لإدارة التنافس في الوصول إلى الكيانات. دعونا نذكر ما هي إدارة التنافس في الوصول:
- تقوم عملية P1 بقراءة سطر L من الجدول [MEDECINS] في الوقت T1. يحتوي السطر على timestamp TS1؛
- تقوم العملية P2 بقراءة نفس السطر L من الجدول [MEDECINS] في الوقت T2. يحتوي السطر على timestamp و TS1 لأن العملية P1 لم تقم بعد بالتحقق من تعديلها؛
- تقوم العملية P1 بالتحقق من صحة تعديلها للسطر L. ثم يتحول timestamp للسطر L إلى TS2؛
- تقوم العملية P2 بالتحقق من صحة تعديلها للسطر L.تطلق ORM استثناءً لأن العملية P2 لها timestamp وTS1 للسطر L يختلفان عن timestamp وTS2 الموجود في قاعدة البيانات.
يُطلق على ذلك اسم الإدارة المتفائلة للوصول المتزامن. مع EF 5، يجب أن يكون للحقل الذي يؤدي هذا الدور أحد السمتين [Timestamp] أو [ConcurrencyCheck]. يتمتع خادم SQL بنوع [timestamp]. يتم إنشاء قيمة العمود الذي يتمتع بهذا النوع تلقائيًا بواسطة خادم SQL عند أي إدراج/تعديل لسطر. يمكن عندئذٍ استخدام هذا العمود لإدارة التنافس في الوصول.
سنوضح هذا التنافس في الوصول باستخدام خيطين سيقومان في نفس الوقت بتعديل نفس الكيان [Client] في قاعدة البيانات. يتطور المشروع على النحو التالي:
![]() | ![]() |
رمز برنامج [AccèsConcurrents] هو كما يلي:
using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// الكائن المتبادل مع سلاسل العمليات
class Data
{
public int Duree { get; set; }
public string Nom { get; set; }
public Client Client { get; set; }
}
// برنامج الاختبار
class AccèsConcurrents
{
static void Main(string[] args)
{
Client client1;
using (var context = new RdvMedecinsContext())
{
// الخيط الرئيسي
Thread.CurrentThread.Name = "main";
// إفراغ قاعدة البيانات الحالية
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// إضافة عميل
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// المتابعة
Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
// النسخ الاحتياطي
context.SaveChanges();
// متابعة
Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
}
// سنقوم بتعديل client1 باستخدام خيطين
// الخيط t1
Thread t1 = new Thread(Modifie);
t1.Name = "t1";
t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
// الخيط t2
Thread t2 = new Thread(Modifie);
t2.Name = "t2";
t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
// ننتظر انتهاء الخيطين
Console.WriteLine("Thread {0} -- début attente fin des deux threads", Thread.CurrentThread.Name);
t1.Join();
t2.Join();
Console.WriteLine("Thread {0} -- fin attente fin des deux threads", Thread.CurrentThread.Name);
// نعرض التعديل - لا بد أن أحدهما قد نجح
using (var context = new RdvMedecinsContext())
{
// استرداد client1 في client2
Client client2 = context.Clients.Find(client1.Id);
Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
}
}
// خيط
static void Modifie(object infos)
{
...
}
- السطر 26: يتم بدء سياق فارغ؛
- السطر 29: يتم تسمية الخيط الحالي لتمييزه عن الخيطين اللذين سيتم إنشاؤهما لاحقًا؛
- الأسطر 31-38: يتم وضع الكيانات [Medecin] و [Client] في حالة "محذوف"؛
- السطران 40-41: يتم وضع عميل في السياق؛
- السطور 43-44: يتم عرضه قبل مزامنة السياق؛
- السطر 46: مزامنة السياق مع قاعدة البيانات: سيتم حذف الكيانات التي في حالة "محذوف" من قاعدة البيانات. سيتم إدراج الكيان [Client] الموجود في السياق في قاعدة البيانات. سيكون هذا هو العنصر الوحيد في قاعدة البيانات؛
- السطور 47-49: يتم عرض العميل بعد مزامنة السياق. في هذه المرحلة، تكون شاشات العرض كما يلي:
يُلاحظ أنه بعد مزامنة السياق، يكون للعميل مفتاح أساسي و timestamp؛
- السطر 50: يتم إغلاق السياق؛
- السطر 53: يتم ربط مؤشر ترابط t1 بالطريقة [Modifie] في السطر 84. وهذا يعني أنه عند تشغيله، سيقوم بتنفيذ الطريقة [Modifie]؛
- السطر 54: يتم تسمية الخيط t1؛
- السطر 55: يتم تشغيل الخيط t1. يتم تمرير معلمات إليه في شكل بنية [Data] المحددة في الأسطر 12-17:
- المدة: سيتوقف الخيط بعد Durée ثانية قبل إنهاء تنفيذه،
- العميل: مرجع للعميل المراد تحديثه في قاعدة البيانات،
- الاسم: الاسم الذي سيُطلق على هذا العميل؛
- الأسطر 57-59: نفس الشيء مع مؤشر ترابط ثانٍ. في النهاية، سيحاول مؤشرا الترابط تغيير اسم العميل نفسه في قاعدة البيانات؛
- السطور 60-63: بعد تشغيل الخيطين، ينتظر الخيط الرئيسي انتهاء تنفيذهما؛
- السطر 62: انتظار انتهاء الخيط t1؛
- السطر 63: انتظار انتهاء الخيط t2؛
- السطر 64: لا نعرف الترتيب الذي ستنتهي به الخيطان. ما هو مؤكد هو أنهما قد انتهتا في السطر 64؛
- الأسطر 66-72: في سياق جديد، سنبحث عن العميل في قاعدة البيانات لمعرفة حالته.
لنرى الآن ما الذي يفعله الخيطان t1 و t2. إنهما ينفذان الطريقة [Modifie] التالية:
static void Modifie(object infos)
{
// يتم استرداد المعلمة
Data data = (Data)infos;
try
{
using (var context = new RdvMedecinsContext())
{
Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
// يتم استرداد client1 في client2
Client client2 = context.Clients.Find(data.Client.Id);
Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
// يتم تعديل client2
client2.Nom = data.Nom;
// ننتظر قليلاً
Thread.Sleep(data.Duree);
// نحفظ التغييرات
context.SaveChanges();
}
}
catch (Exception e)
{
// استثناء
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
}
// نهاية مؤشر الترابط
Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
}
- السطر 4: يتم استرداد معلمات الخيط (المدة، الاسم، العميل)؛
- السطر 7: سياق جديد؛
- السطر 11: يتم نقل العميل إلى السياق؛
- السطران 12-13: متابعة لمعرفة حالة العميل؛
- السطر 15: تغيير اسمه؛
- السطر 17: يتوقف الخيط Duree لمدة أجزاء من الألف من الثانية. وهذا له تأثير مثير للاهتمام. يترك الخيط المعالج الذي كان ينفذه، مما يفسح المجال لخيط آخر. في مثالنا، لدينا ثلاثة خيوط: main، t1، t2. الخيط main متوقف في انتظار انتهاء الخيوط t1 و t2. بافتراض أن الخيط t1 حصل على المعالج أولاً، فإنه يتركه الآن للخيط t2. سيؤدي ذلك إلى أن يقرأ الخيط t2 بالضبط نفس ما قرأه الخيط t1، أي نفس العميل بنفس timestamp؛
- السطر 19: يتم مزامنة السياق مع قاعدة البيانات. لنفترض مرة أخرى أن الخيط t1 يستيقظ أولاً. سيقوم بحفظ العميل باسم "yy". سيتمكن من القيام بذلك لأنه يمتلك نفس timestamp الموجود في قاعدة البيانات. بسبب هذا التحديث، سيقوم SGBD بتعديل timestamp. عندما يستيقظ الخيط t2 بدوره، سيكون لديه عميل برقم timestamp مختلف عن الرقم الموجود حالياً في قاعدة البيانات. سيتم رفض تحديثه.
فيما يلي ما يظهر على الشاشة:
- السطر 4: العميل الموجود في قاعدة البيانات؛
- السطر 9: العميل كما يقرأه الخيط t2؛
- السطر 11: العميل كما يقرأه الخيط t1. وبالتالي، قرأ الخيطان نفس الشيء؛
- السطر 12: ينتهي الخيط t2 أولاً. وبالتالي تمكن من إجراء التحديث. لا بد أن الاسم قد تغير إلى "zz"؛
- السطر 13: يطلق الخيط t1 استثناءً من النوع [System.Data.OptimisticConcurrencyException]. اكتشف EF أنه لا يمتلك timestamp الصحيح؛
- السطر 21: ينتهي الخيط t1 بدوره؛
- السطر 22: انتهى الخيط الرئيسي من انتظاره؛
- السطر 24: يعرض الخيط الرئيسي العميل في قاعدة البيانات. الخيط t2 هو الذي فاز بالفعل. الاسم هو " zz ". تجدر الإشارة إلى أن timestamp قد تغير.
الآن، دعونا ننظر في جانب آخر: المعاملة التي تحدد تزامن سياق الاستمرارية مع قاعدة البيانات.
3.5.9. المزامنة في معاملة
تحتوي الجدولة [CRENEAUX] على قيد تفرد أضفناه يدويًا (انظر الفقرة 2.2.4، الصفحة 12):
سنقوم بما يلي: سنضيف في نفس الوقت موعدين لنفس الطبيب، في نفس اليوم وفي نفس الفترة الزمنية. سنرى ما سيحدث.
يتطور المشروع على النحو التالي:
![]() | ![]() |
رمز البرنامج [SynchronisationTransaction] هو التالي:
using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// برنامج اختبار
class SynchronisationTransaction
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// إفراغ قاعدة البيانات الحالية
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
context.SaveChanges();
}
// يتم إنشاء عميل
Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
// يتم إنشاء طبيب
Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
// يتم إنشاء موعد لهذا الطبيب
Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
// يتم إنشاء موعدين لهذا الطبيب وهذا العميل، في نفس اليوم، وفي نفس الفترة الزمنية
Rv rv1 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
Rv rv2 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
try
{
// يتم وضع كل هذه العناصر في سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
context.Clients.Add(client1);
context.Creneaux.Add(creneau1);
context.Medecins.Add(medecin1);
context.Rvs.Add(rv1);
context.Rvs.Add(rv2);
// نحفظ السياق - يجب أن يكون هناك استثناء
// لأن BD الأساسي لديه قيد التفرد الذي يمنع
// وجود اثنين من RDV في نفس اليوم، وفي نفس الفترة الزمنية
context.SaveChanges();
}
}
catch (Exception e)
{
Console.WriteLine("Erreur : {0}", e);
}
// إذا تمت الحفظ في معاملة، فلا يجب إدراج أي شيء في قاعدة البيانات
// بسبب الاستثناء السابق - يتم التحقق
using (var context = new RdvMedecinsContext())
{
// العملاء
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// الأطباء
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// المواعيد
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// المواعيد
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
- الأسطر 15-27: يتم استخدام سياق استمرارية لإفراغ قاعدة البيانات؛
- السطر 30: إنشاء كائن [Client]؛
- السطر 32: إنشاء كائن [Medecin]؛
- السطر 34: إنشاء كائن [Creneau]؛
- السطر 36: إنشاء كائن [Rv]؛
- السطر 37: إنشاء كائن ثانٍ [Rv] مطابق للكائن السابق؛
- السطر 41: فتح سياق جديد؛
- الأسطر 43-47: يتم ربط الكائنات التي تم إنشاؤها مسبقًا بالسياق الجديد. لاحظ هنا أنه مع أخذ التبعيات في الاعتبار، كان بإمكاننا تقليل عدد عمليات Add. لكن EF سيقوم بتحسين أوامر SQL و INSERT التي سيتم إرسالها إلى قاعدة البيانات؛
- السطر 51: يتم مزامنة السياق مع قاعدة البيانات. كما يشير التعليق، يجب أن يفشل إدراج أحد الموعدين بسبب قيد التفرد في الجدول [RVS]. ولكن أكثر من ذلك، إذا حدثت المزامنة في معاملة، فيجب إلغاء كل شيء. لذلك يجب ألا يتم أي إدراج. يجب أن تظل قاعدة البيانات فارغة؛
- السطر 53: يتم إغلاق السياق؛
- الأسطر 61-90: عرض محتوى قاعدة البيانات. يجب أن تكون فارغة.
عرض الشاشة هو كما يلي:
- السطر 1: استثناء بسبب انتهاك قيد التفرد في الجدول [RVS]؛
- السطور 9-12: قاعدة البيانات فارغة بالفعل. وبالتالي، تمت مزامنة السياق مع قاعدة البيانات في معاملة واحدة.
لا شك أن هناك أمور أخرى يمكن استكشافها في EF 5. لكننا نعرف ما يكفي للعودة إلى دراستنا للبنية متعددة الطبقات. سيجد القارئ في بداية هذا المستند مراجع لمقالات وكتب تسمح له بتعميق معرفته بـ EF 5.
3.6. دراسة بنية متعددة الطبقات تعتمد على EF 5
نعود إلى دراسة الحالة الموصوفة في الفقرة 2. يتعلق الأمر بتطبيق ويب ASP.NET منظم على النحو التالي:
![]() |
سنبدأ ببناء طبقة [DAO] للوصول إلى البيانات. ستعتمد هذه الطبقة على EF5.
3.6.1. المشروع الجديد
نقوم بإنشاء مشروع وحدة تحكم جديد VS 2012 [RdvMedecins-SqlServer-02] في الحل الحالي [1]:
![]() |
نضيف أربعة مجلدات باسم [2] سنقوم بتوزيع أكوادنا عليها. المجلد [Entites] هو نسخة من المجلد [Entites] من المشروع السابق. بعد هذه النسخة، تظهر أخطاء بسبب عدم وجود المراجع الصحيحة. علينا إضافة مرجع إلى Entity Framework 5. وللقيام بذلك، سنتبع الطريقة الموضحة في الفقرة 3.4، الصفحة 21. تصبح قائمة المراجع كما يلي [3]:
![]() |
في هذه المرحلة، يجب ألا يظهر المشروع أي أخطاء في الترجمة. من المشروع السابق، ننسخ أيضًا الملف [App.config] الذي يهيئ الاتصال بقاعدة البيانات:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- لمزيد من المعلومات حول تكوين Entity Framework، تفضل بزيارة http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
<!-- سلسلة الاتصال بالقاعدة -->
<connectionStrings>
<add name="monContexte"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- مزود المصنع -->
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider"
invariant="System.Data.SqlClient"
description=".Net Framework Data Provider for SqlServer"
type="System.Data.SqlClient.SqlClientFactory, System.Data,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/>
</DbProviderFactories>
</system.data>
</configuration>
3.6.2. فئة Exception
سنستخدم فئة استثناء خاصة بالمشروع. وهي الفئة التي ستخرج من الطبقة [DAO]:
![]() |
ستوقف الطبقة [DAO] جميع الاستثناءات التي ستصل إليها وستقوم بتغليفها في استثناء من النوع [RdvMedecinsException]. سيكون هذا الاستثناء كما يلي:
using System;
namespace RdvMedecins.Exceptions
{
public class RdvMedecinsException : Exception
{
// الخصائص
public int Code { get; set; }
// المنشئون
public RdvMedecinsException()
: base()
{
}
public RdvMedecinsException(string message)
: base(message)
{
}
public RdvMedecinsException(int code, string message)
: base(message)
{
Code = code;
}
public RdvMedecinsException(int code, string message, Exception ex)
: base(message, ex)
{
Code = code;
}
// الهوية
public override string ToString()
{
if (InnerException == null)
{
return string.Format("RdvMedecinsException[{0},{1}]", Code, base.Message);
}
else
{
return string.Format("RdvMedecinsException[{0},{1},{2}]", Code, base.Message, base.InnerException.Message);
}
}
}
}
- السطر 5: الفئة مشتقة من الفئة [Exception]؛
- السطر 9: تضيف إلى فئتها الأساسية رمز خطأ؛
- الأسطر 12-32: تدمج المنشئات المختلفة وجود الحقل [Code].
يتطور المشروع على النحو التالي:
![]() |
3.6.3. الطبقة [DAO]
![]() |
الطبقة [DAO] توفر واجهة للطبقة [ASP.NET]. لتحديد هذه الطبقة، يجب الاطلاع على صفحات الويب الخاصة بالتطبيق:
![]() |
- في [1] أعلاه، تم ملء القائمة المنسدلة بقائمة الأطباء. ستوفر الطبقة [DAO] هذه القائمة؛
- في [2]، ستوفر الطبقة [DAO]؛
- قائمة مواعيد الطبيب في يوم معين،
- قائمة المواعيد المتاحة للطبيب،
- معلومات إضافية عن الطبيب المحدد؛
![]() |
- في [3]، ستوفر الطبقة [DAO] قائمة منسدلة بالعملاء؛
![]() |
- في [4]، يقوم المستخدم بتأكيد موعد. يجب أن تتمكن الطبقة [DAO] من إضافته إلى قاعدة البيانات. كما يجب أن تتمكن من تقديم معلومات إضافية عن العميل المحدد؛
![]() |
- في [5]، يقوم المستخدم بحذف موعد. يجب أن تسمح الطبقة [DAO] بذلك.
باستخدام هذه المعلومات، يمكن أن تكون واجهة [IDao] للطبقة [DAO] كما يلي:
using System;
using System.Collections.Generic;
using RdvMedecins.Entites;
namespace RdvMedecins.Dao
{
public interface IDao
{
// قائمة العملاء
List<Client> GetAllClients();
// قائمة الأطباء
List<Medecin> GetAllMedecins();
// قائمة المواعيد المتاحة لطبيب ما
List<Creneau> GetCreneauxMedecin(int idMedecin);
// قائمة RV لطبيب معين، في يوم معين
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// إضافة RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// حذف موعد طبيب
void SupprimerRv(int idRv);
// البحث عن كيان T عبر مفتاحه الأساسي
T Find<T>(int id) where T : class;
}
}
تستند الطرق الواردة في الأسطر 10-20 إلى الدراسة التي تم إجراؤها للتو. أما الطريقة الواردة في السطر 22 فهي موجودة لمعالجة حقيقة أننا نعمل بنظام التحميل المتأخر (Lazy Loading). إذا احتجنا في الطبقة [ASP.NET] إلى تبعية كيان ما، فسنبحث عنها في قاعدة البيانات باستخدام هذه الطريقة.
سيكون تنفيذ [Dao] لهذه الواجهة كما يلي:
using System;
using System.Collections.Generic;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using RdvMedecins.Models;
namespace RdvMedecins.Dao
{
public class Dao : IDao
{
//قائمة العملاء
public List<Client> GetAllClients()
{
// قائمة العملاء
List<Client> clients = null;
try
{
// فتح سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
// قائمة العملاء
clients = context.Clients.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(1, "GetAllClients", ex);
}
// عرض النتيجة
return clients;
}
// قائمة الأطباء
public List<Medecin> GetAllMedecins()
{
// قائمة الأطباء
List<Medecin> medecins = null;
try
{
// فتح سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
// قائمة الأطباء
medecins = context.Medecins.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(2, "GetAllMedecins", ex);
}
// يتم إرجاع النتيجة
return medecins;
}
// قائمة المواعيد المتاحة لطبيب معين
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
...
}
// قائمة بـ RV لطبيب معين ليوم معين
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
...
}
// إضافة RV
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
...
}
// حذف موعد طبيب
public void SupprimerRv(int idRv)
{
...
}
// البحث عن عميل
public Client FindClient(int id)
{
...
}
// البحث عن موعد
public Creneau FindCreneau(int id)
{
...
}
// البحث عن طبيب
public Medecin FindMedecin(int id)
{
....
}
// البحث عن موعد
public Rv FindRv(int id){
...
}
}
}
دعونا نوضح الطريقة [GetAllClients] التي يجب أن تعرض قائمة بجميع العملاء:
- الأسطر 18-31: يتم البحث عن العملاء في try / catch. وينطبق الأمر نفسه على جميع الطرق التالية؛
- السطر 21: فتح سياق جديد؛
- السطر 24: يتم تحميل الكيانات [Client] في السياق ووضعها في قائمة.
الطريقة [GetAllMedecins] التي يجب أن تعرض قائمة بجميع الأطباء مشابهة (الأسطر 37-57).
الطريقة [GetCreneauxMedecin] هي كما يلي:
// قائمة المواعيد المتاحة لطبيب معين
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
// قائمة المواعيد
try
{
// فتح سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
// يتم استرداد بيانات الطبيب مع مواعيده
Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
// قائمة مواعيد الطبيب
return medecin.Creneaux.ToList<Creneau>();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
}
}
- السطر 9: فتح سياق استمرارية جديد؛
- السطر 11: البحث عن الطبيب الذي لدينا مفتاحه الأساسي. نطلب تضمين التبعية [Creneaux] التي هي مجموعة من المواعيد المتاحة للطبيب. إذا لم يكن الطبيب موجودًا، تطلق الطريقة Single استثناءً؛
- السطر 13: يتم عرض قائمة المواعيد.
يجب أن تعرض الطريقة [GetRvMedecinJour] قائمة مواعيد الطبيب ليوم معين. قد يكون كودها كما يلي:
// قائمة بـ RV الطبيب ليوم معين
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
// قائمة المواعيد
List<Rv> rvs = null;
try
{
// فتح سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
// استرداد الطبيب
Medecin medecin = context.Medecins.Find(idMedecin);
if (medecin == null)
{
throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
}
// قائمة المواعيد
rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
}
// إرجاع النتيجة
return rvs;
}
- السطر 13: يتم إدخال الطبيب الذي لدينا مفتاحه الأساسي في السياق؛
- الأسطر 14-17: إذا لم يكن موجودًا، يتم إلقاء استثناء؛
- السطر 19: الاستعلام LINQ لاسترداد المواعيد لهذا الطبيب؛
يجب أن تضيف الطريقة [AjouterRv] موعدًا إلى قاعدة البيانات وأن تعرض المفتاح الأساسي للعنصر الذي تم إدراجه. قد يكون كودها كما يلي:
// إضافة RV
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
// رقم الموعد المضاف
int idRv;
try
{
// فتح سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
// استرداد الموعد
Creneau creneau = context.Creneaux.Find(idCreneau);
if (creneau == null)
{
throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
}
// استرداد العميل
Client client = context.Clients.Find(idClient);
if (client == null)
{
throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
}
// إنشاء الموعد
Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
// الإضافة إلى السياق
context.Rvs.Add(rv);
// حفظ السياق
context.SaveChanges();
// استرداد المفتاح الأساسي للموعد المضاف
idRv = (int)rv.Id;
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(7, "AjouterRv", ex);
}
// النتيجة
return idRv;
}
- السطر 12: البحث عن موعد في قاعدة البيانات؛
- الأسطر 13-16: إذا لم يتم العثور عليه، يتم إصدار استثناء؛
- السطر 18: البحث عن عميل الموعد في قاعدة البيانات؛
- الأسطر 19-22: إذا لم يتم العثور عليه، يتم إلقاء استثناء؛
- السطر 24: يتم إنشاء كائن [Rv] بالمعلومات اللازمة؛
- السطر 26: نضيفه إلى سياق الاستمرارية؛
- السطر 28: نقوم بمزامنة سياق الاستمرارية مع قاعدة البيانات. سيتم عندئذٍ إدراج الموعد في قاعدة البيانات؛
- السطر 30: نعلم أنه بعد مزامنة قاعدة البيانات، تصبح المفاتيح الأساسية للعناصر التي تم إدراجها متاحة. نسترد مفتاح الموعد الذي تمت إضافته؛
- السطر 31: نغلق سياق الاستمرارية.
يجب أن تقوم الطريقة [SupprimerRv] بحذف موعد يتم تمرير المفتاح الأساسي الخاص به إليها.
// حذف RV
public void SupprimerRv(int idRv)
{
try
{
// فتح سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
// استرداد Rv
Rv rv = context.Rvs.Find(idRv);
if (rv == null)
{
throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
}
// حذف Rv
context.Rvs.Remove(rv);
// حفظ السياق
context.SaveChanges();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(8, "SupprimerRv", ex);
}
}
- السطر 7: سياق استمرارية جديد؛
- السطر 10: يتم إدخال الموعد المراد حذفه في السياق؛
- الأسطر 11-15: إذا لم يكن موجودًا، يتم إصدار استثناء؛
- السطر 16: يتم حذفه من السياق؛
- السطر 18: تتم مزامنة السياق مع قاعدة البيانات؛
- السطر 19: يتم إغلاق السياق.
تسمح الطريقة [Find<T>] بالبحث في قاعدة البيانات عن كيان من النوع T، عبر مفتاحه الأساسي. قد يكون كودها كما يلي:
public T Find<T>(int id) where T : class
{
try
{
// فتح سياق الاستمرارية
using (var context = new RdvMedecinsContext())
{
return context.Set<T>().Find(id);
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(20, "Find<T>", ex);
}
}
- السطر 8: تسمح الطريقة Set<T> باسترداد DbSet<T> الذي يمكن تطبيق الطرق المعتادة عليه.
يتطور المشروع على النحو التالي:
![]() |
3.6.4. اختبار الطبقة [DAO]
سنقوم بإنشاء برنامج لاختبار الطبقة [DAO]. ستكون بنية الاختبار كما يلي:
![]() |
يطلب برنامج وحدة التحكم من [Spring.net] إنشاء مثيل للطبقة [DAO]. وبمجرد القيام بذلك، يقوم باختبار الوظائف المختلفة لواجهة الطبقة [DAO]. بدلاً من برنامج وحدة التحكم، كان من الأفضل كتابة برنامج اختبار من النوع NUnit. قد يكون برنامج اختبار الطبقة [DAO] كما يلي:
using System;
using System.Collections.Generic;
using RdvMedecins.Dao;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using Spring.Context.Support;
namespace RdvMedecins.Tests
{
class Program
{
public static void Main()
{
IDao dao = null;
try
{
// إنشاء مثيل لطبقة [DAO] عبر Spring
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
// عرض العملاء
List<Client> clients = dao.GetAllClients();
DisplayClients("Liste des clients :", clients);
// عرض الأطباء
List<Medecin> medecins = dao.GetAllMedecins();
DisplayMedecins("Liste des médecins :", medecins);
// قائمة المواعيد المتاحة للطبيب رقم 0
List<Creneau> creneaux = dao.GetCreneauxMedecin((int)medecins[0].Id);
DisplayCreneaux(string.Format("Liste des créneaux horaires du médecin {0}", medecins[0]), creneaux);
// قائمة مواعيد الطبيب ليوم معين
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
// إضافة RV للطبيب رقم 1 في الموعد رقم 0
Console.WriteLine(string.Format("Ajout d'un RV au médecin {0} avec client {1} le 23/11/2013", medecins[0], clients[0]));
int idRv1 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
Console.WriteLine("Rdv ajouté");
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
// إضافة موعد في فترة زمنية مشغولة بالفعل - يجب أن يؤدي إلى استثناء
int idRv2;
Console.WriteLine("Ajout d'un RV dans un créneau déjà occupé");
try
{
idRv2 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
Console.WriteLine("Rdv ajouté");
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
}
catch (RdvMedecinsException ex)
{
Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
}
// حذف موعد
Console.WriteLine(string.Format("Suppression du RV n° {0}", idRv1));
dao.SupprimerRv(idRv1);
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
}
catch (Exception ex)
{
Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
}
//توقف مؤقت
Console.ReadLine();
}
// طرق المساعدة - تعرض القوائم
public static void DisplayClients(string Message, List<Client> clients)
{
Console.WriteLine(Message);
foreach (Client c in clients)
{
Console.WriteLine(c.ShortIdentity());
}
}
public static void DisplayMedecins(string Message, List<Medecin> medecins)
{
...
}
public static void DisplayCreneaux(string Message, List<Creneau> creneaux)
{
...
}
public static void DisplayRvs(string Message, List<Rv> rvs)
{
...
}
}
}
- السطر 14: الإشارة إلى الطبقة [DAO]. لجعل الاختبار مستقلاً عن التنفيذ الفعلي لهذه الطبقة، تكون هذه الإشارة من نوع الواجهة [IDao] وليست من نوع الفئة [Dao]؛
- السطر 18: يتم إنشاء مثيل للطبقة [DAO] بواسطة Spring. سنعود لاحقًا إلى التكوين اللازم لجعل ذلك ممكنًا. نقوم بتحويل مرجع الكائن الذي يقدمه Spring إلى مرجع من نوع الواجهة [IDao]؛
- السطران 21-22: يعرضان العملاء؛
- السطران 25-26: يعرضان الأطباء؛
- السطران 29-30: يعرضان قائمة المواعيد المتاحة للطبيب رقم 0؛
- السطر 33: يعرض مواعيد الطبيب رقم 0 بتاريخ 23/11/2013. يجب ألا يكون هناك أي مواعيد؛
- السطر 37: يضيف موعدًا للطبيب رقم 0 بتاريخ 23/11/2013؛
- السطر 39: يعرض مواعيد الطبيب رقم 0 بتاريخ 23/11/2013. يجب أن يكون هناك موعد واحد؛
- السطر 46: نضيف نفس الموعد مرة ثانية. يجب أن يكون هناك استثناء؛
- السطر 57: حذف الموعد الوحيد الذي تمت إضافته؛
- السطر 58: يعرض مواعيد الطبيب رقم 0 بتاريخ 23/11/2013. يجب ألا يكون هناك أي موعد.
3.6.5. تكوين Spring.net
في برنامج الاختبار أعلاه، مررنا سريعًا على التعليمات التي تُنشئ طبقة [DAO]:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
الفئة [ContextRegistry] هي فئة Spring في مساحة الأسماء [Spring.Context.Support]. لاستخدام Spring، يتعين علينا إضافة DLL إلى مراجع المشروع. ونقوم بذلك على النحو التالي:
![]() |
- في [1]، نبحث عن الحزم باستخدام أداة [NuGet]؛
![]() |
- في [2]، نبحث عن الحزم عبر الإنترنت؛
- في [3]، نضع الكلمة المفتاحية spring في منطقة البحث؛
- في [4]، يتم عرض الحزم التي تحتوي وصفها على هذه الكلمة المفتاحية. هنا، [Spring.Core] هو ما يناسبنا. نقوم بتثبيته.
تتطور مراجع المشروع على النحو التالي:
![]() |
كان الحزمة [Spring.Core] تعتمد على الحزمة [Common.Logging]. وقد تم تحميل هذه الحزمة أيضًا. في هذه المرحلة، لا ينبغي أن يكون هناك أخطاء في المشروع.
لكن هذا لا يعني أنه سيعمل. علينا أولاً تكوين Spring في الملف [App.config]. هذا هو الجزء الأكثر تعقيدًا في المشروع. الملف الجديد [App.config] هو كما يلي:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- لمزيد من المعلومات حول تكوين Entity Framework، تفضل بزيارة http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
<!-- spring -->
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
<!-- التسجيل العام-->
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<!-- إطار عمل الكيانات -->
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters>
<parameter value="v11.0" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
<!-- سلاسل الاتصال -->
<connectionStrings>
<add name="monContexte" connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</DbProviderFactories>
</system.data>
<!-- تكوين Spring -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
<!-- تكوين common.logging -->
<logging>
<factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
<arg key="showLogName" value="true" />
<arg key="showDataTime" value="true" />
<arg key="level" value="DEBUG" />
<arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
</factoryAdapter>
</logging>
</configuration>
لنبدأ بإزالة كل ما هو معروف بالفعل: Entity Framework، سلاسل الاتصال، ProviderFactory. يتطور الملف على النحو التالي:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- لمزيد من المعلومات حول تكوين Entity Framework، تفضل بزيارة http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" ... />
<!-- spring -->
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
<!-- التسجيل المشترك-->
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</sectionGroup>
</configSections>
...
<!-- تكوين Spring -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
<!-- تكوين common.logging -->
<common>
<logging>
<factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
<arg key="showLogName" value="true" />
<arg key="showDataTime" value="true" />
<arg key="level" value="DEBUG" />
<arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
</factoryAdapter>
</logging>
</common>
</configuration>
- الأسطر 3-15: تحدد أقسام التكوين؛
- السطر 8: يحدد الفئة التي ستدير قسم <spring><context> من الملف XML (الأسطر 19-21)؛
- السطر 9: يحدد الفئة التي ستدير قسم <spring><objects> في الملف XML (الأسطر 22-24)؛
- السطر 13: يحدد الفئة التي ستدير قسم <common><logging> من الملف XML (الأسطر 27-36)؛
- الأسطر 7-14: ثابتة. لا تحتاج إلى تغيير في مشروع آخر؛
- الأسطر 18-25: تكوين Spring. ثابت باستثناء الأسطر 22-24 التي تحدد الكائنات التي سيقوم Spring بإنشاء مثيل لها؛
- السطر 23: تعريف كائن. السمة id حرة. وهي معرف الكائن. تشير السمة type إلى الفئة المراد إنشاء مثيل لها بالصيغة "الاسم الكامل للفئة، التجميع الذي يحتوي على الفئة". الفئة هنا هي التي تنفذ الطبقة [DAO]: [RdvMedecins.Dao.Dao]. لمعرفة التجميع الخاص بها، يجب الاطلاع على خصائص المشروع:
![]() |
في [1]، اسم التجميع المطلوب توفيره؛
- الأسطر 27-36: تكوين "Common Logging" ثابت. قد نضطر إلى تعديل مستوى المعلومات، السطر 32. بعد مرحلة التصحيح، يمكننا نقل المستوى إلى INFO.
في النهاية، على الرغم من أن ملف تكوين Spring يبدو معقدًا للوهلة الأولى، إلا أنه بسيط في الواقع. لا يتعين تعديل سوى:
- الأسطر 22-24 التي تحدد الكائنات المراد إنشاء مثيل لها؛
- السطر 32: مستوى السجلات.
في برنامج الاختبار، فإن التعليمات التي تنشئ الطبقة [DAO] هي التالية:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
[ContextRegistry] هي فئة من Spring تستخدم تكوين Spring الذي تم إجراؤه في ملف [Web.config] أو [App.config]. هنا، ستستخدم القسم التالي من ملف [App.config]:
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
- يستخدم ContextRegistry.GetContext() سياق الأسطر 2-4. تشير السطر 3 إلى أن كائنات Spring محددة في القسم [spring/objects] من ملف التكوين. ويشمل هذا القسم الأسطر 5-7؛
- يستخدم ContextRegistry.GetContext().GetObject("rdvmedecinsDao") القسم الموجود في الأسطر 5-7. وهي تُرجع مرجعًا إلى الكائن الذي يحتوي على السمة id= "rdvmedecinsDao ". هذا هو الكائن المُعرَّف في السطر 6. ثم يقوم Spring بإنشاء مثيل للفئة المُعرَّفة بواسطة السمة type باستخدام مُنشئها بدون معلمات. لذا يجب أن يكون هذا المُنشئ موجودًا. وبمجرد القيام بذلك، يتم إرجاع مرجع الكائن الذي تم إنشاؤه إلى الكود المستدعي. إذا تم طلب الكائن مرة ثانية في الكود، يكتفي Spring بإرجاع مرجع إلى الكائن الأول الذي تم إنشاؤه. هذا هو نمط التصميم (Design Pattern) المسمى singleton.
قد يكون إنشاء الكائن أكثر تعقيدًا. يمكن استخدام منشئ مع معلمات أو تحديد تهيئة حقول معينة من الكائن بمجرد إنشائه. لمزيد من المعلومات حول هذا الموضوع، يمكن قراءة المقال "دليل Spring IOC لـ .NET"، في URL [http://tahe.developpez.com/dotnet/springioc/].
بعد ذلك، يمكننا تشغيل التطبيق. النتائج التي تظهر على الشاشة هي كما يلي:
النتائج مطابقة لما كان متوقعًا. سنعتبر الآن أن طبقتنا [DAO] صالحة. يمكن أن يتوقف البرنامج التعليمي عند هذا الحد. لقد أوضحنا حتى الآن:
- أساسيات ORM Entity Framework 5؛
- طبقة [DAO] تستخدم هذا ORM.
لنتذكر دراسة الحالة التي وصفناها في بداية هذا المستند. نبدأ من تطبيق موجود بهيكلية كالتالي:
![]() |
التي نريد تحويلها إلى هذه:
![]() |
حيث حل EF5 محل NHibernate. لقد أنشأنا للتو الطبقة [DAO2]. في الواقع، لا تتمتع هذه الطبقة بنفس واجهة الطبقة [DAO1] التي كانت واجهتها أصغر:
public interface IDao
{
// قائمة العملاء
List<Client> GetAllClients();
// قائمة الأطباء
List<Medecin> GetAllMedecins();
// قائمة المواعيد المتاحة لطبيب
List<Creneau> GetCreneauxMedecin(int idMedecin);
// قائمة بـ RV لطبيب معين، في يوم معين
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// إضافة RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// حذف موعد طبيب
void SupprimerRv(int idRv);
}
أضافت الطبقة [DAO2] إلى هذه الواجهة الطريقة:
// البحث عن كيان T عبر مفتاحه الأساسي
T Find<T>(int id) where T : class;
يأتي إضافة هذه الطريقة من حقيقة أن ORM EF 5 تعمل بشكل افتراضي في وضع Lazy Loading. تصل الكيانات إلى الطبقة [ASP.NET] بدون تبعياتها. تسمح لنا الطريقة المذكورة أعلاه باستردادها إذا احتجنا إليها، وفي بعض الحالات نحتاج إليها. تعمل NHibernate أيضًا بشكل افتراضي في وضع التحميل المتأخر (Lazy Loading)، لكنني كنت أستخدمها في وضع التحميل الفوري (Eager Loading). كانت الكيانات تصل إلى الطبقة [ASP.NET] مع تبعياتها.
سننهي عملية نقل التطبيق ASP.NET / NHibernate إلى التطبيق ASP.NET / EF 5. ولكن بما أن هذا لم يعد يتعلق بـ EF5، فلن نعلق على كود الويب. سنشرح ببساطة كيفية إعداد تطبيق الويب واختباره. وهو متاح على موقع هذا البرنامج التعليمي.
3.6.6. إنشاء DLL من الطبقة [DAO]
في البنية التالية:
![]() |
ستتوفر للطبقة [ASP.NET] الطبقات الموجودة على يمينها في شكل DLL. لذلك نقوم بإنشاء DLL من الطبقة [DAO].
![]() |
- في [1]، نختار برنامج الاختبار وفي [2] لا ندرجه في DLL الذي سيتم إنشاؤه؛
- في [3]، في خصائص المشروع، نحدد أن التجميع المراد إنشاؤه هو DLL؛
- في [4]، في قائمة VS، يُشار إلى أنه سيتم إنشاء تجميع من النوع [Release] الذي يحتوي على معلومات أقل من تجميع من النوع [Debug]؛
![]() |
- في [5]، يتم إعادة إنشاء ملف التجميع الخاص بالمشروع. سيتم إنشاء ملف DLL؛
- في [6]، يتم عرض جميع ملفات المشروع؛
![]() |
- في [7]، يتم إنشاء DLL لمشروع الطبقة [DAO]. وهذا هو الملف الذي سيستخدمه مشروع الويب ASP.NET؛
- في [8]، نقوم بتحديث عرض المشروع؛
![]() |
- في [9]، يتم تجميع ملفات DLL الموجودة في المجلد [Release] في مجلد خارجي [lib] [10]. وهذا هو المكان الذي سيبحث فيه مشروع الويب عن مراجعه.
3.6.7. الطبقة [ASP.NET]
سنشرح هنا عملية نقل التطبيق [ASP.NET / NHibernate] إلى التطبيق [ASP.NET / EF 5]. سنعمل باستخدام Visual Studio Express 2012 للويب المتاح مجانًا على URL [http://www.microsoft.com/visualstudio/fra/downloads].
سنعمل انطلاقًا من مشروع الويب الحالي الذي تم إنشاؤه باستخدام VS 2010.
![]() |
- في [1]، نفتح المشروع الحالي:
- في [2]، يحتوي المشروع الذي تم تحميله على المراجع التالية [3]:
- [NHibernate] هو DLL لإطار العمل NHibernate،
- [Spring.Core] هو DLL الخاص بإطار العمل Spring.net،
- [log4net] هو DLL الخاص بإطار عمل السجلات log4net. يستخدم هذا الإطار من قبل Spring.net،
- [MySql.Data] هو برنامج التشغيل ADO.NET لـ SGBD MySQL،
- [rdvmedecins] هو DLL للطبقة [DAO] التي تم إنشاؤها باستخدام NHibernate؛
- في [4]، نقوم بتغيير اسم المشروع وفي [5]، نقوم بحذف المراجع السابقة؛
![]() |
- في [6]، نضيف مراجع إلى المشروع؛
- في [7]، نستخدم الخيار [Parcourir] في المساعد؛
![]() |
- في [8]، نختار جميع ملفات DLL الخاصة بالمشروع رقم 2 التي تم وضعها مسبقًا في المجلد [lib]؛
- في [9]، ملخص نقوم بالتحقق منه؛
- في [10]، مشروع الويب مع مراجعه الجديدة.
وبعد ذلك، يصبح شكل المشروع كما يلي:
![]() |
- في [1]، يتم توزيع كود إدارة صفحات الويب على الملفين [Global.asax] و [Default.aspx]. تم وضع كود المساعدة في المجلد [Entites]. وأخيرًا، يتم تكوين التطبيق بواسطة الملف [Web.config]؛
- في [2]، نقوم بإنشاء تجميع المشروع؛
- في [3]، تظهر أخطاء.
دعونا نفحص الأخطاء، على سبيل المثال الخطأ التالي:
![]()
وتفسيرها:
![]()
هل نوع [medecin.Id] هو int؟ في حين أن الطريقة [GetCreneauxMedecin] هي من النوع int. لذا، يلزم وجود cast. يظهر هذا الخطأ بشكل متكرر في جميع أنحاء الكود لأن كيانات مشروع ASP.NET / NHibernate كانت تحتوي على مفاتيح أساسية من النوع int بينما تلك الموجودة في المشروع ASP.NET / EF 5 هي من النوع int?. نقوم بتصحيح جميع الأخطاء من هذا النوع ونعيد إنشاء المشروع. عندها لن يكون هناك أي أخطاء.
يبقى لدينا تفصيل واحد يجب تسويته قبل تنفيذ المشروع: إنشاء مثيل للطبقة [DAO] بواسطة إطار عمل Spring. ويتم ذلك في [Global.asax]:
protected void Application_Start(object sender, EventArgs e)
{
// نقوم بتخزين بعض بيانات قاعدة البيانات في ذاكرة التخزين المؤقت
try
{
// إنشاء مثيل لطبقة [dao]
Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
...
}
catch (Exception ex)
{...
}
}
في برنامج اختبار الطبقة [DAO]، كان هذا البرنامج يقوم بإنشاء مثيل للطبقة [DAO] بالطريقة التالية:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
الطريقتان متطابقتان. نتذكر أن إنشاء مثيل الطبقة [DAO] كان يعتمد على تكوين تم إجراؤه في [App.config]. نستبدل إذن المحتوى الحالي [Web.config] لمشروع الويب بمحتوى [App.config] لمشروع الطبقة [DAO] للحصول على نفس التكوين.
نحن جاهزون للتشغيل الأول. يتم عرض الصفحة الرئيسية [1]:
![]() |
- في [2]، ندخل تاريخ الموعد ونؤكد؛
![]() |
- في [3]، تظهر رسالة خطأ.
عند فحص نص الخطأ المعروض على الصفحة، نلاحظ أن الاستثناء المبلغ عنه هو استثناء Lazy Loading: تمت محاولة تحميل تابع لكائن في حين أن سياق الاستمرارية الذي يديره قد تم إغلاقه. الكائن الآن في حالة "منفصل". يرجع هذا الخطأ إلى أن NHibernate قد استُخدم في وضع التحميل الفوري (Eager Loading) في حين أن EF 5 يعمل افتراضيًا في وضع التحميل المتأخر (Lazy Loading). في السطر المكتوب باللون الأحمر أعلاه:
- يمثل rdv كائن [Rv] الذي تم تحميله بدون تبعياته؛
- لتقييم rdv.Creneau.Id، يحاول التطبيق تحميل التبعية rdv.Creneau. ولكن بما أننا لم نعد في السياق، فإن ذلك غير ممكن، ومن هنا تأتي الاستثناء.
هنا، الحل بسيط. في السطر 108، نقوم بإنشاء إدخال في قاموس باستخدام المفتاح الأساسي لفترة الموعد كمفتاح. ولكن يتبين أن الكيان [Rv] يغلف المفتاح الأساسي للفترة المرتبطة. لذلك نكتب:
dicoRvPris[(int)rdv.CreneauId] = rdv;
نحاول إعادة التنفيذ. هذه المرة، تظهر الرسالة التالية:
![]() |
الخطأ مشابه. في السطر 132، نحاول تحميل التبعية [Client] لكائن [Rv] في الطبقة ASP.NET، أي خارج السياق. يجب البحث عن الكائن [Client] في قاعدة البيانات. ولحل هذه المشكلة، تم إثراء واجهة [IDao] بالطريقة التالية:
// البحث عن كيان T عبر مفتاحه الأساسي
T Find<T>(int id) where T : class;
وستسمح هذه الطريقة بالبحث عن التبعيات. وبالتالي، سيتم إعادة كتابة السطر الخاطئ أعلاه على النحو التالي:
Client client = Global.Dao.Find<Client>(agenda.Creneaux[i].Rdv.ClientId);
مرة أخرى، نلاحظ أهمية أن تتضمن الكيانات مفاتيحها الخارجية. هنا، يتيح لنا الكيان [Rv] الوصول إلى المفتاح الخارجي للتبعية المرتبطة [Creneau]. بعد إجراء هذين التصحيحين، يعمل التطبيق. ندعو القارئ إلى اختبار التطبيق [RdvMedecins-SqlServer-03] الموجود في تنزيلات الأمثلة على الموقع الإلكتروني لهذا المقال.
3.7. Conclusion
لقد أنجزنا عملية نقل تطبيق ASP.NET / NHibernate:
![]() |
إلى تطبيق ASP.NET / EF 5:
![]() |
في حين أن هذه البنية كان من المفترض أن تسمح لنا بالحفاظ على الطبقة [ASP.NET] سليمة، فقد اضطررنا إلى تعديلها لسببين:
- لم تكن الكيانات متطابقة تمامًا. كان نوع المفاتيح الأساسية للكيانات NHibernate هو int بينما كان نوع المفاتيح الأساسية للكيانات EF 5 هو int?. وقد دفعنا ذلك إلى إدخال cast في كود الويب؛
- طريقة تحميل الكيانات لم تكن هي نفسها بالنسبة للكيانين ORM: Eager Loading لـ NHibernate، و Lazy loading لـ EF 5. وقد دفعنا ذلك إلى إثراء واجهة طبقة [DAO] بطريقة عامة تسمح بالبحث عن كيان عبر مفتاحه الأساسي.
ومع ذلك، تبين أن عملية النقل كانت بسيطة إلى حد ما، مما يبرر مرة أخرى، إذا لزم الأمر، بنية الطبقات وحقن التبعيات باستخدام Spring أو إطار عمل آخر لحقن التبعيات.
سنقوم الآن بقياس تأثير تغيير SGBD على البنية السابقة. سنقوم بنقل جميع المشاريع السابقة إلى أربعة SGBD أخرى:
- Oracle Database Express Edition 11g الإصدار 2؛
- MySQL 5.5.28؛
- PostgreSQL 9.2.1؛
- Firebird 2.1.
لن تتغير الرموز. سيتغير فقط ما يلي:
- التعريف في كيانات الحقل المستخدم للتحكم في التنافس على الوصول إلى كيان ما؛
- ملفات التكوين [App.config] أو [Web.config]؛
لن نعلق إلا على العناصر التي تتغير.


















































































































































