3. دراسة حالة باستخدام SQL Server Express 2012
3.1. مقدمة
معظم الأمثلة الموجودة على الإنترنت لـ Entity Framework هي أمثلة تستخدم SQL Server. وهذا أمر طبيعي تمامًا. فمن المرجح أنه نظام إدارة قواعد البيانات الأكثر استخدامًا في عالم .NET المؤسسي. وسنتبع هذا الاتجاه. ثم سيتم توسيع نطاق الأمثلة لتشمل جميع قواعد البيانات المذكورة في القسم 1.2.
3.2. تثبيت الأدوات
لن نصف عملية تثبيت الأدوات. فذلك يتطلب عددًا كبيرًا من لقطات الشاشة التي سرعان ما تصبح قديمة. هذه مهمة (نعترف بأنها ليست سهلة دائمًا) نتركها للقارئ.
نحتاج إلى تثبيت الأدوات التالية:
- نظام إدارة قواعد البيانات SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. قم بتنزيل الإصدار "With Tools"، الذي يتضمن أداة إدارة مع نظام إدارة قواعد البيانات:
بمجرد تثبيت نظام إدارة قواعد البيانات، نقوم بتشغيله:
![]() |
![]() |
- [1]: من قائمة "ابدأ"، قم بتشغيل "SQL Server Configuration Manager"؛
- [2]: في هذا المدير، قم بتشغيل الخادم؛
- [3]: إنه يعمل الآن.
الآن قم بتشغيل أداة إدارة SQL Server:
![]() |
- [1]: من قائمة "ابدأ"، قم بتشغيل "SQL Server Management Studio"؛
- [2]: أداة الإدارة.
سنقوم بالاتصال بالخادم:
![]() |
- في [1]، افتح مستكشف الكائنات؛
- في [2]، أدخل معلمات الاتصال:
- [3]: يشير الخادم (المحلي) (لاحظ الأقواس المطلوبة) إلى الخادم المثبت على الجهاز،
- [4]: حدد مصادقة Windows. يجب أن تكون مسؤولاً على جهاز الكمبيوتر الخاص بك لكي ينجح هذا الاتصال،
![]() |
- [6]: لقد تم الاتصال؛
- [7]: تريد تعديل خصائص معينة للخادم؛
![]() |
- [8]: نطلب أن يكون هناك وضعان للمصادقة:
- مصادقة Windows، كما تم استخدامها للتو. يمكن لمستخدم Windows الذي يتمتع بالأذونات المناسبة عندئذٍ تسجيل الدخول،
- مصادقة SQL Server. يجب أن يكون المستخدم أحد المستخدمين المسجلين في نظام إدارة قاعدة البيانات؛
بمجرد الانتهاء من ذلك، يمكننا التحقق من صحة خصائص الخادم؛
- [9]: قم بتحرير خصائص المستخدم "sa" (مسؤول النظام)؛
![]() |
- في [10]، قم بتعيين كلمة مرور للمستخدم. في بقية هذا المستند، كلمة المرور هي sqlserver2012؛
![]() |
- في [10]، امنحهم إذنًا للاتصال؛
- في [11]، يتم تمكين الاتصال. يمكن الآن تأكيد المعالج؛
- في [12]، قم بتسجيل الخروج من الخادم.
الآن، نعيد الاتصال باستخدام تسجيل الدخول sa/sqlserver2012:
![]() |
- في [1]، نعيد الاتصال؛
- في [2]، أثناء مصادقة SQL Server؛
- في [3]، المستخدم هو sa؛
- في [4]، كلمة المرور هي sqlserver2012؛
- في 5، نقوم بتسجيل الدخول؛
![]() |
- في [6]، نحن مسجلون الدخول.
سنقوم الآن بإنشاء قاعدة بيانات تجريبية:
![]() |
- في [1]، قم بإنشاء قاعدة بيانات جديدة؛
- في [2]، قم بتسميتها demo؛
- في [3]، انقر على "التحقق من الصحة"؛
![]() |
- في [4]، يتم إنشاء قاعدة البيانات؛
- في 5، قم بإنشاء جدول جديد في قاعدة البيانات "demo"؛
![]() |
![]() |
![]() |
![]() |
- في [6]، نحدد جدولاً مكوناً من عمودين، هما ID و NAME؛
- في [7]، نجعل العمود [ID] هو المفتاح الأساسي؛
- في [8]، يتم تمثيل المفتاح الأساسي بمفتاح؛
- في [9]، يتم حفظ الجدول؛
- في [10]، نسميه؛
- في [11]، لكي يظهر الجدول في قاعدة البيانات [demo]، يجب تحديث قاعدة البيانات؛
- في [12]، تم إنشاء الجدول [PERSONNES] بنجاح.
نحن الآن على دراية كافية باستخدام SQL Server Management Studio.
3.3. الخادم المدمج (localdb)\v11.0
يأتي VS Express 2012 مزودًا بخادم SQL Server مدمج. نفترض هنا أن VS Express 2012 قد تم تثبيته [http://www.microsoft.com/visualstudio/fra/downloads]. قم بتشغيل VS 2012 [1]:
![]() |
قم بتشغيل SQL Server 2012 Management Studio [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:
![]() |
يجب أن تعكس الكيانات جداول قاعدة البيانات. تستخدم طبقة الوصول إلى البيانات هذه الكيانات بدلاً من العمل مباشرةً مع الجداول. لنبدأ بجدول [DOCTORS]:
3.4.1. كيان [Medecin]
يحتوي على معلومات حول الأطباء الذين يديرهم تطبيق [RdvMedecins].
![]() | ![]() |
- ID: رقم تعريف الطبيب — المفتاح الأساسي للجدول
- VERSION: الرقم الذي يحدد إصدار الصف في الجدول. يزداد هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تغيير على الصف.
- LAST_NAME: لقب الطبيب
- FIRST NAME: الاسم الأول للطبيب
- TITLE: لقبهم (السيدة، السيدة، السيد)
يمكننا البدء بالفئة [Doctor] التالية:
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".
نضع هذه الفئة في ملف باسم [Entities.cs] [1]. هذا هو المكان الذي سنضع فيه جميع كياناتنا.
![]() |
ونحن لا نزال في مجلد [Models]، نقوم بإنشاء الملف [Context.cs] التالي:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// the context
public class RdvMedecinsContext : DbContext
{
// the doctors
public DbSet<Medecin> Medecins { get; set; }
}
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
}
}
- السطر 8: ستُمثل فئة [RdvMedecinsContext] سياق الاستمرارية، أي مجموعة الكيانات التي يديرها ORM. يجب أن تكون مشتقة من فئة [System.Data.Entity.DbContext]؛
- السطر 11: يمثل الحقل [Medecins] كيانات [Medecin] في سياق الاستمرارية. وهو من النوع DbSet<Medecin>. يوجد عمومًا عدد من [DbSet]s يساوي عدد الجداول في قاعدة البيانات، واحد لكل جدول؛
- السطر 15: نُعرّف فئة [RdvMedecinsInitializer] لتهيئة قاعدة البيانات التي تم إنشاؤها. هنا، تُشتق من فئة [DropCreateDataBaseAlways]، والتي، كما يوحي اسمها، تحذف قاعدة البيانات إذا كانت موجودة بالفعل ثم تعيد إنشائها. وهذا مفيد خلال مرحلة تطوير قاعدة البيانات. المعلمة لفئة [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)
{
// we create the
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>
<!-- For more information on Entity Framework configuration, visit 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 Management Studio ونقوم بتحديث العرض:
![]() |
يمكننا أن نرى أنه تم إنشاء قاعدة بيانات تحمل الاسم الكامل لفئة [RdvMedecinsContext] وأنها تحتوي على جدول باسم [dbo.MEDECINS] (الاسم الذي أطلقناه عليه) مع أعمدة تتطابق مع أسماء حقول كيان [Medecin]. إذا تم تنفيذ الكود بنجاح ولكن قاعدة البيانات المذكورة أعلاه لم تظهر، فتحقق من الخادم المدمج (localdb)\v11.0 (انظر الصفحة 19). مع VS 2012 Pro، يتم استخدام هذا الخادم إذا لم يكن SQL Server نشطًا عند تنفيذ الكود. أما مع VS 2012 Express، فلا يتم استخدامه.
دعونا نفحص بنية جدول [MEDECINS]:
- يستخدم أسماء الحقول من كيان [Medecin]؛
- العمود [Id] هو المفتاح الأساسي. هذه قاعدة EF: إذا كان الكيان E يحتوي على حقل Id أو Eid (MedecinId)، فإن هذا العمود يكون المفتاح الأساسي في الجدول المرتبط؛
- أنواع الأعمدة في الجدول هي نفس أنواع حقول الكيان؛
- بالنسبة لأعمدة Title و Last Name و First Name، تم استخدام نوع [nvarchar(max)]. يمكننا أن نكون أكثر تحديدًا: 5 أحرف للعنوان، و 30 حرفًا للاسم الأخير والاسم الأول؛
- يمكن أن تحتوي أعمدة Title و Last Name و First Name على قيمة NULL. سنقوم بتغيير ذلك.
دعونا نلقي نظرة على خصائص المفتاح الأساسي [Id]:
![]() |
في [1]، نرى أن المفتاح الأساسي من النوع [Identity]، مما يعني أن قيمته يتم إنشاؤها تلقائيًا بواسطة SQL Server. وسنتبنى هذه الاستراتيجية لجميع أنظمة إدارة قواعد البيانات (DBMS).
سنعتمد بشكل أقل على اصطلاحات EF باستخدام التعليقات التوضيحية. يصبح كود الكيان في [Entities.cs] كما يلي:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// data
[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). يمكن العثور على تعليقات توضيحية إضافية على الرابط [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 من الجدول [DOCTORS] في الوقت T1. ويحمل الصف الإصدار V1؛
- تقوم العملية P2 بقراءة نفس الصف L من جدول [DOCTORS] في الوقت 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 طابعًا زمنيًا مختلفًا عن الذي قرأته، لأنه في غضون ذلك، سيكون التعديل الذي أجرته العملية P1 قد غيّره.
تتطور كيان [Doctor] لدينا على النحو التالي:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// data
[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] لأن التطبيق لن يوفر هذه القيمة؛ بل سيقوم نظام إدارة قواعد البيانات (DBMS) نفسه بذلك.
إذا قمنا بتشغيل المشروع باستخدام هذه الكيان الجديد، فإن قاعدة البيانات تتطور على النحو التالي:
![]() |
هناك نقطة أخيرة يجب تناولها. "يعرف" سياق الاستمرارية أنه يجب إدراج كيان في قاعدة البيانات لأن مفتاحه الأساسي فارغ في تلك المرحلة. إن عملية الإدراج في قاعدة البيانات هي التي ستعيّن قيمة للمفتاح الأساسي. هنا، النوع int المعيّن للمفتاح الأساسي [Id] غير مناسب لأن هذا النوع لا يقبل القيمة الفارغة. لذلك نخصص له النوع int?، الذي يقبل قيم int بالإضافة إلى المؤشر null. وبالتالي سيكون الكيان [Medecin] المستخدم كما يلي:
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
...
لا يزال يتعين علينا معرفة كيفية تمثيل مفهوم المفتاح الخارجي بين الجداول في كيان.
3.4.2. كيان [Creneau]
يسرد الجدول [CRENEAUX] الفترات الزمنية التي يمكن فيها تحديد المواعيد:
![]() |
![]() |
- ID: رقم تعريف الفترة الزمنية — المفتاح الأساسي للجدول
- VERSION: الرقم الذي يحدد إصدار الصف في الجدول. يزداد هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تغيير على الصف.
- ID_MEDECIN: رقم التعريف الذي يحدد الطبيب الذي تنتمي إليه هذه الفترة الزمنية – مفتاح خارجي في عمود MEDECINS(ID).
- START_TIME: وقت بدء الفترة الزمنية
- MSTART: دقيقة بدء الفترة الزمنية
- HFIN: وقت انتهاء الفترة الزمنية
- MFIN: الدقائق الأخيرة للفترة الزمنية
يشير الصف الثاني من جدول [SLOTS] (انظر [1] أعلاه) على سبيل المثال إلى أن الفترة رقم 2 تبدأ في الساعة 8:20 صباحًا وتنتهي في الساعة 8:40 صباحًا وتخص الطبيبة رقم 1 (السيدة ماري بيليسييه).
بناءً على ما نعرفه، يمكننا تعريف الكيان [Creneau] على النحو التالي في [Entites.cs]:
[Table("CRENEAUX", Schema = "dbo")]
public class Creneau
{
// data
[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
{
// the context
public class RdvMedecinsContext : DbContext
{
// entities
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
}
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
}
}
يعكس السطر 12 حقيقة أن السياق يحتوي على كيان إضافي يجب إدارته. عند تشغيل المشروع، نحصل على قاعدة البيانات الجديدة التالية:
![]() |
لقد تم بالفعل إنشاء الجدول [CRENEAUX]، والميزة الجديدة هي وجود المفاتيح الخارجية [1] و[2]. تم إنشاء أسمائها من أسماء الحقول المقابلة في الكيان (Medecin) مع إضافة لاحقة "_Id". لعرض خصائص هذه المفتاح الخارجي، نحاول تعديله [3].
![]() |
تُظهر لقطة الشاشة أعلاه أن [Medecin_Id] هو مفتاح خارجي في جدول [CRENEAUX] وأنه يشير إلى المفتاح الأساسي [ID] في جدول [MEDECINS].
إذا أنشأنا الكيانات لقاعدة بيانات موجودة، فلن يُسمى عمود المفتاح الأجنبي بالضرورة [Medecin_Id]. بالنسبة للأعمدة الأخرى، رأينا أن تعليق [Column] حل هذه المشكلة. الغريب أن الأمر أكثر تعقيدًا بالنسبة للمفتاح الأجنبي. يجب أن نتبع الخطوات التالية:
public class Creneau
{
// data
...
[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] مهم. عند قراءة صف من جدول [SLOTS]، سيتلقى قيمة عمود [DOCTOR_ID]، أي قيمة المفتاح الخارجي في جدول [DOCTORS]. وغالبًا ما يكون هذا مفيدًا.
يعكس حقل [Medecin] أعلاه العلاقة متعددة إلى واحد التي تربط كيان [Creneau] بكيان [Medecin]. ترتبط كائنات [Slot] متعددة بنفس [Doctor]. يمكن نمذجة العلاقة العكسية — حيث يرتبط كائن [Doctor] واحد بكائنات [Slot] متعددة — باستخدام حقل إضافي في كيان [Doctor]:
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
...
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
في السطر 8، أضفنا الحقل [Slots]، وهو عبارة عن مجموعة من كائنات [Slot]. سيتيح لنا هذا الحقل الوصول إلى جميع الفترات الزمنية المتاحة للطبيب.
عندما نقوم بتشغيل المشروع مرة أخرى، نلاحظ أن جدول [DOCTORS] لم يتغير:
![]() |
لم تتم إضافة أي أعمدة. العلاقة بين المفتاح الخارجي في جدول [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] و [Appointment]
بما تعلمناه، يمكننا كتابة الكيانين [Client] و [Appointment]. يحتوي الكيان [Client] على معلومات حول العملاء الذين يديرهم تطبيق [DoctorAppointments].
![]() | ![]() |
- ID: رقم تعريف العميل — المفتاح الأساسي للجدول
- VERSION: الرقم الذي يحدد إصدار الصف في الجدول. يزداد هذا الرقم بمقدار 1 في كل مرة يتم فيها إجراء تغيير على الصف.
- LAST_NAME: لقب العميل
- FIRST NAME: الاسم الأول للعميل
- TITLE: لقبهم (السيدة، السيدة، السيد)
يمكن أن تكون كيان [Client] كما يلي:
[Table("CLIENTS", Schema = "dbo")]
public class Client
{
// data
[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; }
// customer rvs
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
فئة [Client] مطابقة تقريبًا لفئة [Doctor]. ويمكن أن تكونا مشتقتين من نفس الفئة الأصلية. العنصر الجديد موجود في السطر 21. وهو يعكس حقيقة أن العميل يمكن أن يكون له عدة مواعيد، ويستمد ذلك من وجود مفتاح خارجي من جدول [RVS] إلى جدول [CLIENTS].
يمثل الكيان [Rv] موعدًا:
![]() |
- ID: الرقم الذي يحدد الموعد بشكل فريد – المفتاح الأساسي
- DAY: يوم الموعد
- SLOT_ID: فترة الموعد – مفتاح خارجي في عمود [ID] من جدول [SLOTS] – يحدد كل من فترة الموعد والطبيب المعني.
- CLIENT_ID: معرف العميل الذي تم تحديد الموعد له – مفتاح خارجي في عمود [ID] من جدول [CLIENTS]
يمكن أن تكون كيان [Rv] كما يلي:
[Table("MEDECINS", Schema = "dbo")]
public class Rv
{
// data
[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
{
// niche Rvs
public ICollection<Rv> Rvs { get; set; }
...
}
السطر 4: مجموعة المواعيد المجدولة لهذه الفترة الزمنية.
عند تشغيل المشروع، تكون قاعدة البيانات التي تم إنشاؤها كما يلي:
![]() |
لم تتغير الجداول [DOCTORS] و[SLOTS]. أما الجداول [CLIENTS] و[APPs] فهي كما يلي:
![]() | ![]() |
هذا ما كان متوقعًا. لا يزال لدينا بعض التفاصيل التي يجب تسويتها:
3.4.4. تعيين اسم قاعدة البيانات
لتعيين اسم قاعدة البيانات التي تم إنشاؤها بواسطة EF، سنستخدم سلسلة اتصال محددة في [App.config]. يتغير ملف التكوين هذا على النحو التالي:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit 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>
<!-- connection chain on base -->
<connectionStrings>
<add name="RdvMedecinsContext"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- the factory provider -->
<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] المستخدمة لسياق الاستمرارية. من المهم تذكر ذلك. يمكن تجاوز هذا القيد في منشئ السياق:
// manufacturer
public RdvMedecinsContext()
: base("monContexte")
{
}
في هذه الحالة، يمكن أن يكون name= "myContext". وهذا هو ما سنستخدمه في بقية المستند.
- السطر 17: سلسلة الاتصال. [Data Source]: اسم الخادم الذي يستضيف نظام إدارة قواعد البيانات (DBMS)؛ [Initial Catalog]: اسم قاعدة البيانات، في هذه الحالة [rdvmedecins-ef]؛ [User Id]: مالك الاتصال؛ [Password]: كلمة مرور المالك. يجب على القارئ تكييف هذه السلسلة مع بيئته؛
- الأسطر 21-29: تعريف [DbProviderFactory]. لا أعرف ما هذا. بناءً على الاسم، قد تكون فئة تُستخدم لإنشاء طبقة [ADO.NET] التي تفصل EF عن نظام إدارة قواعد البيانات:
![]() |
في الواقع، هذه الأسطر غير ضرورية لـ SQL Server، لكنني اضطررت لإضافتها من أجل أنظمة إدارة قواعد البيانات الأخرى. لذا أدرجها هنا كمرجع. وهي لا تسبب أي مشاكل. النقطة المهمة الوحيدة هي الإصدار في السطر 27. إنه إصدار مكتبة DLL [System.Data] المدرجة في مراجع المشروع:
![]() |
ها نحن ذا. نحن جاهزون. نقوم بتشغيل المشروع ونحصل على قاعدة البيانات التالية [rdvmedecins-ef]:
![]() |
ستكون هذه قاعدة البيانات النهائية لدينا. كل ما تبقى هو ملؤها بالبيانات.
3.4.5. ملء قاعدة البيانات
يمكن استخدام فئة تهيئة قاعدة البيانات لإدخال البيانات فيها:
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
protected override void Seed(RdvMedecinsContext context)
{
base.Seed(context);
// initialize the base
// our customers
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);
}
// the doctors
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);
}
// time slots
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);
}
// dates
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
{
// entities
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. تعديل الكيانات
حاليًا، فئتا [Doctor] و[Client] متطابقتان تقريبًا. في الواقع، إذا أزلنا الحقول المضافة لإدارة الاستمرارية باستخدام EF 5، فستكونان متطابقتين. سنجعلهما مشتقتين من فئة [Person]. عندئذٍ تصبح هاتان الكيانات كما يلي:
// a person
public abstract class Personne
{
// data
[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; }
// signature
public override string ToString()
{
return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
...
}
// utility
private string dump(byte[] timestamp)
{
...
}
}
[Table("MEDECINS", Schema = "dbo")]
public class Medecin : Personne
{
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin {0}", base.ToString());
}
}
[Table("CLIENTS", Schema = "dbo")]
public class Client : Personne
{
// customer rvs
public ICollection<Rv> Rvs { get; set; }
// signature
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" على الرابط [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].
سنستخدم الآن هذا الإصدار من الكيانات.
3.4.7. إضافة قيود إلى قاعدة البيانات
هناك تفصيل آخر يجب تناوله. جدول [RVS] للمواعيد هو كما يلي:
![]() |
يجب أن يكون لهذا الجدول قيد تفرد: بالنسبة ليوم معين، لا يمكن حجز فترة زمنية للطبيب إلا مرة واحدة للموعد. فيما يتعلق بالجدول، هذا يعني أن الزوج (DAY, SLOT_ID) يجب أن يكون فريدًا. لا أعرف ما إذا كان يمكن التعبير عن هذا القيد مباشرةً في الكود، سواء على الكيانات أو في السياق. هذا محتمل، لكنني لم أتحقق من ذلك. سنتبع نهجًا مختلفًا. سنستخدم عميل إدارة SQL Server لإضافة هذا القيد.
باستخدام "SQL Server Management Studio"، لم أجد طريقة بسيطة لإضافة هذا القيد بخلاف تنفيذ عبارة SQL التي تنشئه:
![]() |
- في [1] نقوم بإنشاء استعلام SQL لقاعدة البيانات [rdvmedecins-ef]؛
- في [2]، استعلام SQL الذي ينشئ قيد التفرد؛
- في [3]، أدى تنفيذ هذا الاستعلام إلى إنشاء فهرس جديد في الجدول [RVS].
هناك أدوات أخرى لإدارة SQL Server. هنا، سنستخدم أداة 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]، على عمودي DAY و SLOT_ID؛
توفر علامة التبويب DDL كود SQL المطلوب تنفيذه:
![]() |
- في [6]، نقوم بتجميع عبارة SQL؛
![]() |
- في [7]، نقوم بالتأكيد؛
- في [8]، يظهر الفهرس الجديد.
تشبه الواجهة التي يوفرها "SQL Manager Lite for SQL Server" واجهة "SQL Server Management Studio". وتتوفر واجهات مشابهة لقواعد بيانات Oracle وPostgreSQL وFirebird وMySQL. ولذلك سنواصل استخدام هذه المجموعة من أدوات إدارة قواعد البيانات.
للوصول إلى المعلومات المتعلقة بجدول ما، ما عليك سوى النقر عليه مرتين:
![]() |
تتوفر المعلومات المتعلقة بالجدول المحدد في علامات التبويب. في الأعلى، نرى علامة التبويب [الحقول] الخاصة بجدول [العملاء]. تعرض علامة التبويب [البيانات] محتويات الجدول:

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())
{
// empty the current base
// our customers
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
// the doctors
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// save the persistence context
context.SaveChanges();
}
}
}
}
- السطر 9: يتم دائمًا تنفيذ العمليات على سياق الاستمرارية داخل كتلة [using]. وهذا يضمن إغلاق السياق عند انتهاء كتلة [using]؛
- السطر 13: نقوم بالتكرار عبر سياق العملاء [context.Clients]. سيتم وضع جميع العملاء في قاعدة البيانات في سياق الاستمرارية؛
- السطر 15: بالنسبة لكل منهم، نقوم بإجراء عملية [Remove]، التي تزيلهم من السياق. في الواقع، لا يزالون موجودين في السياق ولكن في حالة "تم إزالتهم"؛
- الأسطر 18-21: نقوم بنفس الشيء بالنسبة للأطباء؛
- السطر 23: نقوم بحفظ سياق الاستمرارية في قاعدة البيانات.
عند حفظ السياق في قاعدة البيانات، الكيانات الموجودة في السياق التي:
- لديها مفتاح أساسي فارغ تخضع لعملية SQL INSERT؛
- في حالة "محذوفة" تخضع لعملية SQL DELETE؛
- في حالة "modified" تخضع لعملية SQL UPDATE؛
كما سنرى لاحقًا، يتم تنفيذ عمليات SQL هذه ضمن معاملة. إذا فشلت أي منها، يتم التراجع عن كل ما تم تنفيذه سابقًا.
لنجعل برنامج [Erase] نقطة البداية الجديدة للمشروع [1] ثم نقوم بتشغيل المشروع.
![]() |
دعونا نتحقق من قاعدة البيانات. سنرى أن جميع الجداول فارغة [2]. وهذا أمر مفاجئ، لأننا طلبنا ببساطة حذف الأطباء والعملاء. ومن خلال آلية المفاتيح الخارجية تم إفراغ الجداول الأخرى بشكل متسلسل.
تم تعريف المفتاح الخارجي من الجدول [CRENEAUX] إلى الجدول [MEDECINS] على النحو التالي بواسطة مزود EF 5:
![]() |
- في [1]، حدد الجدول [CRENEAUX]؛
- في [2]، حدد علامة تبويب المفاتيح الخارجية؛
- في [3]، قم بتحرير المفتاح الأجنبي الفردي؛
![]() |
- في [4]، في علامة التبويب DDL، تعريف SQL لقيود المفتاح الأجنبي؛
- في 5، تضمن جملة ON DELETE CASCADE أن يؤدي حذف طبيب إلى حذف الفترات الزمنية المرتبطة به.
يتم تعريف قيود المفاتيح الخارجية للجدول [RVS] بطريقة مماثلة:
- الأسطر 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())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// reset it
// our customers
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);
}
// the doctors
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);
}
// time slots
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);
}
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// save the persistence context
context.SaveChanges();
}
}
}
}
- السطر 10: نفتح سياق الاستمرارية؛
- الأسطر 13–20: تتم إضافة الصفوف من الجدولين [CLIENTS] و [DOCTORS] إلى السياق ثم إزالتها منه. لقد رأينا للتو أن هذا أدى إلى إفراغ قاعدة البيانات تمامًا؛
- الأسطر 22–88: تتم إضافة عناصر إلى سياق الاستمرارية. جميعها تحتوي على مفتاح أساسي فارغ. وبالتالي سيتم إدراجها في قاعدة البيانات؛
- السطر 90: تتم مزامنة التغييرات التي أُجريت على السياق مع قاعدة البيانات. وستخضع قاعدة البيانات لسلسلة من عمليات الحذف (DELETE) بلغة SQL تليها سلسلة من عمليات الإدراج (INSERT) بلغة SQL؛
نجعل برنامج [Fill] الكائن البادئ الجديد للمشروع [1] ثم نقوم بتنفيذه.
![]() |
يمكننا أن نرى في [2] أن الجداول قد تم ملؤها.
3.5.3. عرض محتويات قاعدة البيانات
سنقوم الآن بعرض محتويات قاعدة البيانات باستخدام استعلامات LINQ to Entities. تم تقديم LINQ (Language-Integrated Query) مع .NET Framework 3.5 في عام 2007. وهي تعمل كامتداد للغات .NET، مما يعني أنها مدمجة في اللغة ويتم التحقق من صحة صيغتها بواسطة المُترجم. تتيح لك الاستعلام عن مجموعات متنوعة باستخدام صيغة مشابهة لـ SQL (Structured Query Language) للاستعلام عن قاعدة البيانات. هناك إصدارات مختلفة من LINQ:
- LINQ to Objects، للاستعلام عن المجموعات الموجودة في الذاكرة؛
- LINQ to XML، للاستعلام عن XML؛
- LINQ to Entity، للاستعلام عن قواعد البيانات؛
يعتمد LINQ على العديد من الإضافات للغة .NET. ويمكن استخدام هذه الإضافات خارج LINQ. لن نعرضها هنا، بل سنكتفي بتقديم مرجعين يمكن للقارئ من خلالهما العثور على وصف متعمق لـ LINQ:
- LINQ in Action، بقلم فابريس مارغري، وستيف إيشرت، وجيم وولي، من نشر دار مانينغ؛
- LINQ Pocket Reference، بقلم جوزيف وبن ألباهاري، نشرته دار أورايلي.
لقد قرأت الكتاب الأول ووجدته ممتازًا. لم أقرأ الكتاب الثاني، لكنني قرأت كتاب "C# 3.0 in a Nutshell" للكتاب نفسهما عندما تم إصدار LINQ. وجدت أن هذا الكتاب يفوق بكثير متوسط الكتب التي أقرأها عادةً. يبدو أن الكتب الأخرى لهذين المؤلفين من نفس المستوى. سنستخدم أيضًا LINQPad، وهي أداة تعليمية لـ LINQ كتبها جوزيف ألباهاري.
سنقوم بعرض الكيانات الموجودة في قاعدة البيانات. للقيام بذلك، سنضيف طريقتين للعرض إلى فئاتها. لنبدأ بكيان [Doctor]:
// a doctor
public class Medecin
{
// data
[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; }
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
// utility
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) لتقييم تأثير الطريقتين السابقتين. لقد رأينا أن الكيان يمكن أن يكون له تبعيات على كيان آخر. هذه التبعيات من نوعين:
- واحد إلى عدة، كما هو موضح أعلاه، حيث يرتبط الطبيب بعدة فترات زمنية؛
- متعددة إلى واحد، كما في كيان [Slot] أدناه، حيث ترتبط فتحة واحدة أو أكثر بنفس الطبيب؛
public class Creneau
{
// data
...
[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] أعلاه:
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
لا تعرض طريقة [ToString] التبعية [Slots] في السطر 2. لو كانت قد عرضتها، لكان ذلك قد فرض تحميل جميع فترات عمل الطبيب قبل التنفيذ. ولتجنب هذا التحميل المكلف، لم يتم تضمين التبعية في توقيع الكيان. وبشكل عام، سنقوم بتضمين توقيعين في كل كيان:
- طريقة ToString التي ستعرض الكيان وأي تبعيات من نوع واحد إلى العديد. كما أوضحنا للتو، سيؤدي هذا إلى تحميل التبعية؛
- طريقة ShortIdentity التي لن تشير إلى أي تبعيات. وبالتالي، لن يتم تحميل أي تبعيات؛
ستكون طرق العرض للكيانات الأخرى كما يلي:
الكيان [Client]:
public class Client
{
// data
...
// customer rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
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; }
// niche Rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// short signature
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
{
// data
...
[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; }
// signature
public override string ToString()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
}
// short signature
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)
{
// base dump
using (var context = new RdvMedecinsContext())
{
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// the doctors
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// time slots
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// dates
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
سنشرح الأسطر 17–21، التي تعرض كيانات [Client]. وينطبق الشرح المقدم على الكيانات الأخرى أيضًا.
// our customers
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 Entities. ويتضمن كلمات رئيسية 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: يتم إدخال صفوف جدول [DOCTORS] إلى سياق الاستمرارية وعرضها. لا يتم تحميل أي تبعيات؛
- الأسطر 31-35: يتم جلب صفوف جدول [SLOTS] إلى سياق الاستمرارية وعرضها. رأينا أن طريقة [ToString] الخاصة بهذا الكيان تعرض التبعية [Doctor]. ومع ذلك، فقد تم تحميلها بالفعل. لذلك، لن يكون هناك إعادة تحميل؛
- الأسطر 38–42: يتم جلب صفوف جدول [RVS] إلى سياق الاستمرارية وعرضها. رأينا أن طريقة [ToString] لهذه الكيان تعرض التبعيات [Client] و[Slot]. ومع ذلك، فقد تم تحميلهما بالفعل. لذلك، لن يكون هناك تحميل جديد.
لاحظ أن ترتيب العرض ليس محايدًا. لو كنا نريد عرض كيانات [Rv] أولاً، لكانت طريقة [ToString] الخاصة بها قد أدت إلى تحميل كيانات [Client] و[Creneau] المرتبطة بهذه المواعيد. ولم تكن الكيانات الأخرى لتُحمَّل. بل كانت ستُحمَّل لاحقًا في عرض آخر. وهذا يؤثر على الأداء. يتطلب الكود السابق أربعة أوامر SQL لعرض جميع الكيانات. والآن، لنفترض أننا نستعلم أولاً عن جدول المواعيد [RVS]. يلزم إجراء استعلام SQL أول لجدول [RVS]. بعد ذلك، ستؤدي طريقة [ToString] للكيان [Rv] إلى التحميل المحتمل للكيانات المرتبطة [Client] و[Slot]. يلزم استعلام SQL واحد لكل منها. بافتراض وجود N2 عميل وN3 فترات زمنية وأن جميع هذه الكيانات مشار إليها في جدول [RVS]، سيتطلب عرضها 1+N2+N3 استعلامات SQL. لذلك، يكون الأداء أقل مما هو عليه في الإصدار الذي درسناه. لعرض جدول [RVS] مع تبعياته، سيكون من الضروري إجراء ربط الجداول. يمكن تحقيق ذلك باستخدام LINQ. سنعود إلى هذا الأمر مع مثال. في الوقت الحالي، دعونا نتذكر أنه يجب علينا الانتباه إلى استعلامات SQL الكامنة وراء كود LINQ الخاص بنا.
نقوم بتكوين المشروع لتشغيل هذا الكود الجديد [1] و [2]، ثم ننفذه:
![]() |
إخراج وحدة التحكم كما يلي:
3.5.4. تعلم LINQ باستخدام LINQPad
في الأعلى، استخدمنا استعلامات LINQ to Entity لعرض محتويات جداول قاعدة البيانات. كتب جوزيف ألباهاري برنامجًا لمساعدتك على تعلم الأشكال المختلفة لـ LINQ. سنقدمه الآن.
يتوفر LINQPad على الرابط التالي [http://www.linqpad.net/]. بمجرد تثبيته، نقوم بتشغيله [1]:
![]() |
يمكن للمبتدئين في LINQ البدء باستخدام الأمثلة الموجودة في علامة التبويب [Samples] [2]، والتي توفر مجموعة متنوعة من الأمثلة. دعونا نختار المثال [3]، والذي يفتح بعد ذلك في نافذة جديدة [4]. فيما يلي الكود الكامل للمثال:
// Now for a simple LINQ-to-objects query expression (notice no semicolon):
from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word
// Feel free to edit this... (no-one's watching!) You'll be prompted to save any
// changes to a separate file.
//
// Tip: You can execute part of a query by highlighting it, and then pressing F5.
الأسطر 3-5 هي مثال على استعلام LINQ to Objects. يتبع استعلام LINQ الصيغة التالية:
from variable in collection orderby élément1 select élément2
- يشير المتغير إلى العنصر الحالي في المجموعة. في مثالنا، هذه المجموعة هي قائمة الكلمات الناتجة عن تقسيم السلسلة؛
- يتم فرز المجموعة وفقًا لمعلمة element1 في orderby. في مثالنا، سيتم فرز مجموعة الكلمات حسب الطول؛
- تحدد الكلمة الرئيسية select ما نريد استخراجه من المتغير العنصر الحالي في المجموعة. في مثالنا، سيكون هذا هو الكلمة.
دعونا نُشغّل استعلام LINQ هذا:
![]() |
- في [1]: يتم تنفيذ تعبير LINQ بالضغط على [F5] أو باستخدام زر "تشغيل"؛
- في [2]: العرض. يتم عرض الكلمات حسب طولها. يوضح هذا المثال البسيط قوة LINQ؛
- في [3]، يمكنك تنزيل أمثلة أخرى، بما في ذلك تلك الموجودة في كتاب "LINQ in Action" [4]؛
![]() |
- في 5، نختار مثالاً من الكتاب؛
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
// Group words by length
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 };
// Print each group out
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: يتم تجميع المجموعة (حسب الكلمة الرئيسية) في مجموعة جديدة تسمى lengthGroups. تمثل lengthGroups.Key عامل التجميع (حسب الكلمة الرئيسية)، وهو هنا طول الكلمات. تجمع 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]، يمكن تنزيل برامج تشغيل لأنظمة إدارة قواعد البيانات (DBMS) بخلاف SQL Server؛
- في 5، سنقوم بتنزيل برنامج التشغيل لنظامي إدارة قواعد البيانات MySQL و Oracle؛
![]() |
- في [6]، برنامج التشغيل الذي تم تنزيله؛
- في [7]، سنقوم بالاتصال بقاعدة بيانات SQL Server؛
![]() |
- في [8]، توجد قاعدة البيانات على الخادم المحلي؛
- في [9]، نتصل باستخدام بيانات اعتماد sa / sqlserver2012؛
- في [10]، إلى قاعدة البيانات [rdvmedecins-ef] التي أنشأناها؛
- في [11]، يمكنك اختبار الاتصال؛
- في [12]، قم بإنهاء المعالج؛
- في [13]، يظهر الاتصال في LINQPad.
تم إنشاء الكيانات من الجدول [rdvmedecins-ef]. وهي كما يلي:
![]() |
- في [1]، يمثل [CLIENTS] مجموعة كيانات [Client]. يحتوي كل كيان على:
- الخصائص (ID، TITLE، LAST_NAME، FIRST_NAME، TIMESTAMP)،
- وعلاقة واحد إلى متعدد [CLIENTRVS]؛
- في [2]، يمثل [CRENEAUXes] مجموعة كيانات [Creneau]. يحتوي كل كيان على:
- الخصائص (ID، START_TIME، MIN_TIME، END_TIME، MAX_TIME، DOCTOR_ID، TIMESTAMP)،
- علاقة واحد إلى العديد [CRENEAURVS]،
- علاقة متعددة إلى واحد [DOCTOR]؛
- في [3]، يمثل الكيان [MEDECINS] مجموعة كيانات [Medecin]. يحتوي كل كيان على:
- الخصائص (ID، TITLE، LAST_NAME، FIRST_NAME، TIMESTAMP)،
- علاقة واحد إلى العديد [DOCTOR-SLOTS]؛
- في [4]، تمثل الكيان [RVS] مجموعة كيانات [Rv]. كل كيان له:
- الخصائص (ID، DAY، CLIENT_ID، SLOT_ID، TIMESTAMP)،
- علاقة متعددة إلى واحد [CLIENT]،
- علاقة متعددة إلى واحد [SLOT].
لاحظ أن أسماء الخصائص المذكورة أعلاه تختلف عن الأسماء التي استخدمناها حتى الآن. لا بأس في ذلك. فكل ما نريده هو تعلم المبادئ الأساسية للاستعلام عن قواعد البيانات.
لنرى كيف يمكننا الاستعلام عن قاعدة بيانات الكيانات هذه. على سبيل المثال، نريد قائمة بالأطباء مرتبة حسب TITLE و LAST_NAME:
![]() |
- في [1]، نقوم بإنشاء استعلام جديد؛
- في [2]، نص الاستعلام؛
![]() |
- في [3]، نتيجة الاستعلام؛
- في [4]، نفس الاستعلام باستخدام تعبيرات لامدا. الاستعلام الذي يستخدم تعبيرات لامدا أقل قابلية للقراءة من الاستعلام النصي، وقد تفضل تجنبها. ومع ذلك، فهي أحيانًا لا غنى عنها لأنها تسمح بأشياء معينة لا تسمح بها الاستعلامات النصية. يشير تعبير لامدا إلى دالة ذات معلمة إدخال a ومعلمة إخراج b، بالصيغة a=>b. تقبل طريقة OrderBy أعلاه دالة لامدا كمعلمة وحيدة لها. يوفر هذا المعلمة التي يجب فرز المجموعة وفقًا لها. وبالتالي، فإن MEDECINS.OrderBy(m=>m.TITRE) هي قائمة الأطباء مرتبة حسب ألقابهم. يجب قراءة العبارة كخط أنابيب على مجموعة. يتم توفير مجموعة الأطباء كمدخلات لطريقة OrderBy. ستعالج هذه الطريقة كيانات [Doctor] واحدًا تلو الآخر. في تعبير لامدا m=>m.TITLE، يمثل m المدخلات لدالة لامدا. ويمكن تسميته حسب الرغبة. هنا، ستكون المدخلات لدالة لامدا كيان [Doctor]. تُقرأ الدالة m=>m.TITLE على النحو التالي: إذا أطلقت على m اسم مدخلاتي (كيان [Doctor])، فإن مخرجاتي هي m.TITLE، أي لقب الطبيب. MEDECINS.OrderBy(m=>m.TITRE) هي بدورها مجموعة، وهي مجموعة الأطباء مرتبة حسب ألقابهم. يمكن إدخال هذه المجموعة الجديدة في طريقة أخرى، في هذا المثال طريقة ThenBy. تعمل هذه الطريقة على نفس المبدأ. تُستخدم لتحديد معلمات إضافية لفرز المجموعة.
تعد قراءة كود لامدا المكافئ للكود النصي الذي نكتبه عادةً طريقة جيدة لتعلمه؛
![]() |
- في 5، استعلام SQL المرسل إلى قاعدة البيانات. هنا مرة أخرى، سنقرأ هذا الكود بعناية. فهو يسمح لنا بتقييم التكلفة الفعلية لاستعلام LINQ.
فيما يلي، نقدم بعض الأمثلة على استعلامات LINQ. لكل منها، نعرض النتائج المعروضة ورمز lambda و SQL المكافئ. لفهم هذه الاستعلامات، يجب أن نتذكر العلاقات متعددة إلى واحد التي تربط الكيانات ببعضها البعض. فمن خلالها ننتقل من كيان إلى آخر. وتسمى هذه الخصائص بخصائص التنقل.
![]() |
// العملاء الذين يحملون لقب "Mr" مرتبة حسب الاسم بترتيب تنازلي
النتائج:
![]() |
LINQ | |
Lambda | |
SQL | |
// جميع المواعيد المرتبطة بالطبيب
النتائج (جزئية):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// جميع المواعيد الخاصة بالعميل والطبيب المعنيين
النتائج:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// الأطباء الذين ليس لديهم مواعيد
النتائج:
![]() |
LINQ | |
لامبدا | ![]() |
SQL | |
لا يوجد استعلام LINQ لهذا الطلب. يجب عليك استخدام تعبيرات لامدا. هذا التعبير يُقرأ على النحو التالي: آخذ مجموعة الأطباء (DOCTORS) وأحتفظ (Where) فقط بالأطباء (m) الذين لا أجد لهم موعدًا (rv) مع ذلك الطبيب (m) في مجموعة المواعيد (APPOINTMENTS).
// فترات زمنية السيدة بيليسييه
النتائج (الجزئية):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// عدد المواعيد للسيدة بيليسييه في 8 أكتوبر 2012
النتائج:
![]() |
LINQ | |
Lambda | |
SQL | |
// قائمة العملاء الذين حددوا موعدًا مع السيدة بيليسييه في 10/08/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;
// 1st context
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// add a customer
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// follow-up
Console.WriteLine("client1--avant");
Console.WriteLine(client1);
// save context
context.SaveChanges();
// follow-up
Console.WriteLine("client1--après");
Console.WriteLine(client1);
}
// 2nd context
using (var context = new RdvMedecinsContext())
{
// retrieve client1 from client2
client2 = context.Clients.Find(client1.Id);
// follow-up
Console.WriteLine("client2");
Console.WriteLine(client2);
// modify client2
client2.Nom = "yy";
// save context
context.SaveChanges();
}
// 3rd context
using (var context = new RdvMedecinsContext())
{
// retrieve client2 from client3
client3 = context.Clients.Find(client2.Id);
// follow-up
Console.WriteLine("client3");
Console.WriteLine(client3);
}
}
}
}
- السطر 15: يتم فتح سياق التطبيق؛
- الأسطر 18–25: يتم مسح السياق. وبشكل أكثر دقة، يتم تحميل جميع الكيانات في السياق من قاعدة البيانات ثم تعيينها إلى حالة "محذوفة". لاحظ أنه في هذه المرحلة، لم تتغير قاعدة البيانات. طالما أن السياق غير متزامن مع قاعدة البيانات، تظل قاعدة البيانات دون تغيير. تذكر أن حذف كيانات [Doctor] و [Client] كافٍ لتفريغ قاعدة البيانات من خلال عمليات الحذف التسلسلية؛
- السطور 27-28: تمت إضافة عميل جديد إلى قاعدة البيانات؛
- السطران 30-31: يتم عرض العميل قبل حفظه في قاعدة البيانات؛
- السطر 33: تتم مزامنة السياق مع قاعدة البيانات. ستخضع الكيانات التي تم وضع علامة "محذوف" عليها لعملية SQL DELETE، وسيخضع الكيان المضاف لعملية SQL INSERT؛
- السطران 35-36: يتم عرض العميل بعد المزامنة مع قاعدة البيانات؛
النتيجة المعروضة في وحدة التحكم هي كما يلي:
يرجى ملاحظة النقاط التالية:
- قبل المزامنة مع قاعدة البيانات، لا يمتلك العميل مفتاحًا أساسيًا ولا طابعًا زمنيًا؛
- بعد المزامنة، يصبح لديه كلاهما. تذكر هنا أن المفتاح الأساسي تم تكوينه ليتم إنشاؤه بواسطة SQL Server. وبالمثل، يقوم نظام إدارة قواعد البيانات هذا تلقائيًا بإنشاء الطابع الزمني؛
- السطر 37: يتم إغلاق سياق الاستمرارية. تصبح الكيانات التي احتواها "منفصلة". وهي موجودة ككائنات ولكن ليس ككيانات مرتبطة بسياق استمرارية؛
- السطر 39: يتم بدء سياق فارغ جديد؛
- السطر 42: يتم استرداد العميل مباشرة من قاعدة البيانات عبر مفتاحه الأساسي. ثم يتم إدخاله في السياق. إذا لم يتم العثور عليه، تعرض طريقة Find مؤشرًا فارغًا؛
- السطران 48-49: نقوم بعرضه؛
ينتج عن ذلك النتيجة التالية:
- السطر 47: نقوم بتعديله؛
- السطر 49: نقوم بمزامنة السياق مع قاعدة البيانات. سيكتشف EF أن عناصر معينة من السياق قد تم تعديلها منذ إضافتها إليه. بالنسبة لهذه العناصر، سيقوم بإنشاء عبارات SQL UPDATE لقاعدة البيانات. لذا هنا، ستتكون المزامنة من عبارة UPDATE واحدة؛
- السطر 50: يتم إغلاق السياق الثاني. يتم الآن فصل الكيان client2 الذي كان مرفقًا بالسياق عنه؛
- السطر 52: يتم فتح سياق ثالث فارغ؛
- السطر 55: يتم إحضار العميل الوحيد من قاعدة البيانات إليه مرة أخرى. نريد أن نرى ما إذا كان التعديل الذي تم إجراؤه عليه في السياق السابق قد انعكس في قاعدة البيانات؛
- السطران 57-58: يتم عرض العميل. وهذا يعطي النتيجة التالية:
تم بالفعل تحديث اسم العميل في قاعدة البيانات. لاحظ أن الطابع الزمني الخاص به قد تم تحديثه أيضًا.
- السطر 59: نغلق السياق. وبالمناسبة، لاحظ أنه على عكس الحالتين السابقتين، لم نكن بحاجة إلى مزامنة السياق مع قاعدة البيانات (SaveChanges) مسبقًا لأن السياق لم يتعرض لأي تعديل.
3.5.6. إدارة الكيانات المنفصلة
لنعد إلى البنية الطبقية لتطبيق مثل ذلك الموجود في دراسة الحالة:
![]() |
تستخدم طبقة [DAO] EF5 ORM للوصول إلى البيانات. لدينا المكونات الأساسية لهذه الطبقة. ستفتح كل طريقة سياق استمرارية، وتنفذ العمليات الضرورية (إدراج، تحديث، حذف، استعلام)، ثم تغلقها. سيتم تمرير الكيانات التي تديرها طبقة [DAO] إلى طبقة الويب ASP.NET. في هذه الطبقة، تكون خارج سياق الاستمرارية وبالتالي منفصلة. في طبقة الويب، يمكن للمستخدم تعديل هذه الكيانات (إضافة، تحديث، حذف). وعندما تعود إلى طبقة [DAO]، تظل منفصلة. ومع ذلك، ستحتاج طبقة [DAO] إلى عكس التغييرات التي أجراها المستخدم في قاعدة البيانات. وبالتالي، سيتعين عليها العمل مع الكيانات المنفصلة. دعونا نلقي نظرة على الحالات الثلاث المحتملة:
إضافة كيان منفصل
هذا هو الإجراء القياسي للإضافة. ما عليك سوى إضافة (Add) الكيان المنفصل إلى السياق، مع التأكد من أن مفتاحه الأساسي هو null.
تعديل كيان منفصل
يمكنك استخدام الكود التالي:
- ستقوم طريقة [DbContext].Entry(detached-entity) بإضافة الكيان إلى السياق؛
- يتم تعيين حالة هذا الكيان إلى "modified" بحيث يخضع لعبارة 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;
// empty the current base
Erase();
// add a customer
using (var context = new RdvMedecinsContext())
{
// customer creation
client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
// add customer to context
context.Clients.Add(client1);
// save the context
context.SaveChanges();
}
// basic view
Dump("1-----------------------------");
// client1 is not in the context - we modify it
client1.Nom = "y";
// new context
using (var context = new RdvMedecinsContext())
{
// here we have an empty context
// we put client1 in the context in a modified state
context.Entry(client1).State = EntityState.Modified;
// save the context
context.SaveChanges();
}
// basic view
Dump("2-----------------------------");
// remove out-of-context entity
using (var context = new RdvMedecinsContext())
{
// here we have a new empty context
// we put client1 in the context in a deleted state
context.Entry(client1).State = EntityState.Deleted;
// save the context
context.SaveChanges();
}
// basic view
Dump("3-----------------------------");
}
static void Erase()
{
// empties base
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);
}
// save the context
context.SaveChanges();
}
}
static void Dump(string str)
{
Console.WriteLine(str);
// displays the base
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 إلى الحالة "detached"؛
- السطر 29: تم تعديل اسم الكيان المنفصل؛
- السطر 31: يتم فتح سياق فارغ جديد؛
- السطر 35: يتم وضع الكيان المنفصل client1 في السياق في حالة "modified"؛
- السطر 37: تتم مزامنة السياق مع قاعدة البيانات؛
- السطر 38: يتم إغلاقه؛
- السطر 40: يتم عرض قاعدة البيانات؛
تم تحديث اسم العميل بنجاح في قاعدة البيانات. لاحظ أن الطابع الزمني قد تم تحديثه؛
- السطر 42: فتح سياق فارغ جديد؛
- السطر 46: تم وضع الكيان المنفصل client1 في السياق في حالة "محذوف"؛
- السطر 48: تمت مزامنة السياق مع قاعدة البيانات؛
- السطر 49: يتم إغلاقه؛
- السطر 51: يتم عرض قاعدة البيانات؛
تم حذف الكيان بالفعل من قاعدة البيانات.
الآن، سنلقي نظرة على الوضعين لتحميل تبعيات الكيان: التحميل المتأخر (Lazy) والتحميل الفوري (Eager).
3.5.7. التحميل المؤجل والتحميل الفوري
دعونا نراجع مخطط التبعية "العديد إلى واحد" لكياناتنا الأربعة:
![]() |
في الأعلى، تحتوي الكيان [Creneau] على خاصية تنقلية [Creneau.Medecin] تشير إلى الكيان [Medecin]. وهذا ما يُسمى بالتبعية. وقد رأينا أن هناك أيضًا تبعية من واحد إلى عدة. وينطبق المبدأ الموضح هنا عليها أيضًا.
بشكل افتراضي، يعمل EF 5 في وضع التحميل المتأخر (Lazy Loading): عندما يجلب كيانًا إلى سياق الاستمرارية من قاعدة البيانات، فإنه لا يجلب تبعياته. سيتم تحميل هذه التبعيات عند استخدامها لأول مرة. هذا إجراء منطقي. إذا لم يكن الأمر كذلك، فإن جلب المواعيد إلى السياق سيؤدي، بناءً على التبعيات المذكورة أعلاه، إلى:
- الكيانات [Time Slot] المرتبطة بالمواعيد؛
- كيانات [Doctor] المرتبطة بتلك الفترات؛
- كيانات [Clients] المرتبطة بالمواعيد.
لكن في بعض الأحيان، نحتاج إلى كيان وتبعياته. سنوضح كلا وضعي التحميل.
![]() | ![]() |
فيما يلي كود [LazyEagerLoading]:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class LazyEagerLoading
{
// entities
static Medecin[] medecins;
static Client[] clients;
static Creneau[] creneaux;
static void Main(string[] args)
{
// initialize the base
InitBase();
Console.WriteLine("Initialisation terminée");
// eager loading
Creneau creneau;
int idCreneau = (int)creneaux[0].Id;
using (var context = new RdvMedecinsContext())
{
// crenel n° 0
creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// dependent display
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
}
// lazy loading - default mode
using (var context = new RdvMedecinsContext())
{
// crenel n° 0
creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// dependent display
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()
{
// initialize the base
using (var context = new RdvMedecinsContext())
{
// empty the current base
...
// initialize the base
// our customers
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" }
};
...
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// save the persistence context
context.SaveChanges();
}
}
}
}
- السطر 18: نبدأ من قاعدة معروفة، وهي تلك المستخدمة حتى الآن. بعد هذه العملية، يتم ملء المصفوفات في الأسطر 11–13 بكيانات منفصلة؛
- السطران 21-22: نركز على الفترة الزمنية الأولى والطبيب المرتبط بها؛
- السطر 23: سياق جديد؛
- السطر 26: نضع الفترة الزمنية في السياق مع تبعيتها (التحميل الفوري). ولأن هذا ليس الوضع الافتراضي، يجب أن نطلب هذه التبعية صراحةً. تسمح لنا طريقة Include بالقيام بذلك. معلمتها هي اسم التبعية داخل الكيان الذي تم إدخاله إلى السياق. يستخدم الاستعلام الذي يجلب الكيان إلى السياق تعبيرات لامدا. تسمح لك طريقة Single بتحديد شرط لاسترداد كيان واحد. هنا، نبحث في قاعدة البيانات عن كيان [Creneau] الذي يحتوي على المفتاح الأساسي للفترة رقم 0؛
- السطر 27: نعرض الكيان الذي تم استرداده. دعونا نستعرض طريقتي الكتابة المستخدمتين في الكيانات:
// signature
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// short signature
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] التبعية [Doctor]. إذا لم تكن موجودة بالفعل في السياق، فسيتم البحث عنها في قاعدة البيانات لإضافتها؛
- الأسطر 8-11: لا تعرض طريقة [ShortIdentity] التبعية [Doctor]. وبالتالي، لن يتم البحث عنها في قاعدة البيانات إذا لم تكن موجودة في السياق؛
في هذه المرحلة، يكون إخراج وحدة التحكم كما يلي:
- السطر 28: تم إغلاق السياق؛
- الأسطر 30–37: نحاول كتابة تبعية الكيان [Doctor]. تذكر كيف يعمل التحميل المتأخر: يتم تحميل التبعية عند أول استخدام لها إذا لم تكن موجودة. هنا، تكون موجودة عادةً. الناتج هو كما يلي:
- الأسطر 39-44: في سياق جديد، يتم البحث عن الخانة رقم 0 مرة أخرى في قاعدة البيانات وإدخالها في السياق. هنا، لم يتم طلب التبعية [Doctor] صراحةً. وبالتالي لن يتم إدخالها (التحميل المتأخر)؛
- السطر 43: يتم عرض الهوية المختصرة للفتحة على النحو التالي:
هنا، من المهم استخدام ShortIdentity بدلاً من ToString لعرض الكيان. إذا تم استخدام ToString، فسيتم عرض التبعية [Doctor]، وللقيام بذلك، سيتم البحث عنها في قاعدة البيانات. لكننا لا نريد ذلك.
- السطر 44: تم إغلاق السياق؛
- الأسطر 46–53: نحاول عرض تبعية الكيان. من المهم القيام بذلك خارج السياق؛ وإلا، فسيتم البحث عنها في قاعدة البيانات والعثور عليها. هنا، نحن خارج السياق. كيان [Creneau] منفصل وتبعيته [Medecin] مفقودة (التحميل المتأخر). ماذا سيحدث؟ عرض الشاشة كما يلي:
اكتشف EF أن التبعية [Medecin] مفقودة. حاول تحميلها، ولكن بما أن السياق كان مغلقًا، لم يعد من الممكن إجراء هذه العملية. سنقوم بتدوين هذا الاستثناء [System.ObjectDisposedException] لأنه من سمات تحميل تبعية خارج سياق مفتوح.
الآن دعونا نفحص الوصول المتزامن إلى الكيانات.
3.5.8. التزامن في الوصول إلى الكيانات
دعونا نراجع تعريف كيان [Client]:
public class Client
{
// data
[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; }
// customer rvs
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
...
}
سنركز على حقل [Timestamp] في الصف 23. نعلم أن قيمته يتم إنشاؤها بواسطة نظام إدارة قواعد البيانات (DBMS). كما لاحظنا أن تعليق [Timestamp] في الصف 22 يجعل EF 5 يستخدم الحقل المُعلَّق عليه لإدارة التزامن في الوصول إلى الكيانات. دعونا نستذكر ما هي إدارة التزامن:
- تقوم العملية P1 بقراءة الصف L من الجدول [DOCTORS] في الوقت T1. يحتوي الصف على الطابع الزمني TS1؛
- تقوم العملية P2 بقراءة نفس الصف L من جدول [DOCTORS] في الوقت T2. يحتوي الصف على الطابع الزمني TS1 لأن العملية P1 لم تقم بعد بتثبيت تعديلها؛
- تقوم العملية P1 بتثبيت تعديلها على الصف L. ثم يتغير الطابع الزمني للصف L إلى TS2؛
- تقوم العملية P2 بتثبيت تعديلها على الصف L. ثم يرمي ORM استثناءً لأن العملية P2 لديها طابع زمني TS1 للصف L يختلف عن الطابع الزمني TS2 الموجود في قاعدة البيانات.
وهذا ما يُسمى إدارة التزامن المتفائل. مع EF 5، يجب أن يكون للحقل الذي يؤدي هذا الدور إحدى السمتين التاليتين: [Timestamp] أو [ConcurrencyCheck]. يحتوي SQL Server على نوع [timestamp]. يتم إنشاء قيمة عمود من هذا النوع تلقائيًا بواسطة SQL Server عند أي إدراج أو تعديل لصف. يمكن بعد ذلك استخدام هذا العمود لإدارة التزامن.
سنوضح هذا الوصول المتزامن باستخدام مؤشرين سيقومان في وقت واحد بتعديل نفس الكيان [Client] في قاعدة البيانات. يتطور المشروع على النحو التالي:
![]() | ![]() |
فيما يلي كود برنامج [ConcurrentAccess]:
using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// object exchanged with threads
class Data
{
public int Duree { get; set; }
public string Nom { get; set; }
public Client Client { get; set; }
}
// test program
class AccèsConcurrents
{
static void Main(string[] args)
{
Client client1;
using (var context = new RdvMedecinsContext())
{
// main thread
Thread.CurrentThread.Name = "main";
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// add a customer
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// follow-up
Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
// backup
context.SaveChanges();
// follow-up
Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
}
// we'll modify client1 with two threads
// thead t1
Thread t1 = new Thread(Modifie);
t1.Name = "t1";
t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
// thread t2
Thread t2 = new Thread(Modifie);
t2.Name = "t2";
t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
// we wait for the end of the 2 threads
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);
// the modification is displayed - only one was successful
using (var context = new RdvMedecinsContext())
{
// retrieve client1 from 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());
}
}
// thread
static void Modifie(object infos)
{
...
}
- السطر 26: نبدأ سياقًا فارغًا؛
- السطر 29: نسمي الخيط الحالي لتمييزه عن الخيطين اللذين سيتم إنشاؤهما لاحقًا؛
- الأسطر 31–38: يتم تعيين كيانات [Doctor] و [Client] إلى الحالة "deleted"؛
- السطران 40-41: تتم إضافة عميل إلى السياق؛
- السطور 43-44: عرضه قبل مزامنة السياق؛
- السطر 46: مزامنة السياق مع قاعدة البيانات: سيتم إزالة الكيانات في حالة "محذوف" من قاعدة البيانات. سيتم إدراج كيان [Client] الموجود في السياق في قاعدة البيانات. سيكون العنصر الوحيد في قاعدة البيانات؛
- الأسطر 47-49: يتم عرض العميل بعد مزامنة السياق. في هذه المرحلة، تظهر الشاشة كما يلي:
لاحظ أنه بعد مزامنة السياق، يكون لدى العميل مفتاح أساسي وطابع زمني؛
- السطر 50: يتم إغلاق السياق؛
- السطر 53: يتم ربط مؤشر الترابط t1 بالطريقة [Modify] في السطر 84. وهذا يعني أنه عند تشغيله، سيقوم بتنفيذ الطريقة [Modify]؛
- السطر 54: يتم تسمية الخيط t1؛
- السطر 55: يتم تشغيل الخيط t1. يتم تمرير المعلمات إليه في شكل بنية [Data] محددة في الأسطر 12-17:
- المدة: سيتوقف الخيط قبل X ثانية من إكمال تنفيذه،
- العميل: مرجع إلى العميل المراد تحديثه في قاعدة البيانات،
- الاسم: الاسم الذي سيُطلق على هذا العميل؛
- الأسطر 57-59: نفس الإجراء مع مؤشر ترابط ثانٍ. في النهاية، سيحاول مؤشرا الترابط تغيير اسم العميل نفسه في قاعدة البيانات؛
- الأسطر 60-63: بعد تشغيل الخيطين، ينتظر الخيط الرئيسي انتهاء تنفيذهما؛
- السطر 62: انتظار انتهاء الخيط t1؛
- السطر 63: انتظار انتهاء الخيط t2؛
- السطر 64: لا نعرف الترتيب الذي ستنتهي به الخيطان. ما هو مؤكد هو أنهما قد انتهتا بحلول السطر 64؛
- الأسطر 66-72: في سياق جديد، نبحث عن العميل في قاعدة البيانات لمعرفة حالته.
الآن دعونا نرى ما يفعله الخيطان t1 و t2. يقومان بتنفيذ طريقة [Modify] التالية:
static void Modifie(object infos)
{
// parameter is retrieved
Data data = (Data)infos;
try
{
using (var context = new RdvMedecinsContext())
{
Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
// retrieve client1 from 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());
// modify client2
client2.Nom = data.Nom;
// we wait a little
Thread.Sleep(data.Duree);
// save changes
context.SaveChanges();
}
}
catch (Exception e)
{
// exception
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
}
// end of thread
Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
}
- السطر 4: استرداد معلمات الخيط (المدة، الاسم، العميل)؛
- السطر 7: سياق جديد؛
- السطر 11: إدخال العميل في السياق؛
- السطران 12 و13: المراقبة للتحقق من حالة العميل؛
- السطر 15: تغيير اسمه؛
- السطر 17: يتوقف الخيط لمدة تبلغ أجزاء من الألف من الثانية. وهذا له تأثير مثير للاهتمام. حيث يحرر الخيط المعالج الذي كان ينفذه، مما يفسح المجال لخيط آخر. في مثالنا، لدينا ثلاثة خيوط: main و t1 و t2. يتم إيقاف الخيط الرئيسي مؤقتًا، في انتظار انتهاء الخيوط t1 و t2. بافتراض أن الخيط t1 حصل على المعالج أولاً، فإنه يتنازل عنه الآن للخيط t2. سيؤدي هذا إلى قراءة الخيط t2 لنفس البيانات تمامًا مثل الخيط t1 — نفس العميل بنفس الطابع الزمني؛
- السطر 19: يتم مزامنة السياق مع قاعدة البيانات. لنفترض مرة أخرى أن الخيط t1 يستيقظ أولاً. سيحفظ العميل الذي يحمل الاسم "yy". وسيكون قادراً على القيام بذلك لأنه يحمل نفس الطابع الزمني الموجود في قاعدة البيانات. وبسبب هذا التحديث، سيقوم نظام إدارة قواعد البيانات (DBMS) بتعديل الطابع الزمني. وعندما يستيقظ الخيط t2 بدوره، سيكون لديه عميل يحمل طابعاً زمنياً مختلفاً عن الطابع الموجود حالياً في قاعدة البيانات. وسيتم رفض تحديثه.
تظهر الشاشة كما يلي:
- السطر 4: العميل في قاعدة البيانات؛
- السطر 9: العميل كما قرأه الخيط t2؛
- السطر 11: العميل كما قرأه الخيط t1. وبالتالي، فقد قرأ كلا الخيطين نفس الشيء؛
- السطر 12: ينتهي الخيط t2 أولاً. وبالتالي، تمكن من إجراء التحديث. لا بد أن الاسم قد تغير إلى "zz"؛
- السطر 13: يرمي الخيط t1 استثناء [System.Data.OptimisticConcurrencyException]. اكتشف EF أنه لا يمتلك الطابع الزمني الصحيح؛
- السطر 21: ينتهي الخيط t1 بدوره؛
- السطر 22: انتهى الخيط الرئيسي من الانتظار؛
- السطر 24: يعرض الخيط الرئيسي العميل في قاعدة البيانات. الخيط t2 هو الذي فاز بالفعل. الاسم هو "zz". لاحظ أن الطابع الزمني قد تغير.
الآن، دعونا نفحص جانبًا آخر: المعاملة التي تحكم تزامن سياق الاستمرارية مع قاعدة البيانات.
3.5.9. المزامنة داخل المعاملة
يحتوي الجدول [CRENEAUX] على قيد تفرد أضفناه يدويًا (انظر القسم 2.2.4، الصفحة 12):
سنقوم بما يلي: سنضيف موعدين في نفس الوقت لنفس الطبيب، في نفس اليوم، وفي نفس الفترة الزمنية. لنرى ما سيحدث.
يتطور المشروع على النحو التالي:
![]() | ![]() |
فيما يلي كود برنامج [SynchronisationTransaction]:
using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// test program
class SynchronisationTransaction
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
context.SaveChanges();
}
// create a customer
Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
// we create a doctor
Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
// we create a niche for this doctor
Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
// create two appointments for this doctor and this customer, same day, same time slot
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
{
// we put it all in the context of persistence
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);
// save the context - you should have an exception
// because the underlying BD has a uniqueness constraint preventing
// to have two RDV on the same day, in the same slot
context.SaveChanges();
}
}
catch (Exception e)
{
Console.WriteLine("Erreur : {0}", e);
}
// if the save occurs in a transaction, then nothing must have been inserted in the database
// because of the previous exception - we check
using (var context = new RdvMedecinsContext())
{
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// the doctors
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// time slots
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// dates
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: إنشاء كائن [Doctor]؛
- السطر 34: إنشاء كائن [Slot]؛
- السطر 36: إنشاء كائن [Appointment]؛
- السطر 37: إنشاء كائن [Appointment] ثانٍ مطابق للكائن السابق؛
- السطر 41: فتح سياق جديد؛
- الأسطر 43-47: يتم إرفاق الكائنات التي تم إنشاؤها مسبقًا بالسياق الجديد. لاحظ هنا أنه، من خلال أخذ التبعيات في الاعتبار، كان بإمكاننا تقليل عدد عمليات الإضافة. ومع ذلك، سيقوم 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] إليه، سننظم فيها كودنا. المجلد [Entities] هو نسخة من المجلد [Entities] من المشروع السابق. بعد النسخ، تظهر أخطاء لأننا لا نملك المراجع الصحيحة. نحتاج إلى إضافة مرجع إلى Entity Framework 5. للقيام بذلك، سنتبع الطريقة الموضحة في القسم 3.4، الصفحة 21. تصبح قائمة المراجع كما يلي [3]:
![]() |
في هذه المرحلة، يجب ألا يكون هناك أي أخطاء تجميع في المشروع. من المشروع السابق، نقوم أيضًا بنسخ ملف [App.config]، الذي يقوم بتكوين اتصال قاعدة البيانات:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit 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>
<!-- connection chain on base -->
<connectionStrings>
<add name="monContexte"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- the factory provider -->
<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. فئة الاستثناء
سنستخدم فئة استثناء خاصة بالمشروع. هذه هي الفئة التي ستقوم طبقة [DAO] بإلقائها:
![]() |
ستلتقط طبقة [DAO] جميع الاستثناءات التي تنتقل إليها وتغلفها في استثناء من النوع [RdvMedecinsException]. سيكون هذا الاستثناء كما يلي:
using System;
namespace RdvMedecins.Exceptions
{
public class RdvMedecinsException : Exception
{
// properties
public int Code { get; set; }
// manufacturers
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;
}
// identity
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
{
// customer list
List<Client> GetAllClients();
// list of doctors
List<Medecin> GetAllMedecins();
// list of physician slots
List<Creneau> GetCreneauxMedecin(int idMedecin);
// list of RV from a given doctor on a given day
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// add a RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// delete a RV
void SupprimerRv(int idRv);
// find a T entity via its primary key
T Find<T>(int id) where T : class;
}
}
الطرق الموجودة في الأسطر 10–20 مستمدة من التحليل الذي تم إجراؤه للتو. الطريقة الموجودة في السطر 22 موجودة لمعالجة حقيقة أننا نعمل مع التحميل المتأخر. إذا احتجنا، في طبقة [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
{
//customer list
public List<Client> GetAllClients()
{
// customer list
List<Client> clients = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// customer list
clients = context.Clients.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(1, "GetAllClients", ex);
}
// we return the result
return clients;
}
// list of doctors
public List<Medecin> GetAllMedecins()
{
// list of doctors
List<Medecin> medecins = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// list of doctors
medecins = context.Medecins.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(2, "GetAllMedecins", ex);
}
// we return the result
return medecins;
}
// list of time slots for a given doctor
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
...
}
// list of a doctor's RV for a given day
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
...
}
// add a RV to the list
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
...
}
// delete a RV
public void SupprimerRv(int idRv)
{
...
}
// find a customer
public Client FindClient(int id)
{
...
}
// find a niche
public Creneau FindCreneau(int id)
{
...
}
// find a doctor
public Medecin FindMedecin(int id)
{
....
}
// find an rv
public Rv FindRv(int id){
...
}
}
}
دعونا نوضح طريقة [GetAllClients]، التي من المفترض أن تُرجع قائمة بجميع العملاء:
- الأسطر 18–31: يتم إجراء البحث عن العميل داخل كتلة try/catch. وينطبق الأمر نفسه على جميع الطرق اللاحقة؛
- السطر 21: فتح سياق جديد؛
- السطر 24: يتم تحميل كيانات [Client] في السياق ووضعها في قائمة.
طريقة [GetAllMedecins]، التي تُرجع قائمة بجميع الأطباء، مشابهة (الأسطر 37–57).
طريقة [GetCreneauxMedecin] هي كما يلي:
// list of time slots for a given doctor
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
// list of slots
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the doctor back with his slots
Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
// list of doctor's slots
return medecin.Creneaux.ToList<Creneau>();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
}
}
- السطر 9: فتح سياق استمرارية جديد؛
- السطر 11: البحث عن الطبيب الذي يُعرف مفتاحه الأساسي. طلب تضمين التبعية [Creneaux] — وهي مجموعة من فترات الوقت المتاحة للطبيب. إذا لم يكن الطبيب موجودًا، فإن الطريقة Single ترمي استثناءً؛
- السطر 13: إرجاع قائمة الفترات الزمنية.
يجب أن تُرجع الطريقة [GetRvMedecinJour] قائمة مواعيد الطبيب ليوم معين. يمكن أن يكون كودها كما يلي:
// list of a doctor's RV for a given day
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
// rv list
List<Rv> rvs = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the doctor back
Medecin medecin = context.Medecins.Find(idMedecin);
if (medecin == null)
{
throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
}
// appointment list
rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
}
// we return the result
return rvs;
}
- السطر 13: استرجاع الطبيب باستخدام المفتاح الأساسي المحدد؛
- الأسطر 14-17: إذا لم يكن موجودًا، فقم بإلقاء استثناء؛
- السطر 19: استعلام LINQ لاسترداد المواعيد لهذا الطبيب؛
يجب أن تضيف طريقة [AddAppointment] موعدًا إلى قاعدة البيانات وتُرجع المفتاح الأساسي للعنصر الذي تم إدراجه. قد يكون كودها كما يلي:
// add a RV to the list
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
// rdv n° added
int idRv;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the slot back
Creneau creneau = context.Creneaux.Find(idCreneau);
if (creneau == null)
{
throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
}
// we get the customer back
Client client = context.Clients.Find(idClient);
if (client == null)
{
throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
}
// niche creation
Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
// added in context
context.Rvs.Add(rv);
// save context
context.SaveChanges();
// retrieve the primary key of the added rv
idRv = (int)rv.Id;
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(7, "AjouterRv", ex);
}
// result
return idRv;
}
- السطر 12: البحث عن موعد في قاعدة البيانات؛
- الأسطر 13–16: إذا لم يتم العثور عليه، يتم إصدار استثناء؛
- السطر 18: البحث عن عميل الموعد في قاعدة البيانات؛
- الأسطر 19–22: إذا لم يتم العثور عليه، يتم إصدار استثناء؛
- السطر 24: إنشاء كائن [Rv] بالمعلومات الضرورية؛
- السطر 26: إضافته إلى سياق الاستمرارية؛
- السطر 28: نقوم بمزامنة سياق الاستمرارية مع قاعدة البيانات. سيتم بعد ذلك حفظ الموعد في قاعدة البيانات؛
- السطر 30: نعلم أنه بعد مزامنة قاعدة البيانات، تصبح المفاتيح الأساسية للعناصر التي تم إدراجها متاحة. نسترد المفتاح الخاص بالموعد الذي تمت إضافته؛
- السطر 31: نغلق سياق الاستمرارية.
يجب أن تقوم طريقة [DeleteAppointment] بحذف الموعد الذي تم تمرير المفتاح الأساسي الخاص به إليها.
// delete a RV
public void SupprimerRv(int idRv)
{
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we recover the Rv
Rv rv = context.Rvs.Find(idRv);
if (rv == null)
{
throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
}
// deletion Rv
context.Rvs.Remove(rv);
// save context
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
{
// opening persistence context
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
{
// instantiation layer [DAO] via Spring
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
// customer display
List<Client> clients = dao.GetAllClients();
DisplayClients("Liste des clients :", clients);
// physician display
List<Medecin> medecins = dao.GetAllMedecins();
DisplayMedecins("Liste des médecins :", medecins);
// list of time slots for doctor no. 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);
// list of doctor's appointments for a given day
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)));
// add a RV to doctor n°1 in slot n° 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)));
// add an appointment in an already occupied slot - must trigger an exception
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));
}
// delete an appointment
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));
}
//break
Console.ReadLine();
}
// utility methods - display lists
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]. لجعل الاختبار مستقلاً عن التنفيذ الفعلي لطبقة [DAO]، تكون هذه الإشارة من النوع [IDao] (الواجهة) بدلاً من النوع [Dao] (الفئة)؛
- السطر 18: يتم إنشاء مثيل لطبقة [DAO] بواسطة Spring. سنعود إلى التكوين المطلوب لجعل ذلك ممكنًا. نقوم بتحويل مرجع الكائن الذي يرجعه Spring إلى مرجع من نوع واجهة [IDao]؛
- السطران 21-22: عرض العملاء؛
- السطران 25-26: عرض الأطباء؛
- السطران 29-30: عرض قائمة المواعيد المتاحة للطبيب رقم 0؛
- السطر 33: يعرض مواعيد الطبيب رقم 0 ليوم 23 نوفمبر 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>
<!-- For more information on Entity Framework configuration, visit 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>
<!-- common logging-->
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<!-- Entity Framework -->
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters>
<parameter value="v11.0" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
<!-- Connection chains -->
<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 configuration -->
<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>
<!-- configuration 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>
<!-- For more information on Entity Framework configuration, visit 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>
<!-- common logging-->
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</sectionGroup>
</configSections>
...
<!-- spring configuration -->
<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>
<!-- configuration 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: تكوين "التسجيل العام" مستقر. قد تحتاج إلى تعديل مستوى التسجيل في السطر 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 تقوم ببساطة بإرجاع إشارة إلى الكائن الأول الذي تم إنشاؤه. هذا هو نمط التصميم المعروف باسم singleton.
يمكن أن يكون إنشاء الكائنات أكثر تعقيدًا. يمكنك استخدام منشئ مع معلمات أو تحديد تهيئة حقول معينة للكائن بمجرد إنشاء الكائن. لمزيد من المعلومات حول هذا الموضوع، راجع المقالة "Spring IOC Tutorial for .NET" على [http://tahe.developpez.com/dotnet/springioc/].
بمجرد الانتهاء من ذلك، يمكننا تشغيل التطبيق. وتكون نتائج الشاشة كما يلي:
النتائج كما هو متوقع. سنعتبر الآن طبقة [DAO] صالحة. يمكن أن ينتهي البرنامج التعليمي هنا. حتى الآن، قمنا بتغطية:
- أساسيات Entity Framework 5 ORM؛
- طبقة [DAO] تستخدم هذا ORM.
دعونا نستذكر دراسة الحالة التي وصفناها في بداية هذا المستند. نبدأ بتطبيق موجود بالفعل له البنية التالية:
![]() |
والتي نريد تحويلها إلى هذا:
![]() |
حيث حل EF5 محل NHibernate. لقد أنشأنا للتو طبقة [DAO2]. في الواقع، لا تمتلك هذه الطبقة نفس واجهة طبقة [DAO1]، التي كانت واجهتها أكثر محدودية:
public interface IDao
{
// customer list
List<Client> GetAllClients();
// list of doctors
List<Medecin> GetAllMedecins();
// list of physician slots
List<Creneau> GetCreneauxMedecin(int idMedecin);
// list of RV from a given doctor on a given day
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// add a RV to the list
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// delete a RV
void SupprimerRv(int idRv);
}
أضافت طبقة [DAO2] الطريقة التالية إلى هذه الواجهة:
// find a T entity via its primary key
T Find<T>(int id) where T : class;
تمت إضافة هذه الطريقة لأن EF 5 ORM يعمل في وضع التحميل المتأخر (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 for Web، المتاح مجانًا على [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 لنظام إدارة قواعد البيانات MySQL،
- [rdvmedecins] هو ملف DLL لطبقة [DAO] التي تم إنشاؤها باستخدام NHibernate؛
- في [4]، نقوم بتغيير اسم المشروع، وفي 5، نقوم بإزالة المراجع السابقة؛
![]() |
- في [6]، نضيف إشارات إلى المشروع؛
- في [7]، في المعالج، نستخدم خيار [Browse]؛
![]() |
- في [8]، نختار جميع ملفات DLL من المشروع رقم 2 التي تم وضعها مسبقًا في مجلد [lib]؛
- في [9]، نؤكد الملخص؛
- في [10]، مشروع الويب مع مراجعه الجديدة.
وبمجرد الانتهاء من ذلك، سيبدو المشروع كما يلي:
![]() |
- في [1]، تم تقسيم كود إدارة صفحات الويب بين ملفين هما [Global.asax] و[Default.aspx]. تم وضع كود الأدوات المساعدة في مجلد [Entities]. وأخيرًا، يتم تكوين التطبيق بواسطة ملف [Web.config]؛
- في [2]، نقوم بإنشاء تجميع المشروع؛
- في [3]، تظهر أخطاء.
دعونا نفحص الأخطاء، على سبيل المثال الخطأ التالي:
![]()
وشرحه:
![]()
نوع [medecin.Id] هو int؟، في حين أن طريقة [GetCreneauxMedecin] هي من النوع int. لذلك، يلزم إجراء تحويل. يحدث هذا الخطأ بشكل متكرر في جميع أنحاء الكود لأن الكيانات في مشروع ASP.NET/NHibernate تحتوي على مفاتيح أساسية من النوع int، في حين أن تلك الموجودة في مشروع ASP.NET/EF 5 هي من النوع int؟. نقوم بتصحيح جميع الأخطاء من هذا النوع وإعادة إنشاء المشروع. عندئذٍ لن تكون هناك أخطاء أخرى.
هناك تفصيل آخر يجب معالجته قبل تشغيل المشروع: إنشاء مثيل لطبقة [DAO] بواسطة إطار عمل Spring. يتم ذلك في [Global.asax]:
protected void Application_Start(object sender, EventArgs e)
{
// caching of certain database data
try
{
// layer instantiation [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] بالطريقة التالية:
// find a T entity via its primary key
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. الخلاصة
لقد نجحنا في نقل تطبيق ASP.NET / NHibernate:
![]() |
إلى تطبيق ASP.NET / EF 5:
![]() |
على الرغم من أن هذه البنية كان من المفترض أن تسمح لنا بالحفاظ على طبقة [ASP.NET] سليمة، فقد اضطررنا إلى تعديلها لسببين:
- لم تكن الكيانات متطابقة تمامًا. كان نوع المفتاح الأساسي لكيانات NHibernate هو
int، بينما كان في EF 5 هوint?</span>**<span style="color: #000000">. وقد دفعنا ذلك إلى إدخال عمليات التحويل في كود الويب؛ - لم يكن وضع تحميل الكيانات هو نفسه بالنسبة لـ ORM: التحميل الفوري لـ NHibernate، والتحميل المتأخر لـ EF 5. وقد دفعنا ذلك إلى تحسين واجهة طبقة [DAO] باستخدام طريقة عامة تسمح لنا باسترداد كيان عبر مفتاحه الأساسي.
ومع ذلك، ثبت أن عملية النقل كانت بسيطة إلى حد ما، مما يبرر مرة أخرى — إذا كان هناك حاجة إلى دليل — البنية الطبقية وحقن التبعية باستخدام Spring أو إطار عمل آخر لحقن التبعية.
سنقوم الآن بتقييم تأثير تغيير نظام إدارة قواعد البيانات (DBMS) على البنية السابقة. سنقوم بنقل جميع المشاريع السابقة إلى أربعة أنظمة إدارة قواعد بيانات أخرى:
- Oracle Database Express Edition 11g Release 2؛
- MySQL 5.5.28؛
- PostgreSQL 9.2.1؛
- Firebird 2.1.
سيبقى الكود دون تغيير. لن تتغير سوى العناصر التالية:
- التعريف في كيانات الحقل المستخدم للتحكم في الوصول المتزامن إلى كيان ما؛
- ملفات التكوين [App.config] أو [Web.config]؛
سنعلق فقط على العناصر التي ستتغير.


















































































































































