اذهب إلى المحتوى

تطبيق عملي لتعلم Express - الجزء الثاني: استخدام قاعدة البيانات مع مكتبة Mongoose


Ola Abbas

يقدّم هذا المقال مقدمة موجزة عن قواعد البيانات وكيفية استخدامها مع تطبيقات Node/Express، ثم يوضّح كيفية استخدام مكتبة Mongoose لتوفير الوصول إلى قاعدة بيانات موقع المكتبة المحلية LocalLibrary، ويشرح كيفية التصريح عن مخطط الكائنات object schema والنماذج Models، وأنواع الحقول الرئيسية والتحقق الأساسي من صحة البيانات. يعرض أيضًا بإيجاز بعض الطرق الرئيسية التي يمكنك من خلالها الوصول إلى بيانات النموذج.

يستخدم موظفو المكتبة موقع المكتبة المحلية لتخزين المعلومات حول الكتب والمستعيرين، بينما يستخدمه أعضاء المكتبة لتصفح الكتب والبحث عنها ولمعرفة ما إذا كان هناك أيّ نسخ متاحة ثم حجزها أو استعارتها. لذا سنخزّن المعلومات في قاعدة بيانات لتخزينها واسترجاعها بكفاءة.

يمكن لتطبيقات Express استخدام العديد من قواعد البيانات المختلفة، وهناك العديد من الأساليب التي يمكنك استخدامها لإجراء عمليات الإنشاء والقراءة والتحديث والحذف -أو CRUD اختصارًا. يقدم هذا المقال نظرةً عامةً موجزة عن بعض الخيارات المتاحة ثم ينتقل ليشرح الآليات المختارة بالتفصيل.

قواعد البيانات الممكن استخدامها

يمكن لتطبيقات Express استخدام أيّ قاعدة بيانات تدعمها بيئة Node، إذ لا يحدد إطارعمل Express أيّ سلوك أو متطلبات إضافية محددة لإدارة قاعدة البيانات، وهناك العديد من الخيارات الشائعة بما في ذلك قواعد بيانات PostgreSQL و MySQL و Redis و SQLite و MongoDB وغير ذلك.

يجب أن تأخذ في حساباتك عند اختيار قاعدة بيانات أشياءً مثل منحنى الوقت لتعلمها أو للحصول على نتائج والأداء وسهولة التكرار أو النسخ الاحتياطي والتكلفة ودعم المجتمع وغير ذلك. لا توجد قاعدة بيانات أفضل من الأخرى، ولكن تُعَد أيّ قاعدة بيانات شائعة تقريبًا مقبولة بصورة جيدة لموقع صغير إلى متوسط الحجم مثل موقع مكتبتنا المحلية.

اطلع على تكامل قاعدة البيانات في توثيق Express لمزيد من المعلومات حول هذه الخيارات.

أفضل طريقة للتفاعل مع قاعدة البيانات

هناك طريقتان شائعتان للتفاعل مع قاعدة البيانات، هما:

  • استخدام لغة الاستعلام الأصيلة لقواعد البيانات مثل لغة SQL.
  • استخدام نموذج بيانات الكائن Object Data Model -أو اختصارًا ODM- أو نموذج الكائنات العلاقي Object Relational Model -أو ORM اختصارًا. يمثل نموذج ODM/ORM بيانات موقع الويب بوصفها كائنات جافا سكريبت، والتي تُربَط بعد ذلك بقاعدة البيانات الأساسية، إذ ترتبط بعض نماذج ORM بقاعدة بيانات معينة، بينما يوفر بعضها الآخر واجهة خلفية لا تعتمد على قاعدة البيانات.

يمكن الحصول على أفضل أداء باستخدام لغة SQL أو أيّ لغة استعلام تدعمها قاعدة البيانات، إذ يُعَد نموذج ODM أبطأ لأنه يستخدم شيفرة ترجمة للربط بين الكائنات وتنسيق قاعدة البيانات، إذ يمكن ألّا يستخدم هذا النموذج استعلامات قاعدة البيانات الأكثر كفاءة، وهذا صحيح بصورة خاصة إذا كان نموذج ODM يدعم واجهات خلفية لقواعد بيانات مختلفة، وبالتالي يجب تقديم تنازلات أكبر فيما يتعلق بميزات قاعدة البيانات المدعومة.

تتمثل فائدة استخدام نموذج ORM في أنه يمكن للمبرمجين الاستمرار في التفكير وفق مصطلحات كائنات جافاسكربت بدلًا من دلالات قواعد البيانات، وهذا صحيح بصورة خاصة إذا كنت بحاجة إلى العمل مع قواعد بيانات مختلفة على موقع الويب نفسه أو مواقع ويب مختلفة، ويوفر هذا النموذج أيضًا مكانًا واضحًا لإجراء التحقق من صحة البيانات.

ملاحظة: يؤدي استخدام نماذج ODM/ORM إلى انخفاض تكاليف التطوير والصيانة، إذ يجب أن تفكر كثيرًا في استخدام نموذج ODM إلّا إذا كنت على دراية بلغة الاستعلام الأصيلة أو إذا كان الأداء أمرًا بالغ الأهمية.

نموذج ORM/ODM الذي يجب أن استخدامه

هناك العديد من حلول ODM/ORM المتاحة على موقع مدير الحزم npm (اطلع على odm و orm للتعرف على بعض من هذه الحلول).

إليك بعضًا من هذه الحلول الشائعة:

  • Mongoose: هي أداة نمذجة كائنات قاعدة بيانات MongoDB المُصمَّمة للعمل في بيئة غير متزامنة.
  • Waterline: هو نموذج ORM المُستخرَج من إطار عمل الويب Sails القائم على إطار عمل Express. يوفر واجهة برمجة تطبيقات مُوحَّدة للوصول إلى العديد من قواعد البيانات المختلفة، بما في ذلك Redis و MySQL و LDAP و MongoDB و Postgres.
  • Bookshelf: يتميز بواجهات رد النداء Callback التقليدية القائمة على الوعود Promise، مما يوفر دعمًا لمعامَلات قاعدة البيانات وتحميل العلاقات النشط أو النشط المتداخل والارتباطات متعددة الأشكال ودعم علاقات واحد إلى واحد one-to-one وواحد إلى متعدد one-to-many ومتعدد إلى متعدد many-to-many، ويعمل مع قواعد بيانات PostgreSQL و MySQL و SQLite3. يمكنك الاطلاع على مقال العلاقات بين الجداول في SQL على أكاديمية حسوب لمزيدٍ من المعلومات حول العلاقات بين الجداول.
  • Objection: يسهّل قدر الإمكان استخدام قوة لغة SQL الكاملة ومحرك قاعدة البيانات الأساسي، ويدعم SQLite3 و Postgres و MySQL.
  • Sequelize: هو نموذج ORM مبني على الوعود لكلٍّ من Node.js و io.js، ويدعم الأنواع المختلفة من لغات PostgreSQL و MySQL و MariaDB و SQLite و MSSQL ويتميز بدعمٍ قوي للمعامَلات transaction والعلاقات وتكرار عمليات القراءة وغير ذلك.
  • Node ORM2: هو مدير علاقات الكائنات الخاص ببيئة NodeJS، ويدعم MySQL و SQLite و Progress، مما يساعد على العمل مع قاعدة البيانات باستخدام أسلوب موجَّه بالكائنات.
  • GraphQL: لغة استعلام أساسية لواجهات برمجة التطبيقات restful API، وتحظى لغة GraphQL بشعبية كبيرة ولديها ميزات متاحة لقراءة البيانات من قواعد البيانات.

يجب مراعاة كل من الميزات المتوفرة ونشاط المجتمع (التنزيلات والمساهمات وتقارير الأخطاء وجودة التوثيق وغير ذلك) عند اختيار الحل المناسب، وتُعَد مكتبة Mongoose أكثر نماذج ODM شيوعًا، وهو خيار جيد عند استخدام MongoDB لقاعدة بياناتك.

استخدام مكتبة Mongoose وقاعدة بيانات MongoDB لموقع المكتبة المحلية

سنستخدم في مثالنا مكتبة Mongoose بوصفها نموذج ODM للوصول إلى بيانات مكتبتنا، إذ تتصرف هذه المكتبة بمثابة واجهة أمامية لقاعدة بيانات MongoDB، وهي قاعدة بيانات NoSQL مفتوحة المصدر تستخدم نموذج بيانات موجَّهًا بالمستندات، إذ تشبه مجموعة المستندات في قاعدة بيانات MongoDB جدولًا من الصفوف في قاعدة بيانات علاقية.

تحظى هذه المجموعة من نموذج ODM وقاعدة البيانات بشعبية كبيرة في مجتمع Node، ويرجع ذلك جزئيًا إلى أن تخزين المستندات ونظام الاستعلام يشبه إلى حد كبير JSON، وبالتالي فهو مألوف لمطوري جافا سكريبت.

ملاحظة: لست بحاجة إلى معرفة قاعدة بيانات MongoDB لاستخدام مكتبة Mongoose، بالرغم من أن أجزاءً من توثيق Mongoose أسهل في الاستخدام والفهم إذا كنت على دراية بقاعدة بيانات MongoDB.

سنوضح فيما يلي كيفية تعريف والوصول إلى مخطط ونماذج Mongoose لمثال موقع ويب المكتبة المحلية LocalLibrary.

تصميم نماذج موقع المكتبة المحلية

يفضَّل أخذ بضع دقائق للتفكير في البيانات التي يجب تخزينها والعلاقات بين الكائنات قبل البدء في كتابة شيفرة النماذج، إذ نعلم أننا بحاجةٍ إلى تخزين معلومات حول الكتب (العنوان والملخص والمؤلف ونوع الكتاب ورقم ISBN)، وقد يكون لدينا نسخٌ متعددة متاحة (مع معرّفات فريدة عامة وحالات توفرها وغير ذلك)، ويمكن أن نحتاج إلى تخزين مزيدٍ من المعلومات حول المؤلف أكثر من مجرد اسمه، ويمكن أن يكون هناك عدة مؤلفين لهم الاسم نفسه أو أسماء متشابهة. نريد أن نكون قادرين على فرز المعلومات بناءً على عنوان الكتاب والمؤلف ونوع الكتاب Genre وفئته Category.

من المنطقي أن يكون لديك نماذج منفصلة لكل كائن (مجموعة من المعلومات المتعلقة ببعضها) عند تصميم نماذجك، وبالتالي فإن بعض المرشّحين الواضحين لهذه النماذج هم الكتب ونسخ الكتب والمؤلفون. يمكن أن ترغب في استخدام النماذج لتمثيل خيارات قائمة الاختيار (مثل قائمة اختيارات منسدلة) بدلًا من كتابة شيفرة ثابتة للخيارات في موقع الويب نفسه، إذ يوصَى بذلك عندما لا تكون جميع الخيارات معروفة مسبقًا أو أنها ستتغير، ومن الأمثلة الجيدة لذلك هو نوع الكتاب، مثل النوع الخيالي والخيال العلمي وغير ذلك.

يجب الآن التفكير في العلاقات بين النماذج والحقول بعد تحديدها، إذ يوضح مخطط ارتباط باستخدام لغة UML الآتي النماذج التي سنعرّفها في حالتنا (على شكل صناديق)، إذ أنشأنا نماذجًا للكتاب (تفاصيل الكتاب العامة)، ونسخة الكتاب (حالة نسخ الكتاب الحقيقية المحدَّدة المتاحة في النظام)، والمؤلف، وقررنا أيضًا أن يكون لدينا نموذج لنوع الكتاب بحيث يمكن إنشاء القيم ديناميكيًا. لم ننشئ نموذجًا لحالة نسخة الكتاب BookInstance:status، إذ سنجعل القيم المقبولة ثابتة لأننا لا نتوقع تغييرها. يمكنك رؤية اسم النموذج وأسماء الحقول وأنواعها والتوابع وأنواع الإعادة الخاصة بها في كل صندوق.

يوضح المخطط البياني الآتي أيضًا العلاقات بين النماذج، بما في ذلك درجة تعدّدها Multiplicities، وهي الأعداد الموجودة على المخطط والتي توضح عدد أو الحد الأقصى والحد الأدنى لكل نموذج الذي يمكن أن يكون موجودًا في العلاقة، فمثلًا يوضّح الخط المتصل بين الصناديق أن الكتاب Book والنوع Genre مرتبطان، وتوضح الأعداد القريبة من نموذج الكتاب Book أنه يجب يكون للنوع Genre صفر أو أكثر من الكتب Book (بقدر ما تريد)، بينما توضح الأعداد الموجودة على الطرف الآخر من الخط بجوار نموذج النوع Genre أن الكتاب يمكن أن يكون له صفر أو أكثر من الأنواع Genre المتعلقة به.

ملاحظة: يُفضَّل غالبًا أن يكون لديك الحقل الذي يعرّف العلاقة بين المستندات/النماذج في نموذج واحد فقط كما سنوضّح لاحقًا، ولا يزال بإمكانك العثور على العلاقة العكسية من خلال البحث عن ‎_id المرتبط بها في النموذج الآخر. اخترنا فيما يلي تعريف العلاقة بينBook/Genre و Book/Author في مخطط Schema الكتاب Book، والعلاقة بين Book/BookInstance في مخطط نسخة الكتاب BookInstance، إذ كان هذا الاختيار عشوائيًا إلى حدٍ ما، وكان من الممكن أيضًا أن يكون أحد الحقول موجودًا في المخطط الآخر.

01_library_website_-_mongoose_express.png

ملاحظة: يوفر القسم التالي شرحًا بسيطًا عن كيفية تعريف النماذج واستخدامها، لذلك ضع في بالك أثناء القراءة كيف سنبني كل نموذج من النماذج الموجودة في المخطط البياني السابق.

واجهات برمجة تطبيقات قاعدة البيانات غير المتزامنة

تُعَد توابع قاعدة البيانات لإنشاء السجلات أو العثور عليها أو تحديثها أو حذفها غير متزامنة، وهذا يعني أن التوابع تعيد القيم مباشرةً ويكون تشغيل الشيفرة البرمجية الخاصة بمعالجة نجاح أو فشل التابع في وقت لاحق عند اكتمال العملية. يمكن تنفيذ شيفرة برمجية أخرى أثناء انتظار الخادم لاكتمال عملية قاعدة البيانات، لذلك يمكن أن يظل الخادم مستجيبًا للطلبات الأخرى.

تحتوي لغة جافا سكريبت Javascript على عدد من الآليات لدعم السلوك غير المتزامن، إذ اعتمدت كثيرًا سابقًا على تمرير دوال رد النداء إلى توابع غير متزامنة لمعالجة حالات النجاح والخطأ، وحلّت الوعود Promises محل دوال رد النداء إلى حد كبير في لغة جافا سكربت الحديثة. الوعود هي كائنات يعيدها (مباشرةً) تابع غير متزامن يمثل حالتها المستقبلية، ويستقر كائن الوعد عند اكتمال العملية، ويحقق كائنًا يمثل نتيجة العملية أو الخطأ.

هناك طريقتان رئيسيتان يمكنك من خلالهما استخدام الوعود لتشغيل الشيفرة البرمجية عند استقرار الوعد، إذ نوصي بقراءة كيفية استخدام الوعود للحصول على نظرة عامة عالية المستوى على كلا الأسلوبين. سنستخدم في هذا المقال await لانتظار اكتمال الوعد في async function، لأن هذا الأسلوب يؤدي إلى الحصول على شيفرة برمجية غير متزامنة مفهومة وأكثر قابلية للقراءة.

تتمثل الطريقة التي يعمل بها هذا الأسلوب في أنك تستخدم الكلمة الأساسية async function لتمييز الدالة بوصفها غير متزامنة، ثم تطبّق ضمن هذه الدالة تابع await على أيّ تابع يعيد وعدًا، وتتوقف عملية الدالة غير المتزامنة عند تنفيذها مؤقتًا عند أول تابع await حتى استقرار الوعد، ثم تعيد الدالة غير المتزامنة ويمكنك تشغيل الشيفرة البرمجية الموجودة بعد تلك الدالة من منظور الشيفرة البرمجية المحيطة. يعيد التابع await ضمن الدالة غير المتزامنة النتيجة لاحقًا عند استقرار الوعد، أو يعطي خطأً عند رفض الوعد، ثم تُنفَّذ الشيفرة البرمجية الموجودة في الدالة غير المتزامنة حتى يظهر تابع await آخر، إذ ستتوقف مؤقتًا مرةً أخرى، أو حتى تشغيل الشيفرة البرمجية بأكملها الموجودة في الدالة.

يمكنك أن ترى كيفية عمل هذه الطريقة في المثال الآتي، إذ تُعَد myFunction()‎ دالة غير متزامنة تُستدعَى ضمن كتلة try...catch. يُوقََف تنفيذ الشيفرة البرمجية مؤقتًا في التابع methodThatReturnsPromise()‎ عند تشغيل الدالة myFunction()‎ حتى تحقيق الوعد، وعندها يستمر تنفيذ الشيفرة البرمجية حتى الوصول إلى التابع aFunctionThatReturnsPromise()‎ وينتظر مرةً أخرى. تُشغَّل الشيفرة البرمجية الموجودة في كتلة catch عند رمي خطأ في الدالة غير المتزامنة، إذ سيحدث ذلك عند رفض الوعد الذي يعيده أيّ من هذين التابعين.

async function myFunction {
  // ...
  await someObject.methodThatReturnsPromise();
  // ...
  await aFunctionThatReturnsPromise();
  // ...
}

try {
  // ...
  myFunction();
  // ...
} catch (e) {
 // شيفرة معالجة الخطأ
}

تُشغَّل التوابع غير المتزامنة السابقة تسلسليًا، وإذا لم تعتمد التوابع على بعضها بعضًا، فيمكنك تشغيلها على التوازي وإنهاء العملية بأكملها بسرعة أكبر، ويمكن تحقيق ذلك باستخدام التابع Promise.all()‎ الذي يأخذ تكرارًا من الوعود بوصفها دخلًا ويعيد وعدًا Promise واحدًا. يمكن الوفاء بهذا الوعد المُعاد عند الوفاء بجميع وعود الدخل مع مجموعة من قيم الوفاء، ويرفَض عند رفض أيٍّ من وعود الدخل مع سبب الرفض الأول.

توضح الشيفرة البرمجية التالية كيفية عمل ذلك، إذ لدينا أولًا دالتان تعيدان وعودًا، إذ ننتظرهما await حتى يكتملا باستخدام الوعد الذي يعيده التابع Promise.all()‎. يعيد await بمجرد أن تكتمل كلتا الدالتين وتُملَأ مصفوفة النتائج، ثم يستمر تنفيذ الدالة حتى الوصول إلى تابع await التالي، وتنتظر حتى استقرار الوعد الذي تعيده الدالة anotherFunctionThatReturnsPromise()‎. يمكنك استدعاء الدالة myFunction()‎ في كتلة try...catch لالتقاط الأخطاء.

async function myFunction {
  // ...
  const [resultFunction1, resultFunction2] = await Promise.all([
     functionThatReturnsPromise1(),
     functionThatReturnsPromise2()
  ]);
  // ...
  await anotherFunctionThatReturnsPromise(resultFunction1);
}

تسمح الوعود مع await/async بالتحكم المرن والمنطقي بالتنفيذ غير المتزامن.

مقدمة إلى مكتبة Mongoose

يقدم هذا القسم نظرةً عامة حول كيفية توصيل مكتبة Mongoose بقاعدة بيانات MongoDB، وكيفية تعريف المخططات والنماذج، وكيفية تطبيق الاستعلامات الأساسية.

تثبيت Mongoose و MongoDB

تُثبَّت مكتبة Mongoose في مشروعك (في الملف package.json) مثل أي اعتمادية أخرى باستخدام مدير حزم npm، إذ يمكن تثبيتها باستخدم الأمر التالي في مجلد مشروعك:

npm install mongoose

يضيف تثبيت مكتبة Mongoose جميع اعتمادياتها بما في ذلك مشغِّل قاعدة بيانات MongoDB، لكنه لا يؤدي إلى تثبيت MongoDB. إذا أدرتَ تثبيت خادم MongoDB، فيمكنك تنزيل المثبِّتات لأنظمة تشغيل مختلفة وتثبيتها محليًا، ويمكنك استخدام نسخ من MongoDB المستندة إلى السحابة.

ملاحظة: سنستخدم في هذا المقال قاعدة البيانات المستندة إلى السحابة MongoDB Atlas بوصفها طبقة خدمة مجانية لتوفير قاعدة البيانات، وهي مناسبة لبيئة التطوير والتعلم لأنها تجعل نظام تشغيل التثبيت مستقلًا، وتُعَد قاعدة البيانات التي تمثل خدمة Database-as-a-service أيضًا إحدى الطرق التي يمكن أن تستخدمها لقاعدة بيانات بيئة الإنتاج.

الاتصال بقاعدة بيانات MongoDB

تتطلب مكتبة Mongoose اتصالًا بقاعدة بيانات MongoDB، وذلك باستخدام الدالة require()‎ والاتصال بقاعدة بيانات مستضافة محليًا باستخدام mongoose.connect()‎ كما يلي، ولكن سنتّصل بدلًا من ذلك في هذا المقال بقاعدة بيانات مستضافة عبر الإنترنت:

// استيراد وحدة‫ mongoose
const mongoose = require("mongoose");

// اضبط‫ `strictQuery: false` للاشتراك العام في الترشيح وفق الخاصيات غير المُدرَجة في المخطط
// لأن هذا الخيار ‫يزيل تحذيرات Mongoose 7 الأولية.
// اطّلع على‫ https://mongoosejs.com/docs/migrating_to_6.html#strictquery-is-removed-and-replaced-by-strict
mongoose.set("strictQuery", false);

// ‫حدّد عنوان URL لقاعدة البيانات للاتصال به
const mongoDB = "mongodb://127.0.0.1/my_database";

// انتظر حتى الاتصال بقاعدة البيانات، مع تسجيل خطأ إذا كانت هناك مشكلة
main().catch((err) => console.log(err));
async function main() {
  await mongoose.connect(mongoDB);
}

ملاحظة: ننتظر await الوعد الذي يعيده التابع connect()‎ ضمن دالة مُصرَّح عنها باستخدام async function كما ناقشنا سابقًا في قسم واجهات برمجة التطبيقات لقاعدة البيانات غير المتزامنة من هذا المقال. نستخدم المعالج catch()‎ الخاص بالوعد لمعالجة الأخطاء عند محاولة الاتصال، ولكن يمكن أيضًا استدعاء main()‎ ضمن كتلة try...catch.

يمكنك الحصول على كائن Connection الافتراضي باستخدام mongoose.connection، وإذا كنت بحاجة إلى إنشاء اتصالات إضافية، فيمكنك استخدام التابع mongoose.createConnection()‎ الذي يأخذ صيغة معرّف URI نفسه الخاص بقاعدة البيانات (مع المضيف وقاعدة البيانات والمنفذ والخيارات وإلخ) الذي يستخدمه التابع connect()‎ ويعيد كائن Connection. لاحظ أن createConnection()‎ يعيد مباشرةً، وبالتالي إذا كنت بحاجة إلى الانتظار حتى إنشاء الاتصال، فيمكنك استدعاؤه مع asPromise()‎ لإعادة وعد (mongoose.createConnection(mongoDB).asPromise()‎).

تعريف وإنشاء النماذج

تُعرَّف النماذج باستخدام الواجهة Schema التي تتيح تعريف الحقول المُخزَّنة في المستندات مع متطلبات التحقق من صحة البيانات والقيم الافتراضية. يمكنك أيضًا تعريف التوابع المساعدة الثابتة ونُسَخ منها لتسهيل العمل مع أنواع بياناتك، والخاصيات الافتراضية التي يمكنك استخدامها مثل الحقول الأخرى، ولكنها غير مخزنة في قاعدة البيانات فعليًا (سنناقش ذلك لاحقًا).

تُصرَّف بعد ذلك واجهات Schema إلى نماذج باستخدام التابع mongoose.model()‎، ثم يمكنك استخدام النموذج للعثور على كائنات من نوعٍ محدَّد وإنشائها وتحديثها وحذفها.

ملاحظة: يُربَط كل نموذج بمجموعة من المستندات في قاعدة بيانات MongoDB، إذ ستحتوي المستندات على أنواع الحقول/المخططات Schema المحددة في نموذج Schema.

تعريف المخططات

يوضّح جزء الشيفرة البرمجية التالي كيفية تعريف مخطط بسيط، إذ يجب أولًا طلب مكتبة mongoose باستخدام الدالة require()‎، ثم استخدم باني Schema لإنشاء نسخة من المخطط الجديد، مع تعريف الحقول المختلفة ضمنه في معامل باني الكائن.

// طلب مكتبة‫ Mongoose
const mongoose = require("mongoose");

// تعريف مخطط
const Schema = mongoose.Schema;

const SomeModelSchema = new Schema({
  a_string: String,
  a_date: Date,
});

لدينا في المثال السابق حقلين فقط نوعاهما: سلسلة نصية String وتاريخ Date، وسنعرض في الأقسام التالية بعض أنواع الحقول الأخرى والتحقق من صحتها والتوابع الأخرى.

إنشاء نموذج

تُنشَأ النماذج من المخططات باستخدام التابع mongoose.model()‎ كما يلي:

// تعريف مخطط
const Schema = mongoose.Schema;

const SomeModelSchema = new Schema({
  a_string: String,
  a_date: Date,
});

// تصريف النموذج من المخطط
const SomeModel = mongoose.model("SomeModel", SomeModelSchema);

الوسيط الأول هو الاسم المفرد للمجموعة التي ستُنشَأ لنموذجك، إذ ستنشِئ مكتبة Mongoose مجموعة قاعدة البيانات للنموذج SomeModel السابق، والوسيط الثاني هو المخطط الذي تريد استخدامه في إنشاء النموذج.

ملاحظة: يمكنك استخدام أصناف Classes نموذجك بعد تعريفها لإنشاء سجلات أو تحديثها أو حذفها، ويمكنك تشغيل الاستعلامات للحصول على جميع السجلات أو مجموعات فرعية معينة من السجلات. سنوضح كيفية تحقيق ذلك لاحقًا في قسم استخدام النماذج وعندما ننشئ العروض Views.

أنواع المخططات والحقول

يمكن أن يحتوي المخطط على عدد عشوائي من الحقول، إذ يمثل كلٌّ منها حقلًا في المستندات المخزنة في قاعدة بيانات MongoDB. يوضّح المثال التالي مخططًا يحتوي على العديد من أنواع الحقول وكيفية التصريح عنها:

const schema = new Schema({
  name: String,
  binary: Buffer,
  living: Boolean,
  updated: { type: Date, default: Date.now() },
  age: { type: Number, min: 18, max: 65, required: true },
  mixed: Schema.Types.Mixed,
  _someId: Schema.Types.ObjectId,
  array: [],
  ofString: [String], // يمكنك أيضًا الحصول على مصفوفة لكل نوع من الأنواع الأخرى
  nested: { stuff: { type: String, lowercase: true, trim: true } },
});

لا تحتاج معظم أنواع المخططات SchemaTypes (الواصفات الموجودة بعد "type:‎" أو بعد أسماء الحقول) شرحًا، ولكن هناك بعض الاستثناءات وهي:

  • ObjectId: يمثل نسخًا محدّدة لنموذجٍ في قاعدة البيانات، فمثلًا يمكن أن يستخدم الكتاب هذا النوع لتمثيل كائن مؤلفه، وسيحتوي هذا النوع على معرّف فريد (‎_id) للكائن، ويمكننا استخدام التابع populate()‎ لسحب المعلومات عند الحاجة.
  • Mixed: نوع مخطط عشوائي.
  • []: مصفوفة من العناصر، إذ يمكنك إجراء عمليات مصفوفات جافاسكربت على هذه النماذج (الدفع والسحب إلغاء الإزاحة وإلخ). توضح الأمثلة السابقة مصفوفةً من الكائنات بدون نوع محدد ومصفوفة من كائنات String، ولكن يمكن أن يكون لديك مصفوفة من أيّ نوع من الكائنات.

توضّح الشيفرة البرمجية أيضًا طريقتين للتصريح عن الحقل هما:

  • اسم الحقل ونوعه مثل زوج قيمة-مفتاح (كما هو الحال مع اسم الحقول name و binary و living مثلًا).
  • اسم الحقل متبوعًا بكائن يحدد النوع type وأيّ خيارات أخرى للحقل، إذ تتضمن هذه الخيارات ما يلي:
    • قيم افتراضية.
    • أدوات التحقق المبنية مسبقًا، مثل القيم العليا أو الدنيا، ودوال التحقق من صحة البيانات المُخصَّصة.
    • ما إذا كان الحقل مطلوبًا.
    • ما إذا كان يجب ضبط حقول String تلقائيًا بأحرف صغيرة أو كبيرة أو حذف المسافات في بداية ونهاية السلسلة النصية، مثل:
{ type: String, lowercase: true, trim: true }

اطّلع على أنواع المخططات في توثيق Mongoose لمزيد من المعلومات حول الخيارات.

التحقق من صحة البيانات

توفر مكتبة Mongoose أدوات تحقق من صحة البيانات مبنية مسبقًا ومخصصة، وأدوات تحقق متزامنة وغير متزامنة، وتسمح بتحديد كلٍّ من مجال القيم المقبول ورسالة الخطأ التي تمثل فشل التحقق من صحة البيانات في جميع الحالات.

تتضمن أدوات التحقق من صحة البيانات المبنية مسبقًا ما يلي:

  • تحتوي جميع أنواع المخططات على أداة التحقق required المبنية مسبقًا التي تُستخدم لتحديد ما إذا كان يجب توفير الحقل لحفظ مستندٍ ما.
  • تحتوي الأعداد Numbers على أدوات تحقق من صحة الحد الأدنى min والحد الأعلى max.
  • تحتوي السلاسل النصية Strings على أدوات التحقق التالية:
    • enum: تحدد مجموعة القيم المسموح بها للحقل.
    • match: تحدد التعبير النمطي Regular Expression الذي يجب أن تتطابق معه السلسلة النصية.
    • الطول الأقصى maxLength والطول الأدنى minLength للسلسلة النصية.

يوضح المثال التالي -المأخوذ من توثيق Mongoose- كيفية تحديد بعض أنواع أدوات التحقق من صحة البيانات ورسائل الخطأ:

const breakfastSchema = new Schema({
  eggs: {
    type: Number,
    min: [6, "Too few eggs"],
    max: 12,
    required: [true, "Why no eggs?"],
  },
  drink: {
    type: String,
    enum: ["Coffee", "Tea", "Water"],
  },
});

اطّلع على التحقق من صحة البيانات في توثيق Mongoose للحصول على معلومات كاملة حول التحقق من صحة الحقول.

الخاصيات الافتراضية

الخاصيات الافتراضية هي خاصيات المستند التي يمكنك جلبها وضبطها دون استمرار وجودها في قاعدة بيانات MongoDB، إذ تُعَد الجوالب Getters مفيدة لتنسيق الحقول أو دمجها، وتكون الضوابط Setters مفيدة في تفكيك قيمة واحدة إلى قيم متعددة لتخزينها. يبني المثال الموجود في توثيق Mongoose (ويهدم) خاصية افتراضية للاسم الكامل من حقل الاسم الأول والأخير، ويُعَد ذلك أسهل وأنظف من بناء اسم كامل في كل مرة يُستخدَم أحدها في قالب.

ملاحظة: سنستخدم خاصية افتراضية في موقع المكتبة المحلية لتعريف عنوان URL فريد لكل سجل نموذج باستخدام مسار وقيمة ‎_id الخاصة بالسجل.

اطلع على الخاصيات الافتراضية في توثيق Mongoose لمزيد من المعلومات.

التوابع والاستعلامات المساعدة

يمكن أن يحتوي المخطط أيضًا على نسخ من التوابع Instance methods وتوابع ثابتة static methods واستعلامات مساعدة query helpers، إذ تتشابه نسخ التوابع والتوابع الثابتة، ولكن مع وجود اختلاف واضح في أن نسخ التوابع مرتبطة بسجل معين ويمكنها الوصول إلى الكائن الحالي. تسمح الاستعلامات المساعدة بتوسيع واجهة برمجة تطبيقات باني الاستعلامات القابلة للتسلسل الخاصة بمكتبة mongoose، مثل السماح بإضافة استعلام وفق الاسم "byName"، إضافةً إلى توابع find()‎ و findOne()‎ و findById()‎.

استخدام النماذج

يمكنك بعد إنشاء مخطط استخدامه لإنشاء النماذج، إذ يمثل النموذج مجموعة من المستندات الموجودة في قاعدة البيانات التي يمكنك البحث عنها، بينما تمثل نسخ النموذج المستندات الفردية التي يمكنك حفظها واسترجاعها. سنقدم فيما يلي نظرة عامة موجزة، لذا يمكنك الاطلاع على النماذج في توثيق Mongoose لمزيد من المعلومات.

ملاحظة: يُعَد إنشاء السجلات وتحديثها وحذفها والاستعلام عنها عمليات غير متزامنة تعيد وعدًا. سنوضح في الأمثلة التالية استخدام التوابع المتعلقة بهذا الموضوع والتابع await، أي سنوضح الشيفرة البرمجية الأساسي لاستخدام التوابع، إذ سنحذف دالة async function المحيطة وكتلة try...catch لالتقاط الأخطاء للتوضيح.

إنشاء وتعديل المستندات

يمكنك إنشاء سجل من خلال تعريف نسخة من النموذج ثم استدعاء save()‎. تفترض الأمثلة التالية أن SomeModel هو نموذج (له حقل واحد هو name) أنشأناه من المخطط.

// ‫أنشئ نسخة من النموذج SomeModel
const awesome_instance = new SomeModel({ name: "awesome" });

// احفظ نسخة النموذج الجديدة بطريقة غير متزامنة
await awesome_instance.save();

يمكنك أيضًا استخدام create()‎ لتعريف نسخة من النموذج في الوقت الذي تحفظها فيه. سننشئ فيما يلي نسخة واحدة فقط، ولكن يمكنك إنشاء نسخ متعددة من خلال تمرير مصفوفة من الكائنات.

await SomeModel.create({ name: "also_awesome" });

لكل نموذج اتصاله المرتبط به، والذي سيكون الاتصال الافتراضي عند استخدام mongoose.model()‎، ويمكنك إنشاء اتصال جديد واستدعاء ‎.model()‎ لإنشاء المستندات في قاعدة بيانات مختلفة.

يمكنك الوصول إلى الحقول في هذا السجل الجديد باستخدام الصيغة النقطية وتغيير القيم، ويجب استدعاء save()‎ أو update()‎ لتخزين القيم المُعدَّلة في قاعدة البيانات.

// الوصول إلى قيم حقول النموذج باستخدام الصيغة النقطية
console.log(awesome_instance.name); // يجب تسجيل‫ 'also_awesome' أيضًا

// تغيير السجل من خلال تعديل الحقول ثم استدعاء‫ save()‎
awesome_instance.name = "New cool name";
await awesome_instance.save();

البحث عن السجلات

يمكنك البحث عن السجلات باستخدام توابع الاستعلام من خلال تحديد شروط الاستعلام بوصفها مستند JSON. يوضح جزء الشيفرة التالي كيفية العثور على جميع الرياضيين Athlete الذين يلعبون كرة المضرب في قاعدة بيانات، ويعيد فقط حقول اسم name وعُمر age الرياضي، إذ نحدد فقط حقلًا واحدًا مطابقًا (الرياضة sport)، ولكن يمكنك إضافة مزيدٍ من المعايير أو تحديد معايير التعبير النمطي أو إزالة جميع الشروط لإعادة جميع الرياضيين.

const Athlete = mongoose.model("Athlete", yourSchema);

// ‫العثور على جميع الرياضيين الذين يلعبون كرة المضرب مع تحديد حقول 'name' و 'age'
const tennisPlayers = await Athlete.find(
  { sport: "Tennis" },
  "name age"
).exec();

ملاحظة: من المهم أن تتذكر أن عدم العثور على أي نتائج ليس خطأً في البحث، ولكنه يمكن أن يكون حالة فشل في سياق تطبيقك. إذا توقّع تطبيقك بحثًا ما للعثور على قيمة، فيمكنك التحقق من عدد المدخلات المُعَادة في النتيجة.

تعيد واجهات برمجة تطبيقات الاستعلام مثل find()‎ متغيرًا من النوع Query، ويمكنك استخدام كائن استعلام لبناء استعلام ضمن أجزاء قبل تنفيذه باستخدام التابع exec()‎ الذي ينفّذ الاستعلام ويعيد وعدًا يمكنك انتظاره باستخدام await للحصول على النتيجة.

// العثور على جميع الرياضيين الذين يلعبون كرة المضرب
const query = Athlete.find({ sport: "Tennis" });

// اختيار حقول‫ 'name' و 'age'
query.select("name age");

// قصر نتائجنا على 5 عناصر
query.limit(5);

// الفرز وفق العمر
query.sort({ age: -1 });

// تنفيذ الاستعلام في وقت لاحق
query.exec();

عرّفنا شروط الاستعلام في التابع find()‎، ويمكننا تطبيق ذلك أيضًا باستخدام الدالة where()‎، ويمكننا سَلسَلة جميع أجزاء الاستعلام مع بعضها باستخدام المعامل النقطي (.) بدلًا من إضافتها بصورة منفصلة. جزء الشيفرة البرمجية التالي هو الاستعلام السابق نفسه مع شرط إضافي للعُمر:

Athlete.find()
  .where("sport")
  .equals("Tennis")
  .where("age")
  .gt(17)
  .lt(50) // ‫استعلام where إضافي
  .limit(5)
  .sort({ age: -1 })
  .select("name age")
  .exec();

يحصل التابع find()‎ على جميع السجلات المطابقة، ولكنك تريد الحصول على تطابق واحد فقط في أغلب الأحيان، لذا يمكنك استخدام توابع الاستعلام التالية لسجل واحد:

  • findById()‎: يبحث عن المستند باستخدام المعرّف id، فلكل مستندٍ معرّفٌ فريد.
  • findOne()‎: يبحث عن مستند واحد يطابق معاييرًا محدَّدة.
  • findByIdAndRemove()‎ و findByIdAndUpdate()‎ و findOneAndRemove()‎ و findOneAndUpdate()‎: تبحث عن مستند واحد باستخدام المعرّف id أو المعايير، فإما أن تحدّثه أو تزيله، إذ تُعَد هذه الدوال ملائمة ومفيدة لتحديث السجلات وإزالتها.

ملاحظة: هناك أيضًا التابع countDocuments()‎ الذي يمكنك استخدامه للحصول على عدد العناصر التي تطابق الشروط، ويُعَد مفيدًا إذا أردتَ إجراء تعداد دون جلب السجلات فعليًا.

هناك الكثير من الأمور التي يمكنك تطبيقها على الاستعلامات، لذا اطّلع على الاستعلامات في توثيق Mongoose لمزيد من المعلومات.

العمل مع المستندات- الملء Population

يمكنك إنشاء مراجعٍ من نسخة مستند أو نموذج إلى آخر باستخدام حقل المخطط ObjectId، أو من مستند إلى عدة مستندات باستخدام مصفوفة من ObjectId. يخزّن هذا الحقل معرّف النموذج المرتبط به، وإذا كنت بحاجة إلى محتوى المستند الفعلي، فيمكنك استخدام التابع populate()‎ في استعلام لاستبدال المعرّف بالبيانات الفعلية.

يعرّف المخطط التالي مثلًا المؤلفين والقصص، إذ يمكن أن يكون لكل مؤلف قصص متعددة، والتي نمثلها بوصفها مصفوفة من ObjectId، ويمكن أن يكون لكل قصة مؤلف واحد. تخبر الخاصية ref المخطط بالنموذج الذي يمكن إسناده إلى هذا الحقل.

const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const authorSchema = Schema({
  name: String,
  stories: [{ type: Schema.Types.ObjectId, ref: "Story" }],
});

const storySchema = Schema({
  author: { type: Schema.Types.ObjectId, ref: "Author" },
  title: String,
});

const Story = mongoose.model("Story", storySchema);
const Author = mongoose.model("Author", authorSchema);

يمكننا حفظ المراجع التي تشير إلى المستند من خلال إسناد قيمة ‎_id إليها، إذ سننشئ فيما يلي مؤلفًا ثم ننشئ قصة ونسند معرّف المؤلف إلى حقل مؤلف القصة:

const bob = new Author({ name: "Bob Smith" });

await bob.save();

// ‫Bob موجود الآن، لذا لننشئ قصة‫
const story = new Story({
  title: "Bob goes sledding",
  author: bob._id, // إسناد‫ ‎_id للمؤلف Bob، إذ يُنشَأ هذا المعرّف افتراضيًا
});

await story.save();

ملاحظة: إحدى الفوائد الرائعة لهذا النمط من البرمجة هي أنه لا يتعين علينا تعقيد المسار الرئيسي لشيفرتنا البرمجية من خلال التحقق من الأخطاء، فإذا فشلت أيّ عملية حفظ save()‎، سيُرفَض الوعد وسيُرمَى خطأ. تتعامل شيفرة معالجة الأخطاء مع ذلك بصورة منفصلة (في كتلة catch()‎ عادةً)، لذا يُعَد الهدف من شيفرتنا البرمجية واضحًا جدًا.

يحتوي مستند القصة الآن على مؤلف يُشار إليه باستخدام معرّف مستند المؤلف، ونستخدم التابع populate()‎ كما هو موضح فيما يلي للحصول على معلومات المؤلف في نتائج القصة:

Story.findOne({ title: "Bob goes sledding" })
  .populate("author") // استبدال معرّف المؤلف بمعلومات المؤلف الفعلية في النتائج
  .exec();

ملاحظة: سيلاحظ القراء المتمرسون أننا أضفنا مؤلفًا إلى القصة، لكننا لم نفعل أي شيء لإضافة القصة إلى مصفوفة stories الخاصة بالمؤلف. تتمثل إحدى الطرق للحصول على جميع القصص لمؤلف معين في إضافة القصة إلى مصفوفة القصص، ولكن ذلك يمكن أن يؤدي إلى وجود مكانين للاحتفاظ بالمعلومات المتعلقة بالمؤلفين والقصص. توجد طريقة أفضل، وهي الحصول على معرّف ‎_id المؤلف، ثم استخدام find()‎ للبحث عنه في حقل المؤلف عبر جميع القصص.

Story.find({ author: bob._id }).exec();

اطلّع على Population في توثيق Mongoose لمزيد من المعلومات التفصيلية.

مخطط أو نموذج واحد لكل ملف

يمكنك إنشاء مخططات ونماذج باستخدام أي بنية ملفات تريدها، ولكننا نوصي بتعريف كل مخطط نموذج في وحدته أو ملفه الخاص، ثم تصدير التابع لإنشاء النموذج كما يلي:

// ‫الملف: ‎./models/somemodel.js

// طلب‫ Mongoose
const mongoose = require("mongoose");

// تعريف مخطط
const Schema = mongoose.Schema;

const SomeModelSchema = new Schema({
  a_string: String,
  a_date: Date,
});

// ‫تصدير دالة لإنشاء صنف النموذج "SomeModel"
module.exports = mongoose.model("SomeModel", SomeModelSchema);

يمكنك بعد ذلك طلب النموذج واستخدامه مباشرةً في ملفات أخرى، وسنوضح فيما يلي كيفية استخدامه للحصول على جميع نسخ النموذج:

// إنشاء نموذج‫ SomeModel من خلال طلب الوحدة
const SomeModel = require("../models/somemodel");

// استخدام كائن (نموذج‫) SomeModel للعثور على كافة سجلات SomeModel
const modelInstances = await SomeModel.find().exec();

إعداد قاعدة بيانات MongoDB

تعرّفنا على ما يمكن أن تفعله مكتبة Mongoose وكيفية تصميم نماذجنا، وحان الوقت الآن لبدء العمل على موقع المكتبة المحلية LocalLibrary، وأول شيء يجب فعله هو إعداد قاعدة بيانات MongoDB التي يمكننا استخدامها لتخزين بيانات مكتبتنا.

سنستخدم في هذا المقال قاعدة البيانات التجريبية المُستضافَة على السحابة MongoDB Atlas، إذ لا تُعَد طبقة قاعدة البيانات هذه مناسبة لمواقع الويب في بيئة الإنتاج لأنها لا تحتوي على تكرار Redundancy، ولكنها رائعة لعملية التطوير والنماذج الأولية، وسنستخدمها لأنها مجانية وسهلة الإعداد، ولأنها بائع شائع لقاعدة البيانات التي تمثل خدمة، والتي يمكن أن تختارها لقاعدة بيانات الإنتاج الخاصة بك، وتشمل الخيارات الشائعة الأخرى Compose و ScaleGrid و ObjectRocket.

ملاحظة: يمكنك أيضًا إعداد قاعدة بيانات MongoDb محليًا من خلال تنزيل وتثبيت الملفات الثنائية المناسبة لنظامك، إذ ستكون بقية الإرشادات الواردة في هذا المقال متشابهة باستثناء عنوان URL لقاعدة البيانات الذي يمكن أن تحدده عند الاتصال. نستضيف لاحقًا في مقال نشر تطبيق Express في بيئة الإنتاج كلًا من التطبيق وقاعدة البيانات على منصة Railway، ولكن يمكن أيضًا استخدام قاعدة بيانات على MongoDB Atlas.

يجب أولًا إنشاء حساب على MongoDB Atlas، وهو مجاني ويتطلب فقط إدخال تفاصيل الاتصال الأساسية والإقرار بشروط الخدمة. ستنتقل بعد تسجيل الدخول إلى الشاشة الرئيسية، لذا اتبع الخطوات التالية:

أولًا، انقر على زر  "إنشاء Create" في قسم نظرة عامة Overview.

02 mongodb atlas   createdatabase

ثانيًا، سيؤدي ذلك إلى فتح شاشة نشر قاعدة بيانات سحابية Deploy a cloud database. انقر على زر MO FREE.

03 mongodb atlas   deploy

ثالثًا، سيؤدي ذلك إلى سرد خيارات مختلفة للاختيار بينها:

04 mongodb atlas   createsharedcluster

حدد أي مزوّد من قسم المزوّد والمنطقة Provider & Region، إذ تقدّم المناطق المختلفة مزوّدين مختلفين. يمكنك تغيير اسم العنقود ضمن قسم اسم العنقود Cluster Name، إذ سنسميه Cluster0. انقر بعد ذلك على زر "إنشاء عنقود Create Cluster"، وسيستغرق إنشاء العنقود بضع دقائق.

رابعًا، سيؤدي ذلك إلى فتح قسم بداية سريعة للأمان Security Quickstart.

05 mongodb atlas   securityquickstart

أدخِل اسم المستخدم وكلمة المرور، وتذكّر نسخ الاعتماديات وتخزينها بأمان إذ سنحتاج إليها لاحقًا. انقر على زر "إنشاء مستخدم Create User".

ملاحظة: تجنب استخدام محارف خاصة في كلمة مرور مستخدم MongoDB لأن مكتبة Mongoose يمكن ألّا يحلّل سلسلة الاتصال بصورة صحيحة.

أدخل 0.0.0.0/0 في حقل عنوان IP الذي يخبر قاعدة بيانات MongoDB أننا نريد السماح بالوصول من أيّ مكان، ثم انقر على زر "إضافة إدخال Add Entry".

ملاحظة: يُعَد قصر عناوين IP التي يمكنها الاتصال على قاعدة بياناتك ومواردك الأخرى من أفضل الممارسات، ولكن سنسمح في مثالنا بالاتصال من أيّ مكان لأننا لا نعرف من أين سيأتي الطلب بعد النشر.

انقر بعد ذلك على زر "إنهاء وإغلاق Finish and Close".

خامسًا، سيؤدي ذلك إلى فتح الشاشة التالية، لذا انقر على زر "الانتقال إلى قواعد البيانات Go to Databases".

06 mongodb atlas   accessrules

سادسًا، ستعود بعد ذلك إلى شاشة نظرة عامة Overview. انقر على قسم قاعدة البيانات Database تحت قائمة "Deployment" الموجودة على اليسار وانقر على زر استعراض التجميعات Browse Collections.

07 mongodb atlas   createcollection

سابعًا، سيؤدي ذلك إلى فتح قسم التجميعات Collections. انقر على زر "إضافة بياناتي الخاصة Add My Own Data".

08 mongodb atlas   adddata

ثامنًا، ستظهر الآن شاشة إنشاء قاعدة بيانات Create Database.

09 mongodb atlas   databasedetails

أدخِل الاسم local_library لاسم قاعدة البيانات الجديدة، ثم أدخِل اسم المجموعة Collection0، ثم انقر على زر "إنشاء Create" لإنشاء قاعدة البيانات.

تاسعًا، ستعود إلى شاشة المجموعات Collections مع وجود قاعدة بياناتك التي أنشأتها.

10 mongodb atlas   databasecreated

انقر على نافذة "نظرة عامة Overview" للعودة إلى شاشة نظرة عامة على العنقود.

عاشرًا، انقر على زر "اتصال Connect" من شاشة نظرة عامة Overview للعنقود Cluster0.

11 mongodb atlas   connectbutton

سيؤدي ذلك إلى فتح شاشة الاتصال بالعنقود Connect to Cluster. انقر على خيار Drivers الموجود تحت خيار الاتصال بتطبيقك Connect your application.

12 mongodb atlas   chooseaconnectionmethod

أخيرًا، ستظهر لك شاشة الاتصال Connect.

13 mongodb atlas   connectforshortsrv

اتبع بعد ذلك الخطوات التالية:

  • حدد مشغّل driver ونسخة Node كما هو موضح في الشكل السابق.
  • انقر على أيقونة النسخ Copy لنسخ سلسلة الاتصال.
  • الصقها في محرر نصوصك المحلي.
  • حدّث اسم المستخدم وكلمة المرور بكلمة مرور مستخدمك.
  • أدخِل اسم قاعدة البيانات "local_library" في المسار قبل الخيارات (...mongodb.net/local_library?retryWrites...).
  • احفظ الملف الذي يحتوي على هذه السلسلة في مكان آمن.

أنشأتَ قاعدة البيانات، ولديك عنوان URL (مع اسم مستخدم وكلمة مرور) الذي يمكن استخدامه للوصول إليها، إذ سيبدو كما يلي:

mongodb+srv://your_user_name:your_password@cluster0.lz91hw2.mongodb.net/local_library?retryWrites=true&w=majority

تثبيت Mongoose

افتح موجّه الأوامر وانتقل إلى المجلد الذي أنشأتَ فيه موقعك الهيكلي الخاص بالمكتبة المحلية، ثم أدخِل الأمر التالي لتثبيت مكتبة Mongoose (واعتمادياتها) وضِفها إلى ملف package.json، إن لم تكن قد فعلتَ ذلك مسبقًا عند قراءة فقرة مقدمة إلى مكتبة Mongoose.

npm install mongoose

الاتصال بقاعدة بيانات MongoDB

افتح الملف "‎/app.js" في جذر مشروعك وانسخ النص التالي في مكان التصريح عن كائن تطبيق Express (بعد سطر const app = express();‎). ضع عنوان URL الخاص بالموقع الذي يمثل قاعدة بياناتك (أي باستخدام المعلومات الواردة من mongoDB Atlas) مكان سلسلة عنوان URL لقاعدة البيانات ('insert_your_database_url_here').

// ‫إعداد اتصال mongoose
const mongoose = require("mongoose");
mongoose.set("strictQuery", false);
const mongoDB = "insert_your_database_url_here";

main().catch((err) => console.log(err));
async function main() {
  await mongoose.connect(mongoDB);
}

تنشئ هذه الشيفرة البرمجية الاتصال الافتراضي بقاعدة البيانات ويبلّغ عن وجود أيّ أخطاء في الطرفية.

تعريف مخطط المكتبة المحلية

سنعرّف وحدة منفصلة لكل نموذج كما وضّحنا سابقًا. ابدأ بإنشاء مجلد للنماذج في جذر المشروع (‎/models) ثم أنشئ ملفات منفصلة لكل نموذج كما يلي:

/express-locallibrary-tutorial  // the project root
  /models
    author.js
    book.js
    bookinstance.js
    genre.js

نموذج المؤلف Author

انسخ شيفرة مخطط المؤلف Author التالية والصقها في ملف "‎./models/author.js"، إذ يعرّف هذا المخطط مؤلفًا يحتوي على حقول من نوع المخطط String للاسم الأول واسم العائلة (مطلوبة بحد أقصى 100 محرف) وحقول من النوع Date لتواريخ الميلاد والوفاة.

const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const AuthorSchema = new Schema({
  first_name: { type: String, required: true, maxLength: 100 },
  family_name: { type: String, required: true, maxLength: 100 },
  date_of_birth: { type: Date },
  date_of_death: { type: Date },
});

// الخاصية الافتراضية لاسم المؤلف الكامل
AuthorSchema.virtual("name").get(function () {
  // يمكن تجنب الأخطاء في الحالات التي لا يحمل فيها المؤلف اسم عائلة أو اسمًا أولًا
  // من خلال التأكد من معالجة الاستثناء عبر إعادة سلسلة فارغة لهذه الحالة
 let fullname = "";
  if (this.first_name && this.family_name) {
    fullname = `${this.family_name}, ${this.first_name}`;
  }

  return fullname;
});

// الخاصية الافتراضية لعنوان‫ URL الخاص بالمؤلف
AuthorSchema.virtual("url").get(function () {
  // لا نستخدم دالة سهمية لأننا نحتاج إلى هذا الكائن
  return `/catalog/author/${this._id}`;
});

// تصدير النموذج
module.exports = mongoose.model("Author", AuthorSchema);

صرّحنا عن الخاصية الافتراضية لمخطط المؤلف AuthorSchema بالاسم "url"، والتي تعرض عنوان URL المطلق المطلوب للحصول على نسخة معينة من النموذج، إذ سنستخدم هذه الخاصية في قوالبنا كلما احتجنا إلى الحصول على ارتباط إلى مؤلف معين.

ملاحظة: يُعَد التصريح عن عناوين URL بوصفها خاصيات افتراضية في المخطط فكرةً جيدة لأن عنوان URL لعنصرٍ ما يجب تغييره في مكانٍ واحد فقط. لن يعمل الرابط الذي يستخدم عنوان URL هذا حاليًا، لأنه ليس لدينا أيّ شيفرة لمعالجة الوجهات Routes لنسخ النماذج الفردية، إذ سنضبطها في مقالٍ لاحق.

نصدّر بعد ذلك النموذج في نهاية الوحدة.

نموذج الكتاب Book

انسخ شيفرة مخطط الكتاب Book التالية والصقها في الملف "‎./models/book.js"، والتي تشبه في معظمها نموذج المؤلف، إذ صرّحنا عن مخطط يحتوي على عدد من الحقول من النوع String وخاصية افتراضية للحصول على عنوان URL لسجلات كتاب معينة، ثم صدّرنا النموذج.

const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const BookSchema = new Schema({
  title: { type: String, required: true },
  author: { type: Schema.Types.ObjectId, ref: "Author", required: true },
  summary: { type: String, required: true },
  isbn: { type: String, required: true },
  genre: [{ type: Schema.Types.ObjectId, ref: "Genre" }],
});

// ‫الخاصية الافتراضية لعنوان URL الخاص بالكتاب
BookSchema.virtual("url").get(function () {
  // لا نستخدم دالة سهمية لأننا نحتاج إلى هذا الكائن
  return `/catalog/book/${this._id}`;
});

// تصدير النموذج
module.exports = mongoose.model("Book", BookSchema);

الاختلاف الرئيسي هنا هو أننا أنشأنا مرجعين إلى نماذج أخرى هما:

  • المؤلف author هو مرجع إلى كائن نموذج Author واحد، وهو مطلوب.
  • النوع genre هو مرجع إلى مصفوفة من كائنات نموذج Genre، ولكننا لم نصرّح عن هذا الكائن بعد.

نموذج نسخة الكتاب BookInstance

انسخ شيفرة مخطط BookInstance التالية والصقها في الملف "‎./models/bookinstance.js"، إذ يمثل BookInstance نسخةً محددةً من الكتاب الذي يمكن أن يستعيره شخص ما ويتضمن معلومات حول ما إذا كانت النسخة متوفرة، والتاريخ المتوقع لاسترجاعها، وتفاصيل "الطبعة" أو النسخة.

const mongoose = require("mongoose");

const Schema = mongoose.Schema;

const BookInstanceSchema = new Schema({
  book: { type: Schema.Types.ObjectId, ref: "Book", required: true }, // reference to the associated book
  imprint: { type: String, required: true },
  status: {
    type: String,
    required: true,
    enum: ["Available", "Maintenance", "Loaned", "Reserved"],
    default: "Maintenance",
  },
  due_back: { type: Date, default: Date.now },
});

// الخاصية الافتراضية لعنوان‫ URL الخاص بالكتاب
BookInstanceSchema.virtual("url").get(function () {
  // لا نستخدم دالة سهمية لأننا نحتاج إلى هذا الكائن
  return `/catalog/bookinstance/${this._id}`;
});

// تصدير النموذج
module.exports = mongoose.model("BookInstance", BookInstanceSchema);

الأشياء الجديدة هي خيارات الحقول التالية:

  • enum: يسمح بضبط القيم المسموح بها من نوع السلسلة النصية String، إذ نستخدمه في هذه الحالة لتحديد حالة توفر الكتب. يعني استخدام enum أنه يمكننا منع الأخطاء الإملائية والقيم العشوائية للحالة.
  • default: نستخدمه لضبط الحالة الافتراضية لنسخ الكتاب التي أنشأناها على القيمة "في الصيانة Maintenance" وتاريخ due_back الافتراضي على القيمة now. لاحظ كيفية استدعاء دالة التاريخ Date عند ضبط التاريخ.

يجب أن يكون كل شيء آخر مألوفًا من المخطط السابق.

نموذج نوع الكتاب Genre- التحدي

افتح الملف "‎./models/genre.js" وأنشئ مخططًا لتخزين أنواع الكتب (فئة الكتاب مثل ما إذا كان كتابًا خياليًا أو غير خيالي أو عاطفيًا أو تاريخيًا عسكريًا وغير ذلك).

سيكون التعريف مشابهًا جدًا للنماذج الأخرى:

  • يجب أن يحتوي النموذج على نوع المخطط String بالاسم name لوصف نوع الكتاب.
  • يجب أن يكون هذا الاسم مطلوبًا ويتكون من 3 إلى 100 محرف.
  • التصريح عن الخاصية الافتراضية لعنوان URL الخاص بنوع الكتاب بالاسم url.
  • تصدير النموذج.

إنشاء بعض العناصر للاختبار

لدينا الآن جميع النماذج المُعدَّة للموقع، ويمكننا اختبار النماذج وإنشاء بعض أمثلة الكتب والعناصر الأخرى التي يمكننا استخدامها في المقالات اللاحقة من خلال تشغيل سكربت مستقل لإنشاء عناصر لكل نوع.

أولًا، نزّل أو أنشئ الملف populatedb.js ضمن المجلد express-locallibrary-tutorial (في مستوى الملف package.json نفسه).

ملاحظة: لست بحاجة إلى معرفة كيفية عمل الملف "populatedb.js"، فهو يضيف فقط عينة بيانات إلى قاعدة البيانات.

ثانيًا، شغّل السكريبت باستخدام أمر node في موجه أوامرك مع تمرير عنوان URL لقاعدة بيانات MongoDB (عنوان URL نفسه الذي وضعته مكان العنصر البديل insert_your_database_url_here في الملف app.js سابقًا):

node populatedb <your mongodb url>

ملاحظة: يجب تغليف عنوان URL لقاعدة البيانات ضمن علامات اقتباس (") مزدوجة في نظام ويندوز، ويمكن أن تحتاج إلى علامات اقتباس مفردة (') في أنظمة التشغيل الأخرى.

ثالثًا، يجب تشغيل السكريبت حتى اكتماله، إذ يعرض العناصر أثناء إنشائها في الطرفية.

ملاحظة: انتقل إلى قاعدة بياناتك على MongoDB Atlas في نافذة التجميعات Collections، إذ يجب أن تكون الآن قادرًا على التنقل في مجموعات الكتب والمؤلفين والأنواع ونسخ الكتب والتحقق من المستندات الفردية.

الخلاصة

تعلمنا في هذا المقال بعض الأشياء عن قواعد البيانات ونماذج ORM على Node/Express، وتعرّفنا على كيفية تعريف مخطط ونماذج Mongoose، ثم استخدمنا هذه المعلومات لتصميم وتقديم نماذج Book و BookInstance و Author و Genre لموقع المكتبة المحلية LocalLibrary، واختبرنا نماذجنا من خلال إنشاء عدد من نسخ باستخدام سكريبت مستقل. سنتعرّف في المقال التالي على إنشاء بعض الصفحات لعرض هذه الكائنات.

ترجمة -وبتصرُّف- للمقال Express Tutorial Part 3: Using a Database with Mongoose.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

بتاريخ الآن قال احمد قابل هاشم ألصميدعي:

كيف اقوم بالاتصال بقاعدة البيانات من خلال الملف populateddb.js 

ذلك موضح في المقال حيث ستجد عنوان الاتصال بقاعدة بيانات MongoDB وأسفله التالي:

// استيراد وحدة‫ mongoose
const mongoose = require("mongoose");

// اضبط‫ `strictQuery: false` للاشتراك العام في الترشيح وفق الخاصيات غير المُدرَجة في المخطط
// لأن هذا الخيار ‫يزيل تحذيرات Mongoose 7 الأولية.
// اطّلع على‫ https://mongoosejs.com/docs/migrating_to_6.html#strictquery-is-removed-and-replaced-by-strict
mongoose.set("strictQuery", false);

// ‫حدّد عنوان URL لقاعدة البيانات للاتصال به
const mongoDB = "mongodb://127.0.0.1/my_database";

// انتظر حتى الاتصال بقاعدة البيانات، مع تسجيل خطأ إذا كانت هناك مشكلة
main().catch((err) => console.log(err));
async function main() {
  await mongoose.connect(mongoDB);
}

 

رابط هذا التعليق
شارك على الشبكات الإجتماعية

قمت بانشاء ملف populated.js بهذه الطريقة مع العلم لا تضاف اي بيانات الى قاعدة البيانات 

ويظهر لي الخطا التالي 

MongooseError: Operation `books.insertOne()` buffering timed out after 10000ms

 

ملف populated.js

const mongoose = require('mongoose');

const Book = require('./modles/book');

const Author = require('./modles/author');

const BookInstance = require ('./modles/bookinstance');

const Genre = require('./modles/genre');


 

//اضافة عينة بيانات الى نموذج الكتاب

const samplebook ={

    title:"Gold and fire",

    author:'ahmed',

    summary:'this book talk about the seven kingdom ',

    isbn:'2011',

    genre:'fantasa'

};

Book.create(samplebook)

.then((createdBook) => {

    console.log('تمت إضافة بيانات الكتاب بنجاح:', createdBook);

    mongoose.connection.close(); // إغلاق اتصال بعد إضافة البيانات

  })

  .catch((err) => {

    console.error('حدث خطأ أثناء إضافة بيانات الكتاب:', err);

    mongoose.connection.close(); // إغلاق اتصال في حالة حدوث أخطاء

  });

 

author , genre وضعت نوعها string فقط لتجربة اضافة البيانات 

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ On 26‏/12‏/2023 at 13:27 قال احمد قابل هاشم ألصميدعي:

قمت بانشاء ملف populated.js بهذه الطريقة مع العلم لا تضاف اي بيانات الى قاعدة البيانات 

ويظهر لي الخطا التالي 

MongooseError: Operation `books.insertOne()` buffering timed out after 10000ms

 

ملف populated.js

const mongoose = require('mongoose');

const Book = require('./modles/book');

const Author = require('./modles/author');

const BookInstance = require ('./modles/bookinstance');

const Genre = require('./modles/genre');


 

//اضافة عينة بيانات الى نموذج الكتاب

const samplebook ={

    title:"Gold and fire",

    author:'ahmed',

    summary:'this book talk about the seven kingdom ',

    isbn:'2011',

    genre:'fantasa'

};

Book.create(samplebook)

.then((createdBook) => {

    console.log('تمت إضافة بيانات الكتاب بنجاح:', createdBook);

    mongoose.connection.close(); // إغلاق اتصال بعد إضافة البيانات

  })

  .catch((err) => {

    console.error('حدث خطأ أثناء إضافة بيانات الكتاب:', err);

    mongoose.connection.close(); // إغلاق اتصال في حالة حدوث أخطاء

  });

 

author , genre وضعت نوعها string فقط لتجربة اضافة البيانات 

مرحباً احمد،

قم بمشاركة كامل ملفات المشروع (المجلد الحاوي على المشروع بالكامل)، حتى استطيع تجريب الكود ومعرفة مكان المشكلة.

شكراً لك.

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ On 26‏/12‏/2023 at 02:27 قال احمد قابل هاشم ألصميدعي:

قمت بانشاء ملف populated.js بهذه الطريقة مع العلم لا تضاف اي بيانات الى قاعدة البيانات 

ويظهر لي الخطا التالي 

MongooseError: Operation `books.insertOne()` buffering timed out after 10000ms

 

مرحبا @احمد قابل هاشم ألصميدعي

يحدث هذا الخطأ عندما تستغرق عملية الإضافة وقتًا أطول من فترة المهلة المحددة لإكمالها.

و هناك أكثر من احتمال لوجود هذا الخطأ

  1. قد تكون المشكلة في الاتصال بقاعدة البيانات , فعليك أن تتحقق من أن خادم mongoDB الخاص بك يعمل بشكل صحيح وأن اتصال Mongoose الخاص بك قد تم إنشاؤه بنجاح.
  2. إذا كنت تقوم بعملية كبيرة أو معقدة فقد تستغرق وقتا أطول من المهلة الافتراضية , فيمكنك زيادة المهلة عن طريق إضافة خيار bufferTimeoutMS عند إنشاء اتصال Mongoose الخاص بك:
    mongoose.connect('your mongoDB server url', {
      bufferTimeoutMS: 30000, // Increase the timeout to 30 seconds
    });
    هذه حلول عامة يمكنك تجريبها , و إذا لم تحل المشكلة يمكنك أن تشارك ملفات المشروع لفحصه ومعرفة سبب المشكلة.

شكرالك

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ On 31‏/12‏/2023 at 14:01 قال Hikmat Jaafer:

مرحبا @احمد قابل هاشم ألصميدعي

يحدث هذا الخطأ عندما تستغرق عملية الإضافة وقتًا أطول من فترة المهلة المحددة لإكمالها.

و هناك أكثر من احتمال لوجود هذا الخطأ

  1. قد تكون المشكلة في الاتصال بقاعدة البيانات , فعليك أن تتحقق من أن خادم mongoDB الخاص بك يعمل بشكل صحيح وأن اتصال Mongoose الخاص بك قد تم إنشاؤه بنجاح.
  2. إذا كنت تقوم بعملية كبيرة أو معقدة فقد تستغرق وقتا أطول من المهلة الافتراضية , فيمكنك زيادة المهلة عن طريق إضافة خيار bufferTimeoutMS عند إنشاء اتصال Mongoose الخاص بك:
    mongoose.connect('your mongoDB server url', {
      bufferTimeoutMS: 30000, // Increase the timeout to 30 seconds
    });
    هذه حلول عامة يمكنك تجريبها , و إذا لم تحل المشكلة يمكنك أن تشارك ملفات المشروع لفحصه ومعرفة سبب المشكلة.

شكرالك

السلام عليكم ورحمة الله وبركاته 

اخي الفاضل قمت بانشاء قاعدة البيانات ولكني انشاتها بطريقة لا اعتقد صحيحة بحيث وضعت id adress  عنوان الجهاز الحالي بحيث يجب ادخال هذا الرمز مع كل قطع واتصال بالشبكة على العموم وضعت كملة المرور واستخدم وتم انشاء قاعدة البيانات بعد ذلك نسيت كلمة مرور قاعدة البيانات سؤالي كيف اقوم بمسح قاعدة البيانات واعادة انشائها من البداية بحيث اضع كلمة مستخدم و كلمة مرور جديدة 

رابط هذا التعليق
شارك على الشبكات الإجتماعية



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...