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

البحث في الموقع

المحتوى عن 'قواعد البيانات'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

تاريخ الانضمام

  • بداية

    نهاية


المجموعة


النبذة الشخصية

تم العثور على 9 نتائج

  1. IndexedDB هي قاعدة بيانات مدمجة مع المتصفح، ولها ميزات أقوى بكثير من الكائن localStorage، أهمها: تخزن أي نوع من القيم تقريبًا من خلال المفاتيح ذات الأنواع المختلفة. تدعم الإجرائيات المترابطة transactions لوثوقية أعلى. تدعم الاستعلامات عن المفاتيح ضمن مجالات، كما تدعم الوصول إلى المفتاح بالفهرس index. يمكن أن تخزّن بيانات أحجام أكبر بكثير مما يخزنه الكائن localStorage. إنّ القدرة التي تؤمّنها قاعدة البيانات هذه تفوق المطلوب في تطبيقات (خادم-عميل) التقليدية، فهي مصممة للتطبيقات التي تعمل دون اتصال offline، وذلك لتشترك مع تقنية عمال الخدمات ServiceWorkers وغيرها من التقنيات. تشرح توصيفات قاعدة البيانات IndexedDB الواجهة الأصلية للتعامل مع القاعدة، وتتميز بأنها مقادة بالأحداث، كما يمكن أيضًا استخدام آلية async/await بمساعدة مُغلّف wrapper يعتمد على الوعود promise مثل المُغلّف idb، وعلى الرغم من أن هذه آلية مريحة، إلا أنها ليست مثاليةً، إذ لن يتمكن المغلف من استبدال الأحداث في كل الحالات، لذلك سنبدأ أولًا بالتعرف على الأحداث، ثم نتفهم قاعدة البيانات IndexedDb، ثم سنعود لاستخدام المُغلِّف. الاتصال مع قاعدة البيانات نحتاج أولًا إلى تأسيس اتصال مع قاعدة البيانات IndexedDB باستعمال التابع open قبل البدء بالعمل معها، ولهذا التابع الصيغة التالية: let openRequest = indexedDB.open(name, version); name: قيمة نصية تشير إلى اسم قاعدة البيانات. version: قيمة صحيحة موجبة لنسخة قاعدة البيانات، وهي 1 افتراضيًا. قد توجد قواعد بيانات عديدة بأسماء مختلفة، لكنها تعود جميعها إلى نفس الأصل (نطاق/بروتوكول/منفذ)، ولا يمكن لمواقع الويب المختلفة الوصول إلى قواعد بيانات المواقع الأخرى. يعيد الاستدعاء الكائن openRequest، وينبغي علينا الاستماع إلى الأحداث المتعلقة به، وهي: success: يقع عندما تكون قاعدة البيانات جاهزة، أو لما يتواجد كائن قاعدة بيانات ضمن openRequest.result، والذي علينا استخدامه في الاستدعاءات اللاحقة. error: يقع عند الإخفاق في إنشاء الاتصال مع قاعدة البيانات upgradeneeded: قاعدة البيانات جاهزة، لكن نسختها قديمة. تتميز IndexedDB بوجود آلية مدمجة فيها لتحديد نسخة تخطيطها schema versioning، والتي لا نراها في قواعد البيانات الموجودة في جهة الخادم، فهي قاعدة بيانات تعمل من جهة العميل وتخزّن بياناتها ضمن المتصفح، كما لا يستطيع المطورون الوصول إليها في أي وقت، لذا عندما يزور المستخدم موقع الويب بعد إطلاق نسخة جديدة من التطبيق، فلا بدّ من تحديث قاعدة البيانات، فإذا كانت نسخة قاعدة البيانات أقل من تلك التي يحملها الأمر open، فسيقع الحدث upgradeneeded الذي يوازن بين النسختين ويُحدِّث هياكل البيانات بما يناسب، كما يقع هذا الحدث أيضًا عندما تكون قاعدة البيانات غير موجودة (أي تكون نسختها -تقنيًا- "0")، وهذا ما يجعلنا قادرين على إجراء عملية التهيئة. لنفترض أننا قد أصدرنا النسخة الأولى من تطبيقنا، حيث يمكننا عندها تأسيس الاتصال مع قاعدة بيانات نسختها "1"، وتهيئتها بالاستفادة من معالج الحدث upgradeneeded بالشكل التالي: let openRequest = indexedDB.open("store", 1); openRequest.onupgradeneeded = function() { // يقع عندما لا يمتلك العميل قاعدة بيانات // ...إنجاز التهيئة... }; openRequest.onerror = function() { console.error("Error", openRequest.error); }; openRequest.onsuccess = function() { let db = openRequest.result; // متابعة العمل مع قاعدة البيانات }; ثم أصدرنا لاحقًا النسخة الثانية، عندها سنتمكن من تأسيس الاتصال مع النسخة "2" والتحديث بالشكل التالي: let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = function(event) { // قاعدة البيانات أقل من النسخة 2 أو غير موجودة let db = openRequest.result; switch(event.oldVersion) { // النسخة الموجودة من القاعدة case 0: // لا قاعدة بيانات // تهيئة case 1: // يمتلك المستخدم النسخة 1 من القاعدة // تحديث } }; لاحظ أنه عندما تكون نسختنا الحالية هي "2"، فسيتضمن معالج الحدث onupgradeneeded شيفرةً تعالج حالة النسخة "0"، وهذا الأمر ملائم للمستخدمين الذين يزورون الصفحة للمرة الأولى ولا يمتلكون قاعدة بيانات، كما يتضمن شيفرةً تعالج وجود النسخة "1" لتحديثها. سيقع الحدث openRequest.onsuccess عندما ينتهي معالج الحدث onupgradeneeded بنجاح، وينجح تأسيس الاتصال مع قاعدة البيانات. لحذف قاعدة البيانات: let deleteRequest = indexedDB.deleteDatabase(name) //العملية deleteRequest.onsuccess/onerror يتتبع الحدثان لا يمكن تأسيس اتصال مع قاعدة بيانات بنسخة أقدم إذا كانت النسخة الحالية لقاعدة بيانات المستخدم أعلى من النسخة التي نمررها للاستدعاء، أي نسخة القاعدة "3" ونحاول تأسيس اتصال مع النسخة "2" مثلًا، فسيتولد خطأ وسيقع الحدث openRequest.onerror. وعلى الرغم من ندرة حدوث هذا الأمر، إلا أنه قد يحصل عندما يحاول الزائر تحميل شيفرة JavaScript قديمة، مثل أن تكون من الذاكرة المؤقتة لخادم وكيل مثلًا، حيث ستكون الشيفرة قديمةً والقاعدة حديثة. ولا بدّ من التحقق من نسخة قاعدة البيانات db.version، واقتراح إعادة تحميل الصفحة إذا أردنا الحماية من الأخطاء، كما نستخدم ترويسة HTTP ملائمةً للتعامل مع الذاكرة المؤقتة لتفادي تحميل شيفرة قديمة، وبالتالي لن نواجه مشاكل. مشكلة التحديث المتوازي Parallel update ما دمنا نتكلم عن تحديد نسخة التطبيق، فسنتطرق إلى مشكلة صغيرة مرتبطة بذلك، لنتأمل الحالة التالية: فتح مستخدم موقعنا في نافذة متصفح، وكانت نسخة قاعدة البيانات هي "1". ثم حدّثنا الصفحة وأصبحت الشيفرة أحدث. ثم فتح المستخدم نفسه موقعنا في نافذة أخرى. أي ستكون هناك نافذة متصلة بقاعدة بيانات نسختها "1"، بينما تحاول النافذة الأخرى تحديثها إلى النسخة "2" عبر معالج الحدث upgradeneeded. تتلخص المشكلة بأن قاعدة البيانات مشتركة بين نافذتين، لأنهما تعودان لنفس الموقع ولهما الأصل ذاته، ولا يمكن أن تكونا من النسختين "1" و"2" في نفس الوقت، ولتنفيذ عملية الانتقال إلى النسخة "2"، ينبغي إغلاق كل قنوات الاتصال مع النسخة "1" بما فيها قناة اتصال النافذة الأولى، ولتنظيم ذلك سيقع الحدث versionchange ضمن كائن قاعدة البيانات "المنتهية الصلاحية"، لذا يفترض الاستماع لهذا الحدث، وإغلاق اتصال قاعدة البيانات القديمة. يمكن اقتراح إعادة تحميل الصفحة للحصول على الشيفرة الأحدث، فإذا لم نستمع إلى الحدث versionchange ولم نغلق قناة الاتصال، فلن يُنفَّذ الاتصال الثاني، وسيعطي الكائن openRequest الحدث blocked بدلًا من success ولن تعمل النافذة الثانية. إليك الشيفرة التي تتعامل بشكل صحيح مع التحديث المتوازي، إذ تثبّت معالج الحدث onversionchange الذي يقع عندما يصبح الاتصال الحالي مع قاعدة البيانات منتهي الصلاحية، أي عندما تُحدَّث نسخة قاعدة البيانات في مكان آخر، ويغلق الاتصال. let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = ...; openRequest.onerror = ...; openRequest.onsuccess = function() { let db = openRequest.result; db.onversionchange = function() { db.close(); alert("Database is outdated, please reload the page.") }; // ...قاعدة البيانات جاهزة! استخدمها... }; openRequest.onblocked = function() { }; إن ما نفعله هنا بعبارة أخرى هو: يُعلمنا المستمِع إلى الحدث db.onversionchange عن محاولة التحديث المتوازي، عندما تصبح النسخة الحالية لقاعدة البيانات منتهية الصلاحية. يُعلمنا المستمِع إلى الحدث openRequest.onblocked عن الحالة المعاكسة، وهي وجود اتصال لم يُغلَق بعد مع نسخة منتهية الصلاحية في نافذة ما، وبالتالي لن يعمل الاتصال الجديد. يمكن أن نتعامل مع الموضوع بطريقة ألطف عند استخدام الحدث db.onversionchange الذي يبلغ المستخدم بوجوب حفظ بياناته قبل قطع الاتصال، يمكن أيضًا اعتماد مقاربة أخرى لا تتعلق بإغلاق الاتصال مع قاعدة البيانات في معالج الحدث db.onversionchange، بل باستخدام معالج الحدث onblocked -في نافذة متصفح أخرى- لتنبيه المستخدم بأن النسخة الجديدة لن تُحمَّل قبل إغلاق النافذة الأخرى. لا يحدث التعارض في التحديث إلا نادرًا، ومع ذلك لا بدّ من توقعه والتعامل معه، على الأقل باستخدام معالج الحدث onblocked لمنع انهيار السكربت. مخزن الكائن نحتاج إلى مخزن الكائنات Object Store لتخزين أي شيء في قاعدة البيانات IndexedDB، وهو مفهوم جوهري فيها، ويقابل مفهوم "الجداول tables" أو "المجموعات collections" في قواعد البيانات الأخرى، كما تُخزّن فيه البيانات. وقد تتكون القاعدة من عدة مخازن، يكون الأول فيها للمستخدمين، والثاني للبضائع مثلًا وغيرها. على الرغم من اسم "مخزن الكائن" إلا أنه يمكن تخزين القيم الأولية فيه بالإضافة إلى الكائنات، إذ يمكن تخزين أي قيمة بما فيها الكائنات المعقدة، وتستخدم قاعدة البيانات خوارزمية التفكيك المعيارية standard serialization algorithm لنسخ وتخزين الكائن، ويشابه ذلك استخدام الأمر JSON.stringify لكن مع إمكانيات أكثر، وقدرة على تخزين أنواع أكثر من البيانات. ومن الكائنات التي لا يمكن تخزينها في هذه المخازن، نجد الكائن ذو المراجع الحلقية التي تشكل حلقةً يدل آخرها على أولها، والتي لا يمكن أن تُفكك، وسيفشل الأمر JSON.stringify معها. يجب أن يكون لكل قيمة ستُخزّن في قاعدة البيانات مفتاح فريد key، كما يجب أن تكون هذه المفاتيح من أحد الأنواع التالية: رقم أو تاريخ أو نص أو قيمة ثنائية أو مصفوفة، وسنتمكن من البحث عن القيم أو إزالتها أو تحديثها بواسطة هذه المفاتيح الفريدة. سنرى قريبًا كيف سنحصل على مفتاح عند إضافة قيمة جديدة إلى المخزن، كما يحدث عند استخدام الكائن localStorage، لكن ستسمح لنا قاعدة البيانات IndexedDB عند تخزين كائن؛ بضبط وإعداد خصائصه مثل مفتاح، وهذه مقاربة أفضل بكثير، كما يمكننا توليد المفاتيح تلقائيًا، لكننا سنحتاج إلى إنشاء مخزن للكائن أولًا. إليك الصيغة المستخدمة في إنشاء مخزن لكائن: db.createObjectStore(name[, keyOptions]); name: وتمثل اسم المخزن. keyOptions: وتمثل كائنًا اختياريًا له خاصيتان، هما: keyPath: المسار إلى خاصية الكائن التي سنستخدمها مفتاحًا، مثل id. autoIncrement: إذا كانت قيمته true فسيتولد تلقائيًا مفتاح للكائن الجديد، مثل رقم يتزايد باستمرار. إذا لم نستخدم keyOptions، فلا بدّ حينها من التصريح عن المفتاح لاحقًا عند تخزين الكائن. يستخدم مخزن الكائن التالي الخاصية id مفتاحًا، وبشكل صريح: db.createObjectStore('books', {keyPath: 'id'}); لا يمكن إنشاء أو تعديل مخزن كائن عند تحديث نسخة قاعدة البيانات إلا ضمن معالج الحدث upgradneeded، ويشكل هذا الأمر محدوديةً تقنيةً، إذ يمكن إضافة أو إزالة أو تحديث البيانات خارج المعالج، بينما لا يمكننا إنشاء أو حذف أو تغيير مخزن الكائن إلا خلال تحديث نسخة قاعدة البيانات. وتوجد مقاربتان لتنفيذ عملية تحديث نسخة قاعدة البيانات: يمكن كتابة دوال خاصة لتحديث كل نسخة ممكنة إلى النسخة الجديدة، من "1" إلى "2"، ومن "2" إلى "3" وهكذا، ثم يمكن موازنة النسخ ضمن جسم دالة معالج الحدث upgradeneeded من القديمة 2 إلى الحديثة 4 مثلًا، وتنفيذ الدالة المناسبة لتنفيذ التحديث خطوةً بخطوة، أي من 2 إلى 3، ثم من 3 إلى 4. فحص قاعدة البيانات والحصول على قائمة بمخازن الكائنات الموجودة باستخدام التابع db.objectStoreNames، ويمثل الكائن المُعاد قائمةً من النوع DOMStringList، والذي يزودنا بالتابع (contains(name الذي يتحقق من وجود مخزن باسم محدد، ثم سنتمكن من إجراء التحديث اعتمادًا على ما هو موجود وما هو غير موجود، وهذه المقاربة أبسط لقواعد البيانات الصغيرة، وإليك مثالًا عن استخدامها: let openRequest = indexedDB.open("db", 2); // تحديث وإضافة قواعد البيانات دون تحقق openRequest.onupgradeneeded = function() { let db = openRequest.result; if (!db.objectStoreNames.contains('books')) { // "books" إن لم يكن هناك مخزن باسم db.createObjectStore('books', {keyPath: 'id'}); // أنشئه } }; ولحذف مخزن كائن: db.deleteObjectStore('books') الإجرائيات المترابطة Transactions يُعَد مصطلح "إجرائيات مترابطة" مصطلحًا عامًا، ويُستخدم في قواعد بيانات عديدة، كما يشير إلى مجموعة من العمليات التي ينبغي أن تُنفَّذ مترابطةً، بحيث تنجح معًا أو تخفق معًا، فعندما يشتري شخص شيئًا ما مثلًا، فعلينا: سحب المبلغ من حساب المشتري. إضافة المشتريات إلى سلة مشترياته. سيبدو الأمر سيئًا بالطبع لو أُنجزت الخطوة الأولى بنجاح وفشلت الخطوة الثانية لسبب أو لآخر، فإما أن تنجح الخطوتان -أي يكتمل الشراء- أو تخفق الخطوتان، فعندها لن يخسر المشتري ماله ويستطيع إعادة المحاولة مجددًا. ينبغي أن تُنفَّذ جميع العمليات على البيانات ضمن إجرائيات مترابطة في القاعدة IndexedDB، وللبدء بإجرائية مترابطة يجب تنفيذ الأمر: db.transaction(store[, type]); store: اسم المخزن الذي ستُنفَّّذ عليه إجرائية مترابطة، المخزن "books" مثلًا، ويمكن أن يكون مصفوفةً من أسماء المخازن إذا كنا نريد الوصول إلى عدة مخازن. type: نوع الإجرائية المترابطة، وقد يكون: readonly: يُنفِّذ عمليات قراءة فقط، وهذا هو الخيار الافتراضي. readwrite: يُنفِّذ عمليات قراءة وكتابة فقط، ولا يستطيع إنشاء أو حذف أو تبديل الكائن. كما يمكن تنفيذ إجرائيات مترابطة من النوع versionchange، ويستطيع هذا النوع تنفيذ أي شيء، لكن لا يمكن إنشاؤه يدويًا، إذ تنشئ قاعدة البيانات الإجرائيات المترابطة من النوع versionchange عند تأسيس الاتصال مع قاعدة البيانات لتنفيذ معالج الحدث updateneeded، لذا فهو المكان الوحيد الذي نستطيع فيه تحديث هيكلية قاعدة البيانات أو إنشاء وحذف كائن المخزن. يمكن إضافة العناصر إلى المخزن بعد إنشاء الإجرائية المترابطة بالشكل التالي: let transaction = db.transaction("books", "readwrite"); // (1) // الحصول على كائن المخزن للتعامل معه let books = transaction.objectStore("books"); // (2) let book = { id: 'js', price: 10, created: new Date() }; let request = books.add(book); // (3) request.onsuccess = function() { // (4) console.log("Book added to the store", request.result); }; request.onerror = function() { console.log("Error", request.error); }; تُنفَّذ العملية مبدئيًا وفق خطوات أربع هي: إنشاء إجرائية مترابطة تشير إلى كل المخازن الذي سنصل إليها. الحصول على كائن المخزن باستخدام الأمر (transaction.objectStore(name. تنفيذ العمليات على كائن المخزن (books.add(book. التعامل مع حالة نجاح أو فشل الإجرائية المترابطة على كائن المخزن (books.add(book. تدعم كائنات المخازن تابعين لتخزين القيم، هما: ([put(value,[key: حيث يضيف القيمة value إلى المخزن، ويزوَّد التابع بالمعامل key فقط في الحالة التي لا يمتلك فيها المخزن أحد الخيارين keyPath أو autoIncrement، فإن وجدت قيمة لها نفس المفتاح فستُستبدَل. ([add(value,[key: يشابه التابع السابق، لكن إن وجدت قيمة لها نفس المفتاح فسيخفق الطلب، وسيولِّد خطأً باسم "ConstraintError". يمكن إرسال الطلب (books.add(book مثلًا بما يتناسب مع تأسيس اتصال مع قاعدة بيانات، ومن ثم ننتظر وقوع أحد الحدثين success/error. سيكون مفتاح الكائن الجديد هو نتيجة الطلب request.result للتابع add. فإن وقع خطأ ما فسنجده في الكائن request.error. اكتمال الإجراءات المترابطة بدأنا الإجرائية المترابطة في المثال السابق بتنفيذ الطلب add، لكن -وكما أشرنا سابقًا- قد تتألف الإجرائية المترابطة من عدة طلبات ينبغي أن تنجح معًا أو تخفق معًا، فكيف سنميِّز إذًا انتهاء الإجرائية ولا توجد طلبات أخرى قيد التنفيذ؟ الجواب باختصار هو أننا لا نستطيع، ولا بدّ من وجود طريقة يدوية لإنهاء الإجرائيات المترابطة في النسخة التالية 3.0 من التوصيفات، لكن حاليًا في النسخة 2.0 لا يوجد شيء مشابه لهذا. وستكتمل الإجرائية تلقائيًا عندما تنتهي جميع الطلبات المتعلقة بإجرائية مترابطة، وسيصبح صف المهام المتناهية الصغر microtasks queue فارغًا. توصف الإجرائية عادةً بأنها مكتملة عندما تكتمل كل طلباتها، وينتهي تنفيذ الشيفرة المتعلقة بها، لذا فلا حاجة في الشيفرة السابقة مثلًا إلى استدعاء خاص لإنهاء الإجرائية المترابطة. ويترافق مبدأ الاكتمال التلقائي للإجرائيات المترابطة بتأثير جانبي مهم، فلا يمكن تنفيذ عملية غير متزامنة مثل fetch أو setTimeout أثناء تنفيذ الإجرائية، كما لن تبقي قاعدة البيانات IndexedDB الإجرائية المترابطة في حالة انتظار حتى تُنجز هذه العملية. سيُخفق الطلب request2 في السطر(*) من الشيفرة التالية لأن الإجرائية قد اكتملت بالفعل، ولن نتمكن من تنفيذ طلبات أخرى: let request1 = books.add(book); request1.onsuccess = function() { fetch('/').then(response => { let request2 = books.add(anotherBook); // (*) request2.onerror = function() { console.log(request2.error.name); // TransactionInactiveError }; }); }; لأنّ fetch عملية غير متزامنة وتمثل مهمةً مستقلةً macrotask، وستُغلَق الإجرائية المترابطة قبل أن يبدأ المتصفح بتنفيذ مهمات مستقلة، كما سيرى محررو توصيفات قاعدة البيانات IndexedDB أنّ مدة إنجاز الإجرائيات المترابطة لا بدّ أن تكون قصيرة، وذلك لأسباب تتعلق بالأداء في الغالب. تقفِل الإجرائيات المترابطة من النوع readwrite المخزن عند الكتابة فيه، لذا إذا حاول جزء آخر من التطبيق تنفيذ عملية مشابهة على نفس المخزن، فعليه الانتظار. ستعلَّق الإجرائية المترابطة الجديدة حتى تنتهي الأولى، وهذا ما يسبب تأخيرًا غريبًا إذا استغرقت إجرائية مترابطة ما وقتًا طويلًا، فما العمل إذًا؟ بإمكاننا في المثال السابق تنفيذ إجرائية مترابطة جديدة db.transaction تمامًا قبل الطلب الجديد في السطر "(*)"، لكن يفضَّل -إن أردنا إبقاء العمليات معًا في إجرائية مترابطة واحدة- أن نفصل الإجرائية المترابطة في قاعدة البيانات IndexedDB عن الأمور الأخرى غير المتزامنة. نفِّذ العملية fetch أولًا، ثم حضر البيانات إن تطلب الأمر ذلك، ثم أنشئ إجرائيةً مترابطةً، ونفّذ كل الطلبات وسينجح الأمر. استمع إلى الحدث transaction.oncomplete لمعرفة اللحظة التي تكتمل فيها الإجرائية المترابطة بنجاح. let transaction = db.transaction("books", "readwrite"); // ...perform operations... transaction.oncomplete = function() { console.log("Transaction is complete"); }; تضمن الخاصية complete فقط اكتمال وحفظ الإجرائية بالكامل، وقد تنجح طلبات بمفردها، لكن قد تفشل بالمقابل عملية الكتابة النهائية، بسبب خطأ في منظومة الدخل/خرج مثلًا. استدعي التابع التالي لإيقاف الإجرائية المترابطة يدويًا: transaction.abort(); سيلغي هذا الاستدعاء كل التغييرات التي نفذتها الطلبات، ويتسبب بوقوع الحدث transaction.onabort. معالجة الأخطاء قد تُخفق طلبات الكتابة، ولا بدّ من توقع هذا الأمر -لا نتيجةً للأخطاء المحتملة التي قد نرتكبها فقط- بل لأسباب تتعلق بالإجرائيات المترابطة بحد ذاتها، فقد يحدث تجاوز حجم المخزن المحدد على سبيل المثال، لذا لا بدّ أن نكون مستعدين للتعامل مع حالات كهذه. يوقف إخفاق الطلب الإجرائية المترابطة تلقائيًا ويلغي كل التغييرات التي حدثت، لكننا قد نحتاج إلى التعامل مع حالة إخفاق الطلب، لتجريب طلب آخر مثلًا، دون إلغاء التغييرات التي حدثت، ومن ثم متابعة الإجرائية المترابطة، وهذا أمر ممكن، إذ يمكن لمعالج الحدث request.onerror أن يمنع إلغاء الإجرائية المترابطة عن طريق استدعاء التابع ()event.preventDefault. سنرى في المثال التالي كيف يُضاف كتاب جديد بمفتاح id موجود مسبقًا، وعندها سيولِّد التابع store.add الخطأ "ConstraintError"، الذي نتعامل معه دون إلغاء الإجرائية المترابطة: let transaction = db.transaction("books", "readwrite"); let book = { id: 'js', price: 10 }; let request = transaction.objectStore("books").add(book); request.onerror = function(event) { //عند إضافة قيمة بمفتاح موجود مسبقًا ConstraintError يقع الخطأ if (request.error.name == "ConstraintError") { console.log("Book with such id already exists"); // التعامل مع الخطأ event.preventDefault(); // لا تلغ الإجرائية المترابطة // استخدم مفتاح جديد للكتاب؟ } else { // خطأ غير متوقع لايمكن التعامل معه // ستلغى الإجرائية المترابطة } }; transaction.onabort = function() { console.log("Error", transaction.error); }; تفويض الأحداث هل نحتاج إلى الحدثين onsuccess/onerror عند كل طلب؟ والجواب هو لا، ليس في كل مرة، إذ يمكننا أن نستعمل تفويضًا للحدث event delegation بدلًا من ذلك. تجري عملية انسياب Bubbling الحدث في قاعدة بيانات بالشكل التالي: request → transaction → database والتي تقتضي التقاط العنصر الداخلي ضمن شجرة DOM للحدث، ثم تستمع إليه الأحداث الخارجية بالتتالي. قد تنساب الأحداث في الشجرة DOM للخارج bubbling أو للداخل capturing، لكن يُستخدم عادةً الانسياب نحو الخارج، ويمكن حينها التقاط الأخطاء عن طريق معالج الحدث db.onerror لإظهارها أو لأي أسباب أخرى. db.onerror = function(event) { let request = event.target; // الطلب الذي ولّد الخطأ console.log("Error", request.error); }; لكن ماذا لو تمكنا من التعامل مع الخطأ كاملًا؟ عندها لن نحتاج لإظهار أي شيء، يمكننا إيقاف الانسياب الخارجي للأحداث، وبالتالي إيقاف الحدث db.onerror، باستخدام الأمر ()event.stopPropagation ضمن دالة معالجة الحدث request.onerror. request.onerror = function(event) { if (request.error.name == "ConstraintError") { console.log("Book with such id already exists"); // معالجة الخطأ event.preventDefault(); // لا توقف الإجرائية المترابطة event.stopPropagation(); // لا تجعل الأحداث تنساب للخارج } else { // لا تفعل شيئًا // إيقاف الإجرائية المترابطة // transaction.onabort يمكن التعامل مع الخطأ ضمن } }; عمليات البحث يوجد نوعان أساسيان للبحث في مخزن الكائن: بقيمة المفتاح أو مجال المفتاح، ففي مثالنا عن المخزن "books"، سنتمكن من البحث عن قيمة أو مجال من القيم للمفتاح book.id. باستخدام حقل آخر من حقول الكائن، مثل البحث في مثالنا السابق اعتمادًا على الحقل book.price، ويتطلب هذا البحث هيكليةً إضافيةً للبيانات تُدعى الفهرس index. البحث بالمفتاح لنتعرف أولًا على النوع الأول، وهو البحث بالمفتاح، وتدعم طرق البحث حالة القيمة الدقيقة للمفتاح أو ما يسمى "مجالًا من القيم"، ويمثلها الكائن IDBKeyRange، وهي كائنات تحدد مجالًا مقبولًا من قيم المفاتيح، وتتولد الكائنات IDBKeyRange نتيجةً لاستخدام الاستدعاءات التالية: ([IDBKeyRange.lowerBound(lower, [open: وتعني أن تكون القيم أكبر أو تساوي الحد الأدنى lower، أو أكبر تمامًا إذا كانت قيمة open هي "true". ([IDBKeyRange.upperBound(upper, [open: تعني أن تكون القيم أصغر أو تساوي الحد الأعلى upper، أو أصغر تمامًا إذا كانت قيمة open هي "true". ([IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen: تعني أن تكون القيمة محصورةً بين الحد الأدنى lower والأعلى upper، ولن يتضمن المجال قيمتي الحدين الأعلى والأدنى إذا ضبطنا open على القيمة "true". (IDBKeyRange.only(key: وتمثل مجالًا يتكون من مفتاح واحد، وهو نادر الاستخدام. سنرى التطبيق العملي لهذه الاستدعاءات السابقة قريبًا. ولتنفيذ بحث حقيقي ستجد التوابع التالية التي تقبل الوسيط query، وقد يكون هذا الوسيط قيمةً دقيقةً للمفتاح أو مجالًا: (store.get(query: يبحث عن أول قيمة من خلال مفتاح أو مجال. ([store.getAll([query], [count: يبحث عن جميع القيم، ويكون عدد القيم محدودًا إذا أعطينا قيمةً للوسيط count. (store.getKey(query: يبحث عن أول مفتاح يحقق الاستعلام، وعادةً يكون مجالًا. ([store.getAllKeys([query], [count: يبحث عن كل المفاتيح التي تحقق الاستعلام، ويكون عدد النتائج محدودًا إذا أعطينا count قيمةً. [(store.count([query: يعيد العدد الكلي للمفاتيح التي تحقق الاستعلام، وعادةً يكون مجالًا. قد يكون لدينا على سبيل المثال الكثير من الكتب في مخزننا، وما دام الحقل id هو المفتاح، فستتمكن تلك التوابع من البحث باستعماله، مثل: // الحصول على كتاب واحد books.get('js') // 'css' <= id <= 'html' الحصول على كتاب يكون books.getAll(IDBKeyRange.bound('css', 'html')) // id < 'html' الحصول على كتاب بحيث books.getAll(IDBKeyRange.upperBound('html', true)) // الحصول على كل الكتب books.getAll() // id > 'js' الحصول على كل الكتب التي تحقق books.getAllKeys(IDBKeyRange.lowerBound('js', true)) البحث بالحقول نحتاج إلى إنشاء هيكلية إضافية للبيانات تُدعى الفهرس index، لتنفيذ استعلام باستخدام حقل آخر من حقول الكائن، ويمثِّل الفهرس إضافةً add-on إلى المخزن لتتبع حقل محدد من كائن، ويُخزّن الفهرس -ومن أجل كل قيمة للحقل المحدد- قائمةً من مفاتيح الكائنات التي تمتلك تلك القيمة، وسنوضح ذلك لاحقًا بالتفصيل. إليك صيغة إنشاء الفهرس: objectStore.createIndex(name, keyPath, [options]); name: اسم الفهرس. keyPath: المسار إلى حقل الكائن الذي سيتتبعه الفهرس، وسنبحث اعتمادًا على ذلك الحقل. option: كائن اختياري له الخصائص التالية: unique: إذا كانت قيمته "true"، فيجب أن يوجد كائن واحد في المخزن له القيمة المعطاة في المسار المحدد، وسيجبر الفهرس تنفيذ هذا الأمر بتوليد خطأ إذا حاولنا إضافة نسخة مكررة. multiEntry: ويستخدَم فقط إذا كانت القيمة في المسار المحدد keyPath مصفوفةً، وسيعامل الفهرس في هذه الحالة المصفوفة بأكملها مثل مفتاح افتراضيًا، لكن إذا كانت قيمة multiEntry هي "true"، فسيحتفظ الفهرس بقائمة من كائنات المخزن لكل قيمة في تلك المصفوفة، وهكذا ستصبح عناصر المصفوفة مفاتيح فهرسة. نخزّن في المثال التالي الكتب باستعمال المفتاح id، ولنقل أننا نريد البحث باستعمال الحقل price، سنحتاج أولًا إلى إنشاء فهرس، ويجب أن ننجز ذلك ضمن معالج الحدث upgradeneeded تمامًا مثل مخزن الكائن: openRequest.onupgradeneeded = function() { //لابد من إنشاء الفهرس هنا، ضمن إجرائية تغيير النسخة let books = db.createObjectStore('books', {keyPath: 'id'}); let index = books.createIndex('price_idx', 'price'); }; سيتعقب الفهرس الحقل price. لن يكون السعر حقلًا بقيم فريدة، إذ يمكن وجود عدة كتب لها السعر نفسه، وبالتالي لن نضبط الخيار unique. السعر قيمة مفردة وليس مصفوفةً، لذا لن نطبّق الخيار multiEntry. لنتخيل الآن وجود 4 كتب في مخزننا inventory، إليك الصورة التي تظهر طبيعة الفهرس index: كما قلنا سابقًا، سيحفَظ الفهرس العائد لكل قيمة من قيم price -الوسيط الثاني- قائمةً بالمفاتيح التي ترتبط بهذا السعر، ويُحدَّث الفهرس تلقائيًا، فلا حاجة للاهتمام بهذا الأمر. سنطبِّق ببساطة التوابع السابقة نفسها على الفهرس عندما نريد أن نبحث عن سعر محدد: let transaction = db.transaction("books"); // قراءة فقط let books = transaction.objectStore("books"); let priceIndex = books.index("price_idx"); let request = priceIndex.getAll(10); request.onsuccess = function() { if (request.result !== undefined) { console.log("Books", request.result); // مصفوفة من الكتبالتي سعرها=10 } else { console.log("No such books"); } }; يمكن أن نستخدم IDBKeyRange أيضًا لإنشاء مجال محدد، والبحث عن الكتب الرخيصة أو باهظة الثمن: // جد الكتب التي سعرها يساوي أو أقل من 5 let request = priceIndex.getAll(IDBKeyRange.upperBound(5)); تُصنَّف الفهارس داخليًا وفق الحقل الذي تتعقبه، وهو السعر price في حالتنا، لذا سترتَّب النتائج حسب السعر عندما ننفذ البحث وفق حقل السعر. الحذف من مخزن يبحث التابع delete عن القيم التي يُطلب حذفها عبر استعلام، وله صيغة شبيهة بالتابع getAll. (delete(query: يحذف القيم المطابقة للاستعلام، إليك مثالًا: // id='js' احذف الكتاب الذي يحقق books.delete('js'); إذا أردنا حذف الكتب بناءً على سعرها أو بناءً على أي حقل آخر، فعلينا أولًا إيجاد المفتاح ضمن الفهرس، ثم استدعاء التابع delete: // ايجاد المفتاح في حالة السعر=5 let request = priceIndex.getKey(5); request.onsuccess = function() { let id = request.result; let deleteRequest = books.delete(id); }; ولحذف كل شيء: books.clear(); // افرغ المخزن المؤشرات Cursors تعيد التوابع -مثل getAll/getAllKeys- مصفوفةً من المفاتيح والقيم، وقد يكون كائن التخزين ضخمًا وأكبر من أن تتسع له الذاكرة المتاحة، وبالتالي سيخفق التابع getAll في الحصول على كل سجلات المصفوفة، فما العمل في حالة مثل هذه؟ ستساعدنا المؤشرات على الالتفاف على هذه المشكلة. المؤشرات هي كائنات خاصة تتجاوز كائن التخزين عند تنفيذ استعلام، وتعيد زوجًا واحدًا (مفتاح/قيمة) في كل مرة، وبالتالي ستساعدنا في توفير الذاكرة. ما دام مخزن الكائن سيُصنَّف وفقًا للمفتاح، فسينتقل المؤشر عبر المخزن وفق ترتيب المفتاح تصاعديًا (افتراضيًا). إليك صيغة استخدام المؤشر: // لكن مع مؤشر getAll مثل let request = store.openCursor(query, [direction]); // store.openKeyCursor للحصول على المفاتيح لا القيم query: مفتاح أو مجال لمفتاح، ويشابه في عمله getAll. direction: وهو وسيط اختياري، ويفيد في ترتيب تنقل المؤشر بين السجلات: next: وهي القيمة الافتراضية، حيث يتحرك المؤشر من المفتاح ذي القيمة الأدنى إلى الأعلى. prev: يتحرك المؤشر من القيمة العليا للمفتاح إلى الدنيا. nextunique وprevunique: تشابهان الخيارين السابقين، لكنهما تتجاوزان المفتاح المكرر، وتعملان فقط مع المؤشرات المبنية على فهارس، فعند وجود عدة قيم لن تُعاد إلا قيمة أول سعر يحقق معيار البحث. إن الاختلاف الرئيسي في عمل المؤشرات هو أنها تسبب وقوع الحدث request.onsuccess عدة مرات، مرةً عند كل نتيجة. إليك مثالًا عن استخدام المؤشر: let transaction = db.transaction("books"); let books = transaction.objectStore("books"); let request = books.openCursor(); // يُستدعى من أجل كل كتاب وجده المؤشر request.onsuccess = function() { let cursor = request.result; if (cursor) { let key = cursor.key; // (id) مفتاح الكتاب let value = cursor.value; // كائن الكتاب console.log(key, value); cursor.continue(); } else { console.log("No more books"); } }; للمؤشر التوابع التالية: (advance(count: يدفع المؤشر إلى الأمام بمقدار الوسيط count ويتجاوز القيم. ([continue([key]): يدفع المؤشر إلى القيمة التالية في المجال المطابق للبحث، أو إلى ما بعد مفتاح معين مباشرةً إذا حددنا القيمة key. يُستدعى معالج الحدث onsuccess، سواءً تعددت القيم التي تطابق معيار البحث أو لا، ثم سنتمكن من الحصول على المؤشر الذي يدل على السجل التالي ضمن النتيجة result التي حصلنا عليها، أو أن لا يؤشر إلى شيء unidefined. تدريب أنشئ المؤشر في المثال السابق لمخزن الكائن. يمكن أيضًا أن ننشئ المؤشر اعتمادًا على الفهارس، إذ تسمح الفهارس كما نتذكر بالبحث وفق أي حقل من حقول الكائن، وتتشابه المؤشرات المبنية على الفهارس مع تلك المبنية على أساس مخازن الكائنات، فهي توفر الذاكرة بإعادة قيمة واحدة في كل مرة. سيكون cursor.key مفتاح الفهرسة بالنسبة للمؤشرات المبنية على الفهارس، وينبغي أن نستخدم الخاصية cursor.primaryKey إذا أردنا الحصول على مفتاح الكائن. let request = priceIdx.openCursor(IDBKeyRange.upperBound(5)); // يُستدعى لكل سجل request.onsuccess = function() { let cursor = request.result; if (cursor) { let primaryKey = cursor.primaryKey; // مفتاح مخزن الكائن التالي (id field) let value = cursor.value; // مفتاح مخزن الكائن التالي (book object) let key = cursor.key; //مفتاح الفهرسة التالي (price) console.log(key, value); cursor.continue(); } else { console.log("No more books"); } }; مغلف الوعود Promise wrapper إنّ إضافة الحدثين onsuccess/onerror إلى كل طلب أمر مرهق، ويمكن أحيانًا تحسين الوضع باستخدام التفويض delegation، مثل إعداد معالجات أحداث للإجرائية المترابطة بأكملها، لكن الصيغة async/await أكثر ملائمةً. لنستخدم مُغلَّف الحدث البسيط idb في هذا المقال، حيث ننشئ كائن idb عامًا مزوّدًا بتوابع مبنية على الوعود promisified لقاعدة البيانات IndexedDB، وسنكتب الشيفرة التالية بدلًا من استخدام onsuccess/onerror: let db = await idb.openDB('store', 1, db => { if (db.oldVersion == 0) { // نفذ عملية التهيئة db.createObjectStore('books', {keyPath: 'id'}); } }); let transaction = db.transaction('books', 'readwrite'); let books = transaction.objectStore('books'); try { await books.add(...); await books.add(...); await transaction.complete; console.log('jsbook saved'); } catch(err) { console.log('error', err.message); } وهكذا سنرى الشيفرة المحببة للجميع، "شيفرة غير متزامنة " وكتلة "try…catch". معالجة الأخطاء إذا لم نلتقط الأخطاء، فستقع إلى أن تعترضها أول كتلة try..catch. ستتحول الأخطاء التي لا نعترضها إلى حدث "عملية رفض وعد غير معالجة" ضمن الكائن window، لكن يمكن التعامل مع هذا الخطأ بالشكل التالي: window.addEventListener('unhandledrejection', event => { let request = event.target; // IndexedDB كائن طلب أصلي للقاعدة let error = event.reason; //request.error خطأ غير مُعتَرض للكائن ...report about the error... }); إجرائية مترابطة غير فعالة تكتمل الإجرائية المترابطة -كما رأينا سابقًا- عندما ينتهي المتصفح من تنفيذ شيفرتها ومهامها المتناهية الصغر، فلو وضعنا مهمةً مستقلةً مثل fetch وسط إجرائية مترابطة، فلن تنتظر عندها الإجرائية انتهاء هذه المهمة المستقلة، بل ستكتمل الإجرائية ببساطة وسيخفق الطلب التالي. يبقى الأمر نفسه بالنسبة إلى مُغلَّف الوعود والصيغة async/await، إليك مثالًا عن العملية fetch داخل إجرائية مترابطة: let transaction = db.transaction("inventory", "readwrite"); let inventory = transaction.objectStore("inventory"); await inventory.add({ id: 'js', price: 10, created: new Date() }); await fetch(...); // (*) await inventory.add({ id: 'js', price: 10, created: new Date() }); // Error سيخفق الأمر inventory.add بعد العملية fetch في السطر "(*)" وسيقع الخطأ “inactive transaction” أي "إجرائية مترابطة غير فعالة"، لأنّ الإجرائية المترابطة قد اكتملت بالفعل وأغلقت، وسنلتف على هذه المشكلة كما فعلنا سابقًا، فإما أن ننشئ إجرائيةً مرتبطةً جديدةً، أو أن نفصل الأمور عن بعضها. حضّر البيانات ثم أحضر كل ما يلزم عبر fetch أولًا. احفظ البيانات داخل قاعدة البيانات. الحصول على كائنات أصيلة يُنفِّذ المُغلَّف طلب IndexedDB أصلي داخليًا، ويضيف معالجي الحدثين onerror/onsuccess إليه، ثم يعيد وعدًا يُرفَض أو يُنفَّذ مع نتيجة الطلب. يعمل هذا الأمر جيّدًا في معظم الأوقات، وستجد مثالًا في مستودع GitHub الخاص بمُغلًّف الحدث idb. وفي حالات قليلة نادرة، وعندما نحتاج إلى الكائن request الأصلي، يمكن الوصول إليه باستخدام الخاصية promise.request العائدة للوعد. let promise = books.add(book); // الحصول على الوعد دون انتظار النتيجة let request = promise.request; // كائن الطلب الأصلي let transaction = request.transaction; // كائن الإجرائية المترابطة الأصلي // ...do some native IndexedDB voodoo... let result = await promise; // إن كنا بحاجة لذلك الخلاصة يمكن تشبيه قاعدة البيانات بالكائن "localStorage" لكنها مدعَّمة، وهي قاعدة بيانات (مفتاح-قيمة) لها إمكانيات كبيرة كافية للتطبيقات التي تعمل دون اتصال، كما أنها سهلة الاستخدام. تُمثِّل التوصيفات الخاصة بقاعدة البيانات أفضل دليل لاستخدامها، ونسختها الحالية هي 2.0، كما ستجد بعض التوابع من النسخة 3.0 التي لن تختلف كثيرًا، وهي مدعومة جزئيًا. يمكن تلخيص طريقة استخدامها الأساسية بالشكل التالي: أولًا، الحصول على مُغلِّف وعود مثل idb. ثانيًا، فتح قاعدة البيانات. idb.openDb(name, version, onupgradeneeded) إنشاء مخزن للكائن وفهارس ضمن معالج الحدث onupgradeneeded، أو تنفيذ تحديث للنسخة عند الحاجة. ثالثًا، لتنفيذ الطلبات عليك باتباع الآتي: أنشئ إجرائيةً مترابطةً ('db.transaction('books، مع إمكانية القراءة والكتابة عند الحاجة. احصل على مخزن كائن بالشكل التالي ('transaction.objectStore('books. رابعًا، يمكنك البحث بالمفتاح أو استدعاء التوابع المتعلقة بكائن المخزن مباشرةً. أنشئ فهرسًا للبحث باستخدام حقل آخر من حقول الكائن. خامسًا، إذا لم تتسع الذاكرة للبيانات، فاستخدم مؤشرًا cursor. إليك هذا المثال النموذجي: <!doctype html> <script src="https://cdn.jsdelivr.net/npm/idb@3.0.2/build/idb.min.js"></script> <button onclick="addBook()">Add a book</button> <button onclick="clearBooks()">Clear books</button> <p>Books list:</p> <ul id="listElem"></ul> <script> let db; init(); async function init() { db = await idb.openDb('booksDb', 1, db => { db.createObjectStore('books', {keyPath: 'name'}); }); list(); } async function list() { let tx = db.transaction('books'); let bookStore = tx.objectStore('books'); let books = await bookStore.getAll(); if (books.length) { listElem.innerHTML = books.map(book => `<li> name: ${book.name}, price: ${book.price} </li>`).join(''); } else { listElem.innerHTML = '<li>No books yet. Please add books.</li>' } } async function clearBooks() { let tx = db.transaction('books', 'readwrite'); await tx.objectStore('books').clear(); await list(); } async function addBook() { let name = prompt("Book name?"); let price = +prompt("Book price?"); let tx = db.transaction('books', 'readwrite'); try { await tx.objectStore('books').add({name, price}); await list(); } catch(err) { if (err.name == 'ConstraintError') { alert("Such book exists already"); await addBook(); } else { throw err; } } } window.addEventListener('unhandledrejection', event => { alert("Error: " + event.reason.message); }); </script> وستكون النتيجة كالتالي: ترجمة -وبتصرف- للفصل indexeddb من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: ملفات تعريف الارتباط وضبطها في JavaScript تعرّف على IndexedDB مفهوم Service Worker وتأثيره في أداء وبنية مواقع وتطبيقات الويب
  2. ماهو MySQL؟ MySQL هو برنامج لإدارة قواعد البيانات Database Management System, DBMS يُساعد مستخدميه في تخزين، تنظيم والعثور على البيانات. ينتشر استخدامُ MySQL في مواقع الويب نظرًا لميزاته والمرونة الّتي يُوفّرها. نهدِف في هذا الدّرس تقديم أساسيّات استخدام MySQL بطريقة مُيَسَّرة وسهلة. كيف يُثَبَّت برنامج MySQL على توزيعتيْ Ubuntu وCentOS؟ يُمكِن تنزيلُ وتثبيتُ MySQL سريعًا عبر برنامج إدارة الحزم الخاصّ بالتّوزيعة. 1- على Ubuntu: sudo apt-get install mysql-server 2- على CentOS: sudo yum install mysql-server /etc/init.d/mysqld start ملحوظة: أثناء تثبيت MySQL سيُطلب منك إدخال ثمّ تأكيد كلمة سرّ للمستخدِم الجذر Root user في MySQL (انتبه إلى أنّ المُستخدم الّذي نتحدّث عنه هنا ليس هو المستخدِم الجذر في الخادوم، بل مستخدم جذر آخر خاصّ بـMySQL). كيف يكون الاتصال بالصدفة Shell الخاصة بـ MySQL؟ يُمكن - بعد التّثبيت - الاتّصال بMySQL عن طريق صَدَفة ولوج خاصّة عبر تنفيذ الأمر التّالي: mysql -u root -p سيُطلب منك إدخال كلمة سر المُستخدم الجذر في MySQL. يُمكنك بعدها البدء في بناء قواعد بيانات في MySQL. ينبغي الانتباه للنّقطتيْن التّاليّتيْن: تنتهي كلّ أوامر MySQL بنقطة-فاصلة إنجليزيّة (;). إن لم تختِم جملةً من أوامر MySQL بنقطة-فاصلة فلن تُتنفّذ هذه الأوامر. تُكتَب أوامرُ MySQL بأحرف كبيرة Uppercase وأسماءُ كلّ من قواعد البيانات، المُستخدمين، الجداول Tables والنّصوص بأحرف صغيرة. هذه النّقطة ليست إجبارية؛ سطرُ أوامر MySQL لا يتأثّر بحالة الأحرف Case insensitive ولكن التّفريق المذكور يجعلُ من التّمييز بين الأوامر وغيرها أكثر سهولة. إنشاء وحذف قاعدة بيانات MySQL يُنظِّم MySQL معلوماتِه في قواعد بيانات تتضمّن كلّ واحدة منها جداول ببيانات مُحدَّدة. يُمكنك عرض قواعد البيانات الموجودة في MySQL عبر الأمر التّالي (يُنفَّذ في صَدَفة MySQL وليس سطر أوامر النّظام): SHOW DATABASES; يجب أن يبدو محتوى الشّاشة لديك مشابِهًا لما يلي: mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | test | +--------------------+ 4 rows in set (0.01 sec) من اليسير إنشاءُ قاعدة بيانات عن طريق الأمر التّالي(حيثُ database name يُمثّل اسمَ قاعدة البيانات): CREATE DATABASE database name; لإنشاء قاعدة بيانات باسم events مثلًا نُنفّذ الأمر: CREATE DATABASE events; نُعيد عرضَ قواعد البيانات الموجودة في MySQL: mysql> SHOW DATABASES; +--------------------+ | Database | +--------------------+ | information_schema | | events | | mysql | | performance_schema | | test | +--------------------+ 5 rows in set (0.00 sec) نُلاحِظ وجود قاعدة البيانات الّتي أنشأناها للتّو. تُستخدم غالبًا عبارة DROP لحذف الكائنات Objects في MySQL. إذا أردتَ حذفَ قاعدة بيانات فالأمر المناسب هو DROP DATABASE، كما يلي (حيثُ database name اسم قاعدة البيانات المُراد حذفُها): DROP DATABASE database name; استخدام قاعدة بـيانات في MySQL نبدأ بملْء قاعدة البيانات بعدَ إنشائها عبر إضافة معلومات إليها. أولّ خطوة هيّ إنشاء جدول جديد داخل قاعدة البيانات. يتوجّب أوّلًا فتح قاعدة البيانات المُراد استخدامُها عبر الأمر التّالي: USE events; يُمكن -كما فعلنا مع قواعد البيانات- عرضُ الجداول الموجودة في قاعدة البيانات الّتي نستخدمُها الآن: SHOW tables; تظهر، عندَ تنفيذ الأمر السّابق رسالة Empty set (مجموعة خاليّة) دلالةً على عدم وجود جداول في قاعدة البيانات. هذه النّتيجة طبيعيّة جدًّا نظرًا لجِدّة قاعدة البيانات. إنشاء جدول في قاعدة بيانات MySQL نفترِض أنّنا نُخطّط لحَدَث يجمع بعضَ الأصدقاء. سنستخدم MySQL لتتبّع تفاصيل هذا الحدث. نُنشئ جدولًا في MySQL لهذا الغرض (تذكّر أننا نستخدم قاعدة بيانات باسم events أي أنّ الجدول الّذي سنُنشئه سيكون ضمن قاعدة البيانات هذه): CREATE TABLE potluck (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), food VARCHAR(30), confirmed CHAR(1), signup_date DATE); نفّذنا عبر الأمر أعلاه الإجراءات التّالية: إنشاء جدول باسم potluck ضمن قاعدة البيانات events. إنشاء خمسة أعمِدة ضمن الجدول potluck. هذه الأعمدة هيّ: id (المُعرِِّف)، name (الاسم)، food (الطّعام)، confirmed (حضور مؤكَّد) و signup date (تاريخ التّسجيل). تطبيق الأمر INT NOT NULL PRIMARY KEY AUTO_INCREMENT للعمود id من أجل إعطاء عدد صحيح (INT في الأمر) أتوماتيكيًّا لكل صف جديد. قصرُ العمود name على عدد محارِف لا يتجاوز العشرين (20)VARCHAR. نفس الشيء بالنّسبة للعمود food الّذي يُمثِّل متعلّقات الطّعام الّتي سيجلبها كل واحد من الأصدقاء. اسم الطّعام مُكوّن من محارِف لا يتجاوز عددها الثّلاثين (30)VARCHAR. تسجيل تأكيد الحضور عبر العمود confirmed المُكوّن من محرف واحد فقط (1)CHAR. عند تأكيد الحضور نكتب Y وإلّا N. حفظ تاريخ التّسجيل عبر العمود signup_date. يطلُب MySQL كتابة التّاريخ على الشّكل yyyy-mm-dd، حيثُ yyyy السّنة مكتوبة على 4 أرقام، وmm الشهر في رقمين وdd اليوم في رقميْن أيضًا. 01-01-2015 تاريخ بصيغة صحيحة. ملحوظة: كلّ من CHAR و VARCHAR يُستخدَم للدّلالة على أن العمود يحوي محارف مع تحديد طول المحتوى، ولكن طول المحتوى من نوع CHAR ثابت، أي أنّه يجب أن يكون مساويًّا للعدد بين قوسيْن؛ في حين أنّ طول المحتوى من نوع VARCHAR متغيِّر والعدد المُذكور هو الطّول الأقصى المسموح به. نُعيد عرض الجداول الموجودة في قاعدة البيانات events: mysql> SHOW TABLES; +------------------+ | Tables_in_events | +------------------+ | potluck | +------------------+ 1 row in set (0.01 sec) نُلاحِظ ظهور الجدول potluck. إن أردتَ عرض تنظيم الجدول potluck فالأمر DESCRIBE يؤدّي هذه الوظيفة: DESCRIBE potluck; انتبِه إلى أنّ أسماء الجداول وقواعد البيانات في MySQL حسّاسة لحالة الأحرف، رغم أنّ سطر الأوامر ليس كذلك. جدول باسم potluck ليس هو نفسُه جدول Potluck أو POTLUCK. mysql>DESCRIBE potluck; +-------------+-------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+-------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(20) | YES | | NULL | | | food | varchar(30) | YES | | NULL | | | confirmed | char(1) | YES | | NULL | | | signup_date | date | YES | | NULL | | +-------------+-------------+------+-----+---------+----------------+ 5 rows in set (0.01 sec) إضافة معلومات إلى جدول في MySQL لدينا الآن جدول لتنظيم معلومات الحدَث، بقي ملؤه بالبيانات اللّازمة. استخدِم الأمرَ التّالي لإضافة صف بيانات جديد: INSERT INTO `potluck` (`id`,`name`,`food`,`confirmed`,`signup_date`) VALUES (NULL, "John", "Casserole","Y", '2012-04-11'); يُضيف هذا الأمر مُشاركًا جديدًا في الحدث اسمُه John وسيأتي بطنجرة (Casserole)؛ سجّلَ بتاريخ 2012-04-11 وأكّد حضوره (Y). المُعرّف سيُملأ أوتوماتيكيًّا لذا وضعنا NULL مكان العمود الأول. ستظهر بعد تنفيذ الأمر رسالة شبيهة بالتّاليّة: Query OK, 1 row affected (0.07 sec) نُضيف مشاركين جددًا بتفاصيل مختلفة إلى جدول المُشاركين عبر الأوامر التّاليّة: INSERT INTO `potluck` (`id`,`name`,`food`,`confirmed`,`signup_date`) VALUES (NULL, "Sandy", "Key Lime Tarts","N", '2012-04-14'); INSERT INTO `potluck` (`id`,`name`,`food`,`confirmed`,`signup_date`) VALUES (NULL, "Tom", "BBQ","Y", '2012-04-18'); INSERT INTO `potluck` (`id`,`name`,`food`,`confirmed`,`signup_date`) VALUES (NULL, "Tina", "Salad","Y", '2012-04-10'); لعرض جميع البيانات الموجودة في الجدول نستخدم الأمر أدناه. علامة * تعني معلومات جميع الأعمدة. mysql> SELECT * FROM potluck; +----+-------+----------------+-----------+-------------+ | id | name | food | confirmed | signup_date | +----+-------+----------------+-----------+-------------+ | 1 | John | Casserole | Y | 2012-04-11 | | 2 | Sandy | Key Lime Tarts | N | 2012-04-14 | | 3 | Tom | BBQ | Y | 2012-04-18 | | 4 | Tina | Salad | Y | 2012-04-10 | +----+-------+----------------+-----------+-------------+ 4 rows in set (0.00 sec) إن أردنا الاقتصار على المعلومات الموجودة في بعض الأعمدة وليس كلّها نذكر الأعمدة المعنيّة هنا. مثلا إن أردنا فقط الأسماء والأطعمة ننفّذ الأمر: SELECT name, food FROM potluck; تحديث بيانات موجودة في جدول MySQL يُتيح MySQL إمكانيّة تغيير بيانات موجودة في جدول. في المثال السّابق أضفنا مشارِكةً باسم Sandy. هذم المُشارِكة لم تؤكّد الحضور (قيمة العمود confirmed هي N). أبلغتْنا الآن بأنّها تأكّدت من حضورها. لتحديث عمود confirmed بالنّسبة لـsandy نُنفذ الأمر: UPDATE `potluck` SET `confirmed` = 'Y' WHERE `potluck`.`name` ='Sandy'; يُفهم الأمر أعلاه كما يلي: حدّث بيانات الجدول potluck بضبط عمود confirmed على القيمة Y إذا كانت قيمة العمود name تُساوي Sandy. كما تُلاحظ يوجد شرط للتّحديث وهو الاسم = Sandy. يُمكن أيضًا استخدامُ هذا الأمر لإضافة بيانات إلى خليّة (تقاطع عمود وصفّ) حتى ولو كانت خاويّة. إضافة أوحذف عمود من جدول في MySQL أنشأنا جدولًا ببيانات مُفيدة، ولكن تنقُصُنا معلومة مهمّة: البريد الإلكتروني للمُشترك؛ لذا سنُضيف عمودًا جديدًا نُخزّن فيه هذه المعلومة. ALTER TABLE potluck ADD email VARCHAR(40); يقول هذا الأمر "غيّر الجدول potluck عن طريق إضافة عمود جديد باسم email لا يتجاوز المُحتوى فيه 40 محرفًا". يُضاف هذا العمود افتراضًا في آخر الجدول، إذا كنتَ تُريد وضعَه في مكان مُعيّن يُمكن ذلك كما يلي: ALTER TABLE potluck ADD email VARCHAR(40) AFTER name; أي أنّ العمود الجديد سيُضاف بعد (AFTER) العمود name. بالنّسبة لحذف عمود فهو مُشابه لإضافته: ALTER TABLE potluck DROP email; حذف صفّ من جدول يُمكن إن اقتضت الحاجة حذف صفوف من الجدول عبر الأمر التّالي: DELETE from [table name] where [column name]=[field text]; حيثُ [table name] اسم الجدوَل ، [column name] اسم العمود و[field text] هيّ قيمة العمود في الصّف. نفرض مثلًا أنّنا نُريد حذف Sandy من قائمة المُشاركين: mysql> DELETE from potluck where name='Sandy'; Query OK, 1 row affected (0.00 sec) أي احذَف الصّف الّذي تُساوي فيه قيمةُ عمود الاسم Sandy. في حال وجود أكثر من شخص بهذا الاسم فسيُحذف جميع هؤلاء الأشخاص. نُعيد عرض محتوى الجدول بعد تنفيذ أمر الحذف: mysql> SELECT * FROM potluck; +----+------+-----------+-----------+-------------+ | id | name | food | confirmed | signup_date | +----+------+-----------+-----------+-------------+ | 1 | John | Casserole | Y | 2012-04-11 | | 3 | Tom | BBQ | Y | 2012-04-18 | | 4 | Tina | Salad | Y | 2012-04-10 | +----+------+-----------+-----------+-------------+ 3 rows in set (0.00 sec) لاحِظ عدم تغيّر معرّف أي واحد من المُشتركين المتبقّين. ترجمة -وبتصرّف- للمقال A Basic MySQL Tutorial لصاحبته Etel Sverdlov.
  3. ASP.NET Core هي إطار عمل مفتوح المصدر وعالي الأداء لإنشاء تطبيقات ويب حديثة، ومن المفترض أن يكون النسخة الأفضل من إطار العمل ASP.NET لمايكروسوفت. تمَّ إصداره عام 2016، ويمكن تشغيله على العديد من أنظمة التشغيل مثل لينكس وmacOS، مما يتيح للمطورين استهداف نظام تشغيل معين للتطوير اعتمادًا على متطلبات التصميم. يستطيع المطور باستخدام ASP.NET Core بناء أي نوع من تطبيقات الويب أو الخدمات بغض النظر عن التعقيد والحجم. يمكن للمطورين أيضًا استخدام صفحات Razor لإنشاء تصميم يركز على الصفحة يعمل أعلى النمط التقليدي MVC (اختصارًا للعبارة Model-View-Controller). يوفر ASP.NET Core المرونة للتكامل مع أي إطار عمل للواجهة الأمامية للتعامل مع العمليات المنطقية من طرف العميل أو استخدام خدمة ويب. مثلًا، بإمكانك بناء واجهة برمجية RESTful مع ASP.NET Core واستخدامها ببساطة مع أطر عمل جافاسكربت مثل Angular، React و Vue.js. ستعدّ في هذا الدرس تطبيق ASP.NET Core وتنشره ليكون جاهزًا للإطلاق مع خادم MySQL على أوبنتو 18.04 باستخدام Nginx. ستنشر تطبيق ASP.NET Core تجريبي مماثل للتطبيق من توثيق مايكروسوفت والمُستضاف على GitHub. بمجرد أن يتم النشر، سيسمح لك التطبيق التجريبي بإنشاء قائمة أفلام وتخزينها في قاعدة البيانات. ستكون قادرًا على إنشاء، وقراءة، وتحديث، وحذف سجلات من قاعدة البيانات. يمكنك بدلًا من ذلك استخدام هذا الدرس لتنشر تطبيق ASP.NET Core خاص بك؛ من الممكن أن تحتاج إلى تنفيذ خطواتٍ إضافيةٍ تتضمن إنشاء ملف تهجير (migration) جديد إلى قاعدة البيانات الخاصة بك. المتطلبات الأساسية ستحتاج لما يلي لاتباع هذا الدرس: خادم أوبنتو 18.04 تم ضبطه باستخدام دليل التهيئة الأولية لخادم أوبنتو 18.04، متضمنًا مستخدمًا عاديًا مع وصول sudo وجدار حماية. خادم Nginx مثبَّت باتباع الخطوات كيفية تثبيت Nginx على أوبنتو 18.04. خادم ويب Nginx آمن. بإمكانك اتباع درس كيف تؤمّن خادم Nginx بشهادة Let's Encrypt في أوبنتو للقيام بذلك. ضبط سجلَّي الـ DNS لخادمك. يمكنك اتّباع هذه المقدمة إلى DigitalOcean DNS للتفاصيل حول كيفية إضافتها. سجل A مع إشارة اسم النطاق الخاص بك your-domain إلى عنوان IP العام للخادم. سجل A مع إشارة اسم النطاق الخاص بك مسبوقًا بـ www www.your-domain. إلى عنوان IP العام للخادم. نظام إدارة قواعد البيانات MySQL مثبَّت باتباع خطوات كيف تثبّت الإصدار الأخير من MySQL على أوبنتو 18.0. الخطوة 1: تثبيت مُشِّغل NET Core. الآني مُشِّغل NET Core. الآني (أي NET Core runtime.) مطلوب لتشغيل تطبيق NET Core.، لذا يجب البدء بتثبيته على جهازك. تحتاج أولًا لتسجيل مفتاح مايكروسوفت ومستودع المنتج. بعد ذلك ستثبّت الاعتماديات (dependencies) المطلوبة. أولًا، سجّل الدخول كمستخدم مُنشأ حديثًا، وتأكّد من أنَّك في المجلد الجذر: $ cd ~ ثمّ نفّذ الأمر التالي لتسجيل مفتاح مايكروسوفت ومستودع المنتج: $ wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb استخدم dpkg مع الراية i- لتثبيت الملف المحدد: $ sudo dpkg -i packages-microsoft-prod.deb لتسهيل تثبيت الحزم الأخرى الضرورية لتطبيقك، ستثبّت مستودع universe باستخدام الأمر التالي: $ sudo add-apt-repository universe ثمّ ثبّت حزمة apt-transport لتسمح باستخدام المستودعات التي يتم الوصول إليها عبر بروتوكول HTTP الآمن: $ sudo apt install apt-transport-https نفِّذ الأمر التالي لتنزيل قائمة الحزم من المستودعات وتحديثها للحصول على معلومات حول أحدث إصدارات الحزم واعتمادياتها: $ sudo apt update أخيرًا، يمكنك تثبيت SDK لمشغّل NET. الآني عبر الأمر: $ sudo apt install dotnet-sdk-2.2 ستُعرض تفاصيل ما سيجري تنزيله ويُطلب تأكيد ذلك عبر الضغط على Y أي yes للمتابعة. لقد انتهيت الآن من تثبيت SDK لمشغّل NET Core. الآني على الخادم، وأنت جاهز تقريبًا لتنزيل التطبيق التجريبي من Github لضبط إعدادات النشر. لكن أولًا يجب إنشاء قاعدة بيانات التطبيق. الخطوة 2: إنشاء مستخدم MySQL وقاعدة بيانات ستنشئ في هذا الجزء مستخدم على خادم MySQL، وقاعدة بيانات للتطبيق، وتمنح جميع الصلاحيات الضرورية للمستخدم الجديد ليتصل بقاعدة البيانات من تطبيقك. تحتاج لتبدأ إلى الوصول لـMySQL العميل باستخدام حساب MySQL الجذر كما هو موضح هنا: $ mysql -u root -p سيُطلب منك إدخال كلمة سر الحساب الجذر الذي تمّ إعداده ضمن درس المتطلبات الأساسية. ثم أنشئ قاعدة بيانات MySQL للتطبيق باستخدام الأمر: mysql> CREATE DATABASE MovieAppDb; ستشاهد الخرج التالي في سطر الأوامر: Output Query OK, 1 row affected (0.03 sec) لقد أنشأت الآن قاعدة البيانات بنجاح، بعد ذلك ستُنشئ مستخدم MySQL جديد، وتربطه بقاعدة البيانات التي أُنشئت حديثًا، وتمنحه جميع الصلاحيات. نفّذ الأمر التالي لتُنشئ مستخدم MySQL وكلمة مرور. تذكّر أن تغيّر اسم المستخدم وكلمة المرور ليصبحوا أكثر أمانًا: mysql> CREATE USER 'movie-admin'@'localhost' IDENTIFIED BY 'password'; ستشاهد الخرج التالي: Output Query OK, 0 rows affected (0.02 sec) يحتاج مستخدم MySQL إلى الإذن المناسب للوصول إلى قاعدة بيانات أو تنفيذ إجراء محدد عليها، حاليًا لا يملك المستخدم movie-admin الإذن المناسب على قاعدة بيانات التطبيق. سنغيّر ذلك بتنفيذ الأمر التالي لمنح حق الوصول للمستخدم movie-admin إلى قاعدة البيانات MovieAppDb: mysql> GRANT ALL PRIVILEGES ON MovieAppDb.* TO 'movie-admin'@'localhost'; ستشاهد الخرج التالي: Output Query OK, 0 rows affected (0.01 sec) يمكنك الآن إعادة تحميل جداول الصلاحيات بتشغيل الأمر التالي لتطبيق التغييرات التي قمت بها باستخدام عبارة التدفق: mysql> FLUSH PRIVILEGES; ستشاهد الخرج التالي: Output Query OK, 0 rows affected (0.00 sec) لقد أنشأت مستخدم جديد ومنحه الصلاحيات. لتختبر فيما إذا تمّ الأمر بشكلٍ صحيحٍ، أغلق MySQL العميل: mysql> quit; ثمّ سجّل الدخول مجددًا باستخدام بيانات الاعتماد لمستخدم MySQL الذي أنشأته وأدخل كلمة المرور المناسبة عندما تُطلب منك: $ mysql -u movie-admin -p للتأكد من أن المستخدم movie-admin يمكنه الوصول إلى قاعدة البيانات المُنشأة، تحقق باستخدام الأمر: mysql> SHOW DATABASES; الآن ستشاهد جدول MovieAppDb ضمن قائمة الخرج: Output +--------------------+ | Database | +--------------------+ | MovieAppDb | | information_schema | +--------------------+ 2 rows in set (0.01 sec) الآن، أغلق MySQL العميل: mysql> quit; لقد أنشأت قاعدة بيانات، ومستخدم MySQL جديد للتطبيق التجريبي، ومنحت المستخدم الجديد الصلاحيات الصحيحة للوصول إلى قاعدة البيانات. في الجزء التالي، ستبدأ بضبط التطبيق التجريبي. الخطوة 3: ضبط التطبيق التجريبي وبيانات اعتماد قاعدة البيانات كما ذُكر سابقًا، ستنشر تطبيق ASP.NET Core موجود. تم بناء هذا التطبيق لإنشاء قائمة أفلام باستخدام نمط التصميم (Model-View-Controller) لضمان وجود الهيكل المناسب والفصل بين الاهتمامات. لإنشاء أو إضافة فيلم جديد إلى القائمة، سيملأ المستخدم حقول النموذج بالتفاصيل المناسبة ويضغط على زر إنشاء لإرسال التفاصيل إلى المتحكم (controller). عند ذلك يستقبل المتحكم طلب POST HTTP مع التفاصيل المرسلة وتبقى البيانات في قاعدة البيانات من خلال النموذج (model). ستستخدم Git لسحب (pull) الشيفرة المصدرية لهذا التطبيق التجريبي من GitHub وحفظه في مجلد جديد. يمكنك أيضًا تحميل تطبيق بديل إذا كنت ستنشر تطبيقًا مختلفًا. للبدء، أنشئ مجلدًا جديدًا باسم movie-app من الطرفية (terminal) باستخدام الأمر التالي: $ sudo mkdir -p /var/www/movie-app سيكون هذا المجلد بمثابة المجلد الجذر لتطبيقك. بعد ذلك، عدّل مالك ومجموعة المجلد للسماح لحساب المستخدم العادي بالعمل على ملفات المشروع: $ sudo chown sammy:sammy /var/www/movie-app استبدل sammy باسم المستخدم العادي الذي يملك صلاحيات sudo. يمكنك الآن الانتقال إلى المجلد الأب ونسخ (clone) التطبيق من Github: $ cd /var/www $ git clone https://github.com/do-community/movie-app-list.git movie-app ستشاهد الخرج التالي: Output Cloning into 'movie-app'… remote: Enumerating objects: 91, done. remote: Counting objects: 100% (91/91), done. remote: Compressing objects: 100% (73/73), done. remote: Total 91 (delta 13), reused 91 (delta 13), pack-reused 0 Unpacking objects: 100% (91/91), done. لقد نسخت التطبيق التجريبي بنجاح من GitHub، لذا ستكون الخطوة التالية هي إنشاء اتصال ناجح بقاعدة بيانات التطبيق. ستقوم بذلك عن طريق تعديل خاصية ConnectionStrings ضمن ملف appsettings.json وإضافة تفاصيل قاعدة البيانات. غيّر المجلد ضمن التطبيق: $ cd movie-app ثم افتح الملف للتعديل: $ sudo nano appsettings.json أضف بيانات اعتماد قاعدة البيانات في ملف appsettings.json: { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "MovieContext": "Server=localhost;User Id=movie-admin;Password=password;Database=MovieAppDb" } } بوضع هذه البيانات بطريقةٍ صحيحة فقد أنشأت اتصالًا ناجحًا بقاعدة البيانات. الآن اضغط CTRL+X لحفظ تغييرات الملف واضغط Y للتأكيد. ثم اضغط ENTER لإغلاق الصفحة. تستخدم تطبيقات ASP.NET Core مكتبة NET. قياسية تسمّى Entity Framework (EF) Core لإدارة التفاعل مع قاعدة البيانات. Entity Framework Core هي إصدار خفيف عابر للمنصات (cross-platform) من التقنية الشائعة للوصول إلى بيانات Entity Framework. إنّها تقنية ربط الكائنات بالعلاقات (ORM) التي تتيح لمطوري NET. العمل مع قاعدة البيانات باستخدام أي مزود قاعدة بيانات، مثل MySQL. يمكنك الآن تحديث قاعدة بياناتك مع الجداول من التطبيق التجريبي المنسوخ. نفّذ الأمر التالي لهذا الغرض: $ dotnet ef database update سيحدّث هذا الأمر قاعدة البيانات ويُنشئ المخططات المناسبة. الآن، لتبني المشروع وكلّ اعتمادياته، نفّذ الأمر التالي: $ dotnet build ستشاهد خرجًا مشابهًا للتالي: Output Microsoft (R) Build Engine version 16.1.76+g14b0a930a7 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restore completed in 95.09 ms for /var/www/movie-app/MvcMovie.csproj. MvcMovie -> /var/www/movie-app/bin/Debug/netcoreapp2.2/MvcMovie.dll MvcMovie -> /var/www/movie-app/bin/Debug/netcoreapp2.2/MvcMovie.Views.dll Build succeeded. 0 Warning(s) 0 Error(s) Time Elapsed 00:00:01.91 سيؤدي هذا إلى بناء المشروع وتثبيت أيّة اعتماديات خارجية (third-party dependencies) مذكورة في ملف project.assets.json، ولكن لن يكون التطبيق جاهزًا للنشر بعد. نفّذ الأمر التالي ليصبح التطبيق جاهزًا للنشر: $ dotnet publish ستشاهد التالي: Output Microsoft (R) Build Engine version 16.1.76+g14b0a930a7 for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Restore completed in 89.62 ms for /var/www/movie-app/MvcMovie.csproj. MvcMovie -> /var/www/movie-app/bin/Debug/netcoreapp2.2/MvcMovie.dll MvcMovie -> /var/www/movie-app/bin/Debug/netcoreapp2.2/MvcMovie.Views.dll MvcMovie -> /var/www/movie-app/bin/Debug/netcoreapp2.2/publish/ هذا سيُحزّم التطبيق ويترجمه، ويقرأ اعتمادياته، وينشر مجموعة الملفات الناتجة في مجلد للنشر، ويُنتج ملف dll. عابر للمنصات يستخدم مشغّل NET Core. الآني المثبّت لتشغيل التطبيق. بتثبيت الاعتماديات، وإنشاء اتصال بقاعدة البيانات، وتحديث قاعدة البيانات بالجداول الضرورية، ونشرها للإنتاج، تكون قد أكملت ضبط التطبيق التجريبي. في الخطوة التالية ستضبط إعدادات خادم الويب لجعل التطبيق متاحًا وآمنًا على نطاقك. الخطوة 4: ضبط إعدادات خادم الويب الآن وبعد اتّباع درس كيف تؤمّن خادم Nginx بشهادة Let's Encrypt ستحصل على كتلة الخادم لنطاقك في ‎/etc/nginx/sites-available/your_domain مع توجيه server_name الذي تمّ تعيينه بالفعل بشكلٍ مناسب. في هذه الخطوة ستعدّل كتلة الخادم هذه لتقوم بضبط Nginx كوكيل (proxy) عكسي لتطبيقك. الوكيل العكسي هو خادم يقع أمام خوادم الويب ويعيد توجيه كل طلب من مستعرض الويب إلى خوادم الويب هذه. إنّه يستقبل كل الطلبات من الشبكة ويعيد توجيهها إلى خادم ويب مختلف. في حالة تطبيق ASP.NET Core، يعدّ خادم الويب Kestrel هو الخادم المفضّل المضمّن افتراضيًا، وهذا ممتاز لتقديم المحتوى الديناميكي من تطبيق ASP.NET Core لكونه يوفر أداءً أفضل لمعالجة الطلبات وقد صُمم لجعل ASP.NET أسرع قدر الإمكان. ومع ذلك لا يعد Kestrel خادم ويب كامل المواصفات لأنه لا يمكنه إدارة الأمان وتقديم الملفات الثابتة، ولهذا يُستحسن دائمًا تشغيله خلف خادم ويب. للبدء، تأكد من أنّك داخل المجلد الجذر للخادم: $ cd ~ افتح كتلة الخادم للتعديل بهذا الأمر: $ sudo nano /etc/nginx/sites-available/your_domain كما هو مفصّل في الخطوة 4 من درس كيف تؤمّن خادم Nginx بشهادة Let's Encrypt، إذا اخترت الخيار 2، فإنَّ برنامج Certbot سيضبط تلقائيًا كتلة الخادم هذه لإعادة توجيه حركة مرور HTTP إلى HTTPS مع بعض التعديلات فقط. تابع الإعدادات بضبط أول كتلتين في الملف لتصل إلى ما يلي في ملف ‎/etc/nginx/sites-available/your-domain: server { server_name your-domain www.your-domain; location / { proxy_pass http://localhost:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/your-domain/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/your-domain/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } … هذه الإعدادات في كتلة الخادم ستطلب من خادم Nginx التنصّت على المنفذ 443، الذي هو المنفذ القياسي للمواقع التي تستخدم SSL. بالإضافة إلى ذلك سيقبل Nginx حركة المرور العامة على المنفذ 443 ويعيد توجيه كل طلب مطابق إلى خادم Kestrel المدمج http://localhost:5000. أخيرًا، بعد كتلة الخادم التي عدلتها في الملف، تأكّد من أنّ كتلة الخادم الثانية في الملف ‎/etc/nginx/sites-available/your-domain تبدو كالتالي: … server { if ($host = www.your-domain) { return 301 https://$host$request_uri; } # managed by Certbot if ($host = your-domain) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; listen [::]:80; server_name your-domain www.your-domain; return 404; # managed by Certbot } ستعيد كتلة الخادم توجيه كل الطلبات إلى https://your-domain وhttps://www.your-domain لوصول HTTPS آمن. بعد ذلك، دع Nginx يعكس التغييرات التي قمت بها على كتلة الخادم بتنفيذ الأمر: $ sudo nginx -s reload بعد إكمال إعداد خادم Nginx، تمّ ضبط الخادم بالكامل لإعادة توجيه جميع طلبات HTTPS الموجهة إلى https://your-domain على تطبيق ASP.NET Core الذي يعمل على الخادم Kestrel إلى http://localhost:5000. ولكن لم يتم ضبط Nginx ليدير إجراء الخادم Kestrel. ستستخدم وظائف systemd لمعالجة هذا والتأكّد من أنّ عملية Kestrel تعمل في الخلفية. ستسمح لك ملفات Systemd بأن تدير إجراءًا من خلال توفير وظائف البدء، والإيقاف، وإعادة التشغيل، والدخول، حالما تنشئ إجراء عمل يدعى وحدة. انتقل ضمن مجلد systemd: $ cd /etc/systemd/systems أنشئ ملفًا جديدًا للتعديل: $ sudo nano movie.service وأضف المحتوى التالي له: [Unit] Description=Movie app [Service] WorkingDirectory=/var/www/movie-app ExecStart=/usr/bin/dotnet /var/www/movie-app/bin/Debug/netcoreapp2.2/publish/MvcMovie.dll Restart=always RestartSec=10 SyslogIdentifier=movie User=sammy Environment=ASPNETCORE_ENVIRONMENT=Production Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false [Install] WantedBy=multi-user.target يحدد ملف الإعداد موقع مجلد المشروع مع WorkingDirectory وأمر التنفيذ في بداية العملية في ExecStart. بالإضافة إلى ذلك، لقد اعتدت على استخدام توجيه RestartSec لوصف متى سيتم إعادة تشغيل خدمة systemd إذا تعطلت خدمة مشغّل NET. الآني. الآن، احفظ الملف وفعّل خدمة movie المنشأة حديثًا مع الأمر: $ sudo systemctl enable movie.service بعد ذلك، تابع لتشغيل الخدمة والتحقق من أنّها تعمل عند التشغيل: $ sudo systemctl start movie.service ثمّ تفحّص حالتها: $ sudo systemctl status movie.service ستشاهد الخرج التالي: Output movie.service - Movie app Loaded: loaded (/etc/systemd/system/movie.service; enabled; vendor preset: enabled) Active: active (running) since Sun 2019-06-23 04:51:28 UTC; 11s ago Main PID: 6038 (dotnet) Tasks: 16 (limit: 1152) CGroup: /system.slice/movie.service └─6038 /usr/bin/dotnet /var/www/movie-app/bin/Debug/netcoreapp2.2/publish/MvcMovie.dll يعطيك هذا الخرج نظرةً عامةً عن الحالة الراهنة لخدمة movie.service المنشأة لتحافظ على التطبيق شغّالًا. تشير إلى أنّ الخدمة شغّالة وفعّالة حاليًا. انتقل إلى https://your-domain من متصفحك لتشغيل التطبيق واختباره. ستشاهد الصفحة الرئيسية للتطبيق التجريبي- تطبيق قائمة الأفلام. مع الخادم الوكيل العكسي وKestrel الذي تتم إدارته باستخدام systemd، تم ضبط تطبيق الويب بالكامل ويمكن الوصول إليه من المتصفح. خاتمة نشرت في هذا الدرس تطبيق ASP.NET Core على خادم أوبنتو. وقمت بتثبيت خادم MySQL واستخدمته لاستمرار البيانات وإدارتها، واستخدمت خادم الويب Nginx كوكيل عكسي لخدمة تطبيقك. بعد هذا الدرس، إذا كنت مهتمًا ببناء تطبيق ويب تفاعلي باستخدام #C بدلًا من جافاسكربت، يمكنك تجربة إطار العمل Blazor لواجهة المستخدم للويب من مايكروسوفت. إنّه واجهة المستخدم للويب تعتمد على المكونات ومقادة بالأحداث لتحقيق المنطق من جهة العميل في تطبيق ASP.NET Core. إذا كنت ترغب بنشر تطبيقك، ستحتاج إلى أخذ الإجراءات الأخرى بالحسبان لنشر تطبيقك. الشيفرة المصدرية الكاملة لهذا التطبيق التجريبي تجدها هنا على GitHub. ترجمة -وبتصرف- للمقال How To Deploy an ASP.NET Core Application with MySQL Server Using Nginx on Ubuntu 18.04 لصاحبه Oluyemi Olususi
  4. نشرح طريقة تثبيت وإعداد خادم PostgreSQL على نظام أوبنتو 18.04. مع إعداد كلمة مرور للمستخدم root؛ وإنشاء قاعدة بيانات جديدة ومستخدم جديد لديه الصلاحيات الكاملة عليها. قسم PostgreSQL في أكاديمية حسوب غني بالمقالات المفيدة للتعامل معها: https://academy.hsoub.com/devops/servers/databases/postgresql/ نوفر توثيقًا كاملًا لجميع تعليمات SQL على موسوعة حسوب: https://wiki.hsoub.com/SQL
  5. بعد أن تعرّفنا على طريقة تمرير قيم المُتغيّرات من بايثون إلى ملفّات HTML أصبح بإمكاننا أن نستعمل قاعدة بيانات تحتوي على جدول للمقالات عوضا عن استعمال قائمة أو قاموس. ما هي قاعدة البيانات قاعدة البيانات ببساطة مخزَن للبيانات المُختلفة كأسماء المستخدمين، كلمات المرور، وباقي القيم التي يُمكن أن تحصل عليها ممن يستخدم تطبيقك، ويُمكن كذلك جلب، تعديل وحذف البيانات منها بسهولة. يُمكن أن تكون قاعدة البيانات عبارة عن ملفّ نصي بسيط، بحيث يمثل كل سطر منه قيمة مُستقلّة، ويُمكن أن تكون عبارة عن جدول، بحيث يكون لهذا الجدول أعمدة وخانات، في كلّ عمود نوع محدد من القيم، وفي كلّ خانة القيمة الخاصّة بهذا النوع. سنستخدم في هذا الدّرس نظام SQL لقواعد البيانات، وهو نظام يعتمد على الجداول، وسنستخدم في هذا الدّرس جدولا لتخزين المقالات كالتّالي: رقم المُعرّف عنوان المقال مُحتوى المقال 1 عنوان المقال الأول مُحتوى المقال الأول 2 عنوان المقال الثّاني مُحتوى المقال الثّاني بنية تطبيق "مدونتي" سنعمل في هذا الدّرس على بناء تطبيق مُتكامل يُمكن أن يعمل كنظام إدارة مُحتوى بسيط، ستكون بنية التّطبيق كالآتي: الصّفحة الرّئيسيّة: هنا تُعرض عناوين ومحتويات المقالات المُتواجدة في قاعدة البيانات، بالإضافة إلى زر لحذف كل مقال. صفحة المقال: هنا ستتمكن من قراءة المقال مع رابط تحت المقال لحذفه. إضافة مقال جديد: ستتمكّن من إضافة مقال جديد إلى قاعدة البيانات في الصّفحة الرّئيسيّة مباشرة بعد عرض المقالات الموجودة. وهذه صور للتّطبيق النّهائي: الصفحة الرئيسية صفحة المقال إنشاء قاعدة البيانات وإنشاء جدول المقالات سنستعمل في الدّرس قواعد البيانات Sqlite لإدارة قاعدة البيانات، وذلك لسهولة التّعامل معها وسهولة نقل ملفّات قاعدة البيانات إلى أجهزة أخرى، كما أنّها لا تعمل على خادوم كما هو الحال مع MySQL أو Postgresql. تنويه: من المُفضّل عدم استخدام Sqlite في التّطبيقات التي ستنشرها على الأنترنت أو المشاريع الرّسميّة، ومن المُفضّل استخدام Postgresql أو MySQL في هذه الحالة. سننشئ قاعدة بيانات باسم database. في قاعدة البيانات هذه سنضيف جدولا للمقالات باسم posts. سيتكون جدول المقالات من ثلاثة أعمدة: رقم المقال/المعرّف (ID) عنوان المقال (Title) مُحتوى المقال (Content) لإنشاء قاعدة البيانات وجدول المقالات يُمكنك تنفيذ الشيفرة التّالية، ضعها داخل ملفّ باسم create_db.py وقم بتنفيذه: # -*- coding: utf-8 -*- import sqlite3 # الاتّصال بقاعدة البيانات db = sqlite3.connect('database.db') # إنشاء مُؤشّر في قاعدة البيانات لنتمكّن من تنفيذ استعلامات SQL cursor = db.cursor() # إنشاء الجدول cursor.execute(""" CREATE TABLE posts( id INTEGER PRIMARY KEY, title CHAR(200), content TEXT )""") # إدخال القيم إلى الجدول cursor.execute('''INSERT INTO posts(title, content) VALUES(?,?)''', (u'عنوان المقال الأول', u'محتوى المقال الأول')) cursor.execute('''INSERT INTO posts(title, content) VALUES (?,?)''', (u'عنوان المقال الثّاني', u'مُحتوى المقال الثّاني')) # تطبيق التغييرات db.commit() لاحظ بأنّنا نستدعي الوحدة sqite3 في البداية، وذلك لتنفيذ شيفرة لغة SQL، والشيفرة الممرّرة كمُعاملات للدّالة execute هي شيفرة SQL خاصّة بقاعدة البيانات Sqlite. بعد تنفيذ الشيفرة سنحصل على ملف database.db وهو الذي سيكون قاعدة بيانات التّطبيق، يوجد داخل قاعدة البيانات جدول مقالات باسم posts يحتوي بدوره على 3 أعمدة (رقم مُعرّف المقال، عنوان المقال ومحتواه)، مُعرّف المقال سيزيد بواحد تلقائيّا في كلّ مرّة نُضيف فيها عنوانا ومحتوى جديدين وهذا لأنّه من النّوع PRIMARY KEY، ما يعني بأنّنا نستطيع توفير قيمتين فقط دون الاهتمام بخانة رقم المعرّف. نضيف بعد ذلك مقالين: المقال الأول: عنوانه "عنوان المقال الأول"، مُحتواه "محتوى المقال الأول" المقال الثاني: عنوانه "عنوان المقال الثاني"، مُحتواه "محتوى المقال الثّاني" بعد الانتهاء من إضافة القيم، نقوم باستدعاء الدّالة commit لحفظ التّغييرات إلى قاعدة البيانات. الحصول على المقالات للحصول على رقم مُعرّف وعنوان ومحتوى المقالات يُمكننا تنفيذ الاستعلام التّالي: SELECT * FROM posts; النّجمة عبارة تعني all أو الكل. يُمكننا كذلك الحصول على قيم عمود واحد فقط: SELECT title FROM posts; ويُمكن الحصول على أكثر قيم أكثر من عمود: SELECT title, content FROM posts; لوضع القيم في مُتغيّر وإرجاعه في دالة في بايثون يُمكنك كتابة دالة كالتّالي: import sqlite3 BASE_DIR = path.dirname(path.realpath(__file__)) DB_PATH = path.join(BASE_DIR, 'database.db') def get_posts(): db = sqlite3.connect(DB_PATH) cursor = db.cursor() query = cursor.execute('''SELECT * FROM posts''') posts = query.fetchall() return posts السّطر الأول يستورد مكتبة sqlite3. السّطر الثّاني مسؤول عن الحصول على مسار المُجلّد الحالي، بعدها نقوم بإيصال مسار المُجلّد الحالي مع ملفّ قاعدة البيانات لنحصل على المسار الكامل للملفّ كقيمة للمُتغيّر DB_PATH، وهذا لتفادي بعض الأخطاء التّي قد تحدث عند نقل ملفّات التّطبيق إلى مكان آخر كاستضافة ما أو نظام تشغيل مُختلف. أما الدالة فتقوم أولا بالاتصال بقاعدة البيانات بالدّالة connect ومعامل DB_PATH الذي يُمثّل مسار ملف قاعدة البيانات database.db، بعدها نُنشئ مؤشّرا بالدّالة cursor، ثمّ ننفّذ الاستعلام كمُعامل مُمرّر للدّالة execute، بعدها نُطبّق الدالّة fetchall على نتيجة الاستعلام للحصول على القيم في قائمة من المجموعات، بحيث تحتوي القائمة على مجموعة بثلاثة عناصر العنصر الأول هو رقم المعرّف والعنصر الثّاني يمثّل عنوان المقال والعنصر الثّالث يمثّل محتوى المقال. وبالتّالي فإنّنا سنتمكن من الوصول إلى محتويات المقال كعناصر في مجموعة داخل قائمة، والقائمة تحتوي على العديد من المجموعات. قائمة المقالات ستكون كالتّالي: posts = [(1, u'عنوان المقال الأول', u'محتوى المقال الأول'), (2, u'عنوان المقال الثّاني', u'محتوى المقال الثّاني') ] ما يعني بأنّنا نستطيع الوصول إلى مُعرّف كل مقال، عنوانه ومحتواه بحلقة For بسيطة: posts = get_posts() for post in posts: post[0] # رقم المعرّف post[1] # عنوان المقال post[2] # محتوى المقال احفظ الدّالة get_posts في ملفّ باسم manage_db.py لنستعملها لاحقا كوحدة مع تطبيقنا (انظر درس الوحدات والحزم في لغة بايثون). الحصول على مقال حسب معرفه/رقمه للحصول على مقال حسب رقم مُعرّفه يكفي أن نُضيف جملة WHERE إلى استعلام SQL: SELECT title, content FROM posts WHERE id=1 ستُمكّننا الجملة أعلاه من الحصول على عنوان ومحتوى المقال الذي يمتلك رقم المُعرّف 1. لاستغلال الأمر في لغة بايثون بمُساعدة وحدة sqlite يُمكننا أن نكتب دالة باسم get_post_by_id لنحصل على مقال حسب رقم مُعرّفه، وبالطّبع سيكون للدّالة مُعامل واحد باسم post_id ليحمل قيمة رقم المُعرّف. def get_post_by_id(post_id): db = sqlite3.connect(DB_PATH) cursor = db.cursor() post_id = int(post_id) query = cursor.execute('''SELECT title, content FROM posts WHERE id=?''',(post_id,)) post = query.fetchone() return post بعد الاتّصال بقاعدة البيانات وإنشاء مؤشّر، نقوم أولا بتحويل قيمة رقم المُعرّف إلى عدد صحيح لأن الدّالة رقم المعرّف في قاعدة البيانات عبارة عن عدد صحيح. بعدها نُنفّذ الاستعلام الذي سبق وأن ذكرناه، لكن هذه المرّة قُمنا بتمرير مجموعة من عنصر واحد، وهذا العنصر هو مُعامل الدّالة، بعدها عرّفنا مُتغيّرا باسم post ليحمل بيانات المقال التي حصلنا عليها بتنفيذ الدّالة fetchone على الاستعلام، بعدها نُرجع المُتغيّر post. إذا استدعيت الدّالة مع تمرير قيمة بالعدد 1 فسيكون المُخرج كالتّالي: (u'عنوان المقال الأول', u'محتوى المقال الأول') أضف الدالة get_post_by_id إلى ملفّ manage_db.py واحفظه. حذف مقال حسب رقم المقال طريقة حذف المقال مُشابهة لطريقة الحصول عليه، فقط استبدل SELECT بالأمر DELETE. DELETE FROM posts WHERE id=? ما يعني بأنّنا نستطيع كتابة دالة في لغة بايثون لحذف مقال حسب رقم مُعرّفه: def delete(post_id): db = sqlite3.connect(DB_PATH) cursor = db.cursor() cursor.execute('''DELETE FROM posts WHERE id=?''', (post_id,)) db.commit() الاختلاف هنا هو أنّنا سنحتاج إلى تنفيذ الدّالة commit لتأكيد العمليّة. وكما العادة، أضف دالة الحذف إلى ملفّ manage_db.py. إضافة مقال تعرّفنا في بداية هذا الدّرس على طريقة إضافة مقال إلى قاعدة البيانات. INSERT INTO posts(title, content) VALUES('Title 1','Content 1') يُمكننا في بايثون إدخال قيم المُتغيّرات إلى قاعدة البيانات بالطّريقة التّالية: import sqlite3 db = sqlite3.connect('database.db') cursor = db.cursor() title_variable = 'Title 3' content_variable = 'Content 3' cursor.execute('''INSERT INTO posts(title, content) VALUES(?,?)''', (title_variable, content_variable)) db.commit() لا تنس أن تقوم باستدعاء الدّالة commit لتأكيد العمليّة. إذا قُمت بتنفيذ الشّيفرة أعلاه، وقُمت بعدها بتنفيذ الدّالة get_posts التي أنشأناها سابقا، ستتمكّن من رؤية القيمتين Title 3 و Content 3 كعنصرين من قائمة المقالات. لنضع هذه الشّيفرة في دالة باسم create لإضافتها إلى الوحدة manage_db، ستقبل الدّالة مُعاملين، مُعامل للعنوان، ومُعامل آخر لمُحتوى المقال. def create(title, content): db = sqlite3.connect('DB_PATH') cursor = db.cursor() cursor.execute('''INSERT INTO posts(title, content) VALUES(?,?)''', (title, content)) db.commit() الحصول على القيم وتمريرها إلى القالب بعد أن أنشأنا وحدة تحتوي على أربعة دوال تؤدّي أربعة أوامر أساسيّة: get_posts: الحصول على المقالات على شكل قائمة من المجموعات يُمكن الدّوران حولها get_post_by_id: الحصول على عنوان ومُحتوى مقال حسب رقم مُعرّفه delete: حذف مقال create: إنشاء مقال جديد إذا اطّلعت على الدّرسين السّابقين، ستعرف كيفيّة الحصول على قائمة المقالات وكيفيّة تقديمها في ملفّ HTML دون قراءة الجزء الموالي، لذا فمن الأفضل أن تُحاول ذلك الآن، وعد إلى هنا إذا واجهتك أية مشاكل. مبدأ التطبيق سيحتوي التّطبيق على 4 موجّهات: موجّه الصّفحة الرّئيسية / موجّه إضافة المقال create/ موجّه صفحة المقال الواحد <post/<post_id/ موجّه حذف المقال <delete/<post_id/ موجها إضافة المقال وحذفه لن يقدّما صفحة HTML بل سيُنفّذان دالّة وبعدها سيعيدان التّوجيه إلى الصّفحة الرّئيسيّة مباشرة. الصفحة الرئيسية ستحتوي الصّفحة الرّئيسية على عناوين ومحتويات المقالات لذا سنستخدم الدّالة get_posts من الوحدة manage_db في المُوجّه الرّئيسي ما يعني بأنّنا يجب علينا استدعاء الوحدة، كما سنُقدّم المقالات في ملفّ HTML باسم index.html. في ملّف app.py ضع ما يلي: # -*- coding:utf8 -*- from flask import Flask, render_template import manage_db app = Flask(__name__) # Home Page @app.route("/") def home(): posts = manage_db.get_posts() return render_template('index.html', posts = posts) if __name__ == "__main__": app.run(debug=True) لاحظ بأنّنا استدعينا الدّالة get_posts وأسندنا قيمتها إلى المُتغيّر posts وبعدها نُقدّم الملفّ index.html مع تمرير المُتغيّر posts. بما أنّ المُتغيّر الذي مرّرناه عبارة عن قائمة سنقوم بالدوران حول هذه القائمة والوصول إلى كل عنصر في المجموعة على حدة. الجزء المسؤول عن عرض المقالات في ملفّ index.html: {% for post in posts %} <a href="post/{{ post[0] }}"> <h2> {{ post[1] }} </h2> </a> <a href="delete/{{ post[0] }}"><span class="delete">حذف</span></a> <p> {{ post[2] }} </p> {% endfor %} الشّيفرة أعلاه هي الجزء المسؤول عن عرض المقالات فقط، وقد تجاهلت العناصر الأخرى التي لا تهمّنا مثل الشّعار والتّنسيق وغير ذلك. يُمكنك الحصول على ملفّ index.html كاملا من على Github بعد حلقة For سنحصل على مجموعة بثلاثة عناصر، العنصر الأول عبارة عن رقم مُعرّف المقال، وسنستخدمه لوضع رابطين للمقال، رابط عرض المقال ورابط حذفه، ما يعني بأنّنا نستطيع الوصول مثلا إلى المقال الأول كالتّالي: post/{{ post[0] }} => http://127.0.0.1:5000/post/1 ويُمكن حذفه بالرّابط التّالي: delete/{{ post[0] }} => http://127.0.0.1:5000/delete/1 الرّوابط لن تعمل حاليّا لأنّنا لم ننشئ المُوجّهات بعد. سيُعرض عنوان المقال داخل وسم h2 بالسّطر التّالي: <h2> {{ post[1] }} </h2> سيُعرض مُحتوى المقال داخل وسم p بالسّطر التّالي: <p> {{ post[2] }} </p> صفحة عرض المقال لعرض المقال الواحد، سنستخدم ملفّ HTML آخر وسنسمّيه post.html، أمّا الموجّه المسؤول عن تقديم هذا المقال فسيكون كالتّالي: موجّه post في ملفّ app.py: # Single Post Page @app.route("/post/<post_id>") def post(post_id): post = manage_db.get_post_by_id(post_id) return render_template('post.html', post = post) الشّيفرة أعلاه عبارة عن مُوجّه باسم post يقبل مُعاملا post_id لنتمكّن من تمريره كمُعرّف للمقال للدّالة get_post_by_id من الوحدة manage_db. بعدها نقوم باستدعاء الدّالة للحصول على بيانات المقال على شكل مجموعة يُمكننا أن نصل إلى عناصرها كالتّالي: post[0] # عنوان المقال post[1] # المحتوى صفحة post.html: <div class="main"> <h2> {{ post[0] }} </h2> <p> {{ post[1] }} </p> </div> <a href="{{ url_for('home') }}" class="back_to_home">عُد إلى الصّفحة الرّئيسيّة</a> في الشّيفرة أعلاه، نقوم بعرض عنوان المقال داخل وسم h2 ونقوم بعرض المُحتوى داخل وسم p. السّطر الأخير عبارة عن رابط لتمكين الزّائر من العودة إلى الصّفحة الرّئيسيّة و home اسم الدّالة المسؤولة عن تقديم الصّفحة الرّئيسية (الموجودة مُباشرة بعد الموجّه /). # Home Page @app.route("/") def home(): ... ملحوظة: نستطيع استخدام الدّالة url_for لتوليد روابط الموجّهات، وذلك بوضع اسم الدّالة كمعامل. مثال: لنفرض بأنّ لدينا مُوجّها باسم hello ودالة باسم hello_page، سنتمكّن من إنشاء رابط إلى الموجّه hello كالتّالي: <a href="{{ url_for('hello_page') }}">Link to Hello Page</a> يُمكن كذلك وضع عنوان المقال كعنوان للصّفحة داخل وسم title: <title>{{ post[0] }}</title> حذف مقال طريقة حذف المقال شبيهة بطريقة عرضه، الاختلاف هنا هو أنّنا سنستخدم الدّالة redirect لإعادة توجيه المُستخدم إلى الصّفحة الرّئيسية مُباشرة بعد حذف المقال. لاستخدام الدّالة redirect سيتوجّب علينا استيرادها من وحدة Flask في بداية الملفّ app.py. سنحتاج كذلك إلى الدّالة url_for للتوجيه إلى الرّابط الصّحيح. from flask import Flask, render_template, redirect, url_for موجّه delete سيقبل معاملا باسم post_id لتمريره إلى الدّالة delete من الوحدة manage_db لحذف المقال الذي يحمل رقم المعرّف المُمرّر. # Delete Post @app.route("/delete/<post_id>") def delete(post_id): manage_db.delete(post_id) return redirect(url_for('home')) لاحظ استخدام الدّالة redirect مُباشرة بعد حذف المقال، تقبل الدّالة مُعاملا بقيمة رّابط الصّفحة الرّئيسية والذي حصلنا عليه بالدّالة url_for، ما يعني بأنّنا نقوم بحذف المقال ثمّ توجيه المُستخدم مُباشرة إلى الصّفحة الرّئيسية. إنشاء مقال جديد تعرّفنا مُسبقا على طريقة الحصول على القيم من المستخدم بطريقة طلبات GET من عنوان URL كالآتي: /create?title=post1&content=content1 يُمكننا استخدام request للحصول على القيم كالتّالي: title = request.args.get('title') content = request.args.get('content') يُمكن استخدام هذه الطّريقة لإضافة مقال إلى قاعدة البيانات لكنّها ليست مُجديّة في هذه الحالة، لأنّنا نرغب بأن نُتيح للمُستخدم إرسال بيانات دون تعديل عنوان URL كما يجب علينا أن نُسهّل المأموريّة على المُستخدم العادي. لكي نحصل على العنوان والمُحتوى بطريقة أفضل، سنستخدم نماذج HTML أو HTML Forms، وسنستخدم طريقة POST عوضا عن GET. سنضع النّماذج في ملفّ index.html مُباشرة تحت الجزء المسؤول عن عرض المقالات. <h4>أضف مقالا</h4> <form action="{{ url_for('create') }}" method="POST"> <input class="input" type="text" name="title" placeholder="عنوان المقال"/> <br> <textarea name="content" class="input" rows="10" placeholder="مُحتوى المقال"></textarea> <br> <input type="submit" value="أضف" /> </form> في الوسم form نضع رابط الموجّه create داخل الصّفة action لنُخبر المُتصفّح بأنّ البيانات التّي سيُرسلها المُستخدم يجب أن تذهب إلى موجّه إضافة مقال جديد create. بعدها نُخصّص طريقة إرسال البيانات بوضع كلمة POST داخل الصّفة method. بعد ذلك ننشئ حقلا لعنوان المقال باسم title وحقل نصّ باسم content وبعدها نضيف زرّا لتأكيد الإرسال. بعد أن تملأ النموذج وتضغط على زر "أضف" سيُرسل المُتصفّح البيانات إلى الخادوم وسنتمكّن من الحصول عليها في المُوجّه create عبر الوحدة request، ما يعني بأنّنا سنحتاج إلى استدعاءها في بداية الملف. from flask import Flask, render_template, redirect, url_for, request سننشئ المُوجّه create مع تمرير مُعامل آخر باسم methods يحتوي على قائمة بعنصرين يُمثّلان الطريقتين GET وPOST لأنّ الإعداد الافتراضي هو GET فقط، نضع هذا العامل لكي نتمكّن من استقبال البيانات. @app.route("/create", methods=['GET', 'POST']) بعدها سنتمكّن من الحصول على البيانات وإدخالها إلى قاعدة البيانات كالتّالي: if request.method == 'POST': title = request.form['title'] # الحصول على عنوان المقال content = request.form['content'] # الحصول على مُحتوى المقال manage_db.create(title, content) # إدخال القيم إلى قاعدة البيانات لاحظ بأنّنا نضع شرطا للتأكّد من أن الطلب الذي يرسله المُتصفح من نوع POST. بعدها نحصل على القيم التي أدخلها المُستخدم في النّموذج الموجود بملفّ index.html عبر القاموس form المُتواجد داخل الوحدة request. وكما فعلنا مع الموجّه delete سنقوم بإعادة توجيه المُستخدم إلى الصّفحة الرّئيسية. return redirect(url_for('home')) الموجّه create كاملا: # Create Post Page @app.route("/create", methods=['GET', 'POST']) def create(): if request.method == 'POST': title = request.form['title'] content = request.form['content'] manage_db.create(title, content) return redirect(url_for('home')) أصبح التّطبيق كاملا الآن ويُمكنك مُشاركته مع العالم. يُمكنك إضافة تنسيق css خاصّ بك أو تحميل الملفّات الكاملة للتّطبيق من Github وإضافة ملفّ style.css إلى التّطبيق الذي أنشأته (يُمكنك كذلك تعديله). إذا كان لديك سؤال حول هذا الدّرس، يُمكنك وضعه في قسم الأسئلة والأجوبة. ختاما تعرّفنا على طريقة بناء تطبيق يتفاعل مع المُستخدم ويترك له حريّة الوصول إلى قاعدة البيانات، لكنك تُلاحظ بأنّ الحماية معدومة في التّطبيق، إذ يُمكن لأي شخص أن يحذف جميع المقالات دون أي حاجز (ككلمة مرور مثلا). سنتعلّم في الدّرس القادم على كيفيّة حماية التّطبيق وإتاحة الوصول إلى قاعدة البيانات لمُستخدم واحد فقط، بحيث يُسجّل دخوله إذا أراد حذف أو إضافة مقال، أمّا بقيّة المُستخدمين فلهم إمكانيّة القراءة فقط.
  6. تمهيد تتوسع قواعد البيانات بسرعة مع مرور الزمن، وتكاد في بعض الأحيان أن تملأ المساحة التخزينية المتاحة في نظام الملفات كلها. وقد تتعرض أيضًا إلى مشاكل في الإدخال والإخراج نتيجةً لمحاولة عدِّة خدمات الكتابة على (أو القراءة من) نفس القسم معًا. هذا الدرس سيفيدك لو كنتَ تريد إضافة المزيد من المساحة التخزينية، أو استخدام خصائص جهاز التخزين لزيادة الأداء (ربما عبر استخدام RAID)، أو تتطلّع إلى استعمال ميزات أخرى للتخزين. سيعلِّمُك هذا الدرس طريقة تغيير مجلد تخزين بيانات MySQL. التعليمات المذكورة هنا تناسب الخواديم التي تُشغِّل نسخةً وحيدةً من MySQL، أمّا لو كانت عندك أكثر من نسخة، فسيساعدك درس «How To Move a MySQL Data Directory to a New Location on Ubuntu 16.04» في ذلك، لأنَّه يحتوي معلومات عن كيفية تغيير مكان التخزين عبر تعديل الضبط. المتطلبات المسبقة خادوم أوبنتو 16.04 (أو 14.04) مضبوطٌ كما في درس «الإعداد الابتدائي لخادوم أوبنتو 14.04»، بما في ذلك إعداد حساب مستخدم عادي لكنه يملك امتيازات الجذر (root) عبر الأداة sudo. خادوم MySQL. لو لم يكن عندك خادوم MySQL مضبوطٌ مسبقًا، فسيساعدك الدرس «تثبيت وإعداد نظامي إدارة قواعد البياناتMySQL وPostgreSQL على أوبنتو». نسخة احتياطية من قواعد بياناتك. ما لم تكن تتعامل مع نسخةٍ حديثة التثبيت من MySQL، فاحرص على أخذ نسخة احتياطية من بياناتك. سيساعدك درس «كيف تقوم بالنسخ الاحتياطي لقواعد بيانات MySQL على Ubuntu» على فعل ذلك. سننقل البيانات من جهاز تخزينٍ موصولٍ (mounted) في نقطة الوصل ‎/mnt/volume-nyc1-01.. سيعلّمك هذا الدرس طريقة نقل مجلد تخزين بيانات MySQL إلى مكانٍ جديد بغض النظر عن وسيط التخزين الذي تستخدمه (قرص صلب، أو مصفوفة RAID، أو تخزين شبكي). الخطوة الأولى: نقل مجلد بيانات MySQL لكي نضمن سلامة البيانات، علينا أولًا إيقاف خادوم MySQL: sudo systemctl stop mysql الأمر systemctl لا يُظهِر نتيجة تنفيذ أوامر إدارة الخدمات، لذا إذا أردتَ التحقق أنَّ الخادوم قد أُغلِق بنجاح، فنفِّذ الأمر الآتي: sudo systemctl status mysql انظر إلى آخر سطر من ناتج الأمر السابق الذي يجب أن يخبرك أنَّ الخادوم قد توقف عن العمل: . . . Jul 18 11:24:20 ubuntu-512mb-nyc1-01 systemd[1]: Stopped MySQL Community Server. نستطيع الآن –بعد إغلاق الخادوم– نقل مجلد قواعد البيانات إلى مكانٍ آخر: sudo mv /var/lib/mysql /mnt/volume-nyc1-01/mysql ثم سنُنشِئ وصلةً رمزيةً (symbolic link): sudo ln -s /mnt/volume-nyc1-01/mysql /var/lib/mysql يبدو أنَّنا نستطيع تشغيل خادوم MySQL بعد إنشاء الوصلة الرمزية، لكن هنالك أمرٌ إضافيٌ يجب ضبطه. الخطوة الثانية: ضبط قواعد الوصول في AppArmor بعد أن نقلتَ مجلد MySQL إلى مكانٍ آخر في نظام الملفات، فعليك أن تُعدِّل في ضبط AppArmor، وذلك بتعديل ملف alias التابع لبرمجية AppArmor: sudo nano /etc/apparmor.d/tunables/alias أضف الآن التعليمة الآتية في نهاية الملف: . . . alias /var/lib/mysql/ -> /mnt/volume-nyc1-01/mysql/, لكي تأخذ التعديلات مفعولها، فيجب إعادة تشغيل AppArmor: sudo systemctl restart apparmor ملاحظة: إذا تخطيت خطوة ضبط AppArmor وحاولت تشغيل mysql مباشرةً، فستظهر لك رسالة الخطأ الآتية: Job for mysql.service failed because the control process exited with error code. See "systemctl status mysql.service" and "journalctl -xe" for details. يمكن تلخيص الناتج الظاهر من الأمرَين systemctl و journalctl بما يلي: Jul 18 11:03:24 ubuntu-512mb-nyc1-01 systemd[1]: mysql.service: Main process exited, code=exited, status=1/FAILURE ولمّا كانت رسائل الخطأ لا تُظهِر ربطًا مباشرًا بين AppArmor ومجلد التخزين، فقد يصعب عليك أن تعرف لماذا يحدث هذا الخطأ. لكن إن نظرنا إلى ملف syslog فسنرى ما هي المشكلة: sudo tail /var/log/syslog الناتج: Nov 24 00:03:40 digitalocean kernel: [ 437.735748] audit: type=1400 audit(1479945820.037:20): apparmor="DENIED" operation="mknod" profile="/usr/sbin/mysqld" name="/mnt/volume-nyc1-01/mysql/mysql.lower-test" pid=4228 comm="mysqld" requested_mask="c" denied_mask="c" fsuid=112 ouid=112 يمكننا الآن أن نُشغِّل خدمة MySQL: sudo systemctl start mysql sudo systemctl status mysql بعد أن تُعيد تشغيل MySQL، فتحقق أنَّ بياناتك سليمة وأنَّ خادوم MySQL يعمل عملًا سليمًا دون مشاكل. الخلاصة نقلنا في هذا الدرس مجلد تخزين بيانات MySQL واستعملها وصلةً رمزيةً لكي نخبر MySQL ما هو مكان التخزين الجديد؛ ثم حدّثنا ضبط برمجية AppArmor (الموجودة في توزيعة أوبنتو وغيرها) لكي يتوافق مع ما عدّلناه. وصحيحٌ أننا استعملنا جهاز تخزين مستقل، إلا أنَّك تستطيع اتباع تعليمات هذا الدرس لإعادة تعريف مكان تخزين البيانات بغض النظر عن تقنية التخزين المستعملة. ترجمة -وبتصرّف- للمقال How to Change a MySQL Data Directory to a New Location Using a Symlink لصاحبته Melissa Anderson
  7. يوفر تهجير قواعد البيانات Database migration في Laravel آليات لإنشاء الجداول Tablesوالتعديل عليها بغض النظر نظام إدارة قواعد البيانات المستخدم. يعني هذا أنك لن تضطر للاهتمام بالاختلافات بين نظم إدارة قواعد البيانات في صياغة أوامر SQL. يمكّن التهجير أيضا من التراجع والعودة إلى ما كانت عليه قاعدة البيانات قبل آخر التعديلات. هذا الدرس جزء من سلسلة تعلم Laravel والتي تنتهج مبدأ "أفضل وسيلة للتعلم هي الممارسة"، حيث ستكون ممارستنا عبارة عن إنشاء تطبيق ويب للتسوق مع ميزة سلة المشتريات. يتكون فهرس السلسلة من التالي: مدخل إلى Laravel 5.تثبيت Laravel وإعداده على كلّ من Windows وUbuntu.أساسيات بناء تطبيق باستخدام Laravel.إنشاء روابط محسنة لمحركات البحث (SEO) في إطار عمل Laravel.نظام Blade للقوالب.تهجير قواعد البيانات في Laravel. (هذا الدرس)استخدام Eloquent ORM لإدخال البيانات في قاعدة البيانات، تحديثها أو حذفها.إنشاء سلة مشتريات في Laravel.الاستيثاق في Laravel.إنشاء واجهة لبرمجة التطبيقات API في Laravel.إنشاء مدوّنة باستخدام Laravel.استخدام AngularJS واجهةً أمامية Front end لتطبيق Laravel.الدوّال المساعدة المخصّصة في Laravel.استخدام مكتبة Faker في تطبيق Laravel لتوليد بيانات وهمية قصدَ الاختبار. يمكن النظر إلى تهجير قواعد البيانات كما لو كان نظام إدارة نسخ خاص بقواعد البيانات، إذ يتيح لفريق العمل سهولة تغيير مخطّط Schema البيانات وتشاركه. نكمل في هذا الدرس اعتمادا على ما أنشأناه في الدروس السابقة من السلسلة. يغطي الدرس المواضيع التالية: متطلبات التهجير.أمر Artisan لتهجير قواعد البيانات.بنية التهجير.إنشاء جدول بآلية التهجير.استخدام آلية التهجير للتراجع Rollback عن التعديلات.بذر قواعد البيانات Database seeding.بنية قاعدة البيانات الخاصة بمشروع Larashop.ملفات التهجير لقاعدة بيانات Larashop.متطلبات التهجيريجب أولا إنشاء قاعدة بيانات في نظام إدارة قواعد البيانات المستخدم (MySQL في حالتنا) وإعداد معطيات الاتصال بها في Laravel ولدى أداة سطر الأوامر Artisan. إنشاء قاعدة بياناتنفذ الأمر التالي في سطر أوامر MySQL أو استخدم التطبيق المفضّل لديك (PHPMyAdmin مثلا) لإنشاء قاعدة بيانات larashop: CREATE DATABASE `larashop`;إعداد Laravel للاتصال بقاعدة البياناتأعددنا Laravel في الدرس الأول من هذه السلسلة للاتصال بقاعدة بيانات باسم larashop. في ما يلي تذكير بخطوات الإعداد. افتح الملف config/database.php واعثر على الأسطُر التالية: 'mysql' => [ 'driver' => 'mysql', 'host' => env('DB_HOST', 'localhost'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', 'strict' => false, ],حدّث القيم التالية لتوافق إعدادات MySQL لديك: 'database' => env('DB_DATABASE', 'larashop'), 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', 'melody'), إعداد معطيات اتصال Artisan بقاعدة البياناتيواجه الكثير من المطورين رسالة الخطأ التالية عند العمل على تهجير قواعد البيانات باستخدام أداة Artisan: Access denied for user 'homestead'@' localhost' (using password: YES)ستظهر الرسالة أعلاه حتى ولو كانت معطيات الاتصال في الملف configuration/database.php صحيحة. يعود السبب في ذلك إلى أن Artisan يستخدم المعطيات الموجودة في الملف env.. الحل هو إذن تحرير الملف env. الواقع في مجلد التطبيق، ستجد ما يلي: APP_ENV=local APP_DEBUG=true APP_KEY=aqk5XHULL8TZ8t6pXE43o7MBSFchfgy2 DB_HOST=localhost DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secret CACHE_DRIVER=file SESSION_DRIVER=file QUEUE_DRIVER=sync MAIL_DRIVER=smtp MAIL_HOST=mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null حدث المتغيرات التالية: DB_HOST=localhost DB_DATABASE=homestead DB_USERNAME=homestead DB_PASSWORD=secretلتصبح: DB_HOST=localhost DB_DATABASE=larashop DB_USERNAME=root DB_PASSWORD=melody احرص على موافقة اسم قاعدة البيانات، اسم المستخدم وكلمة مروره للمعطيات لديك. احفظ التعديلات. تهجير قواعد البيانات بأداة Artisanينشئ أمر artisan ملفا على المسار database/migrations لكل عملية تهجير. يمكن تغيير المسار الخاص بحفظ ملفات التهجير إن أردت ولكننا هنا سنكتفي بالمسار المبدئي. ننفذ الأمر التالي لإنشاء أول ملف تهجير: php artisan make:migration create_drinks_tableتظهر رسالة باسم ملف التهجير الجديد. اخترنا اسم create_drinks_table للدلالة على أن التهجير ينشئ جدولا باسم drinks في قاعدة البيانات. نتيجة الأمر هي إنشاء ملف للتهجير بنفس الاسم الذي أعطيناه مع إضافة ختم زمني قبله، مثلا: 2015_12_21_215845_create_drinks_table.phpبنية ملف التهجيرندرس الآن محتوى ملف التهجير الذي أنشأناه للتو. افتح الملف التالي لرؤية محتواه (انتبه إلى أن اسم الملف يبدأ بختم زمني للحظة إنشائه): database/migrations/2015_12_21_215845_create_drinks_table.phpنجد ما يلي: <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateDrinksTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { // } /** * Reverse the migrations. * * @return void */ public function down() { // } }يعرف ملف التهجير صنفا جديدا باسم CreateDrinksTable يمدد الصنف Migration: CreateDrinksTable extends Migrationداخل الصنف CreateDrinksTable توجد دالة باسم up. تنفّذ تعليمات الدالة up عند تشغيل التهجير. توجد أيضا دالة باسم down في الصنف CreateDrinksTable. تنفذ تعليمات الدالة down عند التراجع عن تغييراتِ تهجير.ملف تهجير لإنشاء جدول بقاعدة البياناتليمكن إنشاء جدول في قاعدة البيانات فجيب تعريف حقوله في ملف التهجير. نعيد فتح ملف التهجير السابق ونعدله ليصبح محتواه التالي : <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateDrinksTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('drinks', function (Blueprint $table) { $table->increments('id'); $table->string('name',75)->unique(); $table->text('comments')->nullable(); $table->integer('rating'); $table->date('juice_date'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('drinks'); } }في الدالة up: نستدعي الدالة create المعرَّفة في الصنف Schema ونمرر لها معطيين، الأول اسم الجدول الذي نريد إنشاءه drinks، والمعطى الثاني دالة غير محدّدة الاسم تعرّف حقول الجدول. نستخدم كائنا من صنف Blueprint لتعريف الجدول.نعرف أول حقل من الجدول وهو الحقل id. تعرف الدالة increments التابعة للصنف Blueprint عددا طبيعيا (عدد صحيح إشارته موجبة) يزداد تلقائيّا مع كل إدخال للبيانات في الجدول.الحقل الثاني هو حقل الاسم name، الذي نعرفه بالدالة string. تنشئ الدالة stringحقلا من سلسلة محارف مع تحديد طول السلسلة (75 في المثال). نعلّم الحقل name بالدالة unique \لجعله وحيدا وهو ما يعني أنه لا يمكن لتسجيلتين في الجدول أن تحويا نفس القيمة بالنسبة لهذا الحقل.الحقل الثالث comments نصي، وتستخدم الدالة text لتعريفه. نتيح إمكانية ألا يحوي الحقل بيانات باستخدام الدالة nullable.الحقل الرابع rating للتقيمات. نستخدم الدالة integer للإشارة إلى أنه عدد صحيح.ثم نضيف حقلا لتخزين تاريخ المشروب juice_date ونستخدم الدالة date لهذا الغرض.تُستخدم الدالة timestamps لإضافة حقلين هما created_at وupdated_at في الجدول تلقائيا. الحقلان عبارة عن ختم زمني ل، على التوالي، تاريخ إضافة التسجيلة إلى قاعدة البيانات وتاريخ آخر تحديث عليها.في الدالة down نحذف الجدول drinks من قاعدة البيانات في حالة وجوده.ننفذ بعد حفظ ملف التهجير الأمر التالي: php artisan migrateستظهر مخرجات في سطر الأوامر على النحو التالي: Migration table created successfully. Migrated: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_100000_create_password_resets_table Migrated: 2015_12_21_215845_create_drinks_tableإن نظرت في جداول قاعدة البيانات الآن فستجد التالي: ستلاحظ وجود أربعة جداول من بينها جدول drinks. الجداول الأخرى أنشأها Laravel لأن ملفات تهجيرها تأتي مبدئيا مع Laravel. التراجع عن التعديلاتيوفر التهجير إمكانية التراجع عن تعديلاته والعودة إلى حالة قاعدة البيانات قبل تنفيذه. أنشأنا في الفقرة السابقة جداول في قاعدة البيانات، ننفذ الأمر التالي للتراجع عن ذلك: php artisan migrate:rollbackتظهر الرسائل التالية: Rolled back: 2015_12_21_215845_create_drinks_table Rolled back: 2014_10_12_100000_create_password_resets_table Rolled back: 2014_10_12_000000_create_users_tableإن أعدت التحقق في MySQL سترى أن الجدول drinks لم يعد موجودا. نعيد إنشاء الجدول بتنفيذ التهجير مرة أخرى: php artisan migrateتمكن ملاحظة أن تنفيذ التهجير يكون بالتسلسل الزمني التصاعدي لتاريخ إنشاء ملفات التهجير (من الأقدم إلى الأحدث)، في ما يكون التراجع بتنفيذ ملفات التهجير حسب التسلسل الزمني التنازلي (من الأحدث إلى الأقدم). إدارة جداول البيانات في Laravel باستخدام التهجيرسنرى في هذه الفقرة كيفية استخدام التهجير للقيام بأشغال شائعة على جداول قواعد البيانات. إدراج بياناتسنرى الآن كيفية استخدام التهجير لإدراج بيانات في جدول أثناء إنشائه. ننشئ جدولا بالموظفين employees وندرج فيه 33 تسجيلة بالاعتماد على مكتبة Faker (سنخصص درسا لتفصيل استخدام Faker). نفذ الأمر التالي لإنشاء ملف تهجير لجدول employees: php artisan make:migration employeesنفتح الملف المنشأ للتو ونضيف الشفرة التالية: <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class Employees extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('employees', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->string('contact_number'); $table->timestamps(); }); $faker = Faker\Factory::create(); $limit = 33; for ($i = 0; $i < $limit; $i++) { DB::table('employees')->insert([ //, 'name' => $faker->name, 'email' => $faker->unique()->email, 'contact_number' => $faker->phoneNumber, ]); } } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('employees'); } } ننشئ كائنا من صنف Faker بالتعليمة $faker = Faker\Factory::create();نحدد عدد التسجيلات التي نود إدراجها: limit$. نستخدم حلقة for التكرارية لإضافة التسجيلات إلى الجدول. تولد التعليمة faker->name$ اسما وهميّا، faker->unique()->email$ اسم بريد وحيد وfaker->phoneNumber$ رقم هاتف وهميا.الأمر التالي ينفذ التهجير: php artisan migrateتظهر الرسالة التالية دلالة على تهجير الجدول employees: Migrated: 2015_12_21_225233_employees إن بحثت الآن عن محتوى الجدول employees، مثلا بتنفيذ الاستعلام التالي في سطر أوامر MySQL: SELECT * FROM employees;ستحصُل على أسماء الموظفين، عناوينهم البريدية وأرقام هواتفهم. نتراجع عن إنشاء الجدول employees بتنفيذ الأمر: php artisan migrate:rollbackتظهر رسالة دلالة على التراجع عن إنشاء الجدول. نفتح ملف التهجير للتعديل عليه ثم نضع الشفرة الخاصة بإدراج بيانات وهمية بين علامتي تعليق، هكذا: /* $faker = Faker\Factory::create(); $limit = 33; for ($i = 0; $i < $limit; $i++) { DB::table('employees')->insert([ //, 'name' => $faker->name, 'email' => $faker->unique()->email, 'contact_number' => $faker->phoneNumber, ]); } */احفظ ملف التهجير ثم نفذ الأمر: php artisan migrateسيُنشأ جدول employees من جديد ولكن هذه المرة دون إدراج تسجيلات في الجدول. إضافة عمود إلى جدول أو حذفه منهنفرض أننا نود إضافة عمود جديد gender لتخزين جنس الموظّف، مباشرة بعد العمود contact_number. ننفذ الأمر التالي لإنشاء ملف تهجير باسم add_gender_to_employees مع تحديد الجدول الذي نريد العمل عليه وهو employees: php artisan make:migration add_gender_to_employees --table=employeesتشير التعليمة table=employees-- إلى أننا نريد العمل على الجدول employees الموجود في قاعدة البيانات. افتح ملف التهجير المنشأ بعد الأمر السابق، وعدله ليصبح على النحو التالي: <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AddGenderToEmployees extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('employees', function (Blueprint $table) { $table->string('gender')->after('contact_number'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('employees', function (Blueprint $table) { $table->dropColumn('gender'); }); } } في الدالة up أضفنا حقلا جديدا من نوع string (سلسلة محارف) وحددنا مكانه بأنه بعد العمود contact_number.في الدالة down نحذف الحقل gender.الآن عند تنفيذ أمر التهجير php artisan migrate ستلاحظ إضافة عمود جديد باسم gender بعد عمود contact_number. تغيير نوع عموديحتاج تغيير نوع العمود لتثبيت حزمة Doctrine Database Abstract Layer, DBAL. تُستخدم هذه الحزمة لتهجيرات التعديل على الجداول Alter table. سنستخدم أداة إدارة الاعتماديات Composer لتثبيت الحزمة. افتح ملف composer.json الذي يوجد في مجلد التطبيق. ابحث عن مقطع require: "require": { "php": ">=5.5.9", "laravel/framework": "5.2.*" },توجد في هذا المقطع حزم المكتبات التي يحتاجها تطبيقنا. حتى الآن توجد حزمتان فقط هما php وlaravel. يشير الجزء الأول (قبل النقطتين) إلى اسم الحزمة، في ما يشير الثاني لإصدارها. نضيف حزمة dbal` إلى هذه الاعتماديات، وذلك على النجو التالي: "require": { "php": ">=5.5.9", "laravel/framework": "5.2.*", "doctrine/dbal": "v2.4.2" }, لاحظ الفاصلة اللاتينية التي أضفناها بعد حزمة Laravel. نفذ الأمر التالي لتحديث المشروع: composer updateعند إنشاء العمود gender لم نحدد طول الحقل، أي أنه سيأخذ الطول المبدئي للحقول من نوع string وهو255 محرفا. ننشئ ملف تهجير جديدا لتعديل طول الحقل ليصبح 5 كحد أقصى. نعدل الملف على النحو التالي: php artisan make:migration modify_gender_in_employees --table=employeesقبول فراغ الحقول في الجدوليفترض Laravel عند إنشاء الحقول أنها لا تقبل فراغ القيمة، أي أنه يجب ذكر قيمة للحقل عند إدراج تسجيلات في الجدول. يمكننا تغيير هذا الإعداد المبدئي وجعل قيمة حقل ما اختيارية. سنأخذ الحقل gender للتمثيل به. ننشئ ملفا للتهجير: php artisan make:migration make_gender_null_in_employees --table=employeesثم نعدله على النحو التالي: <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class MakeGenderNullInEmployees extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('employees', function (Blueprint $table) { $table->string('gender', 5)->nullable()->change(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('employees', function (Blueprint $table) { $table->string('gender', 5)->change(); }); } } تجعل الدالة nullable الحقل gender يقبل قيما فارغة.php artisan migrate إضافة مفتاح خارجي Foreign keyنصنف موظفينا حسب القسم الذي يعملون فيه. ننشئ جدولا للأقسام depts ثم نضيف مفتاحا خاريجا في جدول الموظفين employees. الأمر أدناه ينشئ ملف تهجير لجدول الأقسام: php artisan make:migration deptsعدل ملف التهجير: <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class Depts extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('depts', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('depts'); } }ثم ننفذ أمر التهجير لإنشاء الجدول: php artisan migrateيُشترط لتصح علاقة عبر مفتاح خارجي بين جدولين أن يكون المفتاح الخارجي والمفتاح الرئيس Primary key متطابقين في النوع. استخدمنا في تعريف المفتاح الرئيس idضمن الجدول depts دالة increments التي تعطي النوع عددا طبيعيا من عشرة أرقام ;(unsigned integer INT(10 وهو ما يعني أننا سنعطي نفس النوع للمفتاح الخارجي الذي سننشئه في الجدول employees. الفرق أن المفتاح الخارجي لا يزداد تلقائيا لذا سنستخدم الدالة unsignedInteger التي لها نفس مفعول increments من حيث نوع الحقل وطوله، مع فرق أنها لا تضيف الازدياد التلقائي. ملحوظة: حتى تمكن إضافة مفتاح خارجي في الجدول employees يجب أن يكون الجدول فارغا (بدون تسجيلات). لهذا السبب علقنا في فقرة ماضية الشفرة الخاصة بـFaker. نفذ الأمر التالي لإنشاء ملف تهجير لإضافة حقل المفتاح الخارجي dept_id إلى الجدول employees: php artisan make:migration add_dept_id_in_employees --table=employeesثم نعدل ملف التهجير: <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class AddDeptIdInEmployees extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('employees', function (Blueprint $table) { $table-> unsignedInteger ('dept_id')->after('gender'); $table->foreign('dept_id') ->references('id')->on('depts') ->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('employees', function (Blueprint $table) { $table->dropColumn('dept_id'); }); } }ثم ننفذ التهجير: php artisan migrate بذر قواعد البياناتيشير مصطلح البذر Seeding إلى عملية إضافة بيانات وهمية لأغراض الاختبار في قواعد البيانات. نطبق هذا الإجراء على جدول drinks الذي أنشأناه في أول الدرس. نفذ الأمر التالي لإنشاء ملف للبذر: php artisan make:seeder DrinksTableSeederينشئ الأمر ملفا باسم DrinksTableSeeder.php على المسار database/seeds. افتح الملف: <?php use Illuminate\Database\Seeder; class DrinksTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { // } } يمدد الصنف DrinksTableSeeder الصنف Seeder ويعرّف الدالة run التي تُنفّذ عند تشغيل أمر البذر في Artisan. عدل الملف ليصبح محتواه التالي: <?php use Illuminate\Database\Seeder; class DrinksTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::table('drinks')->insert([ 'name' => 'Orange Juice', 'comments' => 'Rich in C vitamin', 'rating' => 9, 'juice_date' => '2015-12-20', ]); } أضفنا في الدالة run أمر إدراج في جدول البيانات drinks ومررنا مصفوفة توافق عناصرها حقول الجدول مع تحديد قيم عناصر المصفوفة. ننفذ الأمر أمر البذر لإضافة التسجيلة أعلاه إلى الجدول: php artisan db:seed --class=DrinksTableSeederنمرر لأمر البذر php artisan db::seed اسم الملف المراد تنفيذه. الآن عند التحقق نجد في جدول قاعدة البيانات التسجيلة التالية: قاعدة البيانات الخاصة بمشروع Larashopتعرفنا في الفقرات الماضية على أساسيات التهجير في Laravel. سنجعل هذه المعرفة موضع التطبيق لإنشاء قاعدة بيانات لمشروع Larashop. ستشنرك جميع الجداول في الحقول التالية التي أنشأناها لأغراض الفحص والتدقيق. التسلسلالحقلنوع البياناتالوصف1created_at Timestamp ختم زمني لتاريخ إدراج التسجيلة2updated_at Timestamp ختم زمني لتاريخ تحديث التسجيلة3created_at_ip (Varchar(45 عنوان IP المستخدم لإدراج التسجيلة4updated_at_ip (Varchar(45 عنوان IP المستخدم لتحديث التسجيلةجدول منشورات المدونةالتسلسلالحقلنوع البياناتالوصف1id INT مفتاح رئيس (AUTOINCREMENT) عدد طبيعي يزداد تلقائيا2url (Varchar(255 رابط الصفحة3title (Varchar(140 عنوان الصفحة4description (Varchar(170 وصف يظهر في محركات البحث5content Text محتوى الصفحة أو المنشور 6conblogtent (Tinyint(1 يحدد ما إذا كان المنشور صفحةجدول التصنيفاتالتسلسلالحقلنوع البياناتالوصف1id INT مفتاح رئيس (AUTOINCREMENT) عدد طبيعي يزداد تلقائيا2name (Varchar(255 اسم التصنيفجدول العلامات التجاريةالتسلسلالحقلنوع البياناتالوصف1id INT مفتاح رئيس (AUTOINCREMENT) عدد طبيعي يزداد تلقائيا2name (Varchar(255 اسم العلامة التجاريةجدول المنتجاتلكل منتج تصنيف وعلامة تجارية وحيدين. التسلسلالحقلنوع البياناتالوصف1id INT مفتاح رئيس (AUTOINCREMENT) عدد طبيعي يزداد تلقائيا2name (Varchar(255 اسم المنتج3title (Varchar(140 عنوان المنتج4description (Varchar(500 عنوان المنتج5price int ثمن المنتج6category_id int معرف تصنيف المنتج7brand_id int معرف العلامة التجارية للمنتجملفات التهجير لجداول قاعدة بيانات المشروعسننشئ في هذه الفقرة ملفات تهجير لجداول البيانات المذكورة أعلاه؛ سنضيف أيضا بعض البيانات الوهمية إلى الجداول باستخدام آلية البذر التي تعرفنا عليها سابقا. توليد ملفات التهجيرافتح سطر الأوامر ونفذ الأوامر التالية لتوليد ملفات الجداول: جدول منشورات المدونة: php artisan make:migration create_posts_table جدول تصنيفات المنتجات php artisan make:migration create_categories_tableجدول العلامات التجارية للمنتجات php artisan make:migration create_brands_tableجدول المنتجات php artisan make:migration create_products_tableتحرير ملفات التهجيرننتقل لتحرير كل ملف من ملفات التهجير. جدول منشورات المدونة <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreatePostsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('posts', function (Blueprint $table) { $table->increments('id'); $table->string('url', 255)->unique(); $table->string('title', 140); $table->string('description', 170); $table->text('content'); $table->boolean('blog'); $table->timestamps(); $table->string('created_at_ip'); $table->string('updated_at_ip'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('posts'); } } جدول التصنيفات <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateCategoriesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('categories', function (Blueprint $table) { $table->increments('id'); $table->string('name', 255)->unique(); $table->timestamps(); $table->string('created_at_ip'); $table->string('updated_at_ip'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('categories'); } } جدول العلامات التجارية <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateBrandsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('brands', function (Blueprint $table) { $table->increments('id'); $table->string('name', 255)->unique(); $table->timestamps(); $table->string('created_at_ip'); $table->string('updated_at_ip'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('brands'); } } جدول المنتجات <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateProductsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('products', function (Blueprint $table) { $table->increments('id'); $table->string('name', 255)->unique(); $table->string('title', 140); $table->string('description', 500); $table->integer('price'); $table->unsignedInteger('category_id'); $table->unsignedInteger('brand_id'); $table->timestamps(); $table->string('created_at_ip'); $table->string('updated_at_ip'); // مفتاح خارجي على جدول التصنيفات $table->foreign('category_id') ->references('id')->on('categories') ->onDelete('cascade'); // مفتاح خارجي على جدول العلامات التجارية $table->foreign('brand_id') ->references('id')->on('brands') ->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('products'); } }بذر قاعدة بيانات المشروعندرج بيانات وهمية في جداول قاعدة البيانات قصدَ الاختبار. أنشئ ملفات البذر بتنفيذ الأوامر أدناه على التوالي: php artisan make:seeder CategoriesTableSeeder php artisan make:seeder BrandsTableSeeder php artisan make:seeder ProductsTableSeederيحوي جدول المنتجات مفتاحين خارجيين لجدولي التصنيف والعلامة التجارية. لذا يجب البدء بهما (لا يصح إدراج مفتاح خارجي لتسجيلة غير موجودة في الجدول الذي مثل المفتاح الخارجي مرجعا إليه). بذر جدول التصنيفات <?php use Illuminate\Database\Seeder; class CategoriesTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::table('categories')->insert(['name' => 'MENS']); DB::table('categories')->insert(['name' => 'WOMENS']); DB::table('categories')->insert(['name' => 'KIDS']); DB::table('categories')->insert(['name' => 'FASHION']); DB::table('categories')->insert(['name' => 'CLOTHING']); } } بذر جدول العلامات التجارية<?php use Illuminate\Database\Seeder; class BrandsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::table('brands')->insert(['name' => 'ACNE']); DB::table('brands')->insert(['name' => 'RONHILL']); DB::table('brands')->insert(['name' => 'ALBIRO']); DB::table('brands')->insert(['name' => 'ODDMOLLY']); } } بذر جدول المنتجات<?php use Illuminate\Database\Seeder; class ProductsTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::table('products')->insert(['name' => 'Mini skirt black edition', 'title' => 'Mini skirt black edition','description' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna','price' => 35,'category_id' => 1,'brand_id' => 1,]); DB::table('products')->insert(['name' => 'T-shirt blue edition', 'title' => 'T-shirt blue edition','description' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna','price' => 64,'category_id' => 2,'brand_id' => 3,]); DB::table('products')->insert(['name' => 'Sleeveless Colorblock Scuba', 'title' => 'Sleeveless Colorblock Scuba','description' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna','price' => 13,'category_id' => 3,'brand_id' => 2,]); } } ثم ننفذ أوامر البذر لكل جدول. خاتمةتعرفنا في هذا الدرس على تهجير قواعد البيانات وبذرها في Laravel. كما أننا حددنا هيكلة قاعدة بيانات المشروع الذي نعمل عليه. في الدروس القادمة سنتعرف على إطار عمل Eloquent الذي سنعتمد عليه للتخاطب مع قاعدة البيانات وعرض محتوياتها عند الاقتضاء. ترجمة -وبتصرّف- لمقال Laravel 5 Migrations لصاحبه Rodrick Kazembe.
  8. توفر أوبنتو خادومَيّ قواعد بيانات شهيرَين هما: قواعد بيانات MySQL.قواعد بيانات PostgreSQL.حيث تتوفران في المستودع الرئيسي (main)؛ ويشرح هذا الدرس كيفية تثبيت وضبط خادومَي قواعد البيانات آنفَيّ الذكر. MySQLإن MySQL هو خادوم قواعد بيانات سريع ومتعدد الخيوط (multi-threaded) ومتعدد المستخدمين ومرن جدًا؛ مُطوَّر للأنظمة الإنتاجية المحورية والتي تتحمل حِملًا ثقيلًا، ويمكن أيضًا تضمينه في البرمجيات سريعة النشر (mass-deployed). التثبيتنفذِّ الأمر الآتي في الطرفية لتثبيت MySQL: sudo apt-get install mysql-serverسيُطلب منك إدخال كلمة مرور للمستخدم الجذر لخادوم MySQL أثناء التثبيت. بعد أن ينتهي التثبيت، فيجب أن يبدأ خادوم MySQL تلقائيًا؛ تستطيع تنفيذ الأمر الآتي في الطرفية للتحقق إذا كان خادوم MySQL يعمل أم لا: sudo netstat -tap | grep mysqlيجب أن تشاهد شيئًا شبيهًا بما يلي بعد تنفيذ الأمر السابق: tcp 0 0 localhost:mysql *:* LISTEN 2556/mysqldإذا لم يكن يعمل الخادوم، فتستطيع تشغيله بالأمر: sudo service mysql restartالضبطتستطيع تعديل الملف ‎/etc/mysql/my.cnf لضبط الإعدادات الأساسية، مثل ملف السجل، ورقم المنفذ ...إلخ. فمثلًا لضبط MySQL ليستمع إلى الاتصالات من مضيفي الشبكة، عليك تعديل قيمة التعليمة bind-address إلى عنوان IP للخادوم: bind-address = 192.168.0.5ملاحظة: عدِّل 192.168.0.5 إلى العنوان الملائم. بعد إجراء التعديلات على ملف ‎/etc/mysql/my.cnf؛ فيجب إعادة تشغيل عفريت MySQL: sudo service mysql restartأدخل الأمر الآتي في الطرفية إذا رغبت بتغيير كلمة مرور المستخدم الجذر (root) في MySQL: sudo dpkg-reconfigure mysql-server-5.5سيُوقَف عمل عفريت MySQL، وستُسأل عن كلمة المرور الجديدة. محركات قاعدة البياناتعلى الرغم من أن الضبط الافتراضي لخادوم MySQL الموفر من حزم أوبنتو يعمل عملًا صحيحًا دون مشاكل، لكن هنالك بعض الأمور التي عليك أخذها بعين الاعتبار قبل الإكمال. صُمِّمَت قواعد بيانات MySQL للسماح بتخزين البيانات بطرقٍ مختلفة؛ يُشار لهذه الطرق إما بمحركات قواعد البيانات أو محركات التخزين (Storage engine)؛ هنالك محركان رئيسيان ستكون مهتمًا بهما: InnoDB و MyISAM؛ لا تتغير طريقة التعامل مع محركات التخزين المختلفة بالنسبة للمستخدم النهائي؛ حيث تتعامل MySQL مع الأمور بطريقة مختلفة وراء الستار، أي أنه بغض النظر عن محرك التخزين الذي تستخدمه، فإنك ستتعامل مع قواعد البيانات بنفس الطريقة تمامًا. لكل محرك إيجابياته وسلبياته؛ وبينما من الممكن دمج عدِّة محركات قواعد بيانات على مستوى الجدول، لكن ذلك خطيرٌ، فربما يقلل ذلك من الفعالية والأداء لأنك تُقسِّم الموارد بين محركين بدلًا من تخصيصها لمحرك واحد فقط. المحرك MyISAM هو الأقدم بين المحركين المذكورين؛ يمكن أن يكون أسرع من InnoDB في حالات معيّنة ويفضل الأعمال التي تتطلب القراءة فقط؛ تتمحور بعض تطبيقات الويب حول MyISAM (على الرغم أنها لن تُبطَئ إذا استخدمت InnoDB)؛ يدعم MyISAM أيضًا نوع البيانات FULLTEXT؛ الذي يسمح بالبحث بسرعة كبيرة في كمياتٍ كبيرةٍ من النص؛ لكن MyISAM قادر على قفل الجدول بأكمله فقط عند الكتابة، هذا يعني أن عمليةً واحدةً فقط تستطيع تحديث الجدول في لحظة زمنية معينّة؛ قد يكون هذا إعاقةً لتوسع تطبيق يعتمد على هذا الجدول؛ ولا يحتوي MyISAM على ميزة «journaling»، وهذا يعني أنه من الصعب استرجاع البيانات بعد حدوث انهيار.المحرك InnoDB هو محرك قواعد بيانات أكثر حداثةً، صُمِّم ليكون متوافقًا مع ACID الذي يضمن إجراء العمليات على قواعد البيانات بطريقة عملية؛ قفل الكتابة يحدث على مستوى السجل (row) ضمن الجدول؛ هذا يعني أنه من الممكن إجراء عدِّة تحديثات لسجلات جدولٍ ما في نفس الوقت؛ التخزين الموقت للبيانات يحدث في الذاكرة ضمن محرك قواعد البيانات، مما يسمح بالتخزين على أساس السجل وليس على أساس كتلة الملف (file block)؛ ولكي يتوافق مع ACID، فإن كل العمليات تحدث بطريقة «journaled» مستقلةً عن الجداول الرئيسية؛ وهذا يؤدي إلى استرجاع البيانات استرجاعًا عمليًا.إن InnoDB هو المحرك الافتراضي في MySQL 5.5 ومن المستحسن بشدة استخدامه بدلًا من MyISAM ما لم تكن تريد استخدام مزايا خاصة بذاك المحرك. الضبط المتقدم: إنشاء ملف ضبط my.cnfهنالك عدد من المعاملات التي يمكن تعديلها في ملف ضبط MySQL مما يسمح لك بتحسين أداء الخادوم مع مرور الوقت؛ ربما تجد الأداة «Percona's my.cnf generating tool» مفيدةً للإعداد الابتدائي؛ ستولد هذه الأداة ملف my.cnf ليكون أكثر ملائمةً لإمكانيات ومتطلبات خادومك. لا تستبدل ملف my.cnf المولد من Percona إذا وضعت بيانات في قاعدة بيانات، بعض التغييرات في الملف لن تسبب مشاكل لأنها تُعدِّل طريقة تخزين البيانات على القرص الصلب ولن تتمكن من تشغيل MySQL؛ إذا أردت استخدامه وكانت لديك بيانات موجودة مسبقًا، فعليك أن تجري mysqldump ثم تعيد التحميل: mysqldump --all-databases --all-routines -u root -p > ~/fulldump.sqlستُسأل عن كلمة مرور المستخدم الجذر لقواعد MySQL قبل إنشاء نسخة من البيانات؛ من المستحسن أن تتأكد أنه لا يوجد مستخدمين أو عمليات تستخدم قاعدة البيانات قبل إجراء هذه الخطوة؛ ربما تأخذ عملية النسخ بعض الوقت بناءً على مقدار البيانات الموجودة في قاعدة البيانات لديك؛ لن ترى شيئًا على الشاشة أثناء تنفيذ الأمر السابق. أغلق خادوم MySQL بعد إكمال عملية التفريغ (dump): sudo service mysql stopخذ الآن نسخةً احتياطيةً من my.cnf واستبدله بالملف الجديد: sudo cp /etc/my.cnf /etc/my.cnf.backup sudo cp /path/to/new/my.cnf /etc/my.cnfالآن احذف وأعد تهيئة مجال قواعد البيانات وتأكد أن الملكية صحيحة قبل إعادة تشغيلMySQL: sudo rm -rf /var/lib/mysql/* sudo mysql_install_db sudo chown -R mysql: /var/lib/mysql sudo service start mysqlكل ما تبقى الآن هو إعادة استيراد بياناتك؛ وللحصول على فكرة عن مدى إتمام عملية الاستيراد، فربما تجد الأداة pv‏ (Pipe Viewer) مفيدةً؛ الأمر الآتي يظهر كيفية تثبيت واستخدام pv لهذه الحالة، ربما لا تريد أن تستخدمها وكل ما عليك فعله هو استبدال pv بالأمر cat؛ تجاهل أية أوقات متوقعة للانتهاء (ETA) مولدة من pv؛ لأنها مبنية على الوقت المستغرق لكي يُعالَج كل سجل من الملف، لكن سرعة إدراج البيانات قد تختلف اختلافًا كبيرًا من سجل إلى سجل: sudo apt-get install pv pv ~/fulldump.sql | mysqlملاحظة: هذا ليس ضروريًا لكل تعديلات my.cnf؛ أغلبية المتغيرات التي قد ترغب في تعديلها لتحسين الأداء يمكن أن تُغيَّر حتى وإن كان يعمل الخادوم؛ تأكد من الحصول على نسخة احتياطية من ملفات الضبط والبيانات قبل إجراء التعديلات. MySQL Tunerالأداة «MySQL Tuner» هي أداة مفيدة تستطيع الاتصال إلى خدمة MySQL التي تعمل وتوفر اقتراحات عن كيفية ضبطها بأفضل ضبط لحالتك؛ وكما كان يعمل الخادوم لوقتٍ أطول، كلما كانت «النصيحة» التي سيوفرها mysqltuner أفضل؛ خذ بعين الاعتبار الانتظار لمدة 24 ساعة في بيئة إنتاجية قبل تشغيل هذه الأداة؛ تستطيع تثبيت mysqltuner من مستودعات أوبنتو: sudo apt-get install mysqltunerثم تشغيلها بعد تثبيتها بالأمر: mysqltunerوانتظر التقرير النهائي، سيوفر القسم العلوي معلوماتٍ عن خادوم قاعدة البيانات، ويوفر القسم السفلي اقتراحاتٍ لكي تعدلها في ملف my.cnf؛ يمكن تعديل أغلبية الاقتراحات على الخادوم مباشرةً دون إعادة تشغيله، انظر إلى توثيق MySQL الرسمي للمتغيرات المناسبة لتعديلها في البيئات الإنتاجية؛ ما يلي هو جزء من تقرير من قاعدة بيانات إنتاجية الذي يُظهِر أن هنالك بعض الفائدة من زيادة مقدار ذاكرة تخزين الطلبية: -------- Recommendations ----------------------------------------------------- General recommendations: Run OPTIMIZE TABLE to defragment tables for better performance Increase table_cache gradually to avoid file descriptor limits Variables to adjust: key_buffer_size (> 1.4G) query_cache_size (> 32M) table_cache (> 64) innodb_buffer_pool_size (>= 22G)تعليق أخير عن ضبط قواعد البيانات: بينما نستطيع أن نقول أن بعض الإعدادات هي الأفضل، لكن قد يختلف الأداء من تطبيق لآخر؛ على سبيل المثال، ما يعمل عملًا ممتازًا لووردبريس (Wordpress) قد لا يكون الأفضل لدروبال (Drupal) أو جوملا (Joomla) أو التطبيقات التجارية؛ الأداء متعلقٌ بأنواع الطلبيات واستخدام الفهارس، وإذا ما كان تصميم قاعدة البيانات جيدًا، وهكذا... ربما من الجيد إنفاق بعض الوقت في البحث عن إعدادات ملائمة لقواعد البيانات بناءً على التطبيقات التي تستخدمها؛ لكن بعد أن تتجاوز التعديلات حدًا معيّنًا، فإن أيّة تغييرات تجريها لا تتسبب إلا بتحسين بسيط جدًا في أداء التطبيق، ومن الأفضل لك تحسين التطبيق نفسه، أو التفكير في توسيع خادوم MySQL إما باستخدام عتاد أفضل أو بإضافة خواديم تابعة (slave). مصادرراجع الموقع الرئيسي لقواعد MySQL لمزيدٍ من المعلومات.التوثيق الكامل متوفر بصيغ online و offline من «MySQL Developers portal».لمعلومات عامة حول SQL، انظر إلى كتاب «Using SQL Special Edition».صفحة ويكي أوبنتو «Apache MySQL PHP» فيها بعض المعلومات المفيدة.PostgreSQLPostgreSQL هي قاعدة بيانات علائقية تعتمد على الكائنات وتملك ميزات أنظمة قواعد البيانات التجارية التقليدية مع تحسينات موجودة في الجيل الجديد من أنظمة DBMS. التثبيتأدخل الأمر الآتي في الطرفية لتثبيت PostgreSQL: sudo apt-get install postgresqlبعد انتهاء التثبيت، عليك ضبط خادوم PostgreSQL بناءً على متطلباتك، على الرغم من أن الضبط الافتراضي قابل للاستخدام. الضبطالاتصال عبر TCP/IP معطَّل افتراضيًا؛ تدعم PostgreSQL عدّة طرق للاستيثاق من العميل؛ طريقة الاستيثاق IDENT تُستعمَل للمستخدمين المحليين ولمستخدم postgres ما لم يُضبَط غير ذلك؛ رجاءً راجع «PostgreSQL Administrator's Guide» إذا أردت ضبط بدائل مثل Kerberos. سنفترض في ما يلي أنك ستُفعِّل اتصالات TCP/IP وتستخدم طريقة MD5 للاستيثاق من العميل؛ تُخزَّن ملفات ضبط PostgreSQL في المجلد ‎/etc/postgresql/<version>/main؛ على سبيل المثال، إذا ثبتت خادوم PostgreSQL 9.1، فإن ملفات الضبط ستُخزَّن في المجلد ‎/etc/postgresql/9.1/main. تنويه: لضبط الاستيثاق بطريقة ident، فأضف مدخلات إلى ‎/etc/postgresql/9.1/‎ main/pg_ident.conf؛ هنالك تعليقات تفصيلية في الملف لتساعدك. لتفعيل اتصالات TCP/IP، عليك تعديل الملف ‎/etc/postgresql/9.1/main/postgresql.conf ومن ثم تحديد السطر: ‎‎#listen_addresses = 'localhost‎‎'‎ ثم تغييره إلى: listen_addresses = '*'ملاحظة: للسماح باتصالات IPv4 و IPv6، استبدل «localhost» بالرمز «::». ربما تريد تعديل بقية المعاملات، إذا كنت تعرف ماذا تفعل! للتفاصيل، ارجع إلى ملف الضبط أو إلى توثيق PostgreSQL. الآن وبعد أن استطعنا الاتصال بخادوم PostgreSQL فإن الخطوة الآتية هي ضبط كلمة مرور للمستخدم postgres؛ نفذ الأمر الآتي في الطرفية للاتصال بقاعدة بيانات PostgreSQL الافتراضية: sudo -u postgres psql template1يتصل الأمر السابق بقاعدة بيانات PostgreSQL المسماة template1 كالمستخدم postgres؛ بعد أن تتصل إلى خادوم PostgreSQL وتحصل على مِحَث لإدخال تعليمات SQL، فيمكنك إدخال أمر SQL الآتي في مِحَث psql لضبط كلمة المرور للمستخدم postgres: ALTER USER postgres with encrypted password 'your_password';بعد ضبط كلمة المرور، عدِّل الملف ‎/etc/postgresql/9.1/main/pg_hba.conf لاستخدام استيثاق MD5 مع المستخدم postgres: local all postgres md5في النهاية، يجب أن تُعيد تشغيل خدمة PostgreSQL لتهيئة الضبط الجديد، وذلك بإدخال الأمر الآتي من الطرفية: sudo service postgresql restartتحذير: الضبط السابق ليس كاملًا بأي شكل من الأشكال، رجاءً راجع «PostgreSQL Administrator's Guide» لمعاملات ضبط إضافية. يمكنك اختبار اتصالات الخادوم من الأجهزة الأخرى باستخدام عملاء PostgreSQL: sudo apt-get install postgresql-client psql -h postgres.example.com -U postgres -Wملاحظة: استبدل اسم النطاق في المثال السابق باسم نطاقك الفعلي. مصادركما ذُكِر سابقًا، فإن «PostgreSQL Administrator's Guide» هو مصدر رائع، وهو متوفر أيضًا في حزمة postgresql-doc-9.1؛ نفذ ما يلي لتثبيت تلك الحزمة:sudo apt-get install postgresql-doc-9.1أدخِل الوصلة file:///usr/share/doc/postgresql-doc-9.1/html/index.html في شريط العنوان في متصفحك لمشاهدة الدليل.راجع أيضًا صفحة ويكي أوبنتو «PostgreSQL» لمزيدٍ من المعلومات.ترجمة -وبتصرف- للمقال Ubuntu Server Guide: Databases.
  9. من التحديات الشائعة التي نصادفها عند التعامل مع أنظمة قواعد البيانات النشطة active هو القيام بنسخ احتياطيّة ساخنة hot backups، وهو إنشاء نسخ احتياطيّة بدون إيقاف خدمة قاعدة البيانات أو جعلها قابلة للقراءة فقط، ينتج عادة عن نسخ ملفّات قاعدة البيانات النشطة ببساطة نسخة من قاعدة البيانات غير مستقرّة داخليًّا، أي تكون غير قابلة للاستخدام أو فاقدة لبعض المعاملات transactions التي حدثت خلال هذا النسخ، من ناحية أخرى فإنّ إيقاف قاعدة البيانات من أجل النسخ الاحتياطي المُجدوَل يجعل الأجزاء المعتمدة على قاعدة البيانات من تطبيقنا غير متوفّرة. Percona XtraBackup هو أداة مفتوحة المصدر تُستَخدم للالتفاف على هذه المشكلة وإنشاء نُسَخ احتياطيّة كاملة full backups أو تزايديّة incremental backups لقواعد بيانات قيد التشغيل من نوع MySQL، MariaDB، و Percona Server، وهي معروفة أيضًا باسم النُسَخ الاحتياطيّة الساخنة hot backups. وعلى النقيض من النُّسَخ الاحتياطيّة المنطقيّة logical backups التي تنتجها أدوات مثل mysqldump، تقوم أداة XtraBackup بإنشاء نُسَخ احتياطيّة ملموسة physical backups لملفّات قواعد البيانات، أي تقوم بعمل نسخة لملفّات البيانات، وتطبّق بعدها سجل المعاملات (transaction log (a.k.a. redo log على النُّسخ الاحتياطيّة الملموسة لتُعيد ملء أي مُعاملات نشطة لم تكتمل خلال إنشاء النُّسَخ الاحتياطيّة، مما يُؤدّي إلى نُسَخ احتياطيّة مستقرّة لقاعدة بيانات قيد التشغيل، يُمكِن بعدها النسخ الاحتياطي لنسخة قاعدة البيانات الاحتياطيّة إلى موقع بعيد remote location باستخدام rsync أو نظام نسخ احتياطي مثل Bacula. سنتعلّم في هذا الدّرس كيفيّة القيام بنسخة احتياطيّة كاملة وساخنة لقواعد بيانات MySQL أو MariaDB باستخدام Percona XtraBackup على Ubuntu، تمّت أيضًا تغطية عمليّة استعادة قاعدة البيانات من نسخة احتياطيّة. المتطلبات الأساسيةيجب أن تمتلك ما يلي من أجل متابعة هذا الدّرس: صلاحيّات مستخدم جذر Superuser على نظام Ubuntuقاعدة بيانات MySQL أو MariaDB قيد التشغيلالنفاذ إلى المستخدم المُدير (admin (root لقاعدة بياناتكومن أجل القيام بنسخة احتياطيّة ساخنة لقاعدة بياناتك يجب أيضًا أن تستخدم مُحرِّك التخزين InnoDB لأنّ XtraBackup تعتمد على سجل معاملات يُحافِظ عليه InnoDB، إن كانت قواعد بياناتك تستخدم مُحرِّك التخزين MyISAM فلا يزال بإمكانك استخدام XtraBackup ولكن سيتم قفل قاعدة البيانات لفترة قصيرة من الوقت في أواخر عمليّة النسخ الاحتياطي. 1- التحقق من محرك التخزينإن لم تكن متأكدًا من مُحرِّك التخزين الذي تستخدمه قاعدة بياناتك فتستطيع معرفته بعدّة طرق، إحدى الطرق هي استخدام وحدة التحكّم console في MySQL لاختيار قاعدة البيانات المطلوبة ومن ثمّ الحصول على حالة كل جدول فيها. ندخل في البداية إلى وحدة التحكّم console في MySQL: mysql -u root -pنقوم بإدخال كلمة سر المستخدم الجذري root في MySQL. وفي مُحث MySQL prompt نختار قاعدة البيانات التي نريد التحقّق منها، تأكد من أن تضع اسم قاعدة بياناتك هنا بدلًا من database_name: mysql> USE database_name;نطبع بعدها حالات جدولها: mysql> SHOW TABLE STATUS\G;ينبغي الإشارة للمحرك لكل صف من قاعدة البيانات: ... *************************** 11. row *************************** Name: wp_users Engine: InnoDB ...نغادر وحدة التحكّم console حالما ننتهي من هذا. mysql> exitفلنقم بتثبيت Percona XtraBackup. تثبيت Percona XtraBackupأسهل طريقة لتثبيت Percona XtraBackup هي استخدام apt-get. نضيف مفتاح مستودع Percona باستخدام هذا الأمر: sudo apt-key adv --keyserver keys.gnupg.net --recv-keys 1C4CBDCDCD2EFD2Aنضيف بعدها مستودع Percona إلى مصادر apt لدينا: sudo sh -c "echo 'deb http://repo.percona.com/apt trusty main' > /etc/apt/sources.list.d/percona.list" sudo sh -c "echo 'deb-src http://repo.percona.com/apt trusty main' >> /etc/apt/sources.list.d/percona.list"نقوم بتنفيذ الأمر التالي لتحديث مصادر apt: sudo apt-get updateنستطيع أخيرًا تنفيذ الأمر التالي لتثبيت XtraBackup: sudo apt-get install percona-xtrabackupتتألف XtraBackup بشكل أساسي من برنامج XtraBackup و script مكتوب بلغة Perl يُدعى innobackupex سنستخدمه لإنشاء نُسَخ احتياطيّة لقواعد بياناتنا. التحضيرات لأول مرةنحتاج قبل استخدام XtraBackup لأوّل مرّة إلى تحضير النظام ومستخدم MySQL الذي سيستخدمه XtraBackup، يُغطّي هذا القسم التحضيرات الأوليّة. 1- مستخدم النظامإذا لم نكن نخطّط لاستخدام المستخدم الجذري root للنظام فيجب علينا القيام ببعض التحضيرات الأساسيّة للتأكد من أنّه يُمكن تنفيذ XtraBackup بشكل صحيح، سنفترض أنّك قمت بتسجيل الدخول باستخدام المستخدم الذي سيشغّل XtraBackup وأنّه يمتلك صلاحيّات مستخدم جذر superuser. نضيف مستخدم النظام إلى المجموعة "mysql" (ضع اسم المستخدم لديك بدلًا من username): sudo gpasswd -a username mysqlنقوم بإنشاء الدليل الذي سنستخدمه لتخزين النُّسَخ الاحتياطيّة التي يقوم XtraBackup بعملها: sudo mkdir -p /data/backups sudo chown -R username: /dataيتأكّد الأمر chown من قدرة المستخدم على الكتابة إلى دليل النُّسَخ الاحتياطيّة. 2- مستخدم MySQLيحتاج XtraBackup إلى مستخدم MySQL لكي يستخدمه لإنشاء النُّسَخ الاحتياطيّة، فلنقم بإنشاء واحد الآن. ندخل إلى وحدة التحكّم console في MySQL بهذا الأمر: mysql -u root -pنقوم بإدخال كلمة سر المستخدم الجذري root في MySQL. نُنشِئ في مُحِث MySQL prompt مستخدم MySQL جديد ونعيّن كلمة سر له، يُدعى المستخدم في هذا المثال "bkpuser" وكلمة السّر هي "bkppassword"، قم بتغييرهما إلى شيء أكثر أمانًا: mysql> CREATE USER 'bkpuser'@'localhost' IDENTIFIED BY 'bkppassword'نقوم بعدها بمنح grant مستخدم MySQL الجديد صلاحيّات إعادة التحميل reload، القفل lock، والتكرار replication لكافّة قواعد البيانات: mysql> GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'bkpuser'@'localhost'; FLUSH PRIVILEGES; وهي الحد الأدنى من الصلاحيّات التي يحتاجها XtraBackup لإنشاء نُسَخ احتياطيّة كاملة لقواعد البيانات. عند الانتهاء نقوم بالخروج من لوحة تحكّم MySQL console: mysql> exitنحن الآن جاهزون لإنشاء نسخة احتياطيّة كاملة لقواعد بياناتنا. القيام بنسخة احتياطية ساخنة كاملةيُغطّي هذا القسم الخطوات الضرورية لإنشاء نسخة احتياطيّة ساخنة كاملة لقاعدة بيانات MySQL باستخدام XtraBackup، بعد التأكّد من أنّ أذونات permissions ملفّات قاعدة البيانات صحيحة سنستخدم XtraBackup لإنشاء create النسخة الاحتياطيّة وتحضيرها prepare. 1- تحديث أذونات Datadirيتم تخزين ملفّات قاعدة بيانات MySQL على Ubuntu في المسار var/lib/mysql/، وهو الذي يتم الإشارة إليه أحيانًا بـ datadir، يتم تقييد النفاذ إلى datadir افتراضيًّا فقط للمستخدم mysql، يتطلّب XtraBackup نفاذًا إلى هذا الدّليل لإنشاء نسخه الاحتياطيّة، لذا نقوم بتنفيذ بعض الأوامر للتأكّد من امتلاك مستخدم النظام الذي أعددناه سابقًا-كعضو في المجموعة mysql- للأذونات المناسبة: sudo chown -R mysql: /var/lib/mysql sudo find /var/lib/mysql -type d -exec chmod 770 "{}" \;تضمن هذه الأوامر أنّ كافّة الأدّلة في datadir قابلة للنفاذ من قبل المجموعة mysql، ويجب تنفيذها قبل كل نسخة احتياطيّة. 2- إنشاء نسخة احتياطيةنحن الآن على استعداد لإنشاء النسخة الاحتياطيّة، نستخدم الأداة innobackupex أثناء تشغيل قاعدة بيانات MySQL لفعل هذا، قم بتنفيذ هذا الأمر بعد وضع اسم مستخدمك وكلمة سرك لتتوافق مع معلومات تسجيل دخول مستخدم MySQL لديك: innobackupex --user=bkpuser --password=bkppassword --no-timestamp /data/backups/new_backupسيقوم هذا بإنشاء نسخة احتياطيّة لقاعدة البيانات في الموقع المُحدّد data/backups/new_backup/: innobackupex output innobackupex: Backup created in directory '/data/backups/new_backup' 150420 13:50:10 innobackupex: Connection to database server closed 150420 13:50:10 innobackupex: completed OK!وبشكل بديل تستطيع إزالة no-timestamp-- لكي يقوم XtraBackup بإنشاء دليل النسخة الاحتياطيّة بناءً على الختم الزمني timestamp الحالي كما يلي: innobackupex --user=bkpuser --password=bkppassword /data/backupsسيقوم هذا بإنشاء نسخة احتياطيّة لقاعدة البيانات في دليل فرعي مُولَّد تلقائيًّا كما يلي: innobackupex output — no timestamp innobackupex: Backup created in directory '/data/backups/2015-04-20_13-50-07' 150420 13:50:10 innobackupex: Connection to database server closed 150420 13:50:10 innobackupex: completed OK!ستعيد أي طريقة نستخدمها العبارة "innobackupex: completed OK" في السّطر الأخير من الخَرْج، ينتج عن النّسخة الاحتياطيّة الناجحة نسخة من قاعدة البيانات في المسار datadir والتي يجب تحضيرها قبل استخدامها. تحضير النسخة الاحتياطيةالخطوة الأخيرة في إنشاء نسخة احتياطيّة ساخنة باستخدام XtraBackup هي تحضيرها، ويتضمّن هذا إعادة "replaying" سجل المعاملات لتطبيق أي معاملة غير مرتبطة بالنسخة الاحتياطيّة، يجعل التحضير بيانات النسخة الاحتياطيّة مستقرة وقابلة للاستخدام من أجل الاستعادة. سنحضّر بحسب مثالنا النسخة الاحتياطيّة التي أنشأناها في المسار data/backups/new_backup/، ضع المسار الفعلي للنسخة الاحتياطيّة لديك: innobackupex --apply-log /data/backups/new_backupيجب أن نرى مرّة أخرى العبارة "innobackupex: completed OK" في السطر الأخير من الخَرْج. تمّ إنشاء النسخة الاحتياطيّة لقاعدة البيانات وهي جاهزة لاستعادتها إلى قاعدة بياناتنا، إن كنت تملك نظام نسخ احتياطي للملفّات مثل Bacula فيجب تضمين هذه النسخة الاحتياطيّة لقاعدة البيانات كجزء من اختيار النسخة الاحتياطيّة. يُغطّي القسم التالي كيفيّة استعادة قاعدة البيانات من النسخة الاحتياطيّة التي أنشأناها للتو. القيام باستعادة النسخة الاحتياطيةتتطلّب استعادة قاعدة البيانات باستخدام XtraBackup إيقاف قاعدة البيانات وأن يكون المسار datadir فارغًا. نوقف خدمة MySQL باستخدام هذا الأمر: sudo service mysql stopننقل أو نحذف محتويات datadir (وهو المسار var/lib/mysql/)، في حالتنا سننقله ببساطة إلى مكان مؤقّت: mkdir /tmp/mysql mv /var/lib/mysql/* /tmp/mysql/نستطيع الآن استعادة قاعدة البيانات من نسختنا الاحتياطيّة "new_backup": innobackupex --copy-back /data/backups/new_backupإن تم الأمر بنجاح فينبغي أن يكون السطر الأخير من الخَرْج هو العبارة "innobackupex: completed OK". تنتمي الملفّات التي تمّت استعادتها غالبًا إلى المستخدم الذي قام بتنفيذ عمليّة الاستعادة، نُعيد الملكيّة إلى mysql لكي تستطيع MySQL قراءة وكتابة الملفّات: sudo chown -R mysql: /var/lib/mysqlأصبحنا مستعدين لبدء MySQL: sudo service mysql startهذا هو كل شيء، يجب أن تكون قاعدة بيانات MySQL التي استعدناها قيد التشغيل الآن. الخاتمةبعد أن أصبحت الآن قادرًا على إنشاء نُسَخ احتياطيّة ساخنة لقاعدة بيانات MySQL باستخدام Percona XtraBackup فهنالك العديد من الأشياء التي يجب أن تنظر إلى إعدادها. يُنصَح أولًا بأتمتة العمليّة بحيث يتم إنشاء النُّسَخ الاحتياطيّة وفق جدول، يجب عليك ثانيًا القيام بنسخ بعيد remote للنسخ الاحتياطيّة في حال حدثت مشاكل في خادوم قاعدة البيانات باستخدام أدوات مثل rsync أو نظام نُسَخ احتياطيّة للملفّات على الشبكة مثل Bacula، سترغب بعدها في النظر في مداورة rotating النسخ الاحتياطيّة لديك (أي حذف النسخ الاحتياطيّة القديمة على أساس مُجدوَل) وإنشاء نُسَخ احتياطيّة تزايديّة (باستخدام XtraBackup) لتوفير مساحة القرص. ترجمة -وبتصرّف- لـ How To Create Hot Backups of MySQL Databases with Percona XtraBackup on Ubuntu 14.04 لصاحبه Mitchell Anicas.
×
×
  • أضف...