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

Ola Abbas

الأعضاء
  • المساهمات

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

  • تاريخ آخر زيارة

كل منشورات العضو Ola Abbas

  1. قد يكون اختيار قاعدة البيانات المناسبة للمشروع أمرًا صعبًا، خاصةً مع وجود العديد من الخيارات. تقدم قاعدتا بيانات MariaDB و MySQL ميزات كثيرة، ولكن يمكن أن تؤثر نقاط القوة والضعف لكل منهما على الأداء وقابلية التوسع والتوافق مع التطبيقات المختلفة مثل WordPress، لذا سنوضح في هذا المقال الاختلافات الأساسية بين قاعدتي البيانات MariaDB و MySQL، وسنتحدث عن ميزاتهما ومدى سرعتهما وأمانهما وكيفية استخدامهما. نظرة عامة على MariaDB و MySQL نشأت كل من MariaDB وMySQL من الجذور نفسها، إذ يعود أصلهما إلى نظام قاعدة بيانات إنجرس Ingres الذي طوّرته جامعة كاليفورنيا في بيركلي. طُوِّرت MySQL لأول مرة في عام 1995 واكتسبت شعبيةً كبيرةً لأنها كانت سهلة الاستخدام وسريعة، ولكن أصبح هناك خلاف بين مالكي MySQL وأوراكل Oracle لاحقًا، لذا جرى تطوير قاعدة بيانات جديدة بالاسم MariaDB. تم تطوير قاعدةَ بيانات MariaDB من طرف نفس مطوري MySQL في عام 2009، وكان الهدف هو الحفاظ على ميزات MySQL نفسها مع إضافة ميزات جديدة أيضًا. تنمو الآن MariaDB و MySQL كل على حدة، ولكل منهما ميزاتها وفوائدها الرئيسية. الميزات والفوائد الرئيسية تتشابه MariaDB و MySQL في الكثير من الأمور، لأنهما نشأتا من قاعدة الشيفرة البرمجية نفسها، ولكنهما تطورتا بطريقة مستقلة، مما أدى إلى بعض الاختلافات في ميزاتهما وفوائدهما. ميزات وفوائد MariaDB تشتهر MariaDB بكونها سريعة وقادرة على التعامل مع عدد كبير من العمليات، ولديها ميزات خاصة تسرّع من عمل الاستعلامات وتجعلها تعمل بطريقة أفضل مع حركة المرور الكبيرة. تحتوي MariaDB أيضًا على طرق جديدة لتخزين البيانات، والتي يمكن أن تساعدها على التصرّف بطريقة أفضل في مواقف معينة. تتضمن بعض ميزات MariaDB الميزة MCS لتحليل البيانات وميزة MaxScale للمحافظة على سير الأمور وموازنة العمل وميزة Galera Cluster لنسخ البيانات بدقة. ميزات وفوائد MySQL تحسّنت قاعدة بيانات MySQL بمرور الوقت، ولا تزال تحظى بشعبية كبيرة لأن العديد من الأشخاص يستخدمونها مع إتاحة المساعدة؛ وتحتوي على طرق مختلفة لتخزين البيانات مثل InnoDB و MyISAM ولكل منها إيجابياتها وسلبياتها. تحتوي MySQL على ميزات مثل البحث عن الكلمات الكاملة والمنبّهات Trigger والإجراءات المُخزَّنة، والتي يمكن أن تكون مفيدةً لإنشاء تطبيقات قواعد بيانات معقدة. مقارنة بين MariaDB و MySQL هناك العديد من الاختلافات الرئيسية بين MySQL و MariaDB بالرغم من وجود بنية ووظائف متشابهة كما هو موضح في الجدول التالي: الميزة قاعدة بيانات MariaDB قاعدة بيانات MySQL محركات التخزين محركات InnoDB و Aria و MyISAM و TokuDB و XtraDB و MariaDB Column Store محرّكات InnoDB و MyISAM و Aria و NDB و TokuDB تحسين السرعة تحسين تنفيذ الاستعلامات وفهرسة أسرع وتحسين أداء مع مجموعات البيانات الكبيرة تحسينات مستمرة في الأداء والتركيز على السرعة اتصالات أكبر وأسرع تدعم المزيد من الاتصالات في وقت واحد وتحسّن الأداء عند الحِمل الكبير تحسين التعامل مع الاتصالات وتحسينات قابلية التوسع تحسين النسخ المتماثل Replication تقنية Galera Cluster للتوافرية العالية والنسخ المتماثل المتزامن ونسخ المجموعات المتماثل النسخ المتماثل غير المتزامن والنسخ المتماثل شبه المتزامن ونسخ المجموعات المتماثل الميزات أو الإضافات الجديدة ميزات وإضافات JSON وأنواع البيانات المكانية Spatial ودوال النافذة و MariaDB Serverless و MariaDB MaxScale ميزات وإضافات JSON وأنواع البيانات المكانية ودوال النافذة و MySQL Shell نموذج قاعدة البيانات الثانوية MariaDB Column Store لأحمال العمل التحليلي MySQL Enterprise Backup للاستعادة في الحالات الكارثية وتخزين البيانات تقنيع البيانات Data Masking إمكانات تقنيع البيانات المُضمَّنة لأمن البيانات والالتزام بها ميزات تقنيع البيانات المتاحة باستخدام إضافات خارجية الأعمدة الديناميكية MariaDB Column Store للتخزين العمودي الديناميكي دعم محدود للأعمدة الديناميكية المراقبة Monitoring أدوات مراقبة متقدمة مدمجة بما في ذلك مخطط الأداء Performance Schema وإضافات المراقبة ميزات المراقبة الأساسية مع الأدوات الإضافية المتوفرة في Enterprise Edition التوجيه Routing إمكانات التوجيه المُضمَّنة مع MaxScale لتحسين معالجة الاستعلامات إمكانات التوجيه المحدودة باستخدام أدوات خارجية أولًا التحليلات Analytics ميزات تحليلية متقدمة مع دعم للاستعلامات وأنواع البيانات المعقدة دعم التحليلات الأساسية مع ميزات أكثر تقدمًا في MySQL Enterprise Edition نجوم GitHub أكثر من 11000 نجمة أكثر من 17000 نجمة عمليات نسخ GitHub أكثر من 2500 عملية نسخ أكثر من 3000 عملية نسخ المقارنة بين معايير قياس الأداء قد نجد صعوبةً في توفير معايير قياس أداء نهائية تنطبق على جميع السيناريوهات، ولكن أظهرت دراسات مختلفة وتجارب واقعية أن كلًا من MariaDB و MySQL يمكنهما تقديم أداء ممتاز في مجموعة متنوعة من حالات الاستخدام. يوضح الرسم البياني التالي عدد العمليات التي يمكن إجراؤها في الثانية OPS لأربعة إصدارات مختلفة من قواعد البيانات (MariaDB 10.0.21 و MariaDB 10.0.18 و MySQL 5.6.27 و MySQL 5.7.9) عند إجراء عمليات قراءة بسيطة مع أعداد مختلفة من المستخدمين من 1 إلى 256. يُظهِر الرسم البياني التالي أن الإصدار MySQL 5.7.9 أفضل من الإصدارات الأخرى دائمًا، مما يعني أن الإصدارات الأحدث أفضل. ترتفع الأرقام مع زيادة عدد المستخدمين، ولكنها تتوقف عن الارتفاع عند حوالي 128 مستخدمًا لمعظم الإصدارات، وهذا يعني أن هناك حدًا لكمية العمليات التي يمكن لقاعدة البيانات التعامل معها بسبب العتاد أو البرمجيات. تُعَد هذه المقارنة مهمةً لفهم الاختلافات في السرعة وكمية العمل التي يمكن لكل إصدار التعامل معها بين MariaDB وMySQL، مما يساعدنا في اتخاذ خيارات مناسبة لترقية قاعدة بياناتنا وإعدادها الصحيح، ولكن يمكن أن تختلف خصائص الأداء المحددة وفقًا لعوامل مثل العتاد وحِمل العمل والضبط. التعامل مع أحمال العمل ذات حركة المرور العالية يمكن ضبط كل من MariaDB و MySQL وتحسينهما للتعامل مع متطلبات التطبيقات ذات الطلب الكبير عند التعامل مع أحمال العمل ذات حركة المرور العالية، حيث تشمل العوامل الرئيسية التي يجب مراعاتها ما يلي: العامل وصفه العتاد Hardware تُعَد الموارد الكافية لوحدة المعالجة المركزية CPU والذاكرة والتخزين ضروريةً للتعامل مع أحمال العمل ذات الحركة العالية الفهرسة Indexing يمكن أن تحسّن الفهرسة المناسبة كثيرًا من أداء الاستعلامات من خلال تقليل كمية البيانات التي يجب مسحها تحسين الاستعلامات Query Optimization يمكن أن يساعد تحسين استعلامات SQL في تقليل استهلاك الموارد وتحسين أوقات الاستجابة التخبئة Caching يمكن أن يقلّل استخدام آليات التخبئة من عدد استعلامات قاعدة البيانات وتحسين الأداء العام النسخ المتماثل Replication والعنقدة Clustering يمكن أن يساعد تطبيق النسخ المتماثل والعنقدة في توزيع حِمل العمل عبر خوادم متعددة، مما يعزز قابلية التوسع والتوافرية المقارنة من حيث محركات التخزين يؤثر اختيار محرّك التخزين تأثيرًا كبيرًا على أداء قاعدة البيانات ووظائفها، حيث تدعم كل من MariaDB و MySQL محرّكات متعددة، ولكن MariaDB تقدم مجموعةً أوسع من الخيارات، بما في ذلك XtraDB و ColumnStore، مما يوسّع إمكاناتها إلى ما هو أبعد من InnoDB و MyISAM وغيرها من محرّكات MySQL. تدعم MariaDB أيضًا كلًا من Blackhole و CSV و Aria و InnoDB و Archive و Connect و Cassandra Storage Engine، والعديد من المحرّكات الأخرى؛ بينما تتضمن محركات التخزين التي تدعمها MySQL أيضًا MyISAM و Merge و Federated و Archive و Memory و CSV و Blackhole و Example. ملاحظة: لا يهم عدد محركات التخزين التي تدعمها قاعدة البيانات، فالمهم هو استخدام قاعدة البيانات التي تدعم المحرّك المناسب للمتطلبات. المقارنة من حيث تحسين الاستعلامات تتضمن كل من MariaDB و MySQL ميزات تحسين الاستعلام لتحسين الأداء وتقليل استهلاك الموارد، إذ يمكن أن تساعد هذه الميزات في ضمان تنفيذ الاستعلامات بكفاءة وتجنب الأعباء غير الضرورية. تتضمن بعض تقنيات تحسين الاستعلامات الشائعة ما يلي: تقنية التحسين وصفها الفهرسة يمكن أن يحسّن إنشاء الفهارس المناسبة كثيرًا من أداء الاستعلامات من خلال تقليل كمية البيانات التي يجب مسحها تخبئة الاستعلامات يمكن أن تقلّل تخبئة الاستعلامات التي يتكرر تنفيذها من الحاجة إلى عمليات بحث متكررة في قاعدة البيانات إعادة كتابة الاستعلام يمكن لقاعدة البيانات في بعض الأحيان إعادة كتابة الاستعلامات لتحسين أدائها أو تجنب الاختناقات المحتملة شرح الخطط يمكن استخدام شرح الخطط لتحليل خطط تنفيذ الاستعلام وتحديد مشكلات الأداء المحتملة المقارنة من حيث البحث عن نص كامل يمكن لكل من MariaDB و MySQL البحث عن كلمات كاملة في النص، ويُعَد ذلك مفيدًا لأشياء مثل محركات البحث وأنظمة إدارة المستندات والمتاجر الإلكترونية. قد تختلف طريقة البحث عن الكلمات الكاملة التي تستخدمها MariaDB و MySQL بعض الشيء في كيفية عملها ومدى سرعتها، لذا يُفضَّل التحقق من احتياجات تطبيقنا ومقارنة كيفية بحث كلتا قاعدتي البيانات عن الكلمات الكاملة. دعم JSON تدعم كل من MariaDB و MySQL تنسيق JSON وتنفذان العديد من الدوال نفسها، ولكن تخزّن MySQL تقارير JSON ككائنات ثنائية، بينما تخزنها MariaDB كسلاسل نصية. تظهر MySQL و MariaDB اختلافات في إمكاناتهما على التعامل مع JSON بالرغم من ارتباطهما الوثيق، حيث تفتخر MariaDB بمجموعة أوسع من دوال JSON بما في ذلك JSON_QUERY و JSON_EXISTS، والتي تفتقر MySQL إليها؛ ولكن MySQL تقدم الدالة JSON_TABLE لهيكلة بيانات JSON ضمن جدول، والتي هي ميزة غير موجودة في MariaDB. الدالة قاعدة بيانات MariaDB قاعدة بيانات MySQL الدالة JSON_ARRAY ✔ ✔ الدالة JSON_EXISTS ✔ ✘ الدالة JSONOBJECTAGG ✔ ✔ الدالة JSON_QUERY ✔ ✘ الدالة JSON_VALUE ✔ ✔ الدالة JSON_TABLE ✘ ✔ الدالة IS_JSON ‫JSON_VALID ‫JSON_VALID التوافق مع Oracle تتمتع MySQL ببعض ميزات أوراكل Oracle الأساسية المتوافقة، ولكن تُعَد MariaDB هي الوحيدة مفتوحة المصدر المتوافقة مع إجراءات وتسلسلات وأنواع البيانات المخزَّنة وغير ذلك لقاعدة بيانات أوراكل. قد تجد المؤسسات ذات الاستثمار الأكبر في نظام أوراكل البيئي أن قاعدة بيانات MySQL بديل جذاب بسبب وضع التوافق مع أوراكل، ولكن يُعَد دعم MySQL للغة PL/SQL الخاصة بأوراكل محدودًا. تُعَد قاعدة بيانات MariaDB خيارًا أفضل، إذ يمكنها العمل مع صيغة أوراكل ولديها دعم كامل للغة PL/SQL، ويكون ذلك مناسبًا للشركات التي تريد التحول من أوراكل مع الاحتفاظ بشيفرتها البرمجية القديمة واستخدام ميزات إجرائية متقدمة. ميزات الأمان تحمي كلٌّ من MySQL و MariaDB البيانات من خلال التحقق من هوية المستخدمين واستخدام التشفير، ولكنهما تطبّقان التشفير بطريقة مختلفة، إذ تتيح MySQL إمكانية اختيار كيفية تشفير سجلات الإعادة والتراجع، ولكنها لا تحمي مساحات الجدول المؤقتة أو السجلات الثنائية. وتحتوي MariaDB على مزيدٍ من الخيارات للتشفير بما في ذلك حماية السجلات الثنائية والجداول المؤقتة، مما يجعل البيانات أكثر أمانًا. تستخدم MySQL و MariaDB أيضًا طرقًا مختلفةً للتحقق من هوية المستخدمين، إذ تحتوي MySQL على validate_password للتأكد من قوة كلمات المرور، وتحتوي MariaDB على مزيد من الخيارات مع إضافات مختلفة للتحقق من صحة البيانات. يُعَد أمان كلمة المرور أفضل في MariaDB باستخدام إضافة الاستيثاق ed25519 في الإصدار 10.4، والتي تُعَد أكثر أمانًا من طريقة SHA-1 القديمة، مما يعني أن MariaDB ملتزمة بأن تكون آمنة جدًا. تجمع الخيوط Thread Pooling تستخدم قواعد البيانات تجمع الخيوط للتعامل مع العديد من الاتصالات في وقت واحد، إذ تستخدم هذه الطريقة الخيوط نفسها للاتصالات الجديدة، مما يوفر من الموارد. تُعَد قاعدة بيانات MariaDB جيدةً جدًا في استخدام تجمّع الخيوط، إذ يحتوي إصدار المجتمع الخاص بها على تجمّع خيوط قوي يمكنه التعامل مع أكثر من 200000 اتصال في وقت واحد؛ وتحتوي MySQL أيضًا على تجمّع خيوط، ولكنها ليست جيدةً بالمقارنة مع تجمع الخيوط الموجود في MariaDB، وهي متوفرة في إصدار المؤسسة المدفوع. التراخيص والقيود تستخدم كل من MariaDB و MySQL ترخيص جنو العمومي General Public License -أو GPL اختصارًا، لكنهما يتبعان خطط ترخيص مختلفة. تُعَد MariaDB مرخصةً بالكامل بموجب ترخيص GPL، مما يعني أنها ستكون مجانيةً ومفتوحة المصدر دائمًا، ويُعَد ذلك مهمًا للمستخدمين الذين يقدّرون أهمية مجانية البرمجيات. تُعَد MySQL مُرخصَّة بترخيص مزدوج، فهي تحتوي على إصدار ترخيص GPL عام وإصدار تجاري خاص؛ إذ يحتوي الإصدار التجاري على مزيد من الميزات والدعم مثل تجمّع الخيوط، كما قد يؤدي ذلك إلى جعلها أسرع، ولكنه يمنع المستخدمين من تغيير الشيفرة البرمجية. قد تختار الشركات استخدام MySQL بسبب دعمها التجاري، ولكنها قد تحدّ من الوصول إلى الشيفرة البرمجية الأساسية نظرًا لاستراتيجيتها المزدوجة. اختيار قاعدة البيانات الصحيحة إذا أردنا قاعدة بيانات أنشأها المجتمع وتتمتع بسرعة وأمان أفضل، فقد يكون استخدام قاعدة بيانات MariaDB مناسبًا. فقد أثبتت بعض الاختبارات أن MariaDB أسرع ويمكنها التعامل مع كمية عمليات أكبر من MySQL، كما أن MariaDB تتمتع ببعض ميزات أمان لا تتوفر في MySQL مثل تشفير البيانات أثناء تخزينها ونقلها، إلى جانب تمتع MariaDB بأمور لا تتوفر في MySQL، مثل الأعمدة الافتراضية ومحرّكات التخزين التسلسلية واستخدام محرّكات تخزين متعددة في جدول واحد. من ناحية أخرى، تم إنشاء قاعدة بيانات MariaDB من طرف المجتمع، لذا تُعَد أكثر انفتاحًا ووضوحًا من MySQL التي تملكها شركة أوراكل، حيث يمكن للمستخدمين المساعدة في جعل البرنامج وتطويره أكثر تركيزًا على احتياجات المجتمع. حالات استخدام MariaDB و MySQL تكون قاعدة MariaDB غالبًا أفضل للتطبيقات الكبيرة والسريعة والاستعلامات المعقدة، فهي تجعلها ميزاتها الخاصة خيارًا جيدًا للعمل الشاق، مثل ميزة Galera Cluster للحفاظ على تشغيل الأشياء وتحسينات محرّك تخزين InnoDB، بينما تكون MySQL غالبًا أفضل للتطبيقات الأصغر واحتياجات قواعد البيانات الأبسط. شركات تستخدم MariaDB شركات تستخدم MySQL سامسونج Samsung بي بي سي BBC شركة Financial Network, Inc.‎ شركة ألعاب Big Fish شركة Virgin Media O2 سبوتيفاي Spotify شركة Campus Cloud Services نتفليكس Netflix شركة Auto Europe ناسا NASA نوكيا Nokia استخدام MariaDB مع WordPress يمكن أن يؤدي استخدام WordPress مع قاعدة بيانات MariaDB إلى تحميل أسرع لموقع الويب، وهذا سيجذب زوّار الموقع أكثر؛ إذ تتميز MariaDB بقدرتها على تحسين الاستعلامات، ويمكنها التعامل مع عدد أكبر من الاتصالات في وقت واحد. من ناحية أخرى، يمكن لقاعدة بيانات MariaDB التعامل مع عدد أكبر من الاتصالات والمعاملات مقارنةً بقاعدة بيانات MySQL. كذلك، تتمتع MariaDB بميزات أمان أفضل مثل تشفير البيانات أثناء تخزينها، وطرق محسّنة للتحقق من هوية المستخدمين، وطرق أفضل لتعقّب ما يحدث. استخدام MariaDB مع موقع Cloudways يوفر موقع Cloudways أحدث إصدارات MariaDB على جميع خوادمه التي أطلقها حديثًا، ويمكن تحديد إصدار MariaDB بناءً على متطلبات المشروع. ملاحظة: لا يمكن تخفيض إصدار MariaDB مرةً أخرى بعد الترقية إلى الإصدار الأعلى. الخلاصة بهذا نكون قد وضحنا ميزات MariaDB و MySQL الرئيسية لإظهار الاختلاف بينهما لتسهيل المفاضلة بين قاعدة بيانات MariaDB أو MySQL. ترجمة -وبتصرّف- للقسم MariaDB vs MySQL: Understanding Key Differences and Choosing the Right Database لصاحبته Hafsa Tahir. اقرأ أيضًا كيفية استيراد وتصدير قواعد بيانات MySQL أو MariaDB كيفية تأمين قواعد البيانات MySQL ,MariaDB على خواديم لينكس كيفية تغيير مجلد تخزين بيانات MariaDB إلى مكانٍ آخر تعلم أساسيات MySQL كيفية ربط قاعدة بيانات MariaDB بتطبيق لارافيل Laravel
  2. سنوضّح في هذا المقال من سلسلة دليل جودو كيفية برمجة صاروخ موجه ، والذي هو مقذوف يبحث عن هدف متحرك، ، حيث سنستخدم عقدة Area2D لتنفيذ حركة الصاروخ، مع إضافة التأثيرات البصرية مثل الدخان والانفجارات' كما سنوضح كيفية استخدام التسارع والتوجيه الذكي Steering لتحريك الصاروخ نحو الهدف مع التحكم في قوة التوجيه لتحقيق حركة أكثر واقعية؛ كما سنوضح أيضًا كيفية استدعاء الدوال المناسبة للتحكم في وقت حياة الصاروخ، وكيفية تفاعله مع البيئة المحيطة. ستساعدنا هذه المفاهيم الأساسية على تطوير أنظمة موجهة أخرى في الألعاب التي تتطلب تفاعلًا ديناميكيًا مع الأهداف المتحركة. برمجة صاروخ موجه للكشف عن الهدف المتحرك لبرمجة صاروخ موجه، والذي هو مقذوف يبحث عن هدف متحرك، سنستخدم عقدة Area2D للمقذوف؛ إذ تُعَد عقد المناطق Areas خيارات جيدة للرصاصات أو الأجسام المتحركة التي تُطلق من الصاروخ لأننا بحاجة إلى كشفها عند ملامستها لشيء ما، ولكن إذا كنا بحاجة أيضًا إلى رصاصة ترجع أو ترتد، فقد تكون العقد من نوع PhysicsBody خيارًا أفضل. يتشابه إعداد العقدة وسلوك الصاروخ مع الإعداد الذي نستخدمه مع الرصاصات العادية، لذا في حال كنا قد أنشأنا مسبقًا عدة أنواع من الرصاص، فيمكننا استخدام الوراثة لإعداد جميع مقذوفاتنا استنادًا على الإعداد الأساسي نفسه. فيما يلي العقد التي سنستخدمها: Area2D: Missile Sprite2D CollisionShape2D Timer: Lifetime يمكن استخدام أي صورة نريدها بالنسبة للخامة Texture كما في المثال التالي: يمكن الآن إعداد العقد وضبط خامة الشخصية الرسومية Sprite وشكل التصادم، مع التأكّد من تدوير عقدة Sprite2D بمقدار 90 درجة، بحيث تشير إلى اليمين، مع التأكد من أنها تتطابق مع الاتجاه الأمامي للعقدة الأب. سنضيف سكربتًا ونتصل بالإشارة body_entered الخاصة بالعقدة Area2D والإشارة timeout الخاصة بالعقدة Timer كما يلي: extends Area2D export var speed = 350 var velocity = Vector2.ZERO var acceleration = Vector2.ZERO func start(_transform): global_transform = _transform velocity = transform.x * speed func _physics_process(delta): velocity += acceleration * delta velocity = velocity.clamped(speed) rotation = velocity.angle() position += velocity * delta func _on_Missile_body_entered(body): queue_free() func _on_Lifetime_timeout(): queue_free() سيؤدي هذا إلى إنشاء صاروخ يتحرك في خط مستقيم عند إطلاقه، ويمكن استخدام هذا المقذوف من خلال إنشاء نسخة منه واستدعاء التابع start()‎ الخاص به مع التحويل Transform2D المطلوب لضبط موضعه واتجاهه. سنستخدم التسارع acceleration لتغيير السلوك للبحث عن الهدف، ولكن لا نريد أن يدور الصاروخ بسرعة كبيرة، لذا سنضيف متغيرًا للتحكم في قوة التوجيه Steering، مما يعطي الصاروخ نصفَ قطر دوران يمكن تعديله مع سلوك مختلف. سنحتاج أيضًا إلى متغير الهدف target حتى يعرف الصاروخ ما الذي يطارده، وسنضعه في التابع start()‎ كما يلي: export var steer_force = 50.0 var target = null func start(_transform, _target): target = _target … يمكن تغيير اتجاه الصاروخ للتحرك نحو الهدف باستخدام التسارع في ذلك الاتجاه، فالتسارع هو تغير في السرعة؛ حيث يريد الصاروخ الحالي التحرك نحو الهدف مباشرةً، ولكن تشير سرعته الحالية إلى اتجاه مختلف، ويمكننا إيجاد هذا الفرق باستخدام الرياضيات الشعاعية كما يلي: يمثل السهم الأخضر التغير المطلوب في السرعة، أي التسارع acceleration، ولكن إذا انعطفنا مباشرةً، فسيبدو الأمر غير طبيعي، لذا يجب أن يكون طول متجه التوجيه محدودًا، وهذا سبب استخدام المتغير steer_force. تحسب الدالة التالية هذا التسارع، ويمكننا ملاحظة أنه لن يكون هناك توجيه عند عدم وجود هدف، لذا سيواصل الصاروخ التحرك في خط مستقيم. func seek(): var steer = Vector2.ZERO if target: var desired = (target.position - position).normalized() * speed steer = (desired - velocity).normalized() * steer_force return steer أخيرًا، يجب تطبيق قوة التوجيه الناتجة في الدالة ‎_physics_process()‎ كما يلي: func _physics_process(delta): acceleration += seek() velocity += acceleration * delta velocity = velocity.clamped(speed) rotation = velocity.angle() position += velocity * delta فيما يلي مثال عن النتائج مع بعض التأثيرات البصرية الإضافية مثل دخان الجسيمات والانفجارات: 03_homing_missiles.webm الكود الكامل للصاروخ الموجه مع التأثيرات البصرية فيما يلي السكربت الكامل الذي يضيف سلوك الصاروخ الموجه باستخدام Area2D للكشف عن الهدف، ويشمل أيضًا التأثيرات البصرية، مثل الانفجارات ودخان الجسيمات عند الاصطدام أو انتهاء الوقت. extends Area2D export var speed = 350 export var steer_force = 50.0 var velocity = Vector2.ZERO var acceleration = Vector2.ZERO var target = null func start(_transform, _target): global_transform = _transform rotation += rand_range(-0.09, 0.09) velocity = transform.x * speed target = _target func seek(): var steer = Vector2.ZERO if target: var desired = (target.position - position).normalized() * speed steer = (desired - velocity).normalized() * steer_force return steer func _physics_process(delta): acceleration += seek() velocity += acceleration * delta velocity = velocity.clamped(speed) rotation = velocity.angle() position += velocity * delta func _on_Missile_body_entered(body): explode() func _on_Lifetime_timeout(): explode() func explode(): $Particles2D.emitting = false set_physics_process(false) $AnimationPlayer.play("explode") await $AnimationPlayer.animation_finished queue_free() الخاتمة بهذا نكون قد وصلنا لنهاية مقالنا الذي تعلمنا كيفية برمجة صاروخ موجه في محرك الألعاب جودو Godot مع تطبيق التسارع والتوجيه الذكي لجعل الصاروخ يتبع هدفًا متحركًا' كما استعرضنا كيفية إضافة تأثيرات بصرية مثل الانفجارات والدخان عند الاصطدام أو انتهاء الوقت. يمكن تجربة تطبيق هذه الأساليب لتطوير أي ألعاب فيها أعداء يتبعون اللاعب، إذ يتغير سلوك الأعداء بناءً على موقع اللاعب وحركته. ترجمة -وبتصرّف- للقسم Homing missile من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: برمجة عدو وحيوان أليف في لعبة Godot إعداد كاميرا ديناميكية وإضافة تصادمات مع خط 2D في Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot فهم RayCast2D واستخداماتها في محرك ألعاب جودو
  3. سنوضّح في هذا المقال من سلسلة دليل جودو كيفية برمجة عدو لمطاردة اللاعب، وكيفية برمجة كائن في اللعبة مثل حيوان أليف ليتبع شخصية اللاعب. كيفية برمجة عدو لمطاردة اللاعب تتمثّل الخطوة الأولى لجعل العدو يطارد اللاعب في تحديد الاتجاه الذي يجب أن يتحرك به العدو، حيث يمكن الحصول على متجه يؤشّر من A إلى B من خلال عملية الطرح B - A، ثم نوحّد Normalize النتيجة ونحصل على متجه الاتجاه. سنحتاج هنا إلى ضبط سرعة العدو في كل إطار للتأشير إلى اتجاه اللاعب كما يلي: velocity = (player.position - position).normalized() * speed يحتوي كائن Vector2 الخاص بمحرّك الألعاب جودو Godot على دالة مساعدة مُضمَّنة لهذا الغرض وهي: velocity = position.direction_to(player.position) * speed سيؤدي ذلك إلى السماح للعدو بمطاردة اللاعب من أيّ مسافة حتى وإن كانت بعيدة، وهذه مشكلة إلى حد ما ستحتاج إلى حلها من خلال إضافة العقدة Area2D إلى العدو ومطاردة اللاعب عندما يكون ضمن نطاق الكشف Detect Radius فقط. سنربط الآن إشارات body_entered و body_exited من العقدة Area2D حتى يعرف العدو ما إذا كان اللاعب ضمن المجال أم لا: extends CharacterBody2D var run_speed = 25 var player = null func _physics_process(delta): velocity = Vector2.ZERO if player: velocity = position.direction_to(player.position) * run_speed move_and_slide() func _on_DetectRadius_body_entered(body): player = body func _on_DetectRadius_body_exited(body): player = null ملاحظة: تفترض الشيفرة البرمجية السابقة أن اللاعب هو الجسم الوحيد الذي يدخل أو يخرج، والذي يحدث عادةً من خلال ضبط طبقات أو أقنعة التصادم المناسبة. 02_chase_02.webm يمكن توسيع هذا المفهوم ليشمل أنواعًا أخرى من الألعاب، فالفكرة الأساسية هي العثور على متجه الاتجاه من العدو إلى اللاعب، حيث إذا كانت لعبتنا مثلًا هي لعبة ذات عرض جانبي أو لها قيود أخرى في الحركة، فيمكننا استخدام المكوِّن x فقط من المتجه الناتج لتحديد الحركة. القيود يمكن ملاحظة أن هذه الطريقة تعطي حركةً خطيةً مستقيمةً بسيطة، إذ لن يتحرك العدو حول العوائق مثل الجدران، كما لن يتوقف إذا اقترب من اللاعب كثيرًا؛ ويعتمد ما يجب فعله هنا عندما يقترب العدو من اللاعب على طبيعة لعبتنا، إذ يمكن إضافة منطقة ثانية أصغر تتسبب في توقف العدو ومهاجمته، أو يمكن جعل اللاعب يتراجع عند التلامس. توجد مشكلة أخرى مع الأعداء سريعي الحركة، إذ سيغير الأعداء الذين يستخدمون هذه التقنية اتجاههم مباشرةً مع تحرّك اللاعب، ويمكن الحصول على حركة طبيعية أكثر من خلال استخدام سلوك التوجيه Steering. برمجة كائن في اللعبة مثل حيوان أليف ليتبع شخصية اللاعب لنفترض أن لدينا كيان في لعبتنا مثل حيوان أليف أو تابع لمتابعة شخصية اللاعب كما يلي: 03_pet_follow.webm سنبدأ أولًا بإضافة عقدة Marker2D إلى الشخصية، والتي تمثل المكان الذي يريد الحيوان الأليف المشي فيه بالقرب من الشخصية. جعلنا عقدة Marker2D في مثالنا ابنًا للعقدة Sprite2D، لأن الشيفرة البرمجية للشخصية تستخدم الخاصية ‎$Sprite2D.scale.x = -1 لقلب الاتجاه الأفقي عندما تتحرك الشخصية إلى اليسار، وبالتالي ستنقلب عقدة Marker2D أيضًا لأنها ابن للعقدة Sprite2D. سكربت الحيوان الأليف فيما يلي سكربت الحيوان الأليف: extends CharacterBody2D @export var parent : CharacterBody2D var speed = 25 @onready var follow_point = parent.get_node("Sprite2D/FollowPoint") يحتوي المتغير parent على مرجع للشخصية التي يجب أن يتبعها الحيوان الأليف، ونحصل بعد ذلك على عقدة FollowPoint منه لنتمكّن من الحصول على موضعه في الدالة ‎_physics_process()‎: func _physics_process(delta): var target = follow_point.global_position velocity = Vector2.ZERO if position.distance_to(target) > 5: velocity = position.direction_to(target) * speed if velocity.x != 0: $Sprite2D.scale.x = sign(velocity.x) if velocity.length() > 0: $AnimationPlayer.play("run") else: $AnimationPlayer.play("idle") move_and_slide() إذا كان الموضع قريبًا من نقطة الهدف، فسنوقف حركة الحيوان الأليف. التنقل عبر المعيقات يجب الانتباه إلى أننا قد نجد الحيوان الأليف عالقًا بين المعيقات الموجودة بعالمنا الخاص باللعبة، لذا يمكن استخدام التنقل لتكون متابعة الحيوان الأليف للشخصية أقوى. ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. ختامًا بهذا نكون قد تعرفنا على كيفية برمجة شخصية عدو قادرة على مطاردة اللاعب، إلى جانب برمجة حيوان أليف قادر تتبع اللاعب داخل اللعبة، عبر محرك جودو Godot. ترجمة -وبتصرّف- للقسمين Chasing the player و Pet Following من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إعداد كاميرا ديناميكية وإضافة تصادمات مع خط 2D في Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot التفاعل بين الشخصيات والأجسام الصلبة في جودو فهم RayCast2D واستخداماتها في محرك ألعاب جودو
  4. سنوضّح في هذا المقال وهو جزء من سلسلة دليل جودو كيفية إعداد كاميرا ديناميكية تتحرّك وتكبّر وتصغّر المشهد لإبقاء عدة عناصر على الشاشة في الوقت نفسه، وسنتعرّف على كيفية إضافة تصادمات مع خط مرسوم ثنائي الأبعاد. طريقة إعداد كاميرا ديناميكية تتبع عدة أهداف في وقت واحد سنوضّح فيما يلي كيفية إعداد كاميرا ديناميكية لإبقاء عدة عناصر على الشاشة في الوقت نفسه. لنفترض أن لدينا لعبة تحتوي على لاعبَين ويجب إبقاء اللاعبين على الشاشة أثناء تحرّكهما، سواءً عند التباعد عن بعضهما البعض أو عند وجودهما قرب بعضهما البعض كما في المثال التالي: قد نكون معتادين على إرفاق الكاميرا باللاعب في لعبة تحتوي على لاعب واحد، بحيث تتبع الكاميرا هذا اللاعب تلقائيًا، ولكننا لا نجد ذلك مطبقًا عند وجود لاعبَين أو أكثر، أو عناصر أخرى في اللعبة نريد إبقاءها على الشاشة طوال الوقت. لحل المشكلة، يمكننا تطبيق ما يلي على الكاميرا: إضافة أو إزالة أيّ عددٍ من الأهداف إبقاء موضع الكاميرا متمركزًا عند نقطة المنتصف للأهداف ضبط تكبير أو تصغير الكاميرا لإبقاء جميع الأهداف على الشاشة سننشئ مشهدًا جديدًا باستخدام عقدة Camera2D ونرفق بها سكربتًا، وسنضيف هذه الكاميرا إلى لعبتنا بعد الانتهاء. سيبدأ السكربت بالتعليمات التالية: extends Camera2D @export var move_speed = 30 # ‫سرعة الاستيفاء الخطي lerp لموضع الكاميرا @export var zoom_speed = 3.0 # ‫سرعة الاستيفاء الخطي lerp لتكبير وتصغير Zoom الكاميرا @export var min_zoom = 5.0 # لن تقترب الكاميرا أكثر من هذه القيمة @export var max_zoom = 0.5 # لن تبتعد الكاميرا أكثر من هذه القيمة @export var margin = Vector2(400, 200) # تضمين بعض المساحة العازلة حول الأهداف var targets = [] # مصفوفة الأهداف التي يجب تعقّبها @onready var screen_size = get_viewport_rect().size تمكّن هذه الإعدادات من ضبط سلوك الكاميرا، إذ سنستخدم الدالة lerp()‎ التي تمثّل الاستيفاء الخطي لجميع تغييرات الكاميرا، ولهذا سيؤدي ضبط سرعات الحركة/التكبير والتصغير على قيم أقل إلى بعض التأخير في تتبّع الكاميرا للتغييرات المفاجئة. تعتمد قيم التكبير أو التصغير العليا والدنيا أيضًا على حجم الكائنات في لعبتنا ومدى القرب أو البعد الذي نريده، لذا سنضبط هذه القيم لتناسب احتياجاتنا. تضيف الخاصية margin مساحةً إضافيةً حول الأهداف، بحيث لا تكون على حافة المنطقة القابلة للعرض بالضبط. ستكون لدينا مصفوفة أهداف، ونحصل على حجم نافذة العرض لنتمكّن من حساب المقياس الصحيح. func add_target(t): if not t in targets: targets.append(t) func remove_target(t): if t in targets: targets.erase(t) توجد دالتان مساعدتان لإضافة وإزالة الأهداف، ويمكنك استخدامهما أثناء اللعب لتغيير الأهداف التي يجب تعقّبها مثل دخول اللاعب 3 إل اللعبة. وكما نلاحظ، لا نريد تعقّب الهدف نفسه مرتين، لذا سنرفضه إذا كان موجودًا مسبقًا. تحدث معظم الوظائف في الدالة ‎_process()‎، ولكن لنبدأ أولًا بتحريك الكاميرا كما يلي: func _process(delta): if !targets: return # إبقاء الكاميرا متمركزة بين الأهداف var p = Vector2.ZERO for target in targets: p += target.position p /= targets.size() position = lerp(position, p, move_speed * delta) سنكرّر ضمن الحلقة التأكيد على مواضع الأهداف ونعثر على المركز المشترك، ونتأكّد باستخدام الدالة lerp()‎ من التحرّك بسلاسة. بعد ذلك، لا بد لنا من التعامل مع التكبير أو التصغير كما يلي: # العثور على التكبير أو التصغير الذي سيحتوي على جميع الأهداف var r = Rect2(position, Vector2.ONE) for target in targets: r = r.expand(target.position) r = r.grow_individual(margin.x, margin.y, margin.x, margin.y) var z if r.size.x > r.size.y * screen_size.aspect(): z = 1 / clamp(r.size.x / screen_size.x, min_zoom, max_zoom) else: z = 1 / clamp(r.size.y / screen_size.y, min_zoom, max_zoom) zoom = lerp(zoom, Vector2.ONE * z, zoom_speed) تأتي الوظيفة الأساسية من الصنف Rect2، إذ نريد العثور على مستطيل يحيط بكل الأهداف، والذي يمكننا الحصول عليه باستخدام التابع expand()‎، ثم نوسّع المستطيل باستخدام الخاصية margin. يجب علينا هنا الضغط على زر Tab بالمستطيل المرسوم لتفعيل هذا الرسم في المشروع التجريبي: نجد المقياس ونثبّته في النطاق الأقصى/الأدنى الذي حدّدناه اعتمادًا على ما إذا كان المستطيل أوسع أو أطول، إذ يتعلق ذلك بالنسبة إلى أبعاد الشاشة. السكربت الكامل سيكون السكربت الكامل للعملية على النحو الآتي: extends Camera2D @export var move_speed = 30 # ‫سرعة الاستيفاء الخطي lerp لموضع الكاميرا @export var zoom_speed = 3.0 # ‫سرعة الاستيفاء الخطي lerp لتكبير وتصغير Zoom الكاميرا @export var min_zoom = 5.0 # لن تقترب الكاميرا أكثر من هذه القيمة @export var max_zoom = 0.5 # لن تبتعد الكاميرا أكثر من هذه القيمة @export var margin = Vector2(400, 200) # تضمين بعض المساحة العازلة حول الأهداف var targets = [] @onready var screen_size = get_viewport_rect().size func _process(delta): if !targets: return # إبقاء الكاميرا متمركزة بين الأهداف var p = Vector2.ZERO for target in targets: p += target.position p /= targets.size() position = lerp(position, p, move_speed * delta) # العثور على التكبير أو التصغير الذي سيحتوي على جميع الأهداف var r = Rect2(position, Vector2.ONE) for target in targets: r = r.expand(target.position) r = r.grow_individual(margin.x, margin.y, margin.x, margin.y) var z if r.size.x > r.size.y * screen_size.aspect(): z = 1 / clamp(r.size.x / screen_size.x, max_zoom, min_zoom) else: z = 1 / clamp(r.size.y / screen_size.y, max_zoom, min_zoom) zoom = lerp(zoom, Vector2.ONE * z, zoom_speed * delta) # لتنقيح الأخطاء get_parent().draw_cam_rect(r) func add_target(t): if not t in targets: targets.append(t) func remove_target(t): if t in targets: targets.remove(t) ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. إضافة تصادمات مع خط مرسوم ثنائي الأبعاد سنوضّح فيما يلي كيفية إضافة تصادمات مع خط مرسوم ثنائي الأبعاد باستخدام عقدة Line2D. إعداد العقد سنحتاج لإضافة العقد التالية إلى مشهدنا، ورسم الخط الذي نريده: Line2D StaticBody2D لا داعي لإضافة شكل تصادم إلى الجسم حاليًا. ملاحظة: يمكن استخدام العقدة Area2D عوضًا عن ذلك إذا أردنا اكتشاف التداخل مع الخط بدلًا من التصادم. يجب بعد ذلك إضافة أشكال تصادم إلى الجسم، حيث يوجد لدينا خياران كما سنوضّح فيما يلي. الخيار الأول: استخدام الشكل SegmentShape2D يمثّل الشكل SegmentShape2D شكل تصادم لخط ومقطع، حيث نريد إنشاء تصادم مقاطع لكل زوج من النقاط على الخط. extends Line2D func _ready(): for i in points.size() - 1: var new_shape = CollisionShape2D.new() $StaticBody2D.add_child(new_shape) var segment = SegmentShape2D.new() segment.a = points[i] segment.b = points[i + 1] new_shape.shape = segment الخيار الثاني: استخدام الشكل RectangleShape2D لا يحتوي الشكل SegmentShape2D على أيّ مكوّن للعرض، لذا إذا أردنا أن يكون لتصادم الخطوط ثخانة، فيمكن استخدام تصادم المستطيل بدلًا من ذلك. extends Line2D func _ready(): for i in points.size() - 1: var new_shape = CollisionShape2D.new() $StaticBody2D.add_child(new_shape) var rect = RectangleShape2D.new() new_shape.position = (points[i] + points[i + 1]) / 2 new_shape.rotation = points[i].direction_to(points[i + 1]).angle() var length = points[i].distance_to(points[i + 1]) rect.extents = Vector2(length / 2, width / 2) new_shape.shape = rect ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. ختامًا بهذا نكون قد وصلنا إلى نهاية المقال الذي حددنا فيه طريقة إعداد الكاميرا لتكون ديناميكية داخل اللعبة، كما تعرفنا على كيفية إضافة تصادمات مع خط مرسوم ثنائي الأبعاد. ترجمة -وبتصرّف- للقسمين Multitarget Camera و Line2D Collision من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إطلاق المقذوفات وترتيب عرض الكائنات في الألعاب ثنائية الأبعاد دليلك الشامل إلى بناء كاميرا خاصة بشاشات اللمس في محرّك اﻷلعاب جودو تعرف على العقد Nodes في محرك ألعاب جودو Godot
  5. عندما نعمل على تطبيق لارافيل Laravel، قد نحتاج لتكرار مهام معينة مثل إنشاء ملفات أو تنفيذ أوامر خاصة بالتطبيق. وبدلاً من أن نكتب كل هذه الأوامر يدويًا في كل مرة، يمكننا أتمتة المهام المتكررة باستخدام أداة سطر الأوامر سهلة الاستخدام PHP Artisan المدمجة مع إطار عمل لارافيل، فهي تتيح لنا تنفيذ أوامر جاهزة مثل إنشاء جداول قواعد البيانات، أو تشغيل خادم التطوير المحلي، كما تتيح لنا تنفيذ أوامر جديدة خاصة بنا ننشؤها بأنفسنا، مثل إرسال بريد لمستخدم، أو تحديث بيانات معينة. سنوضح في هذا المقال كيفية إنشاء أمر Artisan جديد باستخدام الأمر make:command الذي يؤدي لإنشاء صنف جديد في المجلد app/Console/Commands والمستخدَم لإعداد عمليات التهجير Migrations وقوائم الوِجهات Route وإنشاء الأرتال Queueing والأصناف والمهام الأخرى. نظرة عامة على PHP Artisan يستخدم المطورون أوامر Artisan من لارافيل لإكمال المهام الضوروية مثل إنشاء عمليات التهجير، ونشر موارد الحزم. هناك عدة أوامر مدمجة مع Artisan، ويمكننا عرض قائمة بهذه الأوامر من خلال كتابة أمر php artisan list في الطرفية Terminal، إذ يوفر هذا الأمر قائمةً بجميع الأوامر المتاحة. تساعد هذه الأوامر المطورين على العمل بكفاءة أكبر، فباستخدام أوامر Laravel Artisan المختلفة سنتمكن من إنشاء وظائف الاستيثاق Auth والمتحكم Controller والنموذج والبريد والتهجير والعديد من الوظائف الأخرى بسرعة وبوقت أقل. تشغيل تطبيق لارافيل باستخدام خادم PHP Artisan يتيح الأمر serve إمكانية تشغيل التطبيقات على خادم تطوير PHP بسهولة، ويمكن للمطورين استخدام Laravel Artisan لإنشاء واختبار ميزات التطبيق المختلفة، إذ يمكن بدء تشغيل الخادم المحلي على العنوان http://localhost:8000 بكتابة الأمر php artisan serve، مما يسهل اختبار وتطوير التطبيقات بسرعة؛ كما يمكن تخصيص الخادم لاستخدام مضيف ومنفذ مختلف. يتكامل أمر php artisan serve مع نظام تطوير لارافيل Laravel Ecosystem الذي يتضمن ميزات عديدة مثل تهجير قواعد البيانات Migration، وجدولة المهام، وإدارة الأرتال Queues. إنشاء أوامر مخصصة وتشغيلها يمكن إنشاء أوامر Artisan مخصصة، مع اختيار مكان تخزينها وتحميلها باستخدام مدير الحزم Composer. سنشرح في الفقرات التالية أهم الخطوات المتبعة لإنشاء أوامر مخصصة وتسجيلها وتنفيذها. الخطوة 1: إنشاء أمر جديد يمكننا إنشاء أمر جديد باسم customcommand عبر استخدام الأمر make:command الذي سينشئ صنف أوامر جديد تلقائيًا في المجلد app/Console/Commands، وسيتولّد هذا المجلد إن لم يكن موجودًا مسبقًا. لنكتب الآن الأمر التالي في طرفية Artisan: php artisan make:command customcommand قد يبدو ملف الشيفرة البرمجية CustomCommand.php الخاص بهذا الأمر كما يلي: namespace App\Console\Commands; use Illuminate\Console\Command; class CustomCommand extends Command { protected $signature = 'custom:command'; protected $description = 'Description of the custom command'; public function __construct() { parent::__construct(); } public function handle() { // نضع المنطق البرمجي للأمر هنا } } الخطوة 2: تعريف الأمر عند تنفيذ بعض أوامر Artisan، قد نحتاج أحيانًا لأن يُدخل المستخدم بعض المعلومات، مثل اسمه أو بريده الإلكتروني. يتعامل لارافيل مع ذلك باستخدام خاصية التوقيع signature، ويمكننا استخدام الصيغة القصيرة التالية لضبط الوسطاء Arguments والرايات Flags أو الخيارات المطلوبة والاختيارية، وهذا يسهل علينا تحديد الطريقة التي سيتفاعل بها المستخدمون مع الأمر عند تشغيله. لنلقِ نظرةً على الأمر التالي: protected $signature = 'user:update {id} {--name=} {--email=}'; يتوقّع هذا الأمر لأن يدخل المستخدم وسيطًا للمعرّف ID وخيارات الاسم والبريد الإلكتروني الاختيارية. الخطوة 3: دخل وخرج الأمر يوفّر لارافيل طرقًا سهلةً للحصول على قيم الوسطاء والخيارات عند تشغيل الأمر، لذا من المهم استخدام التابعين ‎$this->argument()‎ و ‎$this->option()‎ ضمن التابع handle الخاص بالأمر. وإن لم يكن الوسيط أو الخيار الذي نبحث عنه موجودًا، فسيعيد هذان التابعان القيمة null. يمكننا الحصول على إدخال من المستخدم أثناء تشغيل الأمر بالإضافة إلى عرض الخرج، حيث يعرض التابع ask سؤالًا للمستخدم ويجمع استجابته ثم يرسلها مرةً أخرى إلى الأمر الخاص بنا. $name = $this->ask('What is your name?', 'Taylor'); والأمر الآتي لطلب التأكيد: if ($this->confirm('Do you wish to continue?')) { // متابعة } يمكن جعل موجّه التأكيد يعيد القيمة true دائمًا من خلال تمرير القيمة true كوسيط ثانٍ للتابع confirm. الخطوة 4: تسجيل الأوامر يسجّل لارافيل جميع الأوامر الموجودة في المجلد app/Console/Commands افتراضيًا، ويمكننا إعداده للبحث في مزيد من المجلدات عن أوامر PHP Artisan، وذلك من خلال استخدام التابع withCommands في ملف bootstrap/app.php الخاص بتطبيقنا. ->withCommands([ __DIR__.'/../app/Domain/Orders/Commands', ]) سيجد حاوي الخدمات جميع الأوامر ويسجلها في تطبيقنا باستخدام Artisan. الخطوة 5: تنفيذ الطلبات قد نرغب في تشغيل أمر Artisan خارج واجهة سطر الأوامر من وجهة Route أو متحكم Controller مثلًا، حيث يمكن ذلك باستخدام التابع call مع الصنف Facade من Artisan؛ ويحتوي هذا التابع على وسيطين هما: اسم الأمر أو صنفه قائمة بمعاملات الأمر ويعيد هذا التابع رمز الخروج. يمكن أيضًا تمرير أمر Artisan بالكامل إلى التابع call كسلسلة نصية كما يلي: Artisan::call('mail:send 1 --queue=default'); يمكن استخدام التابع queue مع الصنف Facade من Artisan لإرسال أوامر Artisan إلى عمّال الرتل، ثم نعالَج هذه الأوامر في الخلفية، ونتأكّد من ضبط الرتل وتشغيل مستمع الرتل قبل استخدام هذا التابع. use Illuminate\Support\Facades\Artisan; Route::post('/user/{user}/mail', function (string $user) { Artisan::queue('mail:send', [ 'user' => $user, '--queue' => 'default' ]); // ... }); الخطوة 6: معالجة الإشارات يمكن لأوامر Artisan من لارافيل معالجة إشارات النظام مثل إشارات SIGINT، حيث يمكن دمج معالجة الإشارات مع الأمر الخاص بنا باستخدام التابع trap كما يلي: $this->trap(SIGINT, function () { $this->info('Command interrupted'); }); الخطوة 7: تخصيص الملفات الجذرية Stub يمكن تخصيص ملفات القوالب التي يستخدمها أمر make:command من Artisan لتسهيل إنشاء الأوامر ذات البنى المتناسقة، لذا نحتاج إلى نشر الملفات الجذرية في مشروعنا كما يلي: php artisan stub:publish الخطوة 8: الأحداث هناك ثلاثة أحداث يرسلها Artisan عند تشغيل الأوامر وهي: Illuminate\Console\Events\ArtisanStarting Illuminate\Console\Events\CommandStarting Illuminate\Console\Events\CommandFinished استخدم التابع event لإطلاق حدث كما يلي: event(new CustomCommandExecuted($this)); إنشاء عمليات التهجير Migrations باستخدام الأمر make:migration يُعَد الأمر php artisan make:migration من أوامر Artisan في لارافيل، ويُستخدَم لإنشاء ملف تهجير جديد. عمليات التهجير هي مخططات أولية لمخطط قاعدة البيانات الخاصة بنا، وتحدّد بنية الجداول والأعمدة والفهارس والعلاقات. في ما يلي خطوات هذه العملية: إنشاء عملية تهجير: يؤدي تشغيل الأمر make:migration إلى إنشاء ملف PHP جديد في المجلد database/migrations تحديد التغييرات: يحتوي ملف التهجير على التابعين up و down، حيث يحدّد التابع up التغييرات التي تطرأ على قاعدة البيانات، مثل إنشاء الجداول أو إضافة الأعمدة، ويلغي التابع down هذه التغييرات تشغيل عملية التهجير: استخدام الأمر php artisan migrate لتطبيق التغييرات على قاعدة البيانات يؤدي الأمر التالي مثلًآ إلى إنشاء ملف جديد بالاسم create_users_table في المجلد database/migrations، ويمكن بعد ذلك تحديد بنية جدول users ضمن ملف التهجير. Bash php artisan make:migration create_users_table فوائد الأمر make:migration تتمثل فوائد الأمر make:migration فيما يلي: التحكم في الإصدارات: من خلال تعقّب تغييرات قاعدة البيانات بمرور الوقت التعاون: مشاركة بنية قاعدة البيانات مع أعضاء الفريق بسهولة بذر أو توليد البيانات Seeding لقاعدة البيانات: ملء قاعدة البيانات بالبيانات التجريبية باستخدام مولّد البيانات Seeder التراجع عن تغييرات قاعدة البيانات: يمكن عكس تغييرات قاعدة البيانات بسهولة إن لزم الأمر تساعد عمليات التهجير على إبقاء مخطط قاعدة البيانات واضحًا ومنظمًا، مما يسهّل عملية إدارة التطبيق وتحديثه. قائمة أوامر Laravel Artisan تُعَد واجهة سطر أوامر Artisan في لارافيل أداةً مفيدةً للعمل مع التطبيقات، فهي تحتوي على أوامر لمهام مختلفة مثل إنشاء الشيفرة البرمجية وإدارة بيئة التطبيق. تكون قائمة أوامر Laravel PHP Artisan الكاملة شاملة، حيث يمكننا استخدام أمر PHP Artisan list لجلبها، ولكن سنفصّل فيما يلي الأوامر الأساسية ضمن فئات في Laravel 11. تتمثل الأوامر الأساسية في الآتي: cache: إدارة ذاكرة التطبيق المخبئية مثل المسح والنسيان وإلخ config: تخزين ملفات الضبط Configuration مؤقتًا أو مسحها أو نشرها down: تعطيل التطبيق مؤقتًا env: إدارة متغيرات البيئة key: توليد مفتاح تطبيق جديد migrate: تشغيل عمليات تهجير قاعدة البيانات optimize: تحسين التطبيق للإنتاج queue: إدارة نظام الرتل route: سرد الوِجهات Routes أو مسحها storage: إدارة مجلد التخزين vendor: إدارة اعتماديات مدير الحزم Composer أما أوامر توليد الشيفرة البرمجية، فتتمثل في ما يلي: make: توليد بنى الشيفرة البرمجية المختلفة للمتحكم والنموذج والتهجير وإلخ model: إنشاء نماذج Eloquent migration: إنشاء ملفات التهجير seed: توليد بيانات وهمية لقاعدة البيانات في حين أن أوامر الاختبار تكون كالآتي: test: تشغيل اختبارات التطبيق dusk: تشغيل اختبارات المتصفح باستخدام Dusk إلى جانب وجود بعض الأوامر الإضافية التي يمكن اعتمادها، وتتمثل في: auth: إدارة العمليات المتعلقة بالاستيثاق Authentication breeze: تثبيت نظام استيثاق Breeze config: إدارة ملفات الضبط horizon: إدارة نظام رتل Laravel Horizon passport: إدارة خادم OAuth2 sanctum: إدارة واجهة برمجة التطبيقات API لاستيثاق الرموز Token telescope: إدارة أداة تنقيح أخطاء Telescope الخلاصة بهذا نكون قد وضّحنا في هذا المقال كيفية إنشاء أوامر مخصصة باستخدام أمر Artisan الذي هو أداة PHP وتتيح تطوير أوامر مختلفة بناءً على احتياجات مشروعنا. ترجمة -وبتصرّف- للقسم How to Create Custom Commands in Laravel 11 with PHP Artisan لصاحبته Hafsa Tahir. اقرأ أيضًا إنشاء أوامر مخصصة في Laravel 11 باستخدام PHP Artisan نصائح لتحسين أداء تطبيقات لارافيل أفضل الحزم البرمجية لتحسين تطبيقات لارافيل
  6. يوفّر نظام إدارة قاعدة بيانات مفتوح المصدر MariaDB أداءً وأمانًا ومرونةً مميزة، ويمكن للمطورين استخدام ميزات MariaDB لبناء تطبيقات ويب قوية عند استخدامها مع لارافيل Laravel الذي يُعَد إطار عمل PHP شائع الاستخدام. قد يكون ربط قاعدة بيانات MariaDB بتطبيق لارافيل أمرًا صعبًا بالنسبة لبعض المطورين، وخاصةً المطورين المبتدئين، ولكن يمكن دمج قاعدة بيانات MariaDB بنجاح مع لارافيل باستخدام التوجيه المناسب والموارد الصحيحة. سنوضّح في هذا المقال كيفية ربط قاعدة بيانات MariaDB بتطبيق لارافيل، وسنوضح متطلبات النظام لقاعدة بيانات MariaDB وضبط قاعدة بيانات MariaDB مع إطار عمل لارافيل واختبار الاتصال. نظرة عامة موجزة على MariaDB MariaDB هو نظام إدارة قواعد بيانات عِلاقية مفتوح المصدر يستخدمه مطورو الويب على نطاق واسع، ويُعَد فرعًا من قاعدة بيانات MySQL ويقدم العديد من الميزات المتقدمة مثل تحسين الأداء والأمان ومزيدًا من المرونة من ناحية الضبط Configuration. يستخدم نظام إدارة قواعد البيانات MariaDB لغة SQL لإدارة البيانات ومعالجتها، مما يوفر للمطورين بيئةً مألوفةً ومرنةً للعمل مع البيانات، ويلتزم بالحفاظ على التوافق مع MySQL، مما يسمح بالتهجير Migration أو الانتقال السلس لقواعد بيانات MySQL الموجودة مسبقًا. يمكن لقاعدة بيانات MariaDB التعامل مع عدد كبير من العمليات بكفاءة مع تركيزها على تحسين الأداء وميزاتها المتقدمة، مما يجعلها خيارًا ممتازًا للتطبيقات التي تتراوح من المشاريع الصغيرة إلى الأنظمة التي على مستوى المؤسسات. فوائد استخدام لارافيل Laravel مع MariaDB يمكن أن يوفر استخدام إطار عمل لارافيل مع قاعدة بيانات MariaDB العديد من الفوائد لمطوري الويب مثل: البساطة يسهّل لارافيل من التفاعل مع قواعد البيانات بما في ذلك قاعدة بيانات MariaDB، وبالتالي يمكن للمطورين بسهولة ربط تطبيق لارافيل بقاعدة بيانات MariaDB والاستفادة من ميزاتها القوية. المرونة تدعم قاعدة بيانات MariaDB محركات تخزين أكثر من قاعدة بيانات MySQL، مما يوفر للمطورين مرونةً أكبر عند العمل مع البيانات في تطبيق لارافيل. قابلية التوسع تعمل تقنية عنقود Galera المتقدمة في قاعدة بيانات MariaDB على التخلص من تأخر الخوادم وفقدان المعامَلات وتقليل زمن الاستجابة للعميل، وتحسين قابلية توسيع قراءة العقد، مما يجعلها خيارًا ممتازًا لتطبيقات لارافيل التي تحتاج إلى التعامل مع كميات كبيرة من البيانات. الأمان يركز كل من لارافيل و MariaDB على الأمان، حيث يوفر لارافيل ميزات أمن مُدمَجة معه مثل الاستيثاق Authentication والتصريح Authorization؛ بينما تقدم MariaDB تدابير أمان قوية لحماية البيانات، ويمكن أن يساعد استخدام هاتين التقنيتين مع بعضهما البعض المطورين في بناء تطبيقات ويب آمنة. تشغيل تطبيق لارافيل مع قاعدة بيانات MariaDB يتضمن نشر تطبيق لارافيل باستخدام قاعدة بيانات MariaDB عدة خطوات، سنوضّحها فيما يلي. الخطوة 1: إنشاء مشروع لارافيل جديد لنفتح الطرفية Terminal ونشغل الأوامر التالية لإنشاء مشروع لارافيل جديد: composer global require laravel/installer laravel new YourProjectName cd YourProjectName الخطوة 2: ضبط قاعدة البيانات سنفتح الآن ملف ‎.env في مجلد جذر المشروع ونضبط اتصال قاعدة بيانات MariaDB، وذلك من خلال تحديث الأسطر التالية بمعلومات قاعدة بياناتنا: DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=your_database_name DB_USERNAME=your_database_username DB_PASSWORD=your_database_password الخطوة 3: إنشاء ملفات التهجير Migrations والنماذج Models يستخدم لارافيل ملفات التهجير لإدارة مخطط قاعدة البيانات، حيث يمكننا إنشاء ملف تهجير ونموذج جديد لكيان معين مثل Post من خلال تشغيل الأمر التالي: php artisan make:model Post -m وسيؤدي الأمر السابق إلى إنشاء ملف تهجير في المجلد database/migrations ونموذج في المجلد app. الخطوة 4: تعديل ملف التهجير يمكننا فتح ملف التهجير -مثل الملف xxxx_xx_xx_create_posts_table.php- في المجلد database/migrations وتعريف مخطط الجدول posts كما يلي: public function up() { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->string('title'); $table->text('content'); $table->timestamps(); }); } الخطوة 5: تشغيل ملف التهجير عند هذه الخطوة سنطبق التهجير لإنشاء جدول قاعدة البيانات، وذلك من خلال تشغيل الأمر التالي: php artisan migrate الخطوة 6: إنشاء الوجهات Routes والمتحكمات Controllers سننشئ الآن وجهات في ملف routes/web.php والمتحكمات المقابلة لتطبيقنا. فعلى سبيل المثال، يمكن إنشاء وِجهة لعرض المنشورات من خلال إضافة السطر التالي إلى الملف routes/web.php: Route::get('/posts', 'PostController@index'); الخطوة 7: إنشاء متحكم لنولّد الآن متحكمًا باستخدام Artisan من خلال تشغيل الأمر التالي: php artisan make:controller PostController سنعرّف بعد ذلك المنطق البرمجي لجلب المنشورات وعرضها في المتحكّم PostController. الخطوة 8: إنشاء العروض Views وتشغيل التطبيق سننشئ هنا عروض Blade في المجلد resources/views لإخراج واجهة المستخدم الخاصة بتطبيقنا؛ بعدها يمكننا تشغيل خادم تطوير لارافيل من خلال تشغيل الأمر التالي: php artisan serve سنحتاج الآن إلى الانتقال إلى العنوان http://localhost:8000/posts في متصفحنا لمشاهدة عمل التطبيق. الخطوة 10: إضافة البيانات إلى قاعدة البيانات يمكن استخدام مولّد البيانات Seeder أو إضافة البيانات يدويًا إلى قاعدة البيانات باستخدام أمر Artisan من لارافيل tinker، لذا لنشغّل الأمر التالي لإضافة البيانات يدويًا: php artisan tinker يمكن إنشاء منشور جديد وحفظه ضمن صدفة Shell الأمر tinker كما يلي: $post = new App\Models\Post; $post->title = 'Sample Title'; $post->content = 'Sample Content'; $post->save(); exit; ضبط قاعدة بيانات MariaDB باستخدام منصة Cloudways لنتبع الخطوات التالية لضبط قاعدة بيانات MariaDB باستخدام منصة Cloudways. الخطوة 1: تسجيل الدخول إلى منصة Cloudways ننقر على عرض جميع الخوادم View all Servers بعد تسجيل الدخول إلى الحساب ونختار الخادم الذي نريده. الخطوة 2: تحديد الإعدادات والحزم نحدد خيار الإعدادات والحزم Settings & Packages من شريط القائمة الأيسر، وننقر على تبويب الحزم Packages. الخطوة 3: اختيار إصدار MariaDB نختار إصدار MariaDB المفضل من الخيارات الموجودة، وننقر على حفظ التغييرات Save Changes. وبذلك نكون قد ضبطنا قاعدة بيانات MariaDB باستخدام منصة Cloudways. الإصدارات المدعومة من MariaDB تدعم منصة Cloudways العديد من إصدارات MariaDB، بما في ذلك 10.0 و 10.1 و 10.2 و 10.3 و 10.4 و 10.5 و 10.6، ولكن يجب ملاحظة أن الخوادم الأحدث والخوادم التي تعمل باستخدام توزيعة Debian 10 تأتي مع MariaDB 10.4 بوصفها قاعدة بيانات افتراضية. إصدار قاعدة البيانات قابلة للترقية إلى MySQL 5.5 الإصدارات MariaDB 10.0 و MariaDB 10.1 و MariaDB 10.2 و MariaDB 10.3. MySQL 5.6 الإصدارات MariaDB 10.1 و MariaDB 10.2 و MariaDB 10.3. MySQL 5.7 الإصدارات MariaDB 10.2 و MariaDB 10.3. MariaDB 10.0 الإصدارات MariaDB 10.1 و MariaDB 10.2 و MariaDB 10.3. MariaDB 10.1 الإصدارات MariaDB 10.2 و MariaDB 10.3. MariaDB 10.2 الإصدار MariaDB 10.3 والإصدارات الأحدث. MariaDB 10.3 الإصدار MariaDB 10.4 والإصدارات الأحدث. MariaDB 10.4 الإصدار MariaDB 10.5 والإصدارات الأحدث. MariaDB 10.5 الإصدار MariaDB 10.6 والإصدارات الأحدث. MariaDB 10.6 سيكون من الممكن ترقيته إلى أيّ إصدار جديد من MariaDB عند توفّره على منصة Cloudways. الخلاصة يمكن ربط قاعدة بيانات MariaDB بتطبيق لارافيل من خلال ضبط اتصال قاعدة البيانات في ملف ‎.env وتشغيل عمليات التهجير لإنشاء جداول قاعدة البيانات الضرورية، وتوفر منصة Cloudways طريقةً سهلةً لضبط قاعدة بيانات MariaDB مع منصتها. هناك العديد من الفوائد التي يوفرها استخدام لارافيل مع قاعدة بيانات MariaDB للمطورين مثل البساطة والمرونة وقابلية التوسع والأمان، وبالتالي يمكن للمطورين إنشاء تطبيقات ويب قوية وموثوقة من خلال الاستفادة من هذه الفوائد. ترجمة -وبتصرّف- للقسم How to Connect MariaDB Database to Laravel Application لصاحبه Inshal Ali. اقرأ أيضًا كيفية تأمين قواعد البيانات MySQL ,MariaDB على خواديم لينكس كيفية استيراد وتصدير قواعد بيانات MySQL أو MariaDB كيفية تغيير مجلد تخزين بيانات MariaDB إلى مكانٍ آخر نصائح لتحسين أداء تطبيقات لارافيل
  7. سنوضّح في هذا المقال كيفية إعداد خوارزمية للبحث عن المسار للسماح بالتنقل في بيئة قائمة على الشبكة Grid، حيث يوفّر محرّك الألعاب جودو Godot عددًا من الطرق لتحديد المسار، ولكننا سنستخدم في هذا المقال خوارزمية A*‎، التي لها استخدام واسع في العثور على أقصر مسار بين نقطتين، ويمكن استخدامها في أيّ هيكل بيانات قائمٍ على الرسم البياني Graph، وليس في بيئة شبكية فقط. يُعَد الصنف AStarGrid2D نسخةً متخصصةً من الصنف AStar2D الأعم في جودو، لذا يُعَد إعداده أسرع وأسهل، نظرًا لأنه متخصص للاستخدام مع الشبكة، إذ لسنا مضطرين لإضافة جميع خلايا الشبكة الفردية واتصالاتها يدويًا. إعداد الشبكة يُعَد ضبط حجم الخلايا والشبكة أهم خطوة، حيث سنستخدم الحجم ‎(64, 64)‎ في مثالنا، وسنستخدم حجم النافذة لتحديد عدد الخلايا الملائمة للشاشة، ولكن كل شيء سيعمل بالطريقة نفسها بغض النظر عن حجم الخلية. سنضيف الآن الشيفرة البرمجية التالية إلى العقدة Node2D: extends Node2D @export var cell_size = Vector2i(64, 64) var astar_grid = AStarGrid2D.new() var grid_size func _ready(): initialize_grid() func initialize_grid(): grid_size = Vector2i(get_viewport_rect().size) / cell_size astar_grid.size = grid_size astar_grid.cell_size = cell_size astar_grid.offset = cell_size / 2 astar_grid.update() نقسم حجمَ الشاشة على حجم الخلية cell_size في هذه الشيفرة البرمجية لحساب حجم الشبكة بالكامل، مما يتيح ضبط الخاصية size الخاصة بالصنف AStarGrid2D. تُستخدَم خاصية الإزاحة offset عندما نطلب مسارًا بين نقطتين، ويمثّل استخدام cell_size / 2 حساب المسار من مركز كل خلية بدلًا من زواياها، ويجب استدعاء الدالة update()‎ بعد ضبط أو تغيير خاصيات الصنف AStarGrid2D. رسم الشبكة سنرسم الشبكة على الشاشة في الشيفرة البرمجية للتوضيح، ولكن قد تكون لدينا عقدة TileMap أو أيّ تمثيل مرئي آخر لعالمنا في تطبيق اللعبة. يمكن رسم الشبكة باستخدام الشيفرة البرمجية التالية: func _draw(): draw_grid() func draw_grid(): for x in grid_size.x + 1: draw_line(Vector2(x * cell_size.x, 0), Vector2(x * cell_size.x, grid_size.y * cell_size.y), Color.DARK_GRAY, 2.0) for y in grid_size.y + 1: draw_line(Vector2(0, y * cell_size.y), Vector2(grid_size.x * cell_size.x, y * cell_size.y), Color.DARK_GRAY, 2.0) وسنحصل على الشبكة التالية: رسم المسار نحتاج إلى نقطة بداية ونقطة نهاية للعثور على مسار، لذا علينا إضافة المتغيرات التالية في بداية السكربت: var start = Vector2i.ZERO var end = Vector2i(5, 5) بعد ذلك نضيف الأسطر التالية في الدالة ‎_draw()‎ لإظهار هذه النقاط: draw_rect(Rect2(start * cell_size, cell_size), Color.GREEN_YELLOW) draw_rect(Rect2(end * cell_size, cell_size), Color.ORANGE_RED) يمكننا الآن العثور على المسار بين النقطتين باستخدام التابع get_point_path()‎، ولكننا نحتاج أيضًا إلى إظهاره، لذا من المهم استخدام العقدة Line2D وإضافة إلى المشهد، حيث يمكن الحصول على المسار وإضافة النقاط الناتجة إلى العقدة Line2D كما يلي: func update_path(): $Line2D.points = PackedVector2Array(astar_grid.get_point_path(start, end)) وسنحصل على النتيجة التالية: وكما نلاحظ، لدينا خطًا قطريًا بين النقطتين لأن المسار يستخدم الخطوط القطرية افتراضيًا، ويمكن تعديل ذلك من خلال تغيير الخاصية diagonal_mode باستخدام القيم التالية: DIAGONAL_MODE_ALWAYS: القيمة الافتراضية، وتستخدم الخطوط القطرية DIAGONAL_MODE_NEVER: تكون الحركة عمودية DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE: تسمح هذه القيمة بالخطوط القطرية، ولكنها تمنع المسار من المرور بين العوائق الموضوعة قطريًا DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE: تسمح بالخطوط القطرية في المناطق المفتوحة فقط، وليس بالقرب من العوائق قد يؤدي تعديل هذه الخاصية إلى إعطاء نتائج مختلفة جدًا، لذا تأكد من التجربة بناءً على الإعداد الذي تستخدمه، لذا يجب إضافة ما يلي في الدالة initialize_grid()‎: astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER وأصبح لدينا الآن الحركات المتعامدة فقط كما يلي: إضافة العوائق يمكننا أيضًا إضافة العوائق إلى الشبكة، حيث لن يتضمن المسار خليةً ما من خلال وضع علامة عليها بوصفها صلبة Solid، ويمكن التبديل بين القيمتين صلبة وغير صلبة للخلية باستخدام الدالة set_point_solid()‎. سنضيف الشيفرة البرمجية التالية لرسم الجدران (إن وُجدت) من خلال العثور على الخلايا الصلبة وتلوينها: func fill_walls(): for x in grid_size.x: for y in grid_size.y: if astar_grid.is_point_solid(Vector2i(x, y)): draw_rect(Rect2(x * cell_size.x, y * cell_size.y, cell_size.x, cell_size.y), Color.DARK_GRAY) سنستدعي الآن هذه الدالة في الدالة ‎_draw()‎، ويمكن بعد ذلك استخدام الفأرة للنقر على الخلايا وتبديل حالتها كما يلي: func _input(event): if event is InputEventMouseButton: # إضافة أو إزالة حائط if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: var pos = Vector2i(event.position) / cell_size if astar_grid.is_in_boundsv(pos): astar_grid.set_point_solid(pos, not astar_grid.is_point_solid(pos)) update_path() queue_redraw() يمكن ملاحظة أننا نستخدم التابع is_in_boundsv()‎ أولًا، مما يمنع حدوث أخطاء إذا نقرنا خارج حدود الشبكة. يمكننا الآن رؤية تأثير العوائق على المسار كما يلي: الاختيار الاستكشافي أو التجريبي Heuristic يُعَد الاختيار الاستكشافي الذي نستخدمه عاملًا مهمًا يؤثر على المسار الناتج، حيث يشير مصطلح Heuristic إلى أفضل تخمين، ويمثل ببساطة الاتجاه الذي يجب أن نجرّبه أولًا عند التحرك نحو الهدف في سياق العثور على المسار. تستخدم المسافة الإقليدية مثلًا نظرية فيثاغورس لتقدير المسار الذي يجب تجربته كما يلي: بينما تأخذ مسافة مانهاتن في حساباتها المسافة في اتجاهات الشمال/الجنوب أو الشرق/الغرب فقط كما يلي: وتعطي طريقة التجزيء الاستكشافية Octile Heuristic المسار التالي: يمكن استخدام الاختيار الاستكشافي باستخدام الخاصية التالية: astar_grid.default_estimate_heuristic = AStarGrid2D.HEURISTIC_OCTILE يعتمد تحديد الخيار الأفضل الذي يعطي المسارات المناسب على طبيعة بيئتنا مثل احتوائها على مساحات مفتوحة واسعة مع القليل من العوائق المنتشرة حولها أم أنها متاهة من الممرات المتعرجة، لذا يجب التأكّد من تجربة مشروعنا أولًا. ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github لتجربة الإعداد، ويمكن استخدام أزرار الفأرة الأيمن/الأوسط لتحريك مواقع النهاية/البداية بالإضافة إلى وضع الجدران. ترجمة -وبتصرّف- للقسم Pathfinding on a 2D Grid من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: مفهوم Coyote Time وكيفية إعداد منصة متحركة في مشهد اللعبة إطلاق المقذوفات وترتيب عرض الكائنات في الألعاب ثنائية الأبعاد إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد
  8. سنوضح في هذا المقال تقنية Coyote Time ونضيفها إلى شخصية منصة موجودة مسبقًا، وسنتعرّف على كيفية تحريك المنصات في لعبة المنصات ثنائية الأبعاد. شرح مفهوم Coyote Time قد يكون القفز غير موجود في ألعاب المنصات، حيث لا يتمتع اللاعب بقدرٍ جيد من التحكم، وقد يفشل أحيانًا في القفز من حافة المنصات. يتمثّل الحل لهذه المشكلة في استخدام تقنية Coyote Time التي تمنح اللاعب شعورًا أكبر بالتحكم ومساحة ضئيلة للحركة حول عملية القفز من حواف المنصات، حيث تعمل هذه التقنية بالطريقة التالية: إذا مشى اللاعب بعد حافة منصة، فسنسمح له بالقفز كما لو كان لا يزال على الأرض بعد بضعة إطارات. ملاحظة: أتى اسم هذه التقنية من شخصية الذئب Coyote الكرتونية الشهيرة التي لا تسقط حتى تنظر إلى الأسفل: سنضيف هذه التقنية إلى شخصية منصة موجودة مسبقًا، لذا يمكن الاطلاع على مقال إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد لمعرفة كيفية إعدادها. سنضيف عقدة Timer بالاسم CoyoteTimer ونضبطها على لقطة واحدة One Shot للتعامل مع ضبط الوقت، وتوجد بعض المتغيرات الجديدة التي نحتاجها لتعقّب وقت coyote كما يلي: var coyote_frames = 6 # عدد الإطارات في الهواء المسموح بها للقفز var coyote = false # تتبّع ما إذا كنا في وقت‫ coyote أم لا var last_floor = false # حالة الإطار الأخير على الأرض سنستخدم الإطارات لضبط المدة، وبالتالي يمكننا نقل ذلك إلى وقت ضبط طول العقدة Timer في التابع ‎_ready()‎ كما يلي: $CoyoteTimer.wait_time = coyote_frames / 60.0 سنخزّن القيمة الحالية للتابع is_on_floor()‎ في كل إطار لاستخدامها في الإطار التالي، لذا يجب وضع ما يلي في الدالة ‎_physics_process()‎ بعد الدالة move_and_slide()‎: last_floor = is_on_floor() يجب التحقق مما إذا كانت الشخصية على الأرض أو في وقت Coyote عندما نكتشف إدخال القفز كما يلي: if Input.is_action_just_pressed("jump") and (is_on_floor() or coyote): velocity.y = jump_speed jumping = true يبدأ وقت Coyote إذا مشى اللاعب بعد حافة المنصة، وبالتالي لم يَعُد على الأرض وكان على الأرض في الإطار السابق. يمكننا التحقق من ذلك وبدء تشغيل المؤقت إذا انتقلنا من الأرض إلى فوقها وفق التالي: if !is_on_floor() and last_floor and !jumping: coyote = true $CoyoteTimer.start() تخبرنا العقدة CoyoteTimer متى تنتهي حالة coyote كما يلي: func _on_coyote_timer_timeout(): coyote = false يمكن تطبيق العملية نفسها على الشخصيات ثلاثية الأبعاد. ملاحظة: نُفِّذت شخصية القسم التالي باستخدام تقنية Coyote Time. كيفية إعداد منصة متحركة في مشهد اللعبة سنوضّح فيما يلي كيفية تحريك المنصات في لعبة المنصات ثنائية الأبعاد، حيث توجد طرق متعددة للتعامل مع ذلك؛ إذ سنستخدم عقد AnimatableBody2D للمنصة ونحرّكها باستخدام عقدة الانتقال التدريجي Tween، مما يسمح بمجموعة متنوعة من أنماط الحركة مع تقليل الشيفرة البرمجية التي يجب كتابتها. ملاحظة: يمكن أيضًا تطبيق هذه التقنية لتحريك المنصات باستخدام عقدة AnimationPlayer بدلًا من عقدة Tween، حيث سيبقى معظم الإعداد نفسه في كلتا الطريقتين، ولكن ستحرّك خاصية موضع position الجسم بدلًا من شيفرة Tween البرمجية. الإعداد سنبدأ بإعداد لعبة منصات بسيطة باستخدام الطريقة المتبعة في مقال إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد، حيث ستعمل هذه الحركة الأساسية بنجاح مع المنصات، وإذا عدّلناها أو استخدمنا طريقتنا الخاصة، فيجب أن يعمل كل شيء بالطريقة نفسها. إنشاء المنصة يحتوي مشهد المنصة على العقد التالية: Node2D المنصة المتحركة MovingPlatform: تكون العقدة الأب Node2D موجودةً لتعمل بوصفها نقطة ارتكاز أو نقطة بداية للمنصة، حيث سنحرّك موضع position المنصة بالتناسب مع هذه العقدة الأب AnimatableBody2D: تمثل هذه العقدة المنصة نفسها، وهي العقدة التي ستتحرّك Sprite2D: يمكن استخدام ورقة الشخصية الرسومية Sprite Sheet هنا أو صور فردية أو حتى عقدة TileMap CollisionShape2D: لا يجب أن نجعل مربع الاصطدام كبيرًا جدًا، وإلّا فسيبدو اللاعب وكأنه يحوم فوق حافة المنصة ستُعِدّ خامة Texture العقدة Sprite2D وشكل الاصطدام بطريقة مناسبة، ونضبط خاصية التزامن مع الفيزياء Sync to Physics على القيمة On في العقدة AnimatableBody2D، مما يضمن تحريك الجسم أثناء خطوة الفيزياء بما أننا نحرّكه في الشيفرة البرمجية، وبالتالي يكون متزامنًا مع اللاعب والأجسام الفيزيائية الأخرى. سنضيف الآن السكربت التالي إلى العقدة الجذر Node2D: extends Node2D @export var offset = Vector2(0, -320) @export var duration = 5.0 func _ready(): start_tween() func start_tween(): var tween = get_tree().create_tween().set_process_mode(Tween.TWEEN_PROCESS_PHYSICS) tween.set_loops().set_parallel(false) tween.tween_property($AnimatableBody2d, "position", offset, duration / 2) tween.tween_property($AnimatableBody2d, "position", Vector2.ZERO, duration / 2) حتى الآن قد استخدمنا بعض خيارات عقد Tween لجعل كل شيء يعمل بسلاسة مثل: set_process_mode()‎: يضمن حدوث الحركة أثناء خطوة المعالجة الفيزيائية set_loops()‎: يعمل على تكرار الانتقال التدريجي Tween set_parallel(false)‎: تحدث جميع تغييرات tween_property()‎ في الوقت نفسه افتراضيًا، ويؤدي هذا التابع إلى حدوث هذين الأمرين الواحد تلوَ الآخر، وهما التحرك إلى أحد طرفي الإزاحة ثم العودة إلى البداية يمكن ضبط حركة المنصة باستخدام هاتين الخاصيتين المُصدَّرتين، وهما الإزاحة offset التي يجب أن تضبطها لتحديد المكان الذي يتحرك فيه الانتقال التدريجي Tween بالنسبة لنقطة بدايته، و المدة duration التي يجب أن تضبطها لتحديد المدة التي تستغرقها لإكمال الدورة. سنضيف الان بعض المنصات في المستوى أو العالم الخاص بنا ونجرّبها كما في المثال التالي: 02_moving_platform4.webm ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. ترجمة -وبتصرّف- للقسمين Coyote Time و Moving Platforms من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إطلاق المقذوفات وترتيب عرض الكائنات في الألعاب ثنائية الأبعاد إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد مدخل إلى محرك الألعاب جودو Godot تعرف على واجهة محرك الألعاب جودو
  9. شاع استخدام مصطلح البيانات الضخمة Big Data في الآونة الأخيرة، فبعد أن صارت البيانات تُجمع بمعدل غير مسبوق من كل شيء حولنا، سواءً من أجهزة الاستشعار الخاصة بأجهزة إنترنت الأشياء IoT، أو إشارات نظام تحديد المواقع العالمي GPS من الهواتف المحمولة، أو منشورات وسائل التواصل الاجتماعي، أو المعاملات التجارية التي نجريها عبر الإنترنت، وغيرها من المصادر المتنوعة، مما زاد من حجم البيانات كثيرًا؛ دفع هذا الأمر الشركات للاستفادة من هذه البيانات وتحليلها واتخاذ القرارات بناءً عليها، وعليه صارت الكثير من القطاعات اليوم تسعى للاستفادة من هذا الكم الهائل من هذه البيانات لفهم توقعات عملائها وتعزيز عملياتها التجارية وزيادة قدرتها التنافسية في السوق. وفي ظل هذا التوجه والسعي الكبير لجمع البيانات وتحليلها، قد يخطر ببالنا سؤال مهم وهو هل البيانات الضخمة ضرورية بالفعل لعملنا؟ وهل تمتلك الشركات التي تستفيد من البيانات الضخمة قدرةً أكبر على تطوير الحلول المبتكرة مقارنةً بتلك التي نكتفي بالتعامل مع البيانات الصغيرة والمتوسطة الحجم أو التي تعرف باسم البيانات التقليدية؟ سنحاول في هذا المقال الإجابة على هذه التساؤلات، ونعقد مقارنةً بين البيانات الضخمة والبيانات التقليدية وحالات استخدام كل منهما. ما هي البيانات الضخمة Big Data البيانات الضخمة هي بيانات تستخدم لاستخراج وتحليل وإدارة معالجة أحجام هائلة ومعقدة من البيانات التي تنمو بتسارع. ووفقًا لمعدل أُسِّي، فهي تجمع باستمرار من مختلف المصادر وقد يصل حجمها إلى أرقام مهولة وضخمة جدًا. تتسم البيانات الضخمة بتنوع أشكالها، فهي لا تقتصر على البيانات المنظمة المخزنة في قواعد البيانات، بل تشمل أيضًا بيانات غير منظمة مثل النصوص والصور ومقاطع الفيديو. تتدفق هذه البيانات بسرعة كبيرة، لذا يصعب تسجيلها أو تخزينها أو تحليلها باستخدام أنظمة إدارة قواعد البيانات التقليدية، وهذا يستدعي منا استخدام تقنيات متقدمة للتعامل معها. تتميز البيانات الضخمة بخمس سمات رئيسية يشار لها اختصارًا بـ 5Vs، وهي: الحجم Volume: الذي يشير لكمية البيانات الإجمالية، والذي قد يصل إلى عشرات التيرابايت TeraByte السرعة Velocity: وتشير لسرعة توليد البيانات ومعالجتها وتحليلها التنوع Variety: ويشمل بيانات بأشكال مختلفة الدقة Veracity: وتحدد مدى موثوقية البيانات وصحتها وخلوها من الأخطاء القيمة Value: فتراكم البيانات وحده لا يخلق قيمة للبيانات بل يجب أن توفر هذه البيانات فائدة فعلية للعمل وإلا فلا داعي لتكديسها تُستخدم البيانات الضخمة في العديد من المجالات لتحسين العمليات واتخاذ القرارات الذكية؛ ففي الرعاية الصحية، تساعد في تقديم رعاية مخصصة واكتشاف الأمراض مبكرًا؛ أما في تجارة التجزئة، فتُستخدم لتحليل سلوك العملاء وتقديم توصيات مخصصة وتحسين إدارة المخزون؛ وفي القطاع المالي، تساهم في كشف الاحتيال وتقييم المخاطر الائتمانية؛ أما في قطاع الطاقة، فهي تُستخدم للتنبؤ بالطلب وتحسين كفاءة التوزيع والاستهلاك؛ وفي المدن الذكية، تعزز كفاءة الخدمات عبر تحليل البيانات المتعلقة بالمرور وإدارة الموارد وصيانة البنية التحتية. مصادر البيانات الضخمة قد تكون البيانات الضخمة بأشكال متنوعة ومن مصادر عديدة تشمل: البيانات المنظمة Structured Data على شكل جداول يسهل البحث فيها وتحليلها باستخدام استعلامات محددة تشبه جداول إكسل وقواعد البيانات العلاقية Relational Database، ومن الأمثلة عليها البيانات التي نجمعها من التطبيقات والمواقع الإلكترونية كسجلات المستخدمين أو سجلات المعاملات البنكية. البيانات غير المنظمة Unstructured Data من الصعب هيكلتها ضمن جداول، مثل مقاطع الفيديو والتغريدات ومنشورات وسائل التواصل الاجتماعي، أو بيانات أجهزة إنترنت الأشياء IoT، وهي تحتوي على قيمة تحليلية عالية، لكنها أصعب في التحليل وتحتاج لأساليب متقدمة لتخزين وتحليل البيانات. البيانات شبه المنظمة Semi-Structured Data وهي نوع هجين يحتوي على بعض التنظيم، ولكنه لا يتناسب تمامًا مع قواعد البيانات التقليدية كالبيانات التي تجمعها أجهزة الاستشعار ورسائل البريد الإلكتروني وملفات XML وJSON. أدوات التعامل مع البيانات الضخمة عند اعتماد البيانات الضخمة في بيئة العمل، سنحتاج لاستخدام العديد من الأدوات الاحترافية والمتخصصة للتعامل مع حجمها الهائل وتعقيدها. يمكن تقسيم هذه الأدوات إلى أربع أنواع رئيسية نوضحها فيما يلي: أدوات التخزين يجب أن تتمتع أنظمة تخزين البيانات الضخمة بالقدرة على استيعاب كميات ضخمة من البيانات مع إمكانية التوسع مع مرور الوقت وضمان سرعة نقل البيانات إلى أنظمة المعالجة والتحليل. ومن أبرز التقنيات المستخدمة في هذا المجال منصات تخزين البيانات الموزعة مثل Hadoop HDFS وAmazon S3. أدوات التنقيب في البيانات تهدف إلى استخراج أنماط ورؤى ذات قيمة من كم هائل من البيانات، مما يمكن المؤسسات من التنبؤ بالاتجاهات المستقبلية واتخاذ قرارات أدق. من بين الأدوات المستخدمة في التنقيب في البيانات Apache Spark MLlib وGoogle BigQuery، والتي تعتمد على تقنيات الذكاء الاصطناعي وتعلم الآلة. أدوات تنظيف البيانات قبل تحليل البيانات، يجب تنظيفها والتأكد من خلوها من الأخطاء وتوحيدها قبل معالجتها. تستخدم في هذه المرحلة أدوات مثل Trifacta و OpenRefine لضمان دقة البيانات وتحسين جودتها. أدوات تحليل البيانات بعد تنظيف البيانات، سنحتاج لمعالجتها وتحليلها لاستخلاص رؤى واضحة حول الأنماط والاتجاهات المستقبلية. سنحتاج لأدوات مختلفة لإجراء التحليل مثل Apache Kafka وTableau وPower BI لعرض البيانات بطريقة مرئية تدعم اتخاذ القرارات وتساعدنا على الإجابة على أسئلة معينة حول هذه البيانات وكما نلاحظ، فالعملية ليست بالسهولة التي قد تبدو عليها، إذ يتطلب التعامل مع البيانات الضخمة استخدام عدة أدوات وتقنيات، كما يحتاج لبنية تحتية قوية، ووجود خبرات بشرية متخصصة في هذا المجال، وسنواجه كذلك عدة تحديات أخرى سنناقشها في فقرة لاحقة يجب أخذها بالحسبان قبل أن نقرر جمعها والاعتماد عليها في أعمالنا. حالات استخدام البيانات الضخمة تتيح البيانات الضخمة للشركات والمؤسسات إمكانية جمع ومعالجة أحجام هائلة من البيانات في الوقت الفعلي، لكن البيانات الضخمة التي تجمعها الشركات بحد ذاتها ليس الهدف، فإن لم تكن لدى الشركة أهداف فعلية للاستفادة من هذه البيانات، فلا جدوى من جمعها وتكديسها لديها، وفيما يلي بعض حالات استخدام البيانات الضخمة المفيدة: كشف الاتجاهات والأنماط في سلوك العملاء من أجل توقع احتياجاتهم المستقبلية وتلبيتها تحليل تفضيلات العملاء وسلوكهم الشرائي لتخصيص المنتجات وفقًا لاحتياجاتهم مما يعزز رضاهم ويساهم في زيادة المبيعات المرونة والتكيف مع تغيرات السوق بسرعة والعمل على تحسين المنتجات الحالية أو تطوير منتجات جديدة اتخاذ قرارات استراتيجية في العمل وإيجاد فرص نمو جديدة تحديد نقاط الضعف في العمل واكتشاف الجوانب التي يمكن تحسينها، مثل تقليل التكاليف أو تحسين العمليات التشغيلية تقييم المخاطر والتعرف على التهديدات المحتملة في العمل بوقت مبكر وقبل أن تفع فعلًا، ووضع استراتيجيات فعالة لإدارتها والتخفيف من آثارها التنبؤ في الوقت الفعلي كأن نتوقع متى وأين سيزيد الطلب على منتج معين لتلبية احتياجات السوق وضمان وصوله للعملاء في الوقت المناسب فإذا لم تكن أولويات العمل تحتاج لتحليل سلوك العملاء، واتخاذ قرارات استراتيجية بناءً عليها، فلا داعي لتخزين البيانات بكميات ضخمة وتكبد وقت وجهد وتكلفة في تخزينها ومعالجتها. تحديات التعامل مع البيانات الضخمة ينشأ عن الاعتماد على البيانات الضخمة في العمل عدة تحديات وعوائق يجب الانتباه لها ومن أهمها: الخصوصية والأمان يمكن أن تصبح الكمية الضخمة من البيانات في المؤسسات هدفًا سهلًا للتهديدات الأمنية لاحتوائها على معلومات حساسة؛ فمع تزايد حجم البيانات، سيزداد خطر اختراقها وتسريبها، لذا يتوجب على الشركات الحفاظ على أمن بياناتها من خلال المصادقة المناسبة، والتشفير القوي والامتثال لمعايير صارمة لحمايتها. نمو البيانات بسرعة إن نمو البيانات بمعدل سريع يجعل من الصعب استخلاص الرؤى منها؛ فهناك المزيد والمزيد من البيانات التي يتم إنتاجها كل ثانية. ومن بين هذه البيانات، يجب انتقاء البيانات ذات الصلة والمفيدة فعليًا لتحليلها، كما يصعب على المؤسسات تخزين وإدارة كمية كبيرة من البيانات بدون الأدوات والتقنيات المناسبة. سرعة المعالجة بعض التطبيقات مثل أنظمة كشف الاحتيال تتطلب منا تحليل البيانات في الوقت الفعلي، وبالتالي سنحتاج لاستخدام تقنيات مخصصة، مثل Apache Flink و Spark Streaming لضمان الاستجابة الفورية بالوقت المناسب دون أي تأخير مشكلات جودة البيانات لا يمكن أن تكون البيانات الضخمة دقيقةً بنسبة 100٪، كما قد تحتوي على بيانات مكررة أو غير مكتملة؛ إضافةً إلى وجود ضوضاء وأخطاء عديدة فيها، لذا إن لم ننظف هذه البيانات ونجهز جيدًا فستؤدي إلى تحليلات غير دقيقة واستنتاجات خاطئة في العمل صعوبة دمج وتكامل البيانات تستورد فلبيانات من مصادر مختلفة وبصيغ متعددة، وقد لا تكون البيانات من مصدر معين محدثة مقارنةً مع البيانات من مصدر آخر. نقص الكفاءات المتخصصة يتطلب تحليل البيانات الضخمة مهارات تقنية متقدمة، مثل علم البيانات وتحليل البيانات، وعدد المتخصصين في هذا المجال لا يزال محدودًا لا سيما عربيًا. تحديات إدارية وتنظيمية قد تظهر بعض التحديات الإدارية والتنظيمية خلال التعامل مع الكم الهائل للبيانات الضخمة، مثل ضعف الحوكمة وعدم وجود سياسات وآليات واضحة لضبط جمع بيانات الأشخاص، وعدم وجود توضيحات كافية لطريقة استخدامها تضمن التعامل معها بأمان؛ لذا وقبل أن نقرر التعامل مع البيانات الضخمة، يجب أن تطرح على نفسنا السؤال التالي: هل نحن بحاجة فعلًا لكل هذا الكم الكبير من البيانات وكل هذه التحليلات المعقدة لنجاح عملنا، أم أن البيانات التقليدية ـأي البيانات الصغيرة والمتوسطة الحجم- وحدها تكفينا لتسيير أمور العمل بنجاح واستقرار. ما هي البيانات التقليدية؟ تشير البيانات التقليدية إلى البيانات المهيكلة التي يمكن تخزينها على هيئة جداول مكونة من أسطر وأعمدة بهيكلية واضحة ومنظمة، مثل معلومات العملاء وقوائم المخزون والسجلات المالية. وتعتمد معالجة البيانات في هذه الأنظمة على الأساليب الإحصائية التقليدية والأدوات، مثل لغة الاستعلامات الهيكلية SQL للبحث عن المعلومات واسترجاعها؛ فباستخدام هذه الأدوات، يمكن للشركات اتخاذ قرارات مفيدة وتحسين أدائها؛ وعلى الرغم من أن هذه البيانات المنظمة سهلة في التعامل، إلا أنها تكون قد محدودةً في حال احتجنا لتقديم رؤى متطورة ومعقدة تخص بعض الأعمال، مثل الحاجة لنظام مراقبة ذكي يراقب الأسواق المالية ويكشف الاحتيال في المعاملات البنكية فورًا. أدوات التعامل مع البيانات التقليدية البيانات التقليدية أسهل في التعامل من البيانات الضخمة لأنها غالبًا ما تكون صغيرة الحجم ومنظمةً جيدًا ومن أبرز أدوات البيانات التقليدية نذكر: قواعد البيانات العلائقية RDBM مثل MySQL و Oracle و SQL Server، والتي تستخدم لتخزين البيانات في جداول مترابطة، وهي مثالية للبيانات المنظمة والمهيكلة جداول البيانات Spreadsheets مثل إكسل Excel أو جداول جوجل Google Sheets لتحليل البيانات الصغيرة والمتوسطة خاصة عندما تكون البيانات محدودة ومنظمة برامج إدارة البيانات مثل SQLite التي تستخدم لتخزين وإدارة البيانات الصغيرة وتوفر لنا القدرة على إجراء بعض العمليات البسيطة على البيانات حالات استخدام البيانات التقليدية إذا كانت البيانات التي نجمعها في العمل هي بيانات منظمة مثل قواعد البيانات الجاهزة، فإن الحلول التقليدية مثل قواعد البيانات العلائقية غالبا ستكون كافيةً لإدارتها وتحليلها ولا داعي لأن نرهق أنفسنا بتعقيدات البيانات الضخمة. وفيما يلي بعض الحالات التي تعد فيها البيانات التقليدية هي الأنسب للاستخدام: إدارة السجلات المالية ضمن نظام يتعامل مع كمية محدودة من بيانات الفواتير والمدفوعات ويصدر تقارير مالية منظمة إدارة المخزون في متاجر صغيرة أو متوسطة تتضمن بيانات عن المنتجات والمبيعات ومستويات المخزون وتسعى لتحسين استراتيجيات المبيعات باستخدام قواعد بيانات تقليدية الرعاية الصحية ضمن مستشفيات تنظم وتدير معلومات المرضى، مع تتبع تاريخ المرضى ونتائج فحوصاتهم وتتابع خطط علاجهم وترقب التقدم في حالتهم الصحية إدارة الموظفين في الشركات الصغيرة والمتوسط باستخدام أنظمة تقليدية لتخزين المعلومات الشخصية للموظفين وجداول حضورهم وأدائهم إدارة المنصات التعليمية كالمدارس والمعاهد التعليمية التي تحتاج لإدارة بيانات الطلاب وتخزين درجاتهم وتتبع حضورهم ومستوياتهم الدراسية في كل هذه الحالات والحالات المشابهة، سيكون حجم البيانات محدودًا نسبيًا، أو ستكون البيانات منظمةً جيدًا، مما يسهّل معالجتها باستخدام الأنظمة التقليدية دون الحاجة إلى تقنيات معقدة كتلك التي تتطلبها البيانات الضخمة؛ أما في حال زاد تعقيد البيانات سواءً من حيث الحجم أو السرعة أو تنوع المصادر وكانت هناك حاجة لتحليلها بدقة واتخاذ قرارات بناءً عليها بسرعة، فعندها سيكون اللجوء إلى تقنيات البيانات الضخمة أمرًا ضروريًا. مميزات التعامل مع البيانات التقليدية يتسم التعامل مع البيانات التقليدية بما يلي: السهولة يمكن التعامل مع البيانات المعالجة باستخدام الأساليب التقليدية بسهولة باستخدام الأدوات القياسية، مما يجعلها أكثر سهولةً في التعامل ولا تتطلب معرفةً تقنيةً متقدمة. الوصول السريع للبيانات تقدم قواعد البيانات التقليدية وصولًا سريعًا وموثوقًا إلى البيانات من خلال العمل المستقل على خادم أو حاسوب محلي لا على بيئات سحابية أو خارجية، وتتجاوز مشكلات تأخير الشبكة أو انقطاع الخدمة أو اختراقات الأمان. سهولة حماية البيانات تُعَد البيانات الصغيرة والمتوسطة أسهل في تأمينها وحمايتها نظرًا لصغر حجمها وعدم اعتمادها على الهيكلية الموزعة، خاصةً وأنها لا تعتمد في أغلب الأحيان على التخزين الخارجي، مما يجعلها مناسبةً لإدارة المعلومات الحساسة أو السرية. سهولة إدارة البيانات تقدم قواعد البيانات التقليدية للمستخدمين تحكمًا كبيرًا في إدارة البيانات، حيث يمكن للمستخدمين تعريف أنواع البيانات، وتحديد القواعد بينها، وإنشاء العلاقات المخصصة وفقًا لاحتياجاتهم. تتطلب تكاليف وموارد أقل تتطلب الأساليب التقليدية تكاليف أقل وموارد أقل مقارنة بأنظمة معالجة البيانات الضخمة التي تتطلب تكاليف وموارد ضخمة. الخلاصة أصبحت البيانات الضخمة بلا شك جزءًا أساسيًا من العمليات التجارية والخدمات الحكومية، نظرًا للنمو السريع في حجم البيانات الرقمية. ومع تطور تقنيات الذكاء الاصطناعي، أتاح ذلك للمؤسسات القدرة على استخراج رؤى أكثر دقة واتخاذ قرارات أكثر ذكاءً وتحسينات كبيرة في مختلف القطاعات؛ لكن مع ذلك، ينبغي أن نتذكر أن البيانات الضخمة ليست مجرد جمع كميات هائلة من المعلومات، بل هي عملية تتطلب استراتيجيات مدروسة، وأدوات متخصصة، وتخطيطًا دقيقًا لتحقيق أقصى استفادة منها وتحويلها إلى قرارات تدعم النجاح والنمو. مع ذلك، وبالرغم من هذا التطور الكبير في مجال البيانات الضخمة، لازالت البيانات التقليدية الصغيرة والمتوسطة كافية للعديد من الحالات إن لم نقل أغلبها نظرًا لتوفير هذا النوع من البيانات حلولًا مناسبةً سهلة التعامل وبتكاليف أقل. لذا، متى لم يكن ما نعمل عليه يتطلب بيانات ضخمة للحصول على تحليلات وفهم كاف، فلا حاجة لأن نلجأ في أعمالنا لحلول البيانات الضخمة، لاسيما أنها تحتاج لخبرة وتكاليف أكبر وتعقيدات تقنية للتعامل مع البيانات وتحليلها وإدارتها، خاصةً وأننا إذا لم نحسن التعامل مع هذه البيانات ونستخدمها بطريقة صحيحة، فلن نتمكن من فهمها أو اتخاذ قرارات صائبة بناءً عليها. المصادر ?What is Big Data When to Use Big Data — and When Not To 7‎ Pros and Cons of Big Data Difference Between Traditional Data and Big Data Big Data Analytics Versus Traditional Data Analytics: A Comprehensive Overview اقرأ أيضًا مقدمة إلى مفهوم البيانات الضخمة Big Data المفاهيم الأساسية لتعلم الآلة أساسيات الذكاء الاصطناعي: دليل المبتدئين تعلم الذكاء الاصطناعي
  10. سنوضّح في هذا المقال من سلسلة دليل جودو كيفية إطلاق المقذوفات وترتيب عرض الكائنات بناءً على موقعها على محور Y في الألعاب ثنائية الأبعاد في محرك الألعاب الشهير جودو Godot. إطلاق المقذوفات من أجل إطلاق مقذوفات، سنحتاج بطبيعة الحال إلى القيام بعدة خطوات تسمح لنا بتنفيذ العملية بنجاح. سنوضّح فيما يلي كيفية إطلاق المقذوفات من اللاعب أو الشخصية المتوحشة وغير ذلك في الألعاب ثنائية الأبعاد. إعداد الرصاصة سنضبط أولًا كائن الرصاصة Bullet الذي يمكننا إنشاء نسخة منه، وفيما يلي العقد التي سنستخدمها: Area2D: Bullet Sprite2D CollisionShape2D يمكن استخدام أي صورة نريدها بالنسبة لخامة العقدة Sprite2D كما هو الحال في المثال التالي: يمكن الآن إعداد العقد وضبط الشخصية الرسومية Sprite وشكل التصادم. إذا كانت الخامة الخاصة بنا موجهةً نحو الأعلى كما هو الحال في المثال السابق، فسنتأكد من تدوير العقدة Sprite بمقدار 90 درجة بحيث تشير إلى اليمين. سنتأكد أيضًا من أنها تتطابق مع الاتجاه الأمامي للعقدة الأب. سنضيف الآن سكربت اتصل من خلاله بالإشارة body_entered الخاصة بالعقدة Area2D كما يلي: extends Area2D var speed = 750 func _physics_process(delta): position += transform.x * speed * delta func _on_Bullet_body_entered(body): if body.is_in_group("mobs"): body.queue_free() queue_free() سنزيل الرصاصة في مثالنا إذا أصابت شيئًا ما، وسنحذف أيّ شيء نشير إليه في مجموعة المتوحشين "mobs"، والذي تصيبه الرصاصة. إطلاق النار يجب إعداد موقع ظهور الرصاصات، لذا من المهم إضافة العقدة Marker2D ووضعها في المكان الذي نريد ظهور الرصاصات فيه، حيث وضعناه عند فوهة البندقية مثلًا، وسميناه بالفوهة "Muzzle": نلاحظ بقاء التحويل transform الخاص بالفوهة موجَّهًا مع اتجاه البندقية نفسه عند دوران اللاعب، وسيكون ذلك مناسبًا عند ظهور الرصاصات، حيث يمكن استخدام هذا التحويل للحصول على الموضع والاتجاه المناسبين. سلنضبط الآن التحويل transform الجديد الخاص بالرصاصة ليكون مساويًا لتحويل الفوهة. ملاحظة: ستنجح هذه الطريقة مع أيّ نوع من الشخصيات، وليس فقط مع أسلوب الدوران والتحريك الموضّح في هذا المقال، فما علينا سوى إرفاق العقدة Marker2D حيث نريد ظهور الرصاصات. سنضيف الآن متغيرًا في سكربت الشخصية للاحتفاظ بمشهد الرصاصة لإنشاء نسخة منه كما يلي: @export var Bullet : PackedScene وتحقق أيضًا من إجراء الإدخال المُعرَّف كما يلي: if Input.is_action_just_pressed("shoot"): shoot() يمكننا الآن إنشاء نسخة من الرصاصة وإضافتها إلى الشجرة في الدالة shoot()‎، ولكن تُعَد إضافة الرصاصة كابن للاعب من الأخطاء الشائعة: func shoot(): var b = Bullet.instantiate() add_child(b) b.transform = $Muzzle.transform تكمن المشكلة في تأثّر الرصاصات عندما يتحرك اللاعب أو يدور لأنها أبناء للاعب. يمكن إصلاح هذه المشكلة من خلال التأكد من إضافة الرصاصات إلى المستوى العالمي بدلًا من ذلك، حيث سنستخدم owner في هذه الحالة، والذي يشير إلى العقدة الجذر للمشهد الذي يتواجد فيه اللاعب. وكما نلاحظ، سنحتاج أيضًا إلى استخدام التحويل العام الخاص بالفوهة، وإلّا لن تكون الرصاصة موجودةً في المكان الذي نتوقعه. ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. ترتيب عرض الكائنات بناء على موقعها على محور Y تستخدم العديد من الألعاب ثنائية الأبعاد منظور 3/4، مما يعطي الانطباع بأن الكاميرا تنظر إلى العالم من زاوية معينة، لذا يجب عرض الكائنات الأبعد خلف الكائنات الأقرب، ويعني ذلك عمليًا أننا نريد ترتيب عرضها بناءً على موقعها على محور Y، مما يجعل ترتيب الرسوم مرتبطًا بإحداثيات الكائن على المحور Y، وكلما كان الكائن أعلى على الشاشة، سيكون أبعد؛ وبالتالي سيكون أخفض وفق ترتيب التصيير Render؛ وفيما يلي مثال لهذه المشكلة: تُرسَم هذه الكائنات وفقًا لترتيب التصيير الافتراضي، والذي هو ترتيب الشجرة، حيث يكون ترتيبها في شجرة المشهد كما يلي: يحتوي جودو Godot على خيارٍ مُضمَّن لتغيير ترتيب التصيير، حيث يمكننا تفعيل الخاصية Y Sort Enabled مع أيّ عقدة CanvasItem مثل Node2D أو Control، ثم يمكن فرز جميع العقد الأبناء بناءً على موقعها على محور Y. يمكن تفعيل هذه الخاصية مع عقدة TileMap في المثال السابق، ولكن ستبقى المشكلة موجودةً كما يلي: يعتمد ترتيب الرسم على إحداثيات المحور Y لكل كائن، حيث يكون مركز الكائن افتراضيًا كما هو موضح في الشكل التالي: نريد الآن إعطاء انطباع بأن الكائنات موجودة على الأرض، لذا يمكن حل هذه المشكلة من خلال إزاحة الشخصية الرسومية Sprite لكل كائن بحيث يحاذي موضعُ position الكائن أسفلَ الشخصية الرسومية كما يلي: وبهذا تكون الأمور قد أصبحت أفضل بكثير كما يلي: ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. ترجمة -وبتصرّف- للقسمين Shooting projectiles و Using Y-Sort من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إنشاء وحدة تحكم واقعية للسيارات في ألعاب الفيديو ثنائية الأبعاد آلية الالتفاف حول الشاشة وتحريك الشخصيات في الألعاب ثنائية الأبعاد إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد
  11. سنوضّح في هذا المقال كيفية إنشاء وحدة تحكم للسيارات في الألعاب ثنائية الأبعاد من الأعلى إلى الأسفل. قد لا يتمكّن المبتدئون من إنشاء شيءٍ يتعامل مع لعبةٍ مشابهة لسيارة حقيقية، لذا سنذكر بعض الأخطاء الشائعة التي قد تظهر في ألعاب السيارات للهواة: لا تدور السيارة حول مركزها، إذ لا تنزلق Slide عجلات السيارة الخلفية من جانب إلى آخر إلّا في حالة الانجراف Drifting، وسنتحدث عن ذلك لاحقًا لا يمكن للسيارة أن تستدير إلا عندما تتحرك، إذ لا يمكنها الدوران في مكانها السيارة ليست قطارًا، فهي ليست على قضبان سكة حديدية، لذا يجب أن تتضمن الاستدارة بسرعات عالية بعض الانزلاق أو الانجراف توجد العديد من الأساليب لفيزياء السيارات ثنائية الأبعاد، وتعتمد بصفة أساسية على مدى الواقعية التي نريد أن تكون عليها، لذا نريد الوصول في هذا المقال إلى مستوى معين من الواقعية. ملاحظة: تعتمد الطريقة التي سنوضّحها فيما يلي على الخوارزمية الموجودة في مقال فيزياء توجيه السيارات في الألعاب ثنائية الأبعاد البسيطة. تُقسَم الطريقة المتبعة فيما يلي إلى 5 أجزاء، حيث يضيف كل جزء منها ميزة مختلفة لحركة السيارة، لذا يمكن المزج بينها وفقًا لاحتياجاتنا. إعداد المشهد فيما يلي إعداد لمشهد السيارة: CharacterBody2D Sprite2D CollisionShape2D Camera2D سنضيف أي خامة شخصية رسومية Sprite Texture نريدها، حيث سنستخدم في مثالنا حزمة السباق من موقع Kenney. تُعَد العقدة CapsuleShape2D خيارًا جيدًا للتصادم، بحيث لا تكون للسيارة زوايا حادة قد تتسبب في تعطلها بسبب العوائق. سنستخدم أيضًا أربعة إجراءات إدخال هي: steer_right و steer_left و accelerate و brake، لذا يمكننا ضبطها على أيّ مفاتيح إدخال نفضلها. الجزء الأول: الحركة تتمثل الخطوة الأولى في برمجة الحركة بناءً على الخوارزمية السابقة، لذا سنبدأ ببعض المتغيرات كما يلي: extends CharacterBody2D var wheel_base = 70 # المسافة من العجلة الأمامية إلى العجلة الخلفية var steering_angle = 15 # مقدار دوران العجلة الأمامية بالدرجات var steer_direction نضبط المتغير wheelbase على قيمة تتوافق مع شخصيتنا الرسومية، ويمثّل المتغير steer_direction مقدار دوران العجلات. ملاحظة: نستخدم في مثالنا عناصر تحكم لوحة المفاتيح، لذا سيحدث الدوران أو لن يحدث على الإطلاق، ولكن إذا كنا تستخدم عصا تحكم للعب، فيمكننا بدلًا من ذلك تغيير هذه القيمة بناءً على المسافة التي تتحركها العصا. func _physics_process(delta): get_input() calculate_steering(delta) move_and_slide() يجب التحقق من الإدخال وحساب التوجيه Steering في كل إطار، ثم نمرّر السرعة velocity الناتجة إلى الدالة move_and_slide()‎، ونعرّف الدالتين التاليتين كما يلي: func get_input(): var turn = Input.get_axis("steer_left", "steer_right") steer_direction = turn * deg_to_rad(steering_angle) velocity = Vector2.ZERO if Input.is_action_pressed("accelerate"): velocity = transform.x * 500 سنتحقق من إدخال المستخدم ونضبط السرعة. وكما نلاحظ فقيمة السرعة 500 مؤقتة حتى نتمكّن من اختبار الحركة، وسنعالجها في الجزء التالي. سننفّذ بعد ذلك الخوارزمية السابقة كما يلي: func calculate_steering(delta): # 1. العثور على مواضع العجلات var rear_wheel = position - transform.x * wheel_base / 2.0 var front_wheel = position + transform.x * wheel_base / 2.0 # 2. تحريك العجلات للأمام rear_wheel += velocity * delta front_wheel += velocity.rotated(steer_direction) * delta # 3. العثور على متجه الاتجاه الجديد var new_heading = rear_wheel.direction_to(front_wheel) # 4. ضبط السرعة والدوران على الاتجاه الجديد velocity = new_heading * velocity.length() rotation = new_heading.angle() سنشغّل الآن المشروع ويجب أن تتحرك وتدور السيارة، ولكن لا تزال الحركة غير طبيعية؛ إذ ستبدأ السيارة بالحركة وتتوقف مباشرةً، ويمكن إصلاح ذلك من خلال إضافة التسارع إلى العملية الحسابية. الجزء الثاني: التسارع Acceleration سنحتاج إلى متغير إعدادٍ آخر ومتغيرًا لتتبّع التسارع الكلي للسيارة كما يلي: var engine_power = 900 # قوة التسارع للأمام var acceleration = Vector2.ZERO علينا الآن التعديل شيفرة الإدخال البرمجية لتطبيق التسارع بدلًا من تغيير سرعة velocity السيارة مباشرةً كما يلي: func get_input(): var turn = Input.get_axis("steer_left", "steer_right") steer_direction = turn * deg_to_rad(steering_angle) if Input.is_action_pressed("accelerate"): acceleration = transform.x * engine_power يمكن تطبيق التسارع على السرعة بعد الحصول عليه كما يلي: func _physics_process(delta): acceleration = Vector2.ZERO get_input() calculate_steering(delta) velocity += acceleration * delta move_and_slide() يجب الآن أن تزيد السيارة من سرعتها تدريجيًا عند تشغليها، ولكن ليس لدينا أيّ طريقة لإبطاء السرعة بعد. الجزء الثالث: الاحتكاك Friction/السحب Drag تتعرض السيارة لقوتين مختلفتين لإبطاء السرعة هما: الاحتكاك والسحب. الاحتكاك هو القوة التي تطبقها الأرض، وتكون مرتفعةً جدًا عند القيادة على الرمال، ومنخفضةً جدًا عند القيادة على الجليد، ويتناسب الاحتكاك مع السرعة، فكلما زادت السرعة، زادت هذه القوة؛ أما السحب، فهو القوة الناتجة عن مقاومة الرياح، ويعتمد على المقطع العرضي للسيارة، إذ تتعرض الشاحنة الكبيرة أو الشاحنة الصغيرة لسحب أكبر من سيارة السباق، ويتناسب السحب مع مربع السرعة. يكون الاحتكاك ملحوظًا عند التحرك ببطء، ولكن يغلب السحب عند السرعات العالية. سنضيف كلتا هاتين القوتين إلى العمليات الحسابية الخاصة بنا. ستمنح قيم هاتين القوتين سيارتنا أقصى سرعة، وهي النقطة التي لا تستطيع فيها قوة المحرّك التغلب على قوة السحب. سنوضح فيما يلي القيم الابتدائية لهاتين الكميتين: var friction = -55 var drag = -0.06 تعني هذه القيم أن قوة السحب تتغلب على قوة الاحتكاك عند السرعة 600 كما نرى في هذا الرسم البياني: ملاحظة: يمكن تعديل هذه القيم لمعرفة كيفية تغيرها. سنستدعي في الدالة ‎_physics_process()‎ دالةً لحساب الاحتكاك الحالي ونطبقّه على قوة التسارع كما يلي: func _physics_process(delta): acceleration = Vector2.ZERO get_input() apply_friction(delta) calculate_steering(delta) velocity += acceleration * delta velocity = move_and_slide(velocity) func apply_friction(delta): if acceleration == Vector2.ZERO and velocity.length() < 50: velocity = Vector2.ZERO var friction_force = velocity * friction * delta var drag_force = velocity * velocity.length() * drag * delta acceleration += drag_force + friction_force سنحدد أولًا السرعة الدّنيا، مما يضمن عدم استمرار السيارة في التحرك للأمام بسرعات منخفضة للغاية، لأن الاحتكاك لا يؤدي إلى خفض السرعة إلى الصفر أبدًا؛ ونحسب بعد ذلك القوتين ونضيفهما إلى التسارع الكلي، وستؤثران على السيارة في الاتجاه المعاكس لأن قيمتهما سالبة. 02_car_friction.webm الجزء الرابع: الرجوع للخلف/الفرامل Brake نحتاج إلى متغيرين آخرين للإعدادات: var braking = -450 var max_speed_reverse = 250 سنضيف الآن الدخل إلى الدالة get_input()‎ كما يلي: if Input.is_action_pressed("brake"): acceleration = transform.x * braking يُعَد ذلك جيدًا للتوقف، ولكننا نريد أيضًا أن نتمكن من وضع السيارة في وضع الرجوع للخلف، ولكن ذلك لن ينجح حاليًا، لأن التسارع يُطبَّق في اتجاه التوجّه دائمًا الذي هو إلى الأمام، لذا نحتاج إلى تسارع عكسي عند الرجوع للخلف. func calculate_steering(delta): var rear_wheel = position - transform.x * wheel_base / 2.0 var front_wheel = position + transform.x * wheel_base / 2.0 rear_wheel += velocity * delta front_wheel += velocity.rotated(steer_angle) * delta var new_heading = (front_wheel - rear_wheel).normalized() var d = new_heading.dot(velocity.normalized()) if d > 0: velocity = new_heading * velocity.length() if d < 0: velocity = -new_heading * min(velocity.length(), max_speed_reverse) rotation = new_heading.angle() يمكننا معرفة ما إذا كنا نتسارع للأمام أم للخلف باستخدام حاصل الضرب النقطي Dot Product، حيث إذا كان المتجهان متحاذيين، فستكون النتيجة أكبر من 0، وإذا كانت الحركة في الاتجاه المعاكس لاتجاه السيارة، فسيكون حاصل الضرب النقطي أقل من 0 ويجب أن نتحرك للخلف. 03_car_reverse.webm الجزء الخامس: الانجراف Drift والانزلاق Slide يمكن التوقف عند هذه النقطة وسنحظى بتجربة قيادة جيدة، ولكن ستبقى السيارة أشبه بأنها تسير على قضبان سكة حديدية، إذ تكون المنعطفات مثالية حتى عند السرعة القصوى، وكأنّ لإطارات السيارة تحكّم مثالي. لحل المشكلة، يجب أن تتسبب قوة الانعطاف عند السرعات العالية أو حتى المنخفضة إذا رغبنا في ذلك، في انزلاق الإطارات مما يؤدي إلى حركة انزلاقية، وسنفعل ذلك الآتي: var slip_speed = 400 # السرعة عند تقليل الشدّ‫ Traction var traction_fast = 2.5 # الشد عالي السرعة var traction_slow = 10 # الشد منخفض السرعة سنطبّق هذه القيم عند حساب التوجيه، إذ تُضبَط السرعة مباشرةً على الاتجاه الجديد حاليًا، ولكننا سنستخدم بدلًا من ذلك الاستيفاء باستخدام الدالة lerp()‎ لجعلها تدور جزئيًا نحو الاتجاه الجديد فقط، وستحدّد قيم الشدّ Traction مدى تماسك الإطارات. func calculate_steering(delta): var rear_wheel = position - transform.x * wheel_base / 2.0 var front_wheel = position + transform.x * wheel_base / 2.0 rear_wheel += velocity * delta front_wheel += velocity.rotated(steer_angle) * delta var new_heading = (front_wheel - rear_wheel).normalized() # اختر قيمة الشد التي تريد استخدامها، ويجب أن يكون الانزلاق منخفضًا عند السرعات المنخفضة var traction = traction_slow if velocity.length() > slip_speed: traction = traction_fast var d = new_heading.dot(velocity.normalized()) if d > 0: velocity = lerp(velocity, new_heading * velocity.length(), traction * delta) if d < 0: velocity = -new_heading * min(velocity.length(), max_speed_reverse) rotation = new_heading.angle() نختار قيمة الشد التي نريد استخدامها ونطبق الدالة lerp()‎ على السرعة velocity. 04_car_drift.webm التعديلات Adjustments لدينا في هذه المرحلة عدد كبير من الإعدادات التي تتحكّم في سلوك السيارة، ويمكن أن يؤدي تعديلها إلى تغيير جذري في كيفية قيادة السيارة. سننزّل المشروع الخاص بهذا المقال لتسهيل تجربة قيم مختلفة. سنرى عند تشغيل اللعبة مجموعةً من أشرطة التمرير التي يمكن استخدامها لتغيير سلوك السيارة أثناء القيادة، ويمكن الضغط على <Tab> لإظهار أو إخفاء لوحة أشرطة التمرير. الخاتمة بهذا نكون قد وصلنا لنهاية مقالنا الذي استعرضنا فيه خطوات إنشاء وحدة تحكم لسيارات الألعاب ثنائية الأبعاد بأسلوب واقعي بدءًا من الحركة الأساسية، مرورًا بالتسارع والفرملة، وصولًا إلى الانجراف والانزلاق. يمكن اعتبار هذا المقال بمثابة أساس لتطوير تجربة قيادة أكثر ديناميكية وواقعية في الألعاب. وللتذكير، يمكن تنزيل شيفرة المشروع البرمجية كاملة من Github. ترجمة -وبتصرّف- للقسم Car steering من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: آلية الالتفاف حول الشاشة وتحريك الشخصيات في الألعاب ثنائية الأبعاد إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد دليلك الشامل إلى برمجة الألعاب ألعاب الفيديو: تطورها وأهميتها وخطوات برمجتها
  12. سنوضّح في هذا المقال كيفية الالتفاف حول الشاشة وبرمجة الحركة من الأعلى إلى الأسفل وتحريك الشخصيات بالاعتماد على الشبكة Grid وفي ثمانية اتجاهات مختلفة في الألعاب ثنائية الأبعاد. آلية الالتفاف حول الشاشة يُعَد السماح للاعب بالالتفاف حول الشاشة والانتقال الفوري من أحد جانبي الشاشة إلى الجانب الآخر ميزةً شائعة، وخاصة في الألعاب ثنائية الأبعاد القديمة مثل لعبة باك مان Pac-man، إذ يمكن السماح للاعب بالالتفاف حول الشاشة من خلال اتباع الخطوات. الخطوة الأولى هي بالحصول على حجم الشاشة أو نافذة العرض Viewport كما يلي: @onready var screen_size = get_viewport_rect().size حيث تتوفر الدالة get_viewport_rect()‎ لأي عقدة مشتقة من CanvasItem. أما ثاني خطوة، فتتمثل في مقارنة موضع اللاعب كما يلي: if position.x > screen_size.x: position.x = 0 if position.x < 0: position.x = screen_size.x if position.y > screen_size.y: position.y = 0 if position.y < 0: position.y = screen_size.y وكما نلاحظ، تم استخدام موضع position العقدة الذي يكون عادةً مركزًا للشخصية الرسومية Sprite و/أو مركز الجسم. ثالث خطوة هي عبر تبسيط ما سبق باستخدام الدالة wrapf()‎، إذ يمكن تبسيط الشيفرة البرمجية السابقة باستخدام الدالة wrapf()‎ في لغة GDScript، والتي تكرّر القيمة بين الحدود المحدّدة. position.x = wrapf(position.x, 0, screen_size.x) position.y = wrapf(position.y, 0, screen_size.y) برمجة الحركة من الأعلى إلى الأسفل إذا أردنا إنشاء لعبة ثنائية الأبعاد من الأعلى إلى الأسفل، فيجب أن تتحكم في حركة الشخصية، لذا لنفترض تحديد إجراءات الإدخال التالية: اسم الإجراء المفتاح أو مجموعة المفاتيح "up" المفتاح W أو ↑ "down" المفتاح S أو ↓ "right" المفتاح D أو → "left" المفتاح A أو ← "click" زر الفأرة 1 سنفترض أيضًا أننا نستخدم عقدة CharacterBody2D. هنا سيكون بإمكاننا أيضًا التحكم في حركة الشخصية باستخدام طرق متعددة اعتمادًا على نوع السلوك الذي تبحث عنه كما سنوضح فيما يلي. الخيار الأول: الحركة في 8 اتجاهات يستخدم اللاعب في هذه الحالة مفاتيح الاتجاهات الأربعة للتحرك (بما في ذلك الاتجاهات القطرية) كما يلي: extends CharacterBody2D var speed = 400 # السرعة بالبكسلات في الثانية func _physics_process(delta): var direction = Input.get_vector("left", "right", "up", "down") velocity = direction * speed move_and_slide() الخيار الثاني: التدوير والتحريك تدوّر الإجراءات لليسار/لليمين "left/right" في هذه الحالة الشخصية وتحرّك الإجراءات للأعلى/للأسفل الشخصية للأمام وللخلف في أيّ اتجاه تواجهه، ويُشار إلى ذلك أحيانًا باسم "الحركة التي تحاكي حركة الكويكبات". extends CharacterBody2D var speed = 400 # سرعة الحركة بالبكسل/ثانية var rotation_speed = 1.5 # سرعة الدوران بالراديان/ثانية func _physics_process(delta): var move_input = Input.get_axis("down", "up") var rotation_direction = Input.get_axis("left", "right") velocity = transform.x * move_input * speed rotation += rotation_direction * rotation_speed * delta move_and_slide() ملاحظة: يَعُد محرّك ألعاب جودو Godot أن الزاوية 0 درجة تؤشّر على طول المحور x، مما يعني أن اتجاه العقدة للأمام (transform.x) يتجه إلى اليمين، لذا يجب التأكد من رسم الشخصية الرسومية لتشير إلى اليمين. الخيار الثالث: التصويب بالفأرة يُعَد هذا الخيار مشابهًا للخيار الثاني، ولكننا نتحكم في دوران الشخصية باستخدام الفأرة هذه المرة، أي أنّ الشخصية تشير دائمًا إلى الفأرة، وتُطبَّق الحركة للأمام/للخلف باستخدام المفاتيح كما في السابق. extends CharacterBody2D var speed = 400 # سرعة الحركة بالبكسل/ثانية func _physics_process(delta): look_at(get_global_mouse_position()) var move_input = Input.get_axis("down", "up") velocity = transform.x * move_input * speed move_and_slide() الخيار الرابع: النقر والتحريك تنتقل الشخصية في هذا الخيار إلى الموقع الذي نقرنا عليه. extends CharacterBody2D var speed = 400 # سرعة الحركة بالبكسل/ثانية var target = null func _input(event): if event.is_action_pressed("click"): target = get_global_mouse_position() func _physics_process(delta): if target: # look_at(target) velocity = position.direction_to(target) * speed if position.distance_to(target) < 10: velocity = Vector2.ZERO move_and_slide() وكما نلاحظ، سنتوقف عن الحركة إذا اقتربنا من موضع الهدف، فإن لم نفعل ذلك، فستهتز الشخصية ذهابًا وإيابًا بحيث تتحرك قليلًا بعد الهدف وتعود، ثم تعاود الكرّة وهكذا. يمكننا اختياريًا استخدام الدالة look_at()‎ لتكون مواجهةً لاتجاه الحركة. ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. تحريك الشخصيات بالاعتماد على الشبكة Grid سنوضّح فيما يلي كيفية تحريك شخصية ثنائية الأبعاد في نمط شبكي؛ إذ تعني الحركة المعتمدة على الشبكة Grid أو المربع Tile أن موضع الشخصية مقيد، ولا يمكنها الوقوف إلا على مربع معين، كما لا يمكنها أن تكون بين مربعين أبدًا. إعداد الشخصية سنوضح فيما يلي العقد التي سنستخدمها للاعب: Area2D (اللاعب "Player"): يمثّل استخدام العقدة Area2D أنه يمكننا اكتشاف التداخل لالتقاط الأشياء أو الاصطدام بالأعداء Sprite2D: يمكن استخدام ورقة الشخصية الرسومية Sprite Sheet هنا، وسنوضّح إعداد الرسوم المتحركة Animations لاحقًا CollisionShape2D: يجب أن لا نجعل مربع الاصطدام كبيرًا جدًا، حيث ستكون التداخلات من عند المركز لأن اللاعب سيقف في منتصف المربع RayCast2D: للتحقق مما إذا كانت الحركة ممكنة في اتجاه محدّد AnimationPlayer: لتشغيل الرسوم المتحركة الخاصة بمشي الشخصية سنضيف هنا بعض إجراءات الإدخال إلى خريطة الإدخال Input Map، ويمكننا لأجل ذلك استخدام إجراءات up وdown وleft، و right في هذا المثال. الحركة الأساسية سنبدأ بإعداد الحركة على المربعات الواحد تلو الآخر دون أي رسوم متحركة أو استيفاء Interpolation. extends Area2D var tile_size = 64 var inputs = {"right": Vector2.RIGHT, "left": Vector2.LEFT, "up": Vector2.UP, "down": Vector2.DOWN} يجب ضبط حجم المربعات tile_size ليتطابق مع حجم المربعات الخاصة بنا، ويمكن ضبطه في مشروع أكبر باستخدام المشهد الرئيسي عند إنشاء نسخة من اللاعب، ولكن سنستخدم مربعات بحجم 64x64 في مثالنا. يربط القاموس inputs أسماء إجراءات الإدخال مع متجهات الاتجاه، لذا يجب التأكّد من كتابة الأسماء نفسها هنا وفي خريطة الإدخال مع مراعاة استخدام الحروف الكبيرة. func _ready(): position = position.snapped(Vector2.ONE * tile_size) position += Vector2.ONE * tile_size/2 تتيح الدالة snapped()‎ تقريب الموضع إلى أقرب زيادة في المربع، وتضمَن إضافة كمية بمقدار نصف مربع أن يكون اللاعب في مركز المربع. func _unhandled_input(event): for dir in inputs.keys(): if event.is_action_pressed(dir): move(dir) func move(dir): position += inputs[dir] * tile_size تمثل الشيفرة البرمجية السابقة الحركة الفعلية. حيث إذا ظهر حدث إدخال، فسنتحقق من الاتجاهات الأربعة لمعرفة أيّ منها يتطابق مع الحدث، ثم نمرّره إلى الدالة move()‎ لتغيير الموضع. الاصطدام Collision يمكننا الآن إضافة بعض العوائق، حيث يمكن استخدام عقد StaticBody2D لإضافة بعض العوائق يدويًا، مع تفعيل الالتقاط للتأكد من محاذاتها مع الشبكة، أو استخدام TileMap مع تحديد الاصطدامات كما هو الحال في المثال التالي، وسنستخدم عقدة RayCast2D لتحديد السماح بالانتقال إلى المربع التالي. onready var ray = $RayCast2D func move(dir): ray.target_position = inputs[dir] * tile_size ray.force_raycast_update() if !ray.is_colliding(): position += inputs[dir] * tile_size إذا غيّرنا الخاصية target_position الخاصة بعقدة RayCast2D، فلن يعيد محرّك الفيزياء حساب تصادماته حتى الإطار الفيزيائي التالي. تتيح الدالة force_raycast_update()‎ إمكانية تحديث حالة الشعاع مباشرةً، حيث إن لم يكن هناك تصادم، فسنسمح بالتحرك. ملاحظة: تتمثل إحدى الطرق الشائعة الأخرى في استخدام أربع عقد RayCast2D من خلال استخدام عقدة لكل اتجاه. تنشيط الحركة وأخيرًا، يمكننا استيفاء الموضع بين المربعات، مما يعطي إحساسًا سلسًا بالحركة، حيث سنستخدم عقدة Tween لتحريك خاصية الموضع position. var animation_speed = 3 var moving = false سنضيف الآن مرجعًا إلى عقدة Tween ومتغيرًا لضبط سرعة الحركة كما يلي: func _unhandled_input(event): if moving: return for dir in inputs.keys(): if event.is_action_pressed(dir): move(dir) سنتجاهل أي إدخال أثناء تشغيل الانتقال التدريجي Tween ونزيل تغيير الموضع position المباشر حتى يتمكّن الانتقال التدريجي من التعامل معه. func move(dir): ray.target_position = inputs[dir] * tile_size ray.force_raycast_update() if !ray.is_colliding(): #position += inputs[dir] * tile_size var tween = create_tween() tween.tween_property(self, "position", position + inputs[dir] * tile_size, 1.0/animation_speed).set_trans(Tween.TRANS_SINE) moving = true await tween.finished moving = false سنجرِّب الآن انتقالات تدريجية مختلفة للحصول على تأثيرات حركية مختلفة. ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github. تحريك الشخصيات في 8 اتجاهات مختلفة سنوضّح فيما يلي كيفية تحريك شخصية ثنائية الأبعاد في 8 اتجاهات مختلفة، حيث سنستخدم في مثالنا شخصية محارب التي تحتوي على رسوم متحركة في 8 اتجاهات لحالات عدم الحركة والجري والهجوم والعديد من الحالات الأخرى. تُنظَّم الرسوم المتحركة ضمن مجلدات، مع وجود صورة منفصلة لكل إطار. سنستخدم العقدة AnimatedSprite2D وسنسمّي الرسوم المتحركة بناءً على اتجاهها. على سبيل المثال، يشير الرسم المتحرك idle0 إلى اليمين، ثم ننتقل باتجاه عقارب الساعة حتى الوصول إلى الرسم المتحرك idle7، وتختار الشخصية عند تحرّكها رسومًا متحركة بناءً على اتجاه الحركة: سنستخدم الفأرة للتحرك، حيث ستواجه الشخصية الفأرة دائمًا وتتحرك في هذا الاتجاه عندما نضغط على زر الفأرة. يمكن اختيار الرسوم المتحركة التي ستعمل من خلال الحصول على اتجاه الفأرة وربطه مع هذا المجال نفسه من 0 إلى 7؛ إذ تعطي الدالة get_local_mouse_position()‎ موضع الفأرة بالنسبة للشخصية، ويمكننا بعد ذلك استخدام الدالة snappedf()‎ لضبط زاوية متجه الفأرة إلى أقرب مضاعف للزاوية 45 درجة (أو PI/4 راديان) مما يعطي النتيجة التالية: سنقسّم كل قيمة على 45 درجة ( أو PI/4 راديان) ونحصل على النتيجة التالية: في الأخير، يجب ربط المجال الناتج مع المجال ‎0-7 باستخدام الدالة wrapi()‎، وسنحصل على القيم الصحيحة، حيث تعطي إضافة هذه القيمة إلى نهاية اسم الرسوم المتحركة ("idle" و "run" وغيرها) الرسم المتحرك الصحيح كما يلي: func _physics_process(delta): current_animation = "idle" var mouse = get_local_mouse_position() angle = snappedf(mouse.angle(), PI/4) / (PI/4) angle = wrapi(int(angle), 0, 8) if Input.is_action_pressed("left_mouse") and mouse.length() > 10: current_animation = "run" velocity = mouse.normalized() * speed move_and_slide() $AnimatedSprite2D.animation = current_animation + str(a) وسنشاهد ما يلي عند اختبار الحركة: الإدخال من لوحة المفاتيح إذا كنا نستخدم عناصر التحكم من لوحة المفاتيح بدلًا من الفأرة، فيمكننا الحصول على زاوية الحركة بناءً على المفاتيح التي نضغط عليها، وتسير بقية العملية باستخدام الطريقة نفسها كما يلي: func _process(delta): current_animation = "idle" var input_dir = Input.get_vector("left", "right", "up", "down") if input_dir.length() != 0: angle = input_dir.angle() / (PI/4) angle = wrapi(int(a), 0, 8) current_animation = "run" velocity = input_dir * speed move_and_slide() $AnimatedSprite2D.play(current_animation + str(angle)) الخاتمة استعرضنا في هذا المقال أساليب مختلفة لتحريك الشخصيات في الألعاب ثنائية الأبعاد باستخدام محرك ألعاب جودو، بما يشمل الالتفاف حول الشاشة، والحركة من الأعلى إلى الأسفل، والحركة الشبكية، والحركة في ثمانية اتجاهات. تساعد هذه الأساليب على تحسين تجربة اللعب وتوفير تحكم دقيق ومتناسق مع نوع اللعبة. يُنصح باختيار الطريقة الأفضل حسب تصميم اللعبة وأسلوب اللعب المطلوب. ويمكن تنزيل شيفرة المشروع البرمجية من Github لمزيد من الفهم. ترجمة -وبتصرّف- للأقسام Screen wrap و Top-down movement و Grid-based movement و 8-Directional Movement/Animation من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد فهم RayCast2D واستخداماتها في محرك ألعاب جودو سحب وإفلات جسم صلب RigidBody2D في جودو إنشاء شخصيات ثلاثية الأبعاد في جودو Godot برمجة لعبة متاهة باستخدام محرك يونيتي Unity
  13. سنكتشف في هذا المقال متى يدخل أو يخرج كائن من الشاشة، وسنتعرّف على كيفية إنشاء وتحريك شخصية في ألعاب المنصات ثنائية الأبعاد. إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد سننشئ الأن شخصيةً في ألعاب المنصات ثنائية الأبعاد. يُفاجَأ المطورون المبتدئون في أغلب الأحيان بمدى تعقيد برمجة شخصيات ألعاب المنصات، لذا يوفر محرّك ألعاب جودو Godot بعض الأدوات المُضمَّنة للمساعدة، ولكن يمكن القول بأن هناك عددًا كبيرًا من الحلول التي تضاهي عدد الألعاب الموجودة. لن نتعمق في شرح الميزات مثل ميزات القفزات المزدوجة أو الانحناء أو القفز على الحائط أو الرسوم المتحركة، بل سنناقش أساسيات الحركة في ألعاب المنصات. ملاحظة: يمكن استخدام العقدة RigidBody2D لإنشاء شخصية لألعاب المنصات، ولكننا سنركز على استخدام العقدة CharacterBody2D، إذ تُعَد الأجسام الحركية Kinematic مناسبةً لألعاب المنصات، حيث لن تكون مهتمًا كثيرًا بالفيزياء الواقعية بقدر الاهتمام بالشعور المتجاوب في اللعبة. سنبدأ بعقدة CharacterBody2D، ونضيف إليها عقدتي Sprite2D و CollisionShape2D، ثم نرفق السكربت التالي بالعقدة الجذر للشخصية: extends CharacterBody2D @export var speed = 1200 @export var jump_speed = -1800 @export var gravity = 4000 func _physics_process(delta): # إضافة الجاذبية في كل إطار velocity.y += gravity * delta # ‫يؤثر الدخل على المحور x فقط velocity.x = Input.get_axis("walk_left", "walk_right") * speed move_and_slide() # السماح بالقفز عند وجود الشخصية على الأرض فقط if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = jump_speed يمكننا ملاحظة أننا نستخدم إجراءات الإدخال التي عرّفناها في خريطة الإدخال InputMap وهي: "walk_right" و "walk_left" و "jump"، لذا يمكن الاطلاع على إجراءات الإدخال InputActions لمزيد من التفاصيل. تعتمد القيم المُستخدَمة للسرعة speed والجاذبية gravity وسرعة القفز jump_speed اعتمادًا كبيرًا على حجم الشخصية الرسومية Sprite الخاصة باللاعب، وتكون خامة Texture اللاعب 108x208 بكسل في هذا المثال، وإذا كانت الصورة أصغر حجمًا، فستستخدم قيمًا أصغر. نريد أيضًا قيمًا عالية للشعور بالسرعة والاستجابة، إذ تؤدي الجاذبية المنخفضة إلى لعبة نشعر فيها بعدم التوازن؛ بينما تمثل القيمة العالية العودة إلى الأرض بسرعة والجاهزية للقفز مرةً أخرى. سنتحقق من التابع is_on_floor()‎ بعد استخدام الدالة move_and_slide()‎ التي تضبط قيمة هذا التابع، لذا يجب عدم التحقق منه قبل ذلك، وإلّا سنحصل على القيمة من الإطار السابق. الاحتكاك Friction والتسارع Acceleration تُعَد الشيفرة البرمجية السابقة بداية جيدة، إذ يمكننا استخدامها كأساس لمجموعة متنوعة من متحكمات المنصات، ولكنها تواجه مشكلة الحركة اللحظية Instantaneous Movement، ولكن يمكننا الحصول على شعور طبيعي أكثر من خلال تسارع الشخصية مثلًا حتى تصل إلى سرعتها القصوى، ثم تتوقف عند عدم وجود دخل. إحدى الطرق لإضافة هذا السلوك هي استخدام الاستيفاء الخطي Linear Interpolation أو "lerp"، بحيث ننتقل باستخدام الاستيفاء الخطي من السرعة الحالية إلى السرعة القصوى عند التحرك، وننتقل باستخدام الاستيفاء الخطي من السرعة الحالية إلى الصفر أثناء التوقف، وبالتالي سيوفر تعديل مقدار الاستيفاء الخطي مجموعة متنوعة من أنماط الحركة. ملاحظة: يمكن الاطلاع على مقال الاستيفاء للحصول على مزيد من المعلومات عن الاستيفاء الخطي. extends CharacterBody2D @export var speed = 1200 @export var jump_speed = -1800 @export var gravity = 4000 @export_range(0.0, 1.0) var friction = 0.1 @export_range(0.0 , 1.0) var acceleration = 0.25 func _physics_process(delta): velocity.y += gravity * delta var dir = Input.get_axis("walk_left", "walk_right") if dir != 0: velocity.x = lerp(velocity.x, dir * speed, acceleration) else: velocity.x = lerp(velocity.x, 0.0, friction) move_and_slide() if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = jump_speed سنجرب هنا تغيير قيم الاحتكاك friction والتسارع acceleration لمعرفة مدى تأثيرها على شعورنا أثناء اللعب. فقد يحتاج مستوى الجليد إلى قيم منخفضة جدًا مثلًا، مما يجعل الحركة أصعب. تمنحنا هذه الشيفرة البرمجية نقطة بداية لبناء متحكم منصات خاص بك، لذا يمكن الاطلاع على المقالات اللاحقة لمزيد من ميزات المنصات المتقدمة مثل القفز على الحائط. تحديد متى يدخل كائن إلى الشاشة أو يغادرها يوفر محرّك الألعاب العقدة VisibleOnScreenNotifier2D، حيث إذا أرفقنا هذه العقدة بالكائن، فسنتمكّن من استخدام إشارات screen_entered و screen_exited الخاصة بها. تطبيق عملي 1 ليكن لدينا مثلًا مقذوف يتحرك في خط مستقيم بعد إطلاقه، وإذا استمرينا في الإطلاق، فسنحصل في النهاية على عدد كبير من الكائنات التي يتعقّبها المحرّك، حتى وإن كانت هذه الكائنات خارج الشاشة، مما قد يتسبب في حدوث تأخير. توضح الشيفرة البرمجية التالية حركة المقذوف: extends Area2D var velocity = Vector2(500, 0) func _process(delta): position += velocity * delta يمكن حذف المقذوف تلقائيًا عند تحرّكه خارج الشاشة من خلال إضافة العقدة VisibleOnScreenNotifier2D والاتصال بإشارة screen_exited الخاصة بها. func _on_VisibleOnScreenNotifier2D_screen_exited(): queue_free() تطبيق عملي 2 ليكن لدينا مثلًا عدو ينفّذ بعض الإجراءات مثل التحرك على طول مسار أو تشغيل رسوم متحركة، ولن يظهر على الشاشة سوى عدد قليل من الأعداء في الوقت نفسه في خريطة كبيرة تحتوي على العديد من الأعداء، إذ يمكننا تعطيل إجراءات العدو أثناء وجوده خارج الشاشة باستخدام العقدة VisibleOnScreenNotifier2D كما هو الحال في جزء الشيفرة البرمجية التالي: var active = false func _process(delta): if active: play_animation() move() func _on_VisibleOnScreenNotifier2D_screen_entered(): active = true func _on_VisibleOnScreenNotifier2D_screen_exited(): active = false الخاتمة استعرضنا في هذا المقال كيفية استخدام العقدة VisibleOnScreenNotifier2D لتحديد متى يدخل كائن إلى الشاشة أو يغادرها، مما يساعد في تحسين أداء اللعبة من خلال إدارة الكائنات غير المرئية بكفاءة؛ كما تناولنا أساسيات إنشاء شخصية وتحريكها في ألعاب المنصات ثنائية الأبعاد باستخدام العقدة CharacterBody2D، مع التركيز على مفاهيم مثل الجاذبية، القفز، والتسارع للحصول على حركة سلسة واستجابة طبيعية. يمكن تنزيل شيفرة المشروع البرمجية من Github للاطلاع عليه وفهمه أكثر. ترجمة -وبتصرّف- للقسمين Entering/Exiting the screen و Platform character من توثيقات Kidscancode. اقرأ أيضًا فهم RayCast2D واستخداماتها في محرك ألعاب جودو سحب وإفلات جسم صلب RigidBody2D في جودو إنشاء شخصيات ثلاثية الأبعاد في جودو Godot دليلك الشامل إلى برمجة الألعاب ألعاب الفيديو: تطورها وأهميتها وخطوات برمجتها برمجة لعبة متاهة باستخدام محرك يونيتي Unity
  14. عندما نعمل على تطبيق لارافيل Laravel، قد نحتاج لتكرار مهام معينة خاصة بالتطبيق. وبدلاً من أن نكتب كل هذه الأوامر يدويًا في كل مرة يمكننا أتمتتها باستخدام أداة Artisan المدمجة مع إطار عمل لارافيل، فهذه الأداة تتيح لنا تنفيذ أوامر جاهزة مثل إنشاء جداول قواعد البيانات، أو تشغيل خادم التطوير المحلي، كما تمكننا من تنفيذ أوامر جديدة خاصة بنا كإرسال بريد لمستخدم، أو تحديث بيانات معينة سنوضح في هذا المقال كيفية إنشاء أمر Artisan جديد في إصدار لارافيل 11، سنبدأ بالتعرف على أساسيات Artisan والأوامر التي توفرها، ثم ننتقل بعد ذلك لشرح طريقة إنشاء وتشغيل أوامرنا المخصصة. نظرة عامة على Artisan Artisan هو أداة سطر الأوامر مدمجة مع إطار العمل لارافيل توفر مجموعة من الأوامر المفيدة التي يمكن أن تساعدنا أثناء بناء التطبيقات، مثل إنشاء عمليات التهجير Migrations، ونشر موارد الحزم. هنالك عدة أوامر مدمجة مع Artisan ويمكن عرض قائمة بهذه الأوامر من خلال كتابة أمر php artisan list في طرفية Artisan كما في الصورة التالية: تساعدنا هذه الأوامر على العمل بكفاءة أكبر، حيث سنتمكن على سبيل المثال من إنشاء وظائف الاستيثاق Authentication، والمتحكمات Controllers، والنماذج Models، وإنشاء عمليات التهجير Migrations وغيرها من الوظائف المتقدمة بسهولة وخلال وقت أقل. أساسيات أوامر Laravel Artisan يُعَد الصنف Illuminate\Console\Application أساسًا في Artisan فهو الذي يحدد كيفية التعامل مع الأوامر، كما أنه يوسّع الصنف Symfony\Component\Console\Application، وهذا يسهّل على مطوري إطار عمل سيمفوني Symfony التعامل مع Artisan لأنهم سيجدون بيئة عمل مألوفة. فيما يلي مثال على تعريف أمر بسيط: namespace App\Console\Commands; use Illuminate\Console\Command; class ExampleCommand extends Command { protected $signature = 'example:run {name}'; protected $description = 'Runs an example command'; public function handle() { $name = $this->argument('name'); $this->info("Hello, {$name}!"); } } دورة تطوير تطبيقات الويب باستخدام لغة PHP احترف تطوير النظم الخلفية والووردبريس وتطبيقات الويب من الألف إلى الياء دون الحاجة لخبرة برمجية مسبقة اشترك الآن تشغيل تطبيق لارافيل باستخدام خادم PHP Artisan يتيح الأمر serve تشغيل التطبيقات على خادم تطوير PHP بسهولة، ويمكن للمطورين استخدام Artisan لإنشاء واختبار ميزات التطبيق المختلفة، يمكن بدء تشغيل الخادم المحلي على العنوان http://localhost:8000 بكتابة الأمر التالي: php artisan serve وهذا يسهل اختبار وتطوير التطبيقات بسرعة، كما يمكننا تخصيص الخادم لاستخدام مضيف ومنفذ مختلف. إنشاء أوامر مخصصة وتشغيلها يمكن إنشاء أوامر Artisan مخصصة واختيار مكان تخزينها وتحميلها باستخدام مدير الحزم Composer. سنشرح في الفقرات التالية أهم الخطوات المتبعة لإنشاء أوامر مخصصة وتسجيلها وتنفيذها. الخطوة 1: إنشاء أمر جديد لإنشاء أمر جديد باسم customcommand نستخدام الأمر make:command الذي ينشئ تلقائيًا صنف أوامر جديد في المجلد app/Console/Commands، ويولّد هذا المجلد إن لم يكن موجودًا مسبقًا. لنكتب الآن الأمر التالي في طرفية Artisan: php artisan make:command customcommand يبدو ملف الشيفرة البرمجية الخاص بهذا الأمر كما يلي: namespace App\Console\Commands; use Illuminate\Console\Command; class CustomCommand extends Command { protected $signature = 'custom:command'; protected $description = 'Description of the custom command'; public function __construct() { parent::__construct(); } public function handle() { // نضع المنطق البرمجي للأمر هنا } } الخطوة 2: تعريف الأمر المخصص عند تنفيذ بعض أوامر Artisan، قد يحتاج المستخدم لإدخال بعض المعلومات كاسمه أو بريده الإلكتروني، ويتعامل لارافيل مع ذلك باستخدام خاصية التوقيع signature. يمكننا استخدام الصيغة القصيرة التالية لضبط الوسطاء arguments والرايات flags أو الخيارات الإجبارية والاختيارية، وهذا يسهّل علينا تحديد الطريقة التي سيتفاعل بها المستخدمون مع الأمر. لنلقِ نظرة على الأمر التالي: protected $signature = 'user:update {id} {--name=} {--email=}'; يتوقّع هذا الأمر أن يدخل المستخدم وسيطًا هو المعرف id، ويدخل الاسم والبريد الإلكتروني اختياريًا. الخطوة 3: دخل وخرج الأمر يوفّر لارافيل طرقًا سهلة للحصول على قيم الوسطاء عند تشغيل الأمر، حيث يمكننا استخدام التابعين ‎$this->argument()‎ و ‎$this->option()‎ ضمن التابع handle الخاص بالأمر. إن لم يكن الوسيط أو الخيار الذي نبحث عنه موجودًا، فسيعيد هذان التابعان القيمة null. المطالبة بالإدخال يمكننا الحصول على إدخال من المستخدم أثناء تشغيل الأمر بالإضافة إلى عرض الخرج، حيث يعرض التابع ask سؤالًا للمستخدم ويحصل على استجابته ثم يرسلها مرة أخرى إلى الأمر الخاص بك. $name = $this->ask('What is your name?', 'Taylor'); طلب التأكيد if ($this->confirm('Do you wish to continue?')) { // متابعة } يمكن جعل موجّه التأكيد يعيد القيمة true دائمًا من خلال تمرير القيمة true كوسيط ثانٍ للتابع confirm. الخطوة 4: تسجيل الأوامر يسجّل لارافيل جميع الأوامر الموجودة في المجلد app/Console/Commands افتراضيًا، ويمكننا إعداد لارافيل للبحث في مزيد من المجلدات عن أوامر PHP Artisan من خلال استخدام التابع withCommands في ملف bootstrap/app.php الخاص بالتطبيق. ->withCommands([ __DIR__.'/../app/Domain/Orders/Commands', ]) سيجد حاوي الخدمات جميع الأوامر ويسجلها في التطبيق باستخدام Artisan. الخطوة 5: تنفيذ الطلبات قد نرغب بتشغيل أمر Artisan خارج واجهة سطر الأوامر من وجهة Route أو متحكم Controller مثلًا، ويمكن تحقيق ذلك باستخدام التابع call مع الصنف Facade من Artisan، يحتوي هذا التابع على وسيطين الأول هو اسم الأمر أو اسم الصنف المرتبط بالأمر، والوسيط الثاني هو قائمة بمعاملات الأمر، ويعيد هذا التابع رمز الخروج ليشير إلى ما إذا كان الأمر قد نجح أم لا. يمكن أيضًا تمرير أمر Artisan بالكامل للتابع call كسلسلة نصية كما يلي: Artisan::call('mail:send 1 --queue=default'); ترتيب أوامر Artisan ضمن رتل يمكننا استخدام التابع queue مع الصنف Facade من Artisan لإرسال أوامر Artisan إلى عمّال الرتل Queue Workers، ثم تُعالَج هذه الأوامر في الخلفية، وعلينا التأكّد من ضبط الرتل وتشغيل مستمع الرتل قبل استخدام هذا التابع. use Illuminate\Support\Facades\Artisan; Route::post('/user/{user}/mail', function (string $user) { Artisan::queue('mail:send', [ 'user' => $user, '--queue' => 'default' ]); // ... }); الخطوة 6: معالجة الإشارات يمكن لأوامر Artisan من لارافيل معالجة إشارات النظام مثل إشارات SIGINT، حيث يمكننا دمج معالجة الإشارات مع الأمر الخاص بنا باستخدام التابع trap كما يلي: $this->trap(SIGINT, function () { $this->info('Command interrupted'); }); الخطوة 7: تخصيص الملفات الجذرية Stub يمكن تخصيص ملفات القوالب التي يستخدمها أمر make:command من Artisan لتسهيل إنشاء الأوامر ذات البنى المتناسقة، لذا نحتاج إلى نشر الملفات الجذرية في مشروعنا كما يلي: php artisan stub:publish الخطوة 8: الأحداث هناك ثلاثة أحداث مهمة ترسلها أداة Artisan عند تشغيل الأوامر وهي: الحدث Illuminate\Console\Events\ArtisanStarting الحدث Illuminate\Console\Events\CommandStarting الحدث Illuminate\Console\Events\CommandFinished نستخدم التابع event لإطلاق حدث كما يلي: event(new CustomCommandExecuted($this)); إنشاء عمليات التهجير Migrations يُعَد الأمر php artisan make:migration من أوامر Artisan المفيدة في لارافيل، حيث يستخدم لإنشاء ملف تهجير جديد. وعمليات التهجير هي مخططات أولية لمخطط قاعدة البيانات وتحدّد لنا بنية الجداول والأعمدة والفهارس والعلاقات. فيما يلي خطوات هذه العملية: إنشاء عملية تهجير: يؤدي الأمر make:migration لإنشاء ملف جديد في المجلد database/migrations تحديد التغييرات: يحتوي ملف التهجير على التابعين up و down يحدّد up التغييرات التي تطرأ على قاعدة البيانات مثل إنشاء الجداول، ويلغي down هذه التغييرات تشغيل عملية التهجير: يفيد الأمر php artisan migrate في تطبيق التغييرات على قاعدة البيانات على سبيل المثال سيؤدي الأمر التالي لإنشاء ملف جديد بالاسم create_users_table في المجلد database/migrations، ويمكننا بعد ذلك تحديد بنية جدول users ضمن ملف التهجير. php artisan make:migration create_users_table تتمثل فوائد الأمر make:migration فيما يلي: التحكم في الإصدارات من خلال تعقّب تغييرات قاعدة البيانات بمرور الوقت التعاون ومشاركة بنية قاعدة البيانات مع أعضاء الفريق بسهولة بذر أو توليد البيانات Seeding لقاعدة البيانات لملئها بالبيانات التجريبية باستخدام Seeder التراجع عن تغييرات قاعدة البيانات بسهولة عند الحاجة إبقاء مخطط قاعدة البيانات واضحًا ومنظمًا، مما يسهّل إدارة تطبيقنا وتحديثه قائمة بأهم أوامر Laravel Artisan تحتوي واجهة سطر أوامر Artisan في لارافيل على أوامر لمهام مختلفة مثل إنشاء الشيفرة البرمجية وإدارة بيئة التطبيق ويمكن مطالعة القائمة الشاملة لكافة الأوامر بكتابة الأمر PHP Artisan list كما وضحنا سابقًا، ولكن سنفصّل فيما يلي الأوامر الأساسية الموجودة ضمن لارافيل 11 ضمن فئات. الأوامر الأساسية cache: إدارة ذاكرة التطبيق المخبئية config: تخزين ملفات الضبط Configuration مؤقتًا أو مسحها أو نشرها down: تعطيل التطبيق مؤقتًا env: إدارة متغيرات البيئة key: لتوليد مفتاح تطبيق جديد migrate: تشغيل عمليات تهجير قاعدة البيانات optimize: تحسين التطبيق للإنتاج queue: إدارة نظام الرتل route: سرد الوجهات Routes أو مسحها storage: إدارة مجلد التخزين vendor: إدارة اعتماديات مدير الحزم Composer أوامر توليد الشيفرة البرمجية make: توليد بنى الشيفرة البرمجية المختلفة للمتحكم والنموذج والتهجير ...إلخ. model: إنشاء نماذج Eloquent migration: إنشاء ملفات التهجير seed: توليد بيانات وهمية لقاعدة البيانات أوامر الاختبار test: تشغيل اختبارات التطبيق dusk: تشغيل اختبارات المتصفح باستخدام Dusk أوامر أخرى auth: إدارة العمليات المتعلقة بالاستيثاق Authentication breeze: تثبيت نظام استيثاق Breeze config: إدارة ملفات الضبط horizon: إدارة نظام رتل Laravel Horizon passport: إدارة خادم OAuth2 sanctum: إدارة واجهة برمجة التطبيقات API لاستيثاق الرموز Token telescope: إدارة أداة تنقيح أخطاء Telescope الخلاصة وضّحنا في هذا المقال كيفية إنشاء أوامر مخصصة باستخدام أمر Artisan الذي يوفر إمكانية تطوير أوامر مختلفة واستدعاءها بناءً على احتياجات مشروعنا. بعض الأسئلة الشائعة لنتعرف على أبرز الأسئلة الشائعة حول أوامر Artisan 1. ما الغرض من أوامر PHP Artisan في لارافيل أوامر Artisan هي أداة سطر أوامر في لارافيل تساعدنا على أتمتة المهام وإدارة عمليات التهجير وتوليد الشيفرة البرمجية المساعدة، ويمكننا استخدامها أيضًا لتشغيل الاختبارات وإنجاز مهام التطوير والإدارة الأخرى في تطبيقات لارافيل. 2. لماذا نستخدم أمر PHP Artisan Serve يشغّل أمر PHP Artisan serve خادم تطوير محلي لتطبيق لارافيل الخاص بنا بسرعة، ويُعَد مثاليًا لإنشاء النماذج الأولية السريعة والاختبار أثناء التطوير. 3. كيف نشغّل أوامر PHP Artisan في cpanel نفتح الطرفية Terminal من cPanel وننتقل إلى مجلد مشروع لارافيل الخاص بنا، ثم نشغّل الأمر باستخدام PHP كما يلي: ‎/usr/local/bin/php artisan {command}‎ تحتاج للتأكد من وضع مسار PHP الصحيح للخادم مكان ‎/usr/local/bin/php، والذي يمكننا العثور عليه في MultiPHP Manager ضمن cPanel. 4. هل يمكن تعديل أو توسيع أوامر Artisan الموجودة مسبقًا في لارافيل نعم، لدينا القدرة على تعديل أو توسيع أوامر Artisan الموجودة مسبقًا في لارافيل من خلال إنشاء أمر مخصص يرث صنف الأمر الأساسي، ثم تنفيذ المنطق البرمجي أو الوظيفة المحددة التي نريدها ضمن هذا الأمر المخصص. 5. هل يمكن استخدام أوامر Artisan المخصصة لأتمتة المهام الشائعة في لارافيل نعم، يمكننا استخدام أوامر Artisan المخصصة في لارافيل لأتمتة المهام الشائعة مثل تشغيل عمليات التهجير، وتوليد بيانات لقاعدة البيانات وتوليد الشيفرة البرمجية وتطبيق المهام المجدولة وتنفيذ أي منطق برمجي مخصص. ترجمة -وبتصرّف- للمقال How to Create Custom Commands in Laravel 11 with PHP Artisan للكاتبة Hafsa Tahir. اقرأ أيضًا نصائح لتحسين أداء تطبيقات لارافيل أفضل الحزم البرمجية لتحسين تطبيقات لارافيل كيف تنشئ نموذجا Model في Laravel أساسيات التخبئة Cache في Laravel
  15. سنتعرف في مقال اليوم على العقدة RayCast2D في محرك جودو وكيفية استخدامها بكفاءة في تطوير الألعاب ثنائية الأبعاد، من أجل كشف تصادم الأشعة Raycasting الذي يفيدنا في العديد من حالات الاستخدام. أهمية العقدة RayCast2D في تطوير الألعاب تمثل العقدة RayCast2D في محرك ألعاب جودو شعاعًا ينطلق من نقطة الأصل الممثلة بمركز العقدة إلى نقطة نهاية، والشعاع ray هو خط افتراضي ينطلق من نقطة باتجاه زاوية معينة ويمتد في الفضاء، ويمكننا التحقق فيما إذا كان هذا الشعاع قد اصطدم بشيء ما في المشهد أو وصل إلى نهايته دون أي تصادم. تعد هذه التقنية أساسية في العديد من أنواع الألعاب، مثل ألعاب التصويب أو ألعاب المنصات، حيث يمكن استخدامها للكشف الاصطدامات collisions بين الكائنات أو الأسطح، كأن نحتاج لمعرفة إذا كان اللاعب يرى العدو أو إذا كان يلمس الأرضية أم لا، كما يمكن أن نتحقق من خلالها فيما إذا كانت القذيفة التي أطلقناها قد أصابت هدفًا معينًا. أهم خصائص العقدة RayCast2D تتضمن العقدة RayCast2D في جودو مجموعة من الخصائص المهمة التي تساعدنا على ضبط سلوك الشعاع بشكل دقيق. دعونا ننشئ مشروع جديد للعبة ثنائية الأبعاد، ونضيف للمشهد عقدة RayCast2D ونتحقق من خصائصها الظاهرة في الفاحص Inspector كما يلي: فيما يلي الخاصيات الرئيسية التي ستحتاج إلى فهمها للتعامل مع هذه العقدة: الخاصية Enabled تستخدم للتحكم في تفعيل أو تعطيل شعاع RayCast، فعند تفعيل هذه الخاصية، سيبدأ الشعاع في الكشف عن التصادمات مع الأجسام في كل إطار فيزيائي. وإذا ألغينا تفعيلها فسيتعطل عمل كشف تصادم الأشعة. الخاصية Exclude Parent عند تفعيل هذه الخاصية، سيتجاهل الشعاع التصادم مع العقدة الأب المباشرة. وتكون هذه الخاصية مفعَّلة افتراضيًا لتجنب الكشف عن تصادمات غير مرغوب بها مع الكائن الذي يحتوي على الشعاع. الخاصية Target Position تحدد هذه الخاصية نقطة نهاية الشعاع بالنسبة لموضع العقدة نفسه، أي باستخدام الإحداثيات المحلية، على سبيل المثال، إذا كانت قيمتها (250,0) فسيمتد الشعاع أفقيًا لليمين لمسافة 250 وحدة وهي نقطة الوجهة للشعاع في الإحداثيات المحلية. ملاحظة: تمثل الإحداثيات المحلية Local Coordinates موقع العقدة بالنسبة لنقطة الأصل الخاصة بالعقدة الأم، بينما تمثل الإحداثيات العامة Global Coordinates الموقع المطلق للعقدة داخل المشهد بأكمله. لنلاحظ أيضًا القسم بعنوان Collide With ضمن الفاحص، ففي هذا القسم يمكننا تحديد أنواع الكائنات التي يجب أن يتفاعل معها الشعاع، حيث سيكتشف الشعاع افتراضيًا الأجسام الفيزيائية Bodies فقط مثل KinematicBody2D أو RigidBody2D، ولو أردنا منه اكتشاف المناطق أيضًا مثل Area2D فعلينا تفعيل الخيار Area. دوال مفيدة للعقدة RayCast2D يمكن الاطلاع على القائمة الكاملة لدوال العقدة RayCast2D في توثيق واجهة برمجة التطبيقات API، ولكن سنوضح تاليًا بعض الدوال المفيدة للتعامل مع التصادمات: is_colliding()‎: دالة منطقية تتيح معرفة فيما إذا كان الشعاع يصطدم بشيء ما get_collision_point()‎: إذا اصطدم الشعاع بشيء ما ستعيد هذه الدالة موضع التصادم في الإحداثيات العامة get_collider()‎: إذا اصطدم الشعاع بشيء ما ستعيد هذه الدالة مرجعًا إلى الكائن المتصادم get_collision_normal()‎: تعيد هذه الدالة الشعاع الناظم Normal على الكائن المتصادم عند نقطة الاصطدام أمثلة عملية توجد العديد من الاستخدامات العملية المفيدة لتقنية كشف تصادم الأشعة مثل الرؤية Visibility أي هل يمكن للكائن A أن يرى الكائن B أم أن هناك عائقًا بينهما يحول دون ذلك، والقرب Proximity أي هل اللاعب قريب من جدار أو أرض أو عائق وغير ذلك من الاستخدامات المختلفة. سنوضح فيما يلي بعض الأمثلة العملية المفيدة. المثال الأول: إطلاق النار تواجه المقذوفات سريعة الحركة مشكلة تسمى Tunneling، فهي تتحرك بسرعة كبيرة بحيث لا يمكنها اكتشاف الاصطدام في إطار واحد وبالتالي سيؤدي هذا لمرورها عبر العوائق أو الأسطح بدل أن تصطدم بها، في هذا الحالة يمكننا استخدام العقدة Raycast2D لتمثيل حركة المقذوف على شكل مسار أو شعاع مستمر مثل شعاع الليزر وبهذا نضمن اكتشاف تصادماته بدقة حتى عند السرعات العالية. يمثّل الشكل التالي شخصية اللاعب حيث أضفنا عقدة Raycast2D عند نهاية السلاح، وضبطنا موضع الهدف target_position على القيمة (250,0)لضبط اتجاه الشعاع الذي يُطلق من السلاح. إذا أطلق اللاعب قذيفة، فيجب التحقق فيما إذا كان الشعاع يصطدم بشيء ما كما يلي: func _input(event): if event.is_action_pressed("shoot"): if $RayCast2D.is_colliding(): print($RayCast2D.get_collider().name) المثال الثاني: اكتشاف حافة منصة لنفترض وجود عدو يمشي على منصة ضمن لعبة ما، لكننا لا نريده أن يسقط من حافة هذه المنصة بل نريده أن يرتد ويتحرك في الاتجاه المعاكس، لذا سنضيف إلى العدو عقدتين من النوع Raycast2D كما يلي: نتحقق متى سيتوقف الشعاع عن الاصطدام بأي شيء في سكربت العدو من خلال الدالة ()is_colliding، ففي حال أعادت الدالة false، فهذا يعني أننا وجدنا الحافة ويجب علينا الالتفاف: func _physics_process(delta): velocity.y += gravity * delta if not $RayRight.is_colliding(): dir = -1 if not $RayLeft.is_colliding(): dir = 1 velocity.x = dir * speed $AnimatedSprite.flip_h = velocity.x > 0 velocity = move_and_slide(velocity, Vector2.UP) يعمل الكود أعلاه على تحريك العدو على المنصة مستعينًا بشعاعي Raycast2D للكشف عن الوصول لحافة المنصة، فإذا لم يصطدم الشعاع الأيمن بشيء سيتحرك العدو لليسار، وإذا لم يصطدم الشعاع الأيسر بشيء سيتحرك العدو لليمين. وسيبدو الأمر أثناء العمل كما يلي: الخاتمة نحتاج إلى تنفيذ تقنيات كشف التصادم في معظم أنواع الألعاب، لذا من الضروي أن نفهم جيدًا كيفية إعداد واستخدام العقدة RayCast2D في جودو ونوظفها بشكل صحيح لحل مشكلات شائعة في تطوير الألعاب مثل التصويب واكتشاف حواف المنصات وتجنب العوائق وغيرها من الحالات. ترجمة -وبتصرّف- للقسم RayCast2D من توثيقات Kidscancode. اقرأ أيضًا التفاعل بين الشخصيات والأجسام الصلبة في جودو استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه ترتيب معالجة العقد والتنقل في شجرة المشاهد في Godot تحريك سفينة فضاء باستخدام RigidBody2D في جودو
  16. لقد غير الويب وجه العالم، ولكن جوهره لم يتغير كثيرًا، إذ لا تزال معظم الأنظمة تتبع القواعد نفسها التي وضعها تيم بيرنرز لي Tim Berners-Lee، ولا تزال معظم خوادم الويب تتعامل مع أنواع الرسائل نفسها وبذات الطريقة، لذا سنوضح في هذا المقال الشيفرة البرمجية لخادم ويب بسيط محدود لا يتجازو كوده البرمجي 500 سطر، لنساعدكم على فهم كيفية عمل خادم الويب بشكل أفضل من خلال الاطلاع على شيفرته البرمجية مفتوحة المصدر. معلومات أساسية عن خوادم الويب تستخدم جميع البرامج على الويب تقريبًا مجموعة من معايير الاتصال تسمى بروتوكولات الإنترنت Internet Protocols أو IPs اختصارًا، وما يهمنا من هذه البروتوكولات حاليًا هو بروتوكول التحكم في الإرسال Transmission Control Protocol -أو TCP/IP اختصارًا- والذي يجعل الاتصال بين الحواسيب مشابهًا لقراءة وكتابة الملفات فنحن نفتح قناة اتصال، ثم نقرأ أو نكتب البيانات، ثم نغلق القناة. تتواصل البرامج التي تستخدم بروتوكول الإنترنت من خلال المقابس Sockets، حيث يمثل كل مقبس أحد طرفي قناة اتصال من نقطة إلى نقطة مثل الهاتف الذي يكون على أحد طرفي مكالمة هاتفية. يتكون المقبس من عنوان IP يحدد جهازًا معينًا ورقم منفذ على هذا الجهاز، حيث يتألف عنوان IP من أربعة أرقام مكونة من 8 بتات مثل ‎174.136.14.108‎، ويطابق نظام أسماء النطاقات Domain Name System أو DNS هذه الأرقام مع أسماء مثل ‎aosabook.org‎، مما يسهل علينا تذكرها. رقم المنفذ هو رقم ضمن المجال من 0 إلى 65535، والذي يحدد المقبس الفريد على الجهاز المضيف، فإذا كان عنوان IP يشبه رقم هاتف شركة، فيمكن القول بأن رقم المنفذ هو امتداد لهذا الرقم. وتكون المنافذ ذات الأرقام من 0 إلى 1023 محجوزة ليستخدمها نظام التشغيل، ويمكن لأي شخص آخر استخدام المنافذ المتبقية. يمثل بروتوكول نقل النص التشعبي Hypertext Transfer Protocol أو HTTP إحدى الطرق التي تتبادل البرامج البيانات من خلالها عبر بروتوكول IP، ويُعدّ بروتوكول HTTP بسيطًا إذ يرسل العميل طلبًا يحدد من خلاله ما يريده عبر اتصال المقبس، ثم يرسل الخادم بعض البيانات في الاستجابة كما في الشكل التالي: دورة HTTP عندما يرسل العميل أو متصفح الإنترنت طلبًا إلى الخادم، فإن الخادم يرد عليه بإرسال البيانات المطلوبة. ويمكن لهذه البيانات أن تأتي إما من ملف مخزَّن مسبقًا على القرص الصلب مثل صفحة HTML محفوظة سابقًا، أو يتم توليدها وإنشاؤها لحظيًا عند طلبها، أو الاثنين فقد تكون صفحة HTML ثابتة ويُضاف لها محتوى مولّد ديناميكيًا. فالخادم عند استجابته لا يكون محدودًا بمصدر واحد للبيانات، بل يمكنه أن يختار الأنسب أو يجمع بين عدة مصادر. تكمن أهمية طلب HTTP الذي يرسله المتصفح إلى السيرفر في أنه مجرد نص، إذ يمكن لأي برنامج إنشاء طلب أو تحليله. ولكن يجب أن يحتوي هذا النص على الأجزاء الموضحة في الشكل التالي لفهمه: طلب HTTP تكون طريقةHTTP إما GET لجلب المعلومات أو POST لإرسال بيانات النموذج أو رفع الملفات. ويحدد عنوان URL ما يريده العميل، حيث يكون في أغلب الأحيان مسارًا إلى ملف على القرص الصلب مثل ‎/research/experiments.html‎، ولكن يقرر الخادم ما يجب فعله به. ويكون إصدار HTTP إما HTTP/1.0 أو HTTP/1.1، ولسنا بصدد معرفة الاختلافات بينهما في نطاق المقال الحالي. ترويسات HTTP هي أزواج مفتاح وقيمة كما في الأمثلة الثلاث التالية: Accept: text/html Accept-Language: en, fr If-Modified-Since: 16-May-2024 قد تظهر المفاتيح عدة مرات في ترويسات HTTP على عكس جداول التعمية Hash التي تسمح بمفتاح واحد فقط لكل قيمة، وهذا يسمح للطلب بتحديد استعداده لقبول عدة أنواع من المحتوى. يتكون جسم الطلب من أي بيانات إضافية مرتبطة بالطلب، ويُستخدَم عند إرسال البيانات عبر نماذج الويب أو عند رفع الملفات وغير ذلك. يجب أن يكون هناك سطر فارغ بين آخر ترويسة وبداية جسم الطلب للإشارة إلى نهاية الترويسات، وتخبر إحدى الترويسات التي اسمها ‎Content-Length‎ الخادمَ بعدد البايتات المتوقع قراءتها في جسم الطلب. سيكون تنسيق الاستجابة HTTP Response مشابهًا لتنسيق الطلب HTTP Request كما في الشكل التالي: استجابة HTTP إذًا سيكون للإصدار والترويسات وجسم الطلب التنسيق والمعنى نفسه في كل من الطلب والاستجابة. لكن سيتضمن السطر الأول من الاستجابة رمز حالة Status Code الذي يشير إلى ما حدث عند معالجة الطلب، حيث يدل الرمز 200 على النجاح، ويدل الرمز 404 على أن المورد المطلوب غير موجود، ويكون للرموز الأخرى معانٍ أخرى، بينما تكرر عبارة الحالة هذه المعلومات في صيغة مفهومة بشريًا مثل ‎OK‎ أوnot found‎. هناك شيئان آخران إضافيان نحتاج إلى معرفتهما حول بروتوكول HTTP وهما أن بروتوكول HTTP عديم حالة Stateless، بمعنى أنه يعالج طلب بمفرده، ولا يتذكر الخادم أي شيء بين طلب وآخر ولا من أنت وما فعلته من قبل، حتى لو كنت قبل دقيقة على نفس الموقع. فإذا أراد تطبيقٌ ما تتبع شيء معين مثل هوية المستخدم، فسيفعل ذلك باستخدام ملف تعريف الارتباط Cookie، التي تتضمن سلسلة نصية من أحرف قصيرة يرسلها الخادم إلى العميل، ثم يعود العميل إلى الخادم لاحقًا. فإذا أراد المستخدم أداء بعض الوظائف التي تتطلب حفظ الحالة عبر عدة طلبات، فسينشئ الخادم ملف تعريف ارتباط جديد ويخزنه في قاعدة بيانات ويرسله إلى متصفحه، وبالتالي يستخدم الخادم ملف تعريف الارتباط للبحث عن معلومات حول ما يفعله المستخدم عندما يعيد المتصفح إرسال ملف تعريف الارتباط. الشيء الثاني الذي نحتاج إلى معرفته حول HTTP هو أنه يمكن إلحاق عنوان URL بمعاملات لتوفير مزيد من المعلومات، فمثلًا إذا استخدمنا محرك بحث، فيجب تحديد مصطلحات البحث الخاصة بنا بإضافتها إلى المسار في عنوان URL، ولكن يجب إضافة معاملات إلى عنوان URL من خلال إضافة الرمز ? إلى عنوان URL متبوعًا بأزواج ‎key=value‎ نفصل بينها بالرمز &. يطلب عنوان ‎http://www.google.ca?q=Python‎ من جوجل مثلًا البحث عن صفحات متعلقة ببايثون، فالمفتاح هو الحرف ‎q‎ والقيمة هي ‎Python‎، بينما يخبر الاستعلام ‎http://www.google.ca/search?q=Python&client=Firefox‎ جوجل أننا نستخدم متصفح فايرفوكس. يمكننا تمرير أي معاملات نريدها هنا، ولكن يتحكم التطبيق الذي يعمل على موقع الويب في تحديد المعاملات التي يجب الانتباه إليها وكيفية تفسيرها. إذا كانت الرموز ? و & من المحارف الخاصة، فيحب الهروب منها، ويجب إيجاد طريقة لوضع علامة اقتباس مزدوجة ضمن سلسلة محارف مُحدَّدة بهذه العلامات، لذا يمثل معيار ترميز URL المحارف الخاصة باستخدام الرمز % متبوعًا برمز مكون من رقمين مع استبدال المسافات بالمحرف +، وبالتالي يمكنك البحث في جوجل عن ‎grade = A+‎ مع المسافات من خلال استخدام العنوان ‎http://www.google.ca/search?q=grade+%3D+A%2B‎ مكتبات بايثون للاتصال بالخادم للسهولة يستخدم معظم المطورين مكتبات خاصة لفتح المقابس وإنشاء طلبات HTTP وتحليل الاستجابات مثل مكتبة ‎urllib2‎ في بايثون، وهي بديل لمكتبة سابقة اسمها ‎urllib‎، كما تُعَد مكتبة ‎Requests‎ بديلًا أسهل في الاستخدام من مكتبة ‎urllib2‎. يستخدم المثال التالي مكتبة ‎Requests‎ لتنزيل صفحة من موقع للكتب: import requests response = requests.get('http://aosabook.org/en/500L/web-server/testpage.html') print 'status code:', response.status_code print 'content length:', response.headers['content-length'] print response.text status code: 200 content length: 61 <html> <body> <p>Test page.</p> </body> </html> يرسل التابع ‎request.get‎ طلب GET إلى الخادم ويعيد كائن يحتوي على الاستجابة، ويكون العضو ‎status_code‎ الخاص بهذا الكائن هو رمز حالة الاستجابة، والعضو ‎content_length‎ هو عدد البايتات في بيانات الاستجابة، و ‎text‎ هو البيانات الفعلية وهو في حالتنا صفحة HTML. تطوير خادم ويب بسيط بعد أن تعرفنا على أساسيات عمل بروتوكلات الإنترنت، أصبحنا الآن مستعدين لكتابة خادم ويب بسيط، حيث تتألف فكرته الأساسية من الخطوات البسيطة التالية: الانتظار حتى يتصل شخص ما بهذا الخادم ويرسل طلب HTTP تحليل هذا الطلب اكتشاف ما يطلبه جلب هذه البيانات أو توليدها ديناميكيًا تنسيق البيانات بتنسيق HTML إرسال هذه البيانات تكون الخطوات 1 و 2 و 6 هي نفسها في جميع التطبيقات، لذا تحتوي مكتبة بايثون المعيارية على الوحدة ‎BaseHTTPServer‎ التي تنجز هذه الخطوات نيابة عنا، لذا نهتم فقط بالخطوات من 3 إلى 5 كما هو موضح فيما يلي: import BaseHTTPServer class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): '''Handle HTTP requests by returning a fixed 'page'.''' # الصفحة المراد إرسالها Page = '''\ <html> <body> <p>Hello, web!</p> </body> </html> ''' # معالجة طلب‫ GET‬ def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/html") self.send_header("Content-Length", str(len(self.Page))) self.end_headers() self.wfile.write(self.Page) #---------------------------------------------------------------------- if __name__ == '__main__': serverAddress = ('', 8080) server = BaseHTTPServer.HTTPServer(serverAddress, RequestHandler) server.serve_forever() يحلل الصنف ‎BaseHTTPRequestHandler‎ طلب HTTP الوارد ويحدد التابع التي يحتوي عليه، حيث إذا كان التابع هو GET، فسيستدعي هذا الصنف التابع ‎do_GET‎. يعدل الصنف ‎RequestHandler‎ هذا التابع لتوليد صفحة بسيطة ديناميكيًا، حيث يُخزَّن النص في متغير على مستوى الصنف ‎Page‎، والذي نرسله إلى العميل بعد إرسال رمز استجابة 200، وتخبر الترويسة ‎Content-Type‎ العميل بتفسير البيانات على أنها بتنسيق HTML، وتخبره بطول الصفحة. يؤدي استدعاء التابع ‎end_headers‎ إلى إدراج السطر الفارغ الذي يفصل الترويسات عن الصفحة نفسها. نحتاج إلى الأسطر الثلاثة الأخيرة لبدء تشغيل الخادم، حيث يحدد السطر الأول من هذه الأسطر عنوان الخادم كمجموعة مؤلفة من سلسلة نصية فارغة تعني التشغيل على الجهاز الحالي والقيمة 8080 التي تمثل المنفذ. ننشئ بعد ذلك نسخة من ‎BaseHTTPServer.HTTPServer‎ مع هذا العنوان واسم صنف معالج الطلب كمعاملات، ثم نطلب منه التشغيل إلى الأبد حتى إنهائه باستخدام الاختصار ‎Control-C‎. لن يؤدي تشغيل هذا البرنامج من سطر الأوامر إلى عرض أي شيء: $ python server.py ننتقل بعد ذلك إلى العنوان ‎http://localhost:8080‎ باستخدام المتصفح، وسنحصل على ما يلي في المتصفح: Hello, web! وسنحصل على ما يلي في الصدفة Shell: 127.0.0.1 - - [24/Feb/2014 10:26:28] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [24/Feb/2014 10:26:28] "GET /favicon.ico HTTP/1.1" 200 - لم نطلب هنا ملفًا معينًا، لذا طلب المتصفح المجلد الجذري لأي شيء يقدمه الخادم / في السطر الأول، ويظهر السطر الثاني لأن المتصفح يرسل تلقائيًا طلبًا ثانيًا لملف صورة بالاسم ‎/favicon.ico‎، والذي سيعرضه كأيقونة في شريط العناوين عند وجوده. عرض القيم لنعدل خادم الويب الخاص بنا لعرض بعض القيم المضمَّنة في طلب HTTP بهدف التدريب، حيث سنفصل إنشاء الصفحة عن إرسالها للحفاظ على ترتيب شيفرتنا البرمجية كما يلي: class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # قالب الصفحة def do_GET(self): page = self.create_page() self.send_page(page) def create_page(self): # نضع شيئًا هنا def send_page(self, page): # نضع شيئًا هنا ويكون التابع ‎send_page‎ كما يلي: def send_page(self, page): self.send_response(200) self.send_header("Content-type", "text/html") self.send_header("Content-Length", str(len(page))) self.end_headers() self.wfile.write(page) يُعَد القالب الخاص بالصفحة التي نريد عرضها مجرد سلسلة نصية تحتوي على جدول HTML مع بعض العناصر النائبة Placeholders للتنسيق كما يلي: Page = '''\ <html> <body> <table> <tr> <td>Header</td> <td>Value</td> </tr> <tr> <td>Date and time</td> <td>{date_time}</td> </tr> <tr> <td>Client host</td> <td>{client_host}</td> </tr> <tr> <td>Client port</td> <td>{client_port}s</td> </tr> <tr> <td>Command</td> <td>{command}</td> </tr> <tr> <td>Path</td> <td>{path}</td> </tr> </table> </body> </html> ''' ويكون التابع الذي يملأ هذه الصفحة كما يلي: def create_page(self): values = { 'date_time' : self.date_time_string(), 'client_host' : self.client_address[0], 'client_port' : self.client_address[1], 'command' : self.command, 'path' : self.path } page = self.Page.format(**values) return page لم تتغير البنية الرئيسية للبرنامج، فهو ينشئ نسخة من الصنف ‎HTTPServer‎ مع عنوان ومعالج الطلب كمعاملات، ثم يخدم الطلبات إلى الأبد، حيث إذا شغلنا البرنامج وأرسلنا طلب من متصفح على العنوان ‎http://localhost:8080/something.html‎، فسنحصل على النتيجة التالية: Date and time Mon, 24 Feb 2014 17:17:12 GMT Client host 127.0.0.1 Client port 54548 Command GET Path /something.html لم نحصل على خطأ 404 بالرغم من أن الصفحة ‎something.html‎ غير موجودة كملف على القرص الصلب، وسبب ذلك هو أن خادم الويب هو مجرد برنامج، ويمكنه أن يتخذ أي إجراء بناءً على البرمجة التي أعد بها، ويمكنه بدلاً من إرجاع صفحة 404 Not Found -إذا كان الملف غير موجود- أن يرسل صفحة افتراضية مع رسالة تخبر المستخدم أن الصفحة غير موجودة، أو يرسل صفحة عشوائية مثلاً صفحة من ويكيبيديا أو صفحة أخرى تفاعلية، أو يُعيد توجيهنا إلى صفحة أخرى إرسال الصفحات الثابتة أبسط شكل من أشكال خوادم الويب هو الذي يرسل ملفات HTML أو أي موارد أخرى كما هي إلى المتصفح، لذا سنعمل في هذه الخطوة على توفير صفحات الويب من القرص الصلب بدلًا من توليدها تلقائيًا، حيث سنبدأ بإعادة كتابة التابع ‎do_GET‎ كما يلي: def do_GET(self): try: # اكتشاف المطلوب بالضبط full_path = os.getcwd() + self.path # المطلوب غير موجود if not os.path.exists(full_path): raise ServerException("'{0}' not found".format(self.path)) # المطلوب هو ملف elif os.path.isfile(full_path): self.handle_file(full_path) # المطلوب هو شيء لا نستطيع معالجته else: raise ServerException("Unknown object '{0}'".format(self.path)) # معالجة الأخطاء except Exception as msg: self.handle_error(msg) يفترض هذا التابع أنه يمكن توفير أي ملفات موجودة ضمن المجلد الذي يعمل فيه خادم الويب، والذي نحصل عليه باستخدام التابع ‎os.getcwd‎، ويدمجه مع المسار المقدم في عنوان URL الذي تضعه المكتبة في ‎self.path‎ تلقائيًا، ويبدأ هذا المسار دائمًا بالرمز / للحصول على مسار الملف الذي يريده المستخدم. إن لم يكن هذا المسار موجودًا أو إن لم يكن ملفًا، فسيبلغ التابع عن خطأ من خلال رفع استثناء والتقاطه، بينما إذا كان المسار يتطابق مع ملف، فسيستدعي تابعًا مساعدًا بالاسم ‎handle_file‎ لقراءة المحتويات وإعادتها. يقرأ التابع التالي مثلًا الملف فقط ويستخدم التابع ‎send_content‎ الموجود مسبقًا لإرساله إلى العميل: def handle_file(self, full_path): try: with open(full_path, 'rb') as reader: content = reader.read() self.send_content(content) except IOError as msg: msg = "'{0}' cannot be read: {1}".format(self.path, msg) self.handle_error(msg) نلاحظ هنا أننا نفتح الملف في الوضع الثنائي الممثل بالحرف ‎b‎ في ‎rb‎، هذا يعني أن بايثون تقرأ الملف كما هو دون محاولة تعديل أو تغيير تنسيق النصوص مثل نهاية السطور. وتُعَد قراءة الملف بالكامل في الذاكرة عند تقديمه فكرة سيئة في الحياة الواقعية، فقد يحتوي الملف على عدة جيجابايتات من بيانات الفيديو، ولن نتطرق إلى هذه الحالة في مقالنا. نحتاج إلى كتابة تابع لمعالجة الأخطاء وقالب لصفحة الإبلاغ عن الأخطاء كما يلي: Error_Page = """\ <html> <body> <h1>Error accessing {path}</h1> <p>{msg}</p> </body> </html> """ def handle_error(self, msg): content = self.Error_Page.format(path=self.path, msg=msg) self.send_content(content) يعمل هذا البرنامج بنجاح، ولكن توجد مشكلة فيه تتمثل في أنه يعيد دائمًا رمز الحالة 200 حتى عندما لا تكون الصفحة المطلوبة موجودة. تحتوي الصفحة المرسلة في هذه الحالة على رسالة خطأ، ولكن لن يعرف المتصفح بفشل الطلب لأنه لا يستطيع قراءة اللغة الإنجليزية، لذا نحتاج إلى تعديل التابعين ‎handle_error‎ و ‎send_content‎ كما يلي: # معالجة الكائنات غير المعروفة def handle_error(self, msg): content = self.Error_Page.format(path=self.path, msg=msg) self.send_content(content, 404) # إرسال المحتوى الفعلي def send_content(self, content, status=200): self.send_response(status) self.send_header("Content-type", "text/html") self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) لا نرفع الاستثناء ‎ServerException‎ عند عدم العثور على ملف، بل نولد صفحة خطأ بدلًا من ذلك، فالغرض من هذا الاستثناء هو الإشارة إلى خطأ في شيفرة الخادم، بينما تظهر صفحة الخطأ التي أنشأها التابع ‎handle_error‎ عندما يخطئ المستخدم في شيء ما مثل إرسال عنوان URL لملف غير موجود. ملاحظة: سنستخدم التابع ‎handle_error‎ عدة مرات في هذا المقال، بما في ذلك الحالات لا يكون فيها رمز الحالة 404 مناسبًا، لذا لنحاول التفكير في كيفية توسيع هذا البرنامج بحيث يمكن توفير رمز استجابة الحالة بسهولة. سرد محتويات المجلدات يمكننا تعليم خادم الويب لعرض قائمة بمحتويات مجلد عندما يكون المسار في عنوان URL يمثل مجلدًا وليس ملفًا، كما يمكن جعله يبحث في هذا المجلد عن الملف ‎index.html‎ لعرضه وعرض قائمة بمحتويات المجلد فقط إن لم يكن هذا الملف موجودًا. سيؤدي بناء ذلك في التابع ‎do_GET‎ خطأ، إذ سيحتوي التابع الناتج على تداخل طويل من تعليمات ‎if‎ التي تتحكم في سلوكيات الخادم الخاصة، فالحل الصحيح هو العودة إلى الخطوة السابقة وحل المشكلة العامة من خلال معرفة ما يجب فعله بعنوان URL، لذا سنعيد كتابة التابع ‎do_GET‎ كما يلي: def do_GET(self): try: # اكتشاف المطلوب بالضبط self.full_path = os.getcwd() + self.path # اكتشاف كيفية معالجة المطلوب for case in self.Cases: handler = case() if handler.test(self): handler.act(self) break # معالجة الأخطاء except Exception as msg: self.handle_error(msg) تبقى الخطوة الأولى نفسها، والتي تتمثل في اكتشاف المسار الكامل للشيء المطلوب، ثم تتكرر هذه النسخة من الشيفرة البرمجية ضمن حلقة على مجموعة من الحالات المخزنة في قائمة بدلًا من استخدام مجموعة من الاختبارات المضمنة، ويكون لكل حالة كائن مع تابعين هما: ‎test‎ الذي يخبرنا ما إذا كان الخادم قادرًا على معالجة الطلب، و ‎act‎ الذي يتخذ بعض الإجراءات، حيث يُعالَج الطلب ونخرج من الحلقة بمجرد العثور على الحالة الصحيحة. تغير أصناف الحالات الثلاثة التالية سلوك الخادم السابق كما يلي: class case_no_file(object): '''File or directory does not exist.''' def test(self, handler): return not os.path.exists(handler.full_path) def act(self, handler): raise ServerException("'{0}' not found".format(handler.path)) class case_existing_file(object): '''File exists.''' def test(self, handler): return os.path.isfile(handler.full_path) def act(self, handler): handler.handle_file(handler.full_path) class case_always_fail(object): '''Base case if nothing else worked.''' def test(self, handler): return True def act(self, handler): raise ServerException("Unknown object '{0}'".format(handler.path)) نبني قائمة معالجات الحالات في بداية الصنف ‎RequestHandler‎ كما يلي: class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): ''' If the requested path maps to a file, that file is served. If anything goes wrong, an error page is constructed. ''' Cases = [case_no_file(), case_existing_file(), case_always_fail()] ...everything else as before... أدى هذا التعديل على شيفرتنا البرمجية إلى جعل الخادم أكثر تعقيدًا، إذ زاد حجم الملف من 74 إلى 99 سطرًا دون إنجاز وظيفة جديدة، ولكن سنرى الفائدة من هذا التعديل عندما نحاول تعليم الخادم عرض صفحة ‎index.html‎ لمجلد ما عند وجود هذه الصفحة، وعرض قائمة بمحتويات المجلد عند عدم وجودها، حيث يكون المعالج للحالة الأولى كما يلي: class case_directory_index_file(object): '''Serve index.html page for a directory.''' def index_path(self, handler): return os.path.join(handler.full_path, 'index.html') def test(self, handler): return os.path.isdir(handler.full_path) and \ os.path.isfile(self.index_path(handler)) def act(self, handler): handler.handle_file(self.index_path(handler)) يبني التابع المساعد ‎index_path‎ مسار الوصول إلى الملف ‎index.html‎، حيث يمنع وضع هذا التابع في معالج الحالات من تعقيد الصنف ‎RequestHandler‎ الرئيسي. ويتحقق التابع ‎test‎ مما إذا كان المسار يمثل مجلدًا يحتوي على الصفحة ‎index.html‎، ويطلب التابع ‎act‎ من معالج الطلب الرئيسي تقديم هذه الصفحة. التغيير الوحيد المطلوب في الصنف ‎RequestHandler‎ هو إضافة كائن ‎case_directory_index_file‎ إلى قائمة الحالات ‎Cases‎ كما يلي: Cases = [case_no_file(), case_existing_file(), case_directory_index_file(), case_always_fail()] إذا كان المجلد لا يحتوي على صفحة ‎index.html‎، فسيبقى تابع الاختبار نفسه بحيث يتحقق مما إذا كان المسار يمثل مجلدًا يحتوي على الصفحة ‎index.html‎ دون إدراجها باستخدام ‎not‎. class case_directory_no_index_file(object): '''Serve listing for a directory without an index.html page.''' def index_path(self, handler): return os.path.join(handler.full_path, 'index.html') def test(self, handler): return os.path.isdir(handler.full_path) and \ not os.path.isfile(self.index_path(handler)) def act(self, handler): ??? يجب أن ينشئ التابع ‎act‎ قائمة بمحتويات المجلد ويعيدها، ولكن لا تسمح الشيفرة البرمجية الحالية بذلك، إذ يستدعي ‎RequestHandler.do_GET‎ التابع ‎act‎، لكنه لا يتوقع أو يتعامل مع قيمة معادة منه. لنضف الآن تابعًا إلى الصنف ‎RequestHandler‎ لتوليد قائمة بمحتويات المجلد، ونستدعيه من التابع ‎act‎ الخاص بمعالج الحالة كما يلي: class case_directory_no_index_file(object): '''Serve listing for a directory without an index.html page.''' # يبقى‫ index_path و test كما في السابق‬ def act(self, handler): handler.list_dir(handler.full_path) class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): # ما تبقى من الشيفرة البرمجية # كيفية عرض قائمة المجلد Listing_Page = '''\ <html> <body> <ul> {0} </ul> </body> </html> ''' def list_dir(self, full_path): try: entries = os.listdir(full_path) bullets = ['<li>{0}</li>'.format(e) for e in entries if not e.startswith('.')] page = self.Listing_Page.format('\n'.join(bullets)) self.send_content(page) except OSError as msg: msg = "'{0}' cannot be listed: {1}".format(self.path, msg) self.handle_error(msg) بروتوكول CGI لن يرغب معظم الأشخاص في تعديل شيفرة خادم الويب الخاص بهم لإضافة وظائف جديدة، لذا تدعم الخوادم آلية تسمى واجهة البوابة المشتركة Common Gateway Interface -أو CGI اختصارًا- والتي توفر طريقة معيارية لخادم الويب لتشغيل برنامج خارجي لتلبية الطلبات. لنفترض مثلًا أننا نريد تمكين الخادم من عرض الوقت المحلي في صفحة HTML في برنامج مستقل كما يلي: from datetime import datetime print '''\ <html> <body> <p>Generated {0}</p> </body> </html>'''.format(datetime.now()) نضيف معالج الحالة التالي ليشغّل خادم الويب هذا البرنامج: class case_cgi_file(object): '''Something runnable.''' def test(self, handler): return os.path.isfile(handler.full_path) and \ handler.full_path.endswith('.py') def act(self, handler): handler.run_cgi(handler.full_path) يكون الاختبار بسيطًا، حيث إذا انتهى مسار الملف باللاحقة ‎.py‎، فسيشغل الصنف ‎RequestHandler‎ هذا البرنامج. def run_cgi(self, full_path): cmd = "python " + full_path child_stdin, child_stdout = os.popen2(cmd) child_stdin.close() data = child_stdout.read() child_stdout.close() self.send_content(data) ولكن يُعَد هذا الاختبار غير آمن، فإذا عرف شخصٌ ما مسار الوصول إلى ملف بايثون على الخادم، فسيسمح له بتشغيله دون القلق بشأن البيانات التي يمكنه الوصول إليها أو من احتوائه على حلقة لا نهائية أو أي شيء آخر. تستخدم شيفرتنا البرمجية دالة المكتبة ‎popen2‎ التي أُوقِفت مع استخدام وحدة subprocess، ولكن ‎popen2‎ هي الأداة الأنسب للاستخدام في مثالنا. تُعَد الفكرة الأساسية لبروتوكول CGI بسيطة بغض النظر عن هذه المشكلة الأمنية، حيث تتألف من الخطوات التالية: تشغيل البرنامج في عملية فرعية التقاط كل ما ترسله هذه العملية الفرعية إلى الخرج إرسال الخرج الناتج إلى العميل الذي قدم الطلب يكون بروتوكول CGI الكامل أكبر من ذلك، فمثلًا يسمح بالمعاملات في عنوان URL التي يمررها الخادم إلى البرنامج المُشغَّل، ولكن لا تؤثر هذه التفاصيل على البنية العامة للنظام التي أصبحت متداخلة، إذ كان لدى الصنف ‎RequestHandler‎ تابع واحد في البداية هو ‎handle_file‎ للتعامل مع المحتوى، وأضفنا حالتين خاصتين في التابعين ‎list_dir‎ و ‎run_cgi‎. ليست هذه التوابع الثلاثة في مكانها الصحيح لأن التوابع الأخرى تستخدمها، ويتمثل الحل في إنشاء صنف أب لجميع معالجات الحالات، ثم ننقل التوابع الأخرى إلى هذا الصنف إذا كانت مشتركة فقط بين معالجين أو أكثر. سيكون الصنف ‎RequestHandler‎ في النهاية كما يلي: class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): Cases = [case_no_file(), case_cgi_file(), case_existing_file(), case_directory_index_file(), case_directory_no_index_file(), case_always_fail()] # كيفية عرض الخطأ Error_Page = """\ <html> <body> <h1>Error accessing {path}</h1> <p>{msg}</p> </body> </html> """ # تصنيف الطلب ومعالجته def do_GET(self): try: # اكتشاف المطلوب بالضبط self.full_path = os.getcwd() + self.path # اكتشاف كيفية معالجة المطلوب for case in self.Cases: if case.test(self): case.act(self) break # معالجة الأخطاء except Exception as msg: self.handle_error(msg) # معالجة الكائنات غير المعروفة def handle_error(self, msg): content = self.Error_Page.format(path=self.path, msg=msg) self.send_content(content, 404) # إرسال المحتوى الفعلي def send_content(self, content, status=200): self.send_response(status) self.send_header("Content-type", "text/html") self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) ويكون الصنف الأب لمعالجات الحالة الخاصة بنا كما يلي: class base_case(object): '''Parent for case handlers.''' def handle_file(self, handler, full_path): try: with open(full_path, 'rb') as reader: content = reader.read() handler.send_content(content) except IOError as msg: msg = "'{0}' cannot be read: {1}".format(full_path, msg) handler.handle_error(msg) def index_path(self, handler): return os.path.join(handler.full_path, 'index.html') def test(self, handler): assert False, 'Not implemented.' def act(self, handler): assert False, 'Not implemented.' ويكون المعالج لملف موجود مسبقًا كما يلي: class case_existing_file(base_case): '''File exists.''' def test(self, handler): return os.path.isfile(handler.full_path) def act(self, handler): self.handle_file(handler, handler.full_path) الخاتمة من خلال فهم وتطبيق الأفكار الواردة في هذه المقال، سنتمكن من بناء خادم ويب مرن وسهل التوسع باستخدام بايثون، وسيسهل علينا إضافة مميزات جديدة أو تعديل السلوك دون التأثير على بقية النظام، جرب تعديل الشيفرة وبناء الخادم الخاص بك إذ يمكن على سبيل المثال إضافة معالجات لحالات جديدة فإذا أردنا دعم حالة معينة مثل عرض صفحة خاصة للمستخدمين المسجلين فيمكن إضافة صنف جديد مع معالج لهذه الحالة ببساطة عن طريق تعديل الكود في RequestHandler دون التأثير على بقية الخادم. كما يمكن إضافة وظائف جديدة عبر كتابة برنامج CGI خارجي يمكنه معالجة نوع معين من الطلبات، مثل تحليل البيانات الواردة من نماذج الويب أو إجراء عمليات معقدة على الخادم، دون الحاجة لتعديل الشيفرة الأساسية للخادم. أو يمكن لخادم الويب أن يقرأ ملف إعدادات لتحميل الأصناف المخصصة لمعالجة الطلبات. فهذا يسمح لك بتغيير سلوك الخادم أو إضافة معالجات جديدة دون تعديل الشيفرة الأساسية، فقط بتعديل ملف الإعدادات، كما يمكن توسيع الخادم ليدعم أنواع محتوى إضافية على سبيل المثال، يمكنك إضافة دعم ملفات الفيديو أو الصور الضخمة عن طريق كتابة صنف يعالج هذه الأنواع ويعرض المحتوى بشكل فعال ودون التأثير على طلبات أخرى. ترجمة -وبتصرّف- للمقال A Simple Web Server لصاحبه Greg Wilson اقرأ أيضًا شبكة الإنترنت باستخدام بروتوكول IP كيفية كتابة تطبيقات الويب إعداد خادم اختبار محلي مشاريع بايثون عملية تناسب المبتدئين تعرف على عناوين بروتوكول الإنترنت والشبكات الفرعية والتوجيه غير الصنفي بين النطاقات
  17. عند استخدام قاعدة بيانات علاقية Relational Database، سنحتاج إلى استخدام استعلامات فردية باستخدام لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- لاسترجاع البيانات أو معالجتها مثل استعلامات SELECT أو INSERT أو UPDATE أو DELETE من شيفرة التطبيق مباشرةً، إذ تعمل هذه التعليمات مع جداول قاعدة البيانات الأساسية وتعالجها فورًا. لكن إذا استخدمنا التعليمات أو مجموعة التعليمات نفسها ضمن تطبيقات متعددة يمكنها الوصول إلى قاعدة البيانات نفسها، وتكررت هذه التعليمات عدة مرات فيمكننا في هذه الحالة استخدام الإجراءات المخزَّنة Stored Procedures التي يدعمها MySQL كحال العديد من أنظمة إدارة قواعد البيانات العلاقية الأخرى، حيث تساعد هذه الإجراءات المخزنة في تجميع تعليمة SQL واحدة أو أكثر لإعادة استخدامها باسم مشترك من خلال تغليف منطق العمل المشترك ضمن قاعدة البيانات نفسها، ويمكن استدعاء مثل هذه الإجراءات من التطبيق الذي يصل إلى قاعدة البيانات لاسترجاع البيانات أو معالجتها. تساعدنا الإجراءات المخزنة على إنشاء برامج قابلة لإعادة الاستخدام للمهام الشائعة التي سنستخدمها عبر تطبيقات متعددة، كما توفر طريقة للتحقق من صحة البيانات، أو تقديم طبقة إضافية من أمان الوصول إلى البيانات من خلال تقييد مستخدمي قاعدة البيانات من الوصول إلى الجداول الأساسية مباشرةً وإنشاء استعلامات عشوائية. سنتعلّم في هذا المقال ما هي الإجراءات المخزَّنة وكيفية إنشاء إجراءات مخزنة بسيطة تعيد البيانات، وإجراءات تستخدم كلًا من معاملات الدخل والخرج. مستلزمات العمل يجب أن يكون لدينا حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية RDBMS المستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو مع مستخدم بصلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر معرفة أساسية بتنفيذ استعلامات SELECT لاسترجاع البيانات من قاعدة البيانات ملاحظة: تجدر الإشارة لأنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، ولا تُعَد صيغة الإجراءات المخزنة جزءًا من معيار SQL الرسمي. حيث ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن تُعَد الإجراءات المخزنة خاصة بقاعدة البيانات، وبالتالي قد نجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات فارغة يمكن من خلالها إنشاء جداول توضّح استخدام الإجراءات المخزنة، ويمكن مطالعة القسم التالي للحصول على تفاصيل حول الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية، والتي سنستخدمها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية سنتتصل بخادم MySQL وننشئ قاعدة بيانات تجريبية لاتباع الأمثلة الواردة في هذا المقال، حيث سنستخدم مجموعة سيارات افتراضية، ونخزّن تفاصيل السيارات المملوكة حاليًا مع نوعها وطرازها وسنة بنائها وقيمتها. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نتصل بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات باسم procedures: mysql> CREATE DATABASE procedures; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات procedures من خلال تنفيذ تعليمة USE التالية: $ USE procedures; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، ويمكنك الآن إنشاء جداول تجريبية ضمنها. سيحتوي الجدول cars على بيانات مبسَّطة حول السيارات الموجودة في قاعدة البيانات، حيث سيحتوي على الأعمدة التالية: make: نوع كل سيارة مملوكة، ونمثّل باستخدام نوع البيانات varchar بحد أقصى 100 محرف model: اسم طراز السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 100 محرف year: سنة صنع السيارة باستخدام نوع البيانات int للاحتفاظ بالقيم العددية value: قيمة السيارة باستخدام نوع البيانات decimal بحد أقصى 10 أرقام ورقمين بعد الفاصلة العشرية أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE cars ( mysql> make varchar(100), mysql> model varchar(100), mysql> year int, mysql> value decimal(10, 2) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) سندرج بعض البيانات التجريبية في الجدول cars من خلال تنفيذ عملية INSERT INTO التالية: mysql> INSERT INTO cars mysql> VALUES mysql> ('Porsche', '911 GT3', 2020, 169700), mysql> ('Porsche', 'Cayman GT4', 2018, 118000), mysql> ('Porsche', 'Panamera', 2022, 113200), mysql> ('Porsche', 'Macan', 2019, 27400), mysql> ('Porsche', '718 Boxster', 2017, 48880), mysql> ('Ferrari', '488 GTB', 2015, 254750), mysql> ('Ferrari', 'F8 Tributo', 2019, 375000), mysql> ('Ferrari', 'SF90 Stradale', 2020, 627000), mysql> ('Ferrari', '812 Superfast', 2017, 335300), mysql> ('Ferrari', 'GTC4Lusso', 2016, 268000); تضيف العملية INSERT INTO عشر سيارات رياضية نموذجية للجدول، حيث توجد أربع سيارات من نوع Porsche وخمسة سيارات من نوع Ferrari. يشير الخرج التالي إلى إضافة جميع الصفوف العشرة: الخرج Query OK, 10 rows affected (0.00 sec) Records: 10 Duplicates: 0 Warnings: 0 نحن الآن جاهزون لمتابعة هذا المقال والبدء باستخدام الإجراءات المُخزَّنة في لغة SQL. مقدمة إلى الإجراءات المخزنة Stored Procedures الإجراءات المخزنة في MySQL وفي العديد من أنظمة قواعد البيانات العلاقية الأخرى هي كائنات مُسمَّاة تحتوي على تعليمة واحدة أو أكثر تضعها وتنفّذها قاعدة البيانات عند استدعائها. يمكن للإجراء المخزن حفظ تعليمة مشتركة ضمن برنامج قابل لإعادة الاستخدام مثل استرجاع البيانات من قاعدة البيانات باستخدام المرشّحات Filters المُستخدَمة كثيرًا، حيث يمكنك مثلًا إنشاء إجراء مخزن لاسترجاع عملاء متجر الكتروني قدّموا طلبات خلال عدد معين من الأشهر. يمكن للإجراءات المخزنة أيضًا تمثيل البرامج الشاملة التي تصف منطق الأعمال المعقد للتطبيقات القوية في السيناريوهات الأكثر تعقيدًا. يمكن أن تتضمن مجموعة التعليمات في إجراء مخزَّن تعليمات SQL شائعة مثل استعلامات SELECT أو INSERT التي تعيد البيانات أو تعالجها، ويمكن للإجراءات المخزنة الاستفادة مما يلي: المعاملات المُمرَّرة إلى الإجراء المخزن أو المُعادة منه المتغيرات المُصرَّح عنها لمعالجة البيانات المُسترجَعة من شيفرة الإجراء البرمجية مباشرةً التعليمات الشرطية التي تسمح بتنفيذ أجزاء من شيفرة الإجراء المخزن البرمجية وفق شروط معينة مثل تعليمات IF أو CASE الحلقات مثل WHILE و LOOP و REPEAT لتنفيذ أجزاء من الشيفرة البرمجية عدة مرات تعليمات معالجة الأخطاء مثل إعادة رسائل الخطأ إلى مستخدمي قاعدة البيانات الذين يمكنهم الوصول إلى الإجراء. استدعاءات إجراءات مخزنة أخرى في قاعدة البيانات ملاحظة: تسمح الصيغة الموسَّعة Extensive Syntax التي يدعمها MySQL بكتابة برامج قوية وحل المشكلات المعقدة باستخدام الإجراءات المخزنة مثل التحكم في تدفق البرنامج باستخدام التعليمات الشرطية واستخدام المتغيرات والحلقات ومعالجة الأخطاء المخصصة وغيرها من الاستخدامات ولكن سيغطي هذا المقال فقط الاستخدام الأساسي للإجراءات المخزنة مع تعليمات SQL المُضمَّنة في جسم الإجراء المخزَّن ومعاملات الدخل والخرج، إذ سيكون تنفيذ التعليمات الشرطية واستخدام المتغيرات والحلقات ومعالجة الأخطاء المُخصَّصة خارج نطاق هذا المقال، لذا يمكن مطالعة توثيق MySQL الرسمي لمعرفة المزيد حول الإجراءات المخزنة. إذا استدعينا الإجراء باسمه، فسينفّذه محرّك قاعدة البيانات كما هو مُعرَّف تعليمةً تلو الأخرى. يجب أيضًا أن يكون لدى مستخدم قاعدة البيانات الأذونات المناسبة لتنفيذ الإجراء المُحدَّد، حيث توفر هذه الأذونات المطلوبة طبقة من الأمان، مما يمنع الوصول المباشر إلى قاعدة البيانات مع منح المستخدمين إمكانية الوصول إلى إجراءات فردية مضمونة الأمان لتنفيذها. تُنفَّذ الإجراءات المخزَّنة على خادم قاعدة البيانات مباشرةً مع إجراء جميع العمليات الحسابية محليًا وإعادة النتائج إلى المستخدم المستدعي عند الانتهاء فقط. وإذا أردنا تغيير سلوك الإجراء، فيمكن تحديث الإجراء في قاعدة البيانات، وستلتقط التطبيقات التي تستخدمه الإصدار الجديد تلقائيًا، وسيبدأ جميع المستخدمين باستخدام الشيفرة البرمجية للإجراء الجديد مباشرةً دون الحاجة لتعديل تطبيقاتهم. فيما يلي الهيكل العام لشيفرة SQL المستخدَمة لإنشاء إجراءٍ مخزَّن: mysql> DELIMITER // mysql> CREATE PROCEDURE procedure_name(parameter_1, parameter_2, . . ., parameter_n) mysql> BEGIN mysql> instruction_1; mysql> instruction_2; mysql> . . . mysql> instruction_n; mysql> END // mysql> DELIMITER ; التعليمتان الأولى والأخيرة في مقطع الشيفرة البرمجية السابق هما DELIMITER //‎ و DELIMITER ;‎، حيث يستخدم MySQL رمز الفاصلة المنقوطة ; لتحديد التعليمات والإشارة إلى بدايتها ونهايتها. إذا نفّذنا تعليمات متعددة في طرفية MySQL مع الفصل بينها بفواصل منقوطة، سيكون التعامل معها كأنها أوامر منفصلة مع تنفيذ كل تعليمة تنفيذًا مستقلًا عن التعليمات الأخرى واحدة تلو الأخرى. يمكن للإجراء المخزَّن أيضًا أن يتضمّن أوامر متعددة ستُنفَّذ تسلسليًا عند استدعائه، مما يشكّل صعوبة عند محاولة إخبار MySQL بإنشاء إجراء جديد، إذ سيرى محرّك قاعدة البيانات علامة الفاصلة المنقوطة في جسم الإجراء المُخزَّن ويعتقد أنه يجب أن يتوقف عن تنفيذ التعليمة، وبالتالي تكون التعليمة المقصودة في هذه الحالة هي الشيفرة البرمجية لإنشاء الإجراء بالكامل، وليس تعليمةً واحدة ضمن الإجراء نفسه، لذا قد يسيء MySQL تفسير ما نقصده. يمكن التغلب على هذا القيد من خلال استخدام الأمر DELIMITER لتغيير هذا المحدِّد مؤقتًا من ; إلى // طوال مدة استدعاء التعليمة CREATE PROCEDURE، ثم ستُمرَّر جميع الفواصل المنقوطة الموجودة في جسم الإجراء المخزَّن إلى الخادم كما هي، ثم يتغير المحدِّد مرة أخرى إلى ; في آخر تعليمة DELIMITER ;‎ بعد الانتهاء من الإجراء بالكامل. يمثّل الاستدعاء CREATE PROCEDURE وبعده اسم الإجراء procedure_name في المثال السابق جوهر الشيفرة البرمجية الخاصة بإنشاء إجراء جديد، ويتبع اسم الإجراء قائمة اختيارية من المعاملات التي سيقبلها الإجراء. الجزء الأخير من الشيفرة البرمجية هو جسم الإجراء المضمَّن ضمن تعليمتي BEGIN و END، ويوجد في الداخل شيفرة الإجراء البرمجية، والتي يمكن أن تحتوي على تعليمة SQL واحدة مثل استعلام SELECT أو شيفرة برمجية أكثر تعقيدًا. ينتهي الأمر END بالرمز //، والذي يُعَد محدِّدًا مؤقتًا عوضًا عن الفاصلة المنقوطة النموذجية. سننشئ في القسم التالي إجراءً مخزنًا بسيطًا بدون معاملات تتضمن استعلامًا واحدًا. إنشاء إجراء مخزن بدون معاملات سننشئ في هذا القسم أول إجراء مخزّن يغلِّف تعليمة SQL واحدة هي التعليمة SELECT لإعادة قائمة السيارات المملوكة المرتبة حسب نوعها وقيمتها بترتيب تنازلي. نبدأ بتنفيذ التعليمة SELECT التي ستستخدمها كما يلي: mysql> SELECT * FROM cars ORDER BY make, value DESC; ستعيد قاعدة البيانات قائمة السيارات من الجدول cars مع ترتيبها حسب نوعها أولًا ثم حسب قيمتها بترتيب تنازلي ضمن نوع السيارة الواحد كما يلي: الخرج +---------+---------------+------+-----------+ | make | model | year | value | +---------+---------------+------+-----------+ | Ferrari | SF90 Stradale | 2020 | 627000.00 | | Ferrari | F8 Tributo | 2019 | 375000.00 | | Ferrari | 812 Superfast | 2017 | 335300.00 | | Ferrari | GTC4Lusso | 2016 | 268000.00 | | Ferrari | 488 GTB | 2015 | 254750.00 | | Porsche | 911 GT3 | 2020 | 169700.00 | | Porsche | Cayman GT4 | 2018 | 118000.00 | | Porsche | Panamera | 2022 | 113200.00 | | Porsche | 718 Boxster | 2017 | 48880.00 | | Porsche | Macan | 2019 | 27400.00 | +---------+---------------+------+-----------+ 10 rows in set (0.00 sec) نلاحظ ظهور سيارة الفيراري الأعلى قيمة في أعلى القائمة، وظهور سيارة البورش الأدنى قيمة في الأسفل. لنفترض استخدام هذا الاستعلام بصورة متكررة في تطبيقات متعددة أو سيستخدمه مستخدمون متعددون ونريد التأكّد من أن الجميع سيستخدمون الطريقة نفسها لترتيب النتائج، لذا يجب إنشاء إجراء مخزن يحفظ تعليمة هذا الاستعلام ضمن إجراء مُسمَّى قابل لإعادة الاستخدام من خلال تنفيذ جزء الشيفرة البرمجية التالي: mysql> DELIMITER // mysql> CREATE PROCEDURE get_all_cars() mysql> BEGIN mysql> SELECT * FROM cars ORDER BY make, value DESC; mysql> END // mysql> DELIMITER ; نلاحظ أن الأمرين الأول DELIMITER //‎ والأخير DELIMITER ;‎ يخبران MySQL بالتوقف عن التعامل مع محرف الفاصلة المنقوطة بوصفه محدِّدًا للتعليمات طوال مدة إنشاء الإجراء كما هو موضّح في القسم السابق. يتبع أمر SQL الذي هو CREATE PROCEDURE اسم الإجراء get_all_cars الذي يمكن تعريفه لوصف ما يفعله الإجراء، ثم يوجد زوج من الأقواس () يمكننا إضافة معاملات ضمنه، ولكن لا يستخدم هذا الإجراء معاملات في مثالنا، لذا ستكون الأقواس فارغة، ثم تُكتَب تعليمة SELECT نفسها المُستخدَمة سابقًا بين الأمرين BEGIN و END اللذين يحددان بداية ونهاية كتلة شيفرة الإجراء البرمجية. ملاحظة: قد يظهر الخطأ ERROR 1044 (42000): Access denied for user 'user'@'localhost' to database 'procedures'‎ عند تنفيذ الأمر CREATE PROCEDURE بناءً على أذونات مستخدم MySQL الخاص بنا. يمكن منح الأذونات اللازمة لإنشاء وتنفيذ الإجراءات المخزنة للمستخدم من خلال تسجيل الدخول إلى MySQL كمستخدم جذر وتنفيذ الأوامر التالية وتغيير اسم مستخدم MySQL والمضيف حسب الحاجة: mysql> GRANT CREATE ROUTINE, ALTER ROUTINE, EXECUTE on *.* TO 'user'@'localhost'; mysql> FLUSH PRIVILEGES; نحدّث أذونات المستخدم، ثم نسجّل الخروج كمستخدم جذر، ونسجّل الدخول مرة أخرى كمستخدم عادي، ثم نعيد تشغيل تعليمة CREATE PROCEDURE. يمكن معرفة المزيد حول تطبيق الأذونات الخاصة بالإجراءات المخزنة لمستخدمي قاعدة البيانات في توثيق MySQL الرسمي الخاص بصلاحيات MySQL والبرامج المُخزَّنة. ستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 0 rows affected (0.02 sec) أصبح الإجراء get_all_cars الآن محفوظًا في قاعدة البيانات، وستُنفَّذ التعليمة المحفوظة كما هي عند استدعائه. يمكن تنفيذ الإجراءات المخزَّنة المحفوظة من خلال استخدام أمر SQL الذي هو CALL متبوعًا باسم الإجراء. نجرّب الآن تشغيل الإجراء الذي أنشأناه كما يلي: mysql> CALL get_all_cars; نحتاج اسم الإجراء get_all_cars فقط لاستخدام هذا الإجراء، إذ لم تَعُد بحاجة إلى كتابة أيّ جزء من تعليمة SELECT التي استخدمناها سابقًا يدويًا، وستعرض قاعدة البيانات النتائج مثل خرج التعليمة SELECT التي نفّذتها سابقًا كما يلي: الخرج +---------+---------------+------+-----------+ | make | model | year | value | +---------+---------------+------+-----------+ | Ferrari | SF90 Stradale | 2020 | 627000.00 | | Ferrari | F8 Tributo | 2019 | 375000.00 | | Ferrari | 812 Superfast | 2017 | 335300.00 | | Ferrari | GTC4Lusso | 2016 | 268000.00 | | Ferrari | 488 GTB | 2015 | 254750.00 | | Porsche | 911 GT3 | 2020 | 169700.00 | | Porsche | Cayman GT4 | 2018 | 118000.00 | | Porsche | Panamera | 2022 | 113200.00 | | Porsche | 718 Boxster | 2017 | 48880.00 | | Porsche | Macan | 2019 | 27400.00 | +---------+---------------+------+-----------+ 10 rows in set (0.00 sec) Query OK, 0 rows affected (0.00 sec) نجحنا في إنشاء إجراء مخزَّن بدون معاملات، حيث يعيد هذا الإجراء جميع السيارات من الجدول cars مرتبةً بطريقة معينة، ويمكن استخدام هذا الإجراء في تطبيقات متعددة. سننشئ في القسم التالي إجراء يقبل المعاملات لتغيير سلوك الإجراء وفقًا لدخل المستخدم. إنشاء إجراء مخزن مع معامل دخل سنضمِّن في هذا القسم معاملات دخل في تعريف الإجراء المخزَّن للسماح للمستخدمين الذين ينفّذون الإجراء بتمرير البيانات إليه، فمثلًا يمكن للمستخدمين توفير مرشّحات للاستعلام. يسترجع الإجراء المخزن get_all_cars الذي أنشأناه مسبقًا جميع السيارات من الجدول cars المصنعة في جميع سنوات التصنيع، ولننشئ الآن إجراء آخر للعثور على السيارات المُصنَّعة في سنة معينة، حيث سنعرِّف معاملًا في تعريف الإجراء من خلال تشغيل الشيفرة البرمجية التالية: mysql> DELIMITER // mysql> CREATE PROCEDURE get_cars_by_year( mysql> IN year_filter int mysql> ) mysql> BEGIN mysql> SELECT * FROM cars WHERE year = year_filter ORDER BY make, value DESC; mysql> END // mysql> DELIMITER ; توجد العديد من التغييرات على شيفرة إنشاء الإجراء مقارنة بالشيفرة المستخدمة في القسم السابق، حيث تغيّر الاسم ليكون get_cars_by_year ليمثل عمل الإجراء، وهو استرجاع السيارات بناءً على سنة إصدارها. كما أصبحت الأقواس الفارغة سابقًا محتوية على تعريف معامل واحد هو IN year_filter int، حيث تخبر الكلمة المفتاحية IN قاعدة البيانات بأن المستخدم المستدعِي سيمرّر المعامل إلى الإجراء. يُعَد year_filter اسمًا عشوائيًا للمعامل، حيث سنستخدمه للإشارة إلى المعامل في شيفرة الإجراء البرمجية، و int هو نوع البيانات، حيث نمثّل سنة التصنيع بقيمة عددية. يظهر المعامل year_filter المُعرَّف بعد اسم الإجراء في تعليمة SELECT ضمن التعليمة WHERE year = year_filter، مما يؤدي إلى ترشيح الجدول cars وفقًا لسنة التصنيع، وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 0 rows affected (0.02 sec) نفّذ الإجراء بدون تمرير أي معاملات إليه كما فعلنا سابقًا: mysql> CALL get_cars_by_year; وستعيد قاعدة بيانات MySQL رسالة الخطأ التالية: رسالة خطأ ERROR 1318 (42000): Incorrect number of arguments for PROCEDURE procedures.get_cars_by_year; expected 1, got 0 يتوقع الإجراء المخزن هذه المرة توفير معامل له، ولكن لم نقدّم له أيّ معامل، لذا يمكننا استدعاء إجراء مخزَّن مع معاملات من خلال توفير قيم المعاملات بين قوسين بنفس الترتيب الذي يتوقعه الإجراء. سننفّذ الإجراء التالي لاسترجاع السيارات المصنعة في عام 2017: mysql> CALL get_cars_by_year(2017); سيُنفَّذ الإجراء المستدعَى الآن تنفيذًا صحيحًا ويعيد قائمة السيارات من عام 2017، وسينتج الخرج التالي: الخرج +---------+---------------+------+-----------+ | make | model | year | value | +---------+---------------+------+-----------+ | Ferrari | 812 Superfast | 2017 | 335300.00 | | Porsche | 718 Boxster | 2017 | 48880.00 | +---------+---------------+------+-----------+ 2 rows in set (0.00 sec) Query OK, 0 rows affected (0.00 sec) تعلّمنا في المثال السابق كيفية تمرير معاملات الدخل إلى الإجراءات المخزنة واستخدامها في الاستعلامات ضمن الإجراء لتوفير خيارات الترشيح، وسنستخدم في القسم التالي معاملات الخرج لإنشاء إجراءات تعيد قيمًا مختلفة متعددة في تنفيذ واحد. إنشاء إجراء مخزن مع معاملات دخل وخرج في الإجراءات المخزَّنة التي أنشأناها في المثالين السابقين استدعينا تعليمة SELECT للحصول على مجموعة نتائج، ولكن قد نحتاج في بعض الحالات إلى إجراء مخزّن يعيد قيمًا مختلفة متعددة مع بعضها البعض بدل إعادة مجموعة نتائج واحدة لاستعلام فردي. لنفترض أننا تريد إنشاء إجراء يوفّر معلومات عن السيارات الصادرة في سنة معينة بما في ذلك كمية السيارات في المجموعة وقيمتها السوقية -الحد الأدنى والحد الأقصى والمتوسط- من خلال استخدام معاملات OUT عند إنشاء إجراء مخزّن جديد. تحتوي معاملات OUT مثل معاملات IN على أسماء وأنواع بيانات مرتبطة بها، ولكن يمكن ملء هذه المعاملات بالبيانات باستخدام الإجراء المخزن بدل تمرير البيانات إلى الإجراء المخزن لإعادة القيم إلى المستخدم المستدعِي. لننشئ الآن الإجراء get_car_stats_by_year التالي الذي سيعيد بيانات موجزة عن السيارات من سنة إنتاج معينة باستخدام معاملات خرج: mysql> DELIMITER // mysql> CREATE PROCEDURE get_car_stats_by_year( mysql> IN year_filter int, mysql> OUT cars_number int, mysql> OUT min_value decimal(10, 2), mysql> OUT avg_value decimal(10, 2), mysql> OUT max_value decimal(10, 2) mysql> ) mysql> BEGIN mysql> SELECT COUNT(*), MIN(value), AVG(value), MAX(value) mysql> INTO cars_number, min_value, avg_value, max_value mysql> FROM cars mysql> WHERE year = year_filter ORDER BY make, value DESC; mysql> END // mysql> DELIMITER ; استخدمنا معامل IN الذي هو year_filter لترشيح السيارات حسب سنة الإصدار، وعرّفنا أربعة معاملات OUT ضمن كتلة الأقواس. نمثّل المعامل cars_number بنوع البيانات int وسنستخدمه لإعادة عدد السيارات في المجموعة، وتمثّل المعاملات min_value و avg_value و max_value القيمة السوقية وتُعرَّف باستخدام نوع البيانات decimal(10, 2)‎ مثل العمود value في الجدول cars، وتُستخدَم هذه المعاملات لإعادة معلومات حول أرخص وأغلى السيارات من المجموعة، بالإضافة إلى متوسط أسعار جميع السيارات المطابقة. تستعلم التعليمة SELECT عن أربع قيم من الجدول cars باستخدام دوال SQL الرياضية وهي: COUNT للحصول على العدد الإجمالي للسيارات، و MIN و AVG و MAX للحصول على القيمة الدنيا والمتوسط والقيمة العليا من العمود value. يمكن مطالعة مقال كيفية استخدام التعابير الرياضية والدوال التجميعية في لغة SQL لمعرفة المزيد حول استخدام الدوال الرياضية في لغة SQL. يمكننا إخبار قاعدة البيانات بأننا نريد تخزين نتائج هذا الاستعلام في معاملات الخرج للإجراء المخزَّن من خلال تقديم كلمة مفتاحية جديدة هي INTO، ونضع بعدها أسماء أربعة معاملات إجراء تقابل البيانات المُسترجَعة، وبالتالي سيحفظ MySQL قيمة COUNT(*)‎ في المعامل cars_number، ونتيجة MIN(value)‎ في المعامل min_value ...إلخ. تؤكد قاعدة البيانات إنشاء الإجراء بنجاح كما يلي: الخرج Query OK, 0 rows affected (0.02 sec) لنشغّل الآن الإجراء الجديد من خلال تنفيذ الأمر التالي: mysql> CALL get_car_stats_by_year(2017, @number, @min, @avg, @max); تبدأ المعاملات الأربعة الجديدة بالإشارة @، وهي أسماء متغيرات محلية في طرفية MySQL يمكنك استخدامها لتخزين البيانات مؤقتًا، وإذا مرّرنا هذه المعاملات إلى الإجراء المخزَّن الذي أنشأناه، فسيدرج الإجراءُ قيمًا في هذه المتغيرات. وستستجيب قاعدة البيانات بالخرج التالي: الخرج Query OK, 1 row affected (0.00 sec) يختلف هذا الخرج عن السلوك السابق، حيث كانت النتائج تُعرَض على الشاشة مباشرةً، لأن نتائج الإجراء المخزَّن محفوظة في معاملات الخرج دون إعادتها كنتيجة للاستعلام، ولكن يمكننا الوصول إلى النتائج من خلال استخدام التعليمة SELECT مباشرةً في صدفة MySQL كما يلي: mysql> SELECT @number, @min, @avg, @max; نحدّد قيمًا من المتغيرات المحلية باستخدام الاستعلام السابق، ولا نستدعي الإجراء مرة أخرى، ويحفظ الإجراء المخزَّن نتائجه في تلك المتغيرات، وستبقى البيانات متاحة حتى قطع الاتصال بالصدفة. ملاحظة: يمكن مطالعة على قسم المتغيرات التي يعرِّفها المستخدم في توثيق MySQL الرسمي لمعرفة المزيد حول استخدام هذه المتغيرات. ستختلف طرق الوصول إلى البيانات المُعادة من الإجراءات المخزنة في لغات البرمجة وأطر العمل المختلفة عند استخدامها في تطوير التطبيقات، لذا يتوجب الاطلاع على توثيق اللغة وإطار العمل لمعرفة الطريقة المناسبة. يعرض الخرج قيم المتغيرات التي استعلمنا عنها كما يلي: الخرج +---------+----------+-----------+-----------+ | @number | @min | @avg | @max | +---------+----------+-----------+-----------+ | 2 | 48880.00 | 192090.00 | 335300.00 | +---------+----------+-----------+-----------+ 1 row in set (0.00 sec) تتوافق القيم مع عدد السيارات المُصنَّعة في عام 2017، والقيمة السوقية الدنيا والمتوسطة والعليا للسيارات في هذه السنة من الإنتاج. تعلّمنا في المثال السابق كيفية استخدام معاملات الخرج لإعادة قيم مختلفة متعددة من الإجراء المخزن لاستخدامها لاحقًا، وسنتعلّم في القسم التالي كيفية إزالة الإجراءات التي أنشأناها. إزالة الإجراءات المخزنة سنزيل في هذا القسم الإجراءات المخزنة الموجودة في قاعدة البيانات، فقد لا تكون هناك حاجة إلى الإجراء الذي أنشأناه في بعض الأحيان، أو قد نرغب في تغيير طريقة عمل الإجراء، حيث لا يسمح MySQL بتغيير تعريف الإجراء بعد إنشائه، فالطريقة الوحيدة لذلك هي إزالة الإجراء أولًا وإعادة إنشائه مرة أخرى مع التغييرات المطلوبة. لنحذف الآن الإجراء الأخير get_car_stats_by_year باستخدام التعليمة DROP PROCEDURE كما يلي: mysql> DROP PROCEDURE get_car_stats_by_year; وستؤكد قاعدة البيانات حذف الإجراء برسالة النجاح التالية: الخرج Query OK, 0 rows affected (0.02 sec) يمكن التحقق من حذف الإجراء من خلال محاولة استدعائه باستخدام الأمر التالي: mysql> CALL get_car_stats_by_year(2017, @number, @min, @avg, @max); سنرى رسالة خطأ تفيد بأن الإجراء غير موجود في قاعدة البيانات كما يلي: رسالة خطأ ERROR 1305 (42000): PROCEDURE procedures.get_car_stats_by_year does not exist تعلمنا في هذا القسم كيفية حذف الإجراءات المخزنة الموجودة في قاعدة البيانات. الخلاصة تعلمنا في هذا المقال ما هي الإجراءات المخزنة وأنواعها المختلفة وكيفية استخدامها في MySQL لحفظ البيانات القابلة لإعادة الاستخدام في إجراءات مسمَّاة وتنفيذها لاحقًا، يمكن استخدام الإجراءات المخزنة لإنشاء برامج قابلة لإعادة الاستخدام، وتوحيد طرق الوصول إلى البيانات عبر تطبيقات متعددة، بالإضافة إلى تنفيذ سلوكيات معقدة تتجاوز الإمكانيات التي توفرها استعلامات SQL الفردية. غطى هذا المقال فقط أساسيات استخدام الإجراءات المخزنة، لذا لمزيد من المعلومات ننصح بالاطلاع على توثيق MySQL للإجراءات المخزنة لمعرفة مزيد من التفاصيل. ترجمة -وبتصرف- للمقال How To Use Stored Procedures in MySQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: استخدام المفاتيح الرئيسية Primary Keys في لغة SQL الاستعلامات الفرعية والإجراءات في SQL جلب الاستعلامات عبر SELECT في SQL حذف الجداول وقواعد البيانات في SQL
  18. تتميز قواعد البيانات العلاقية Relational Databases بكونها تهيكل البيانات ضمن بنية منظّمة، فهي تستخدم جداول ذات أعمدة ثابتة وتتبع أنواع بيانات مُعرَّفة بدقة وتضمن بأن جميع الصفوف لها الشكل نفسه. ومن المهم أن نكون قادرين في هذه البينة على العثور على الصفوف في الجداول والإشارة إليها دون التباس عند تخزين هذه البيانات ضمن صفوف الجداول. ويمكننا تحقيق ذلك في لغة الاستعلام البنيوية SQL باستخدام المفاتيح الرئيسية Primary Keys، والتي تعمل كمعرّفات مميزة للصفوف في جداول قاعدة البيانات العلاقية. سنتعرّف في هذا المقال على مفهوم المفاتيح الرئيسية، وكيفية استخدام أنواعها المختلفة لتحديد الصفوف الفريدة في جداول قاعدة البيانات، وسننشئ مفاتيح رئيسية من أعمدة فردية وأعمدة متعددة، كما سننشئ مفاتيح تسلسلية مع زيادة تلقائية باستخدام بعض البيانات التجريبية النموذجية. مستلزمات العمل يجب أن يكون لدينا حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- مستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو مع مستخدم بصلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر معرفة أساسية بتنفيذ استعلامات SELECT كما هو موضّح في مقال كيفية الاستعلام عن السجلات من الجداول في SQL ملاحظة: تجدر الإشارة لأنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر في هذا المقال بنجاح مع معظم هذه الأنظمة، تُعَد المفاتيح الرئيسية جزءًا من معيار SQL، ولكن هناك بعض الميزات خاصة بقاعدة البيانات، لذا قد نجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات فارغة يمكن من خلالها إنشاء جداول توضّح استخدام المفاتيح الرئيسية كما سنشرح بالتفصيل في الفقرات التالية. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل في هذا القسم بخادم MySQL وننشئ قاعدة بيانات تجريبية. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نتصل بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بك مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم primary_keys: mysql> CREATE DATABASE primary_keys; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات primary_keys من خلال تنفيذ تعليمة USE التالية: $ USE primary_keys; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، ويمكن الآن إنشاء جداول تجريبية ضمنها، وبذلك أصبحنا جاهزًا لمتابعة هذا المقال والبدء في العمل مع المفاتيح الرئيسية. مقدمة إلى المفاتيح الرئيسية Primary Keys تُخزَّن البيانات في قاعدة البيانات العلاقية في جداول ذات بنية موحَّدة ومحدَّدة من الصفوف الفردية، ويوضّح تعريف الجدول الأعمدة الموجودة فيه وأنواع البيانات التي يمكن حفظها في الأعمدة الفردية، وهذا كافي لتخزين المعلومات في قاعدة البيانات والعثور عليها لاحقًا باستخدام معايير ترشيح مختلفة باستخدام تعليمة WHERE، ولكن لا تضمن هذه البنية إمكانية العثور على أي صف بعينه دون التباس مع صفوف أخرى. لنفترض أن لدينا قاعدة بيانات لجميع السيارات المسجَّلة المسموح لها بالقيادة على الطرق العامة، حيث ستحتوي قاعدة البيانات على معلومات مثل نوع السيارة وطرازها وسنة تصنيعها ولون طلائها، ولكن إذا بحثنا عن سيارة شيفروليه كامارو Chevrolet Camaro حمراء اللون ومصنوعة في عام 2007، فيمكن العثور على أكثر من سيارة، إذ سيبيع المصنعون سيارات متماثلة لعملاء متعددين، لذا تحتوي السيارات المُسجَّلة على أرقام لوحات ترخيص تحدّد كل سيارة فريدة. إذا بحثنا عن سيارة تحمل لوحة الترخيص OFP857، فيمكن التأكد من أن هذا المعيار سيجد سيارة واحدة فقط، لأن أرقام اللوحات تحدد السيارات المسجلة بطريقة فريدة قانونيًا، ويسمى هذا الجزء من البيانات في قاعدة البيانات العلاقية بالمفتاح الرئيسي Primary Key. المفاتيح الرئيسية هي معرّفات فريدة موجودة في عمود واحد أو مجموعة من الأعمدة يمكنها تحديد كل صف في جدول قاعدة البيانات دون التباس. ومن أبرز خصائص المفاتيح الرئيسية نذكر: يجب أن يستخدم المفتاح الرئيسي قيمًا فريدة، وإذا تكوّن المفتاح الرئيسي من أكثر من عمود واحد، فيجب أن تكون مجموعة القيم في هذه الأعمدة فريدة في الجدول بأكمله، إذ لا يمكن أن يظهر المفتاح الرئيسي أكثر من مرة لأنه مخصّص لتحديد كل صف بطريقة فريدة يجب ألا يحتوي المفتاح الرئيسي على قيم NULL يمكن لكل جدول في قاعدة البيانات استخدام مفتاح رئيسي واحد فقط يفرض محرّك قاعدة البيانات هذه القواعد، لذا يمكنك الوثوق بصحة هذه الخاصيات عند تعريف المفتاح الرئيسي لجدول ما، ويجب أن تضع في بالنا محتوى البيانات وما الذي يمكن اختياره منها لتمثيل المفتاح الرئيسي بشكل مناسب. المفاتيح الطبيعية Natural keys هي معرّفات موجودة مسبقًا في مجموعة البيانات، والمفاتيح البديلة Surrogate Keys هي معرّفات اصطناعية، ويمكن مطالعة مقال فهم قيود SQL لمزيد من التفاصيل. تحتوي بعض هياكل البيانات على مفاتيح رئيسية تظهر في مجموعة البيانات طبيعيًا مثل أرقام لوحات الترخيص في قاعدة بيانات السيارات أو أرقام الضمان الاجتماعي في دليل المواطنين، ولا تكون هذه المعرّفات مؤلَّفة من قيمة واحدة بل مؤلفة من زوج أو مجموعة من عدة قيم في بعض الأحيان، فمثلًا لا يمكن تحديد منزل فريد باستخدام اسم الشارع أو رقم الشارع فقط في دليل مدينة محلية للمنازل، إذ يمكن أن يكون هناك عدة منازل في شارع واحد، ويمكن أن يظهر الرقم نفسه في شوارع متعددة، ولكن يمكن افتراض أن كلًا من اسم الشارع ورقمه هو معرّف منزل فريد. تسمّى هذه المعرّفات بالمفاتيح الطبيعية. لكن لا يمكن في بعض الأحيان تمييز البيانات تمييزًا فريدًا باستخدام قيم عمود واحد أو مجموعة فرعية صغيرة من الأعمدة، وبالتالي يجب إنشاء مفاتيح رئيسية اصطناعية باستخدام تسلسل من الأرقام مثلًا أو معرّفات مُولَّدة عشوائيًا مثل معرّفات UUID، وتسمّى هذه المفاتيح بالمفاتيح البديلة. سننشئ في الأقسام التالية مفاتيح طبيعية بناءً على عمود واحد أو أعمدة متعددة، كما سنولّد مفاتيح بديلة للجداول التي لا يكون المفتاح الطبيعي خيارًا فيها. إنشاء مفتاح رئيسي من عمود واحد تحتوي مجموعة البيانات على عمود واحد طبيعيًا في العديد من الحالات، ويمكن استخدام هذا العمود لتحديد الصفوف في الجدول تحديدًا فريدًا، وبالتالي يمكن إنشاء مفتاح طبيعي لوصف هذه البيانات. لنفترض أن لدينا الجدول التالي باتباع المثال السابق لقاعدة بيانات السيارات المُسجَّلة: جدول بسيط +---------------+-----------+------------+-------+------+ | license_plate | brand | model | color | year | +---------------+-----------+------------+-------+------+ | ABC123 | Ford | Mustang | Red | 2018 | | CES214 | Ford | Mustang | Red | 2018 | | DEF456 | Chevrolet | Camaro | Blue | 2016 | | GHI789 | Dodge | Challenger | Black | 2014 | +---------------+-----------+------------+-------+------+ يمثّل الصف الأول والثاني سيارة فورد موستانج Ford Mustang حمراء مصنوعة في عام 2018، ولن تتمكّن من تحديد السيارة بطريقة فريدة باستخدام نوع السيارة وطرازها فقط، لذا تختلف لوحة الترخيص لكل سيارة في كلتا الحالتين، ممّا يوفّر معرّفًا فريدًا جيدًا لكل صف في الجدول. يُعَد رقم لوحة الترخيص جزءًا من البيانات، لذا يؤدي استخدامه بوصفه مفتاحًا رئيسيًا إلى إنشاء مفتاح طبيعي. إذا أنشأنا الجدول دون استخدام مفتاح رئيسي في العمود license_plate، فستخاطر بظهور لوحة مكررة أو فارغة في مجموعة البيانات لاحقًا. لننشئ الآن جدولًا يشبه الجدول السابق مع استخدام العمود license_plate بوصفه مفتاحًا رئيسيًا مع الأعمدة التالية: license_plate: رقم لوحة الترخيص، ونمثله باستخدام نوع البيانات varchar brand: نوع السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا model: طراز السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا color: لون السيارة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 20 محرفًا year: سنة تصنيع السيارة، ونمثّله باستخدام نوع البيانات int لتخزين البيانات العددي. ولننشئ الآن الجدول cars من خلال تنفيذ تعليمة SQL التالية: mysql> CREATE TABLE cars ( mysql> license_plate varchar(8) PRIMARY KEY, mysql> brand varchar(50), mysql> model varchar(50), mysql> color varchar(20), mysql> year int mysql> ); تكون تعليمة PRIMARY KEY بعد تعريف نوع بيانات license_plate، حيث يمكنك استخدام الصيغة المبسَّطة لإنشاء المفتاح وكتابة PRIMARY KEY في تعريف العمود عند التعامل مع المفاتيح الرئيسية المستندة إلى أعمدة مفردة. إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول ببعض الصفوف التجريبية المعروضة في المثال السابق من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO cars VALUES mysql> ('ABC123', 'Ford', 'Mustang', 'Red', 2018), mysql> ('CES214', 'Ford', 'Mustang', 'Red', 2018), mysql> ('DEF456', 'Chevrolet', 'Camaro', 'Blue', 2016), mysql> ('GHI789', 'Dodge', 'Challenger', 'Black', 2014); وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 4 rows affected (0.010 sec) Records: 4 Duplicates: 0 Warnings: 0 يمكن الآن التحقق من أن الجدول الذي أنشأناه يحتوي على البيانات والتنسيق المتوقع باستخدام تعليمة SELECT التالية: mysql> SELECT * FROM cars; وسيظهر الخرج جدولًا مشابهًا للجدول الموجود في بداية هذا القسم: الخرج +---------------+-----------+------------+-------+------+ | license_plate | brand | model | color | year | +---------------+-----------+------------+-------+------+ | ABC123 | Ford | Mustang | Red | 2018 | | CES214 | Ford | Mustang | Red | 2018 | | DEF456 | Chevrolet | Camaro | Blue | 2016 | | GHI789 | Dodge | Challenger | Black | 2014 | +---------------+-----------+------------+-------+------+ يمكن بعد ذلك التحقق من ضمان قواعد المفتاح الرئيسي باستخدام محرّك قاعدة البيانات، لذا نجرّب إدخال سيارة لها رقم لوحة مكرّر من خلال تنفيذ الأمر التالي: mysql> INSERT INTO cars VALUES ('DEF456', 'Jeep', 'Wrangler', 'Yellow', 2019); وسيستجيب MySQL برسالة الخطأ التالية التي تفيد بأن لوحة الترخيص DEF456 تمثّل إدخالًا مكررًا للمفتاح الرئيسي: الخرج ERROR 1062 (23000): Duplicate entry 'DEF456' for key 'cars.PRIMARY' ملاحظة: تُطبَّق المفاتيح الرئيسية باستخدام الفهارس الفريدة Unique Indexes وتشترك في العديد من الخاصيات مع الفهارس التي قد ننشئها يدويًا لأعمدة أخرى في الجدول، وتعمل فهارس المفاتيح الرئيسية أيضًا على تحسين أداء الاستعلام في الجدول للعمود الذي عرّفنا الفهرس له. اطّلع على مقال كيفية استخدام الفهارس في SQL لمزيد من المعلومات. يمكن الآن التأكد من عدم السماح باستخدام لوحات ترخيص مكرّرة، ولنتحقق الآن من إمكانية إدخال سيارة لها لوحة ترخيص فارغة كما يلي: mysql> INSERT INTO cars VALUES (NULL, 'Jeep', 'Wrangler', 'Yellow', 2019); وستستجيب قاعدة البيانات برسالة الخطأ التالية: الخرج ERROR 1048 (23000): Column 'license_plate' cannot be null يمكن التأكّد من أن المفتاح الرئيسي license_plate يحدّد كلّ صف في الجدول تحديدًا فريدًا بفضل القاعدتين السابقتين اللتين تفرضهما قاعدة البيانات، وبالتالي إذا استعلمنا في الجدول عن أي لوحة ترخيص، فسنتوقع إعادة صف واحد في كل مرة. نشرح في القسم التالي كيفية استخدام المفاتيح الرئيسية مع أعمدة متعددة. إنشاء مفتاح رئيسي من أعمدة متعددة إن لم يكن عمود واحد كافيًا لتحديد صف فريد في الجدول، فيمكن إنشاء مفاتيح رئيسية تستخدم أكثر من عمودٍ واحد. لنفترض مثلًا وجود سجلٍ للمنازل بحيث لا يكفي اسم الشارع أو رقمه فقط لتحديد أيّ منزل فردي كما يلي: جدول بسيط +-------------------+---------------+-------------------+------+ | street_name | street_number | house_owner | year | +-------------------+---------------+-------------------+------+ | 5th Avenue | 100 | Bob Johnson | 2018 | | Broadway | 1500 | Jane Smith | 2016 | | Central Park West | 100 | John Doe | 2014 | | Central Park West | 200 | Tom Thompson | 2015 | | Lexington Avenue | 5001 | Samantha Davis | 2010 | | Park Avenue | 7000 | Michael Rodriguez | 2012 | +-------------------+---------------+-------------------+------+ يظهر اسم الشارع Central Park West أكثر من مرة في الجدول، ويظهر رقم الشارع 100 أكثر من مرة أيضًا، ولكن لا تظهَر أي أزواج مكررة من أسماء الشوارع وأرقامها، وبالتالي يمكن استخدام زوج من هاتين القيمتين لتحديد كل صف في الجدول تحديدًا فريدًا في الحالة التي لا يمكن فيها لأي عمود بمفرده أن يكون مفتاحًا رئيسيًا. لننشئ الآن جدولًا يشبه الجدول السابق ويحتوي على الأعمدة التالية: street_name: اسم الشارع الذي يقع فيه المنزل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. street_number: رقم شارع المنزل، ونمثّله باستخدام نوع البيانات varchar، ويمكن لهذا العمود تخزين ما يصل إلى 5 محارف، ولا يستخدم نوع البيانات العددي int لأن بعض أرقام الشوارع قد تحتوي على محارف مثل 200B. house_owner: اسم مالك المنزل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. year: العام الذي بُني فيه المنزل، ونمثّله باستخدام نوع البيانات int لتخزين القيم العددية. سيستخدم المفتاح الرئيسي هذه المرة العمودين street_name و street_number بدلًا من استخدام عمود واحد من خلال تنفيذ تعليمة SQL التالية: mysql> CREATE TABLE houses ( mysql> street_name varchar(50), mysql> street_number varchar(5), mysql> house_owner varchar(50), mysql> year int, mysql> PRIMARY KEY(street_name, street_number) mysql> ); تظهر تعليمة PRIMARY KEY بعد تعريفات الأعمدة على عكس المثال السابق، وتليها أقواس تحتوي على اسمي عمودين هما: street_name و street_number. تنشئ هذه الصيغة المفتاح الرئيسي في الجدول houses الذي يمتد إلى عمودين. إذا ظهر الخرج التالي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول بالصفوف التجريبية المعروضة في المثال السابق من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO houses VALUES mysql> ('Central Park West', '100', 'John Doe', 2014), mysql> ('Broadway', '1500', 'Jane Smith', 2016), mysql> ('5th Avenue', '100', 'Bob Johnson', 2018), mysql> ('Lexington Avenue', '5001', 'Samantha Davis', 2010), mysql> ('Park Avenue', '7000', 'Michael Rodriguez', 2012), mysql> ('Central Park West', '200', 'Tom Thompson', 2015); وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 6 rows affected (0.000 sec) Records: 6 Duplicates: 0 Warnings: 0 يمكن الآن التحقق من أن الجدول الذي أنشأناه يحتوي على البيانات والتنسيق المتوقع باستخدام تعليمة SELECT التالية: mysql> SELECT * FROM houses; وسيُظهِر الخرج جدولًا مشابهًا للجدول الموجود في بداية هذا القسم: الخرج +-------------------+---------------+-------------------+------+ | street_name | street_number | house_owner | year | +-------------------+---------------+-------------------+------+ | 5th Avenue | 100 | Bob Johnson | 2018 | | Broadway | 1500 | Jane Smith | 2016 | | Central Park West | 100 | John Doe | 2014 | | Central Park West | 200 | Tom Thompson | 2015 | | Lexington Avenue | 5001 | Samantha Davis | 2010 | | Park Avenue | 7000 | Michael Rodriguez | 2012 | +-------------------+---------------+-------------------+------+ 6 rows in set (0.000 sec) لنتحقق الآن من سماح قاعدة البيانات بإدراج صفوف تحتوي على أسماء وأرقام شوارع مكرَّرة، مع تقييد ظهور عناوين كاملة مكررة في الجدول. لنبدأ أولًا بإضافة منزل آخر في شارع Park Avenue كما يلي: mysql> INSERT INTO houses VALUES ('Park Avenue', '8000', 'Emily Brown', 2011); سيستجيب MySQL برسالة النجاح التالية لأن العنوان ‎8000 Park Avenue‎ لم يظهر في الجدول سابقًا: الخرج Query OK, 1 row affected (0.010 sec) وستظهر نتيجة مماثلة عند إضافة منزل في العنوان ‎8000 Main Street‎ مع تكرار رقم الشارع كما يلي: mysql> INSERT INTO houses VALUES ('Main Street', '8000', 'David Jones', 2009); مما سيؤدي إلى إدراج صف جديد بنجاح بسبب عدم تكرار العنوان الكامل: الخرج Query OK, 1 row affected (0.010 sec) نحاول الآن إضافة منزل آخر في العنوان ‎100 5th Avenue‎ باستخدام تعليمة INSERT التالية: mysql> INSERT INTO houses VALUES ('5th Avenue', '100', 'Josh Gordon', 2008); وستستجيب قاعدة البيانات برسالة الخطأ التالية لإعلامك بوجود إدخال مكرر للمفتاح الرئيسي لزوج القيم 5th Avenue و 100: الخرج ERROR 1062 (23000): Duplicate entry '5th Avenue-100' for key 'houses.PRIMARY' تطبّق قاعدة البيانات قواعد المفتاح الرئيسي بطريقة صحيحة، مع تعريف المفتاح لزوج من الأعمدة، وبالتالي يمكن التأكّد من عدم تكرار العنوان الكامل المكون من اسم الشارع ورقمه في الجدول. أنشأنا في هذا القسم مفتاحًا طبيعيًا مع زوج من الأعمدة لتحديد كل صف في الجدول house بوصفه صفًا فريدًا، ولكن لا يمكن دائمًا استخلاص المفاتيح الرئيسية من مجموعة البيانات، لذا سنستخدم في القسم التالي المفاتيح الرئيسية الاصطناعية التي لا تأتي من البيانات مباشرةً. إنشاء مفتاح رئيسي تسلسلي Sequential Primary Key أنشأنا مفاتيح رئيسية فريدة باستخدام أعمدة موجودة في مجموعات البيانات التجريبية النموذجية، ولكن البيانات ستتكرّر في بعض الحالات، مما يمنع الأعمدة من أن تكون معرّفات فريدة جيدة، لذا يمكن إنشاء مفاتيح رئيسية تسلسلية باستخدام معرّفات مُولَّدة. تسمَّى المفاتيح الرئيسية التي أنشأناها من المعرّفات الاصطناعية بالمفاتيح البديلة Surrogate Keys عندما تتطلب البيانات المتاحة ابتكار معرّفات جديدة لتحديد الصفوف بطريقة فريدة. لنفترض أن لدينا قائمة بأعضاء نادي الكتاب، وهو تجمع غير رسمي يمكن لأي شخص الانضمام إليه دون بطاقة هوية حكومية، وبالتالي هناك احتمال أن ينضم إلى النادي في وقتٍ ما أشخاص يحملون أسماء متطابقة: جدول بسيط +------------+-----------+ | first_name | last_name | +------------+-----------+ | John | Doe | | Jane | Smith | | Bob | Johnson | | Samantha | Davis | | Michael | Rodriguez | | Tom | Thompson | | Sara | Johnson | | David | Jones | | Jane | Smith | | Bob | Johnson | +------------+-----------+ يتكرر الاسمان Bob Johnson و Jane Smith في الجدول، لذا نحتاج إلى استخدام معرّف إضافي للتأكد من هوية كل منهما، ولا يمكن تحديد الصفوف تحديدًا فريدًا في هذا الجدول بأيّ طريقة، ولكن إذا احتفظنا بقائمة أعضاء نادي الكتاب على الورق، فيمكن الاحتفاظ بمعرّفات مساعدة للتمييز بين الأشخاص الذين يحملون الأسماء نفسها في المجموعة. يمكن إجراء شيء مماثل في قاعدة بيانات علاقية باستخدام عمود إضافي يحتوي على معرّفات مُولَّدة بدون معنى لغرض وحيد هو الفصل بين جميع الصفوف في الجدول بطريقة فريدة، ولنسمي هذا العمود بالاسم member_id، ولكن سيكون من الصعب التوصّل إلى مثل هذا المعرّف كلما أردنا إضافة عضو آخر في نادي الكتاب إلى قاعدة البيانات. يمكن حل هذه المشكلة باستخدام الميزة التي يوفرها MySQL، والتي هي ميزة خاصة بالأعمدة الرقمية المتزايدة تلقائيًا، حيث توفّر قاعدة البيانات قيمة العمود بتسلسل متزايد من الأعداد الصحيحة تلقائيًا. لننشئ الآن جدولًا يشبه الجدول السابق، ولكن سنضيف عمودًا إضافيًا متزايدًا تلقائيًا member_id ليكون محتويًا على العدد المُسنَد لكل عضو في النادي تلقائيًا، وسيمثّل هذا العدد المُسنَد تلقائيًا مفتاحًا رئيسيًا للجدول الذي سيحتوي على الأعمدة التالية: member_id: معرّف رقمي متزايد تلقائيًا، ونمثّله باستخدام نوع البيانات int. first_name: الاسم الأول لأعضاء النادي، والذي نمثله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. last_name: يالاسم الأخير لأعضاء النادي، والذي نمثله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا. لننشئ هذا الجدول من خلال تنفيذ تعليمة SQL التالية: mysql> CREATE TABLE club_members ( mysql> member_id int AUTO_INCREMENT PRIMARY KEY, mysql> first_name varchar(50), mysql> last_name varchar(50) mysql> ); تظهَر تعليمة PRIMARY KEY بعد تعريف نوع العمود مثل المفتاح الرئيسي لعمود واحد، ولكن تظهَر سمة Attribute إضافية قبلها هي AUTO_INCREMENT التي تخبر MySQL بتوليد قيم تلقائيًا لهذا العمود باستخدام تسلسلٍ متزايد من الأرقام إن لم تكن متوفّرة صراحةً. ملاحظة: تُعَد الخاصية AUTO_INCREMENT لتعريف عمود خاصةً بقاعدة بيانات MySQL، ولكن توفّر قواعد البيانات الأخرى طرقًا مماثلة لتوليد مفاتيح تسلسلية مع اختلاف صيغتها بين المحرّكات، لذا نشجّعك على الرجوع إلى التوثيق الرسمي لنظام إدارة قواعد البيانات العلاقية الخاص بك لمزيد من التفاصيل. إذا ظهر الخرج التالي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول بالصفوف التجريبية النموذجية المعروضة في المثال السابق من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO club_members (first_name, last_name) VALUES mysql> ('John', 'Doe'), mysql> ('Jane', 'Smith'), mysql> ('Bob', 'Johnson'), mysql> ('Samantha', 'Davis'), mysql> ('Michael', 'Rodriguez'), mysql> ('Tom', 'Thompson'), mysql> ('Sara', 'Johnson'), mysql> ('David', 'Jones'), mysql> ('Jane', 'Smith'), mysql> ('Bob', 'Johnson'); تتضمن تعليمة INSERT الآن قائمة بأسماء الأعمدة first_name و first_name، مما يضمن معرفة قاعدة البيانات بأن العمود member_id غير مُدرَج في مجموعة البيانات، لذا يجب أخذ القيمة الافتراضية له بدلًا من ذلك. وستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 10 rows affected (0.002 sec) Records: 10 Duplicates: 0 Warnings: 0 نستخدم تعليمة SELECT التالية للتحقق من البيانات الموجودة في الجدول الذي أنشأناه: mysql> SELECT * FROM club_members; وسيعرض الخرج التالي جدولًا مشابهًا للجدول الموجود في بداية هذا القسم: الخرج +-----------+------------+-----------+ | member_id | first_name | last_name | +-----------+------------+-----------+ | 1 | John | Doe | | 2 | Jane | Smith | | 3 | Bob | Johnson | | 4 | Samantha | Davis | | 5 | Michael | Rodriguez | | 6 | Tom | Thompson | | 7 | Sara | Johnson | | 8 | David | Jones | | 9 | Jane | Smith | | 10 | Bob | Johnson | +-----------+------------+-----------+ 10 rows in set (0.000 sec) ولكن سيظهر العمود member_id في النتيجة، والذي يحتوي على تسلسل من الأرقام من 1 إلى 10، وبالتالي أصبح من الممكن التمييز بين الصفوف Jane Smith و Bob Johnson المكرّرة، إذ سيرتبط كل اسم بمعرّف فريد member_id. لنتحقّق الآن مما إذا كانت قاعدة البيانات ستسمح بإضافة بعضو آخر اسمه Tom Thompson إلى قائمة أعضاء النادي كما يلي: mysql> INSERT INTO club_members (first_name, last_name) VALUES ('Tom', 'Thompson'); وسيستجيب MySQL برسالة النجاح التالية: الخرج Query OK, 1 row affected (0.009 sec) ولنتحقق الآن من المعرّف الرقمي الذي أسندَته قاعدة البيانات للإدخال الجديد من خلال تنفيذ استعلام SELECT التالي: mysql> SELECT * FROM club_members; وسيظهر الخرج التالي الذي سيحتوي على صفٍ جديد: الخرج +-----------+------------+-----------+ | member_id | first_name | last_name | +-----------+------------+-----------+ | 1 | John | Doe | | 2 | Jane | Smith | | 3 | Bob | Johnson | | 4 | Samantha | Davis | | 5 | Michael | Rodriguez | | 6 | Tom | Thompson | | 7 | Sara | Johnson | | 8 | David | Jones | | 9 | Jane | Smith | | 10 | Bob | Johnson | | 11 | Tom | Thompson | +-----------+------------+-----------+ 11 rows in set (0.000 sec) أُسنِد الرقم 11 إلى الصف الجديد تلقائيًا في عمود member_id باستخدام ميزة AUTO_INCREMENT في قاعدة البيانات. إذا لم يكن للبيانات التي نعمل عليها مرشَّحين طبيعيين ليكونوا مفاتيح رئيسية، ولا نريد توفير معرّفات اصطناعية في كل مرة نضيف فيها بيانات جديدة إلى قاعدة البيانات، فيمكن الاعتماد على المعرّفات المولَّدة تسلسليًا لتكون مفاتيح رئيسية. الخلاصة تعلّمنا في هذا المقال ما هي المفاتيح الرئيسية وكيفية إنشاء أنواعها الشائعة في MySQL لتحديد الصفوف الفريدة في جداول قاعدة البيانات، حيث بنينا مفاتيح رئيسية طبيعية، وأنشأنا مفاتيح رئيسية تمتد على أعمدة متعددة، واستخدمنا مفاتيح تسلسلية متزايدة تلقائيًا عند عدم وجود مفاتيح طبيعية. يمكن استخدام المفاتيح الرئيسية لتشكيل بنية قاعدة البيانات، مما يضمن إمكانية التعرّف على صفوف البيانات بطريقة فريدة. وضّح هذا المقال أساسيات استخدام المفاتيح الرئيسية فقط، لذا يمكن مطالعة توثيق MySQL للقيود لمزيد من المعلومات، ويمكن أيضًا الاطلاع على مقال فهم قيود SQL ومقال كيفية استخدام القيود في SQL. ترجمة -وبتصرف- للمقال How To Use Primary Keys in SQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: الفهارس متعددة الأعمدة في SQL كيفية استخدام القوادح Triggers في SQL كيفية إنشاء وإدارة الجداول في SQL مفاتيح الجداول
  19. سنتعلم في مقال اليوم كيفية حفظ البيانات المحلية بين جلسات اللعب وتحميل هذه البيانات عند الحاجة لها، هذا الموضوع مهم بشكل خاص عندما نريد الاحتفاظ بتقدم اللاعب أو إعدادات اللعبة عبر عدة جلسات لعب، حتى بعد إغلاق اللعبة وإعادة فتحها. كيف نحفظ البيانات المحلية في جودو يعتمد نظام إدخال وإخراج الملفات الخاص بمحرك الألعاب جودو Godot على كائن يسمى FileAccess ويمكنا فتحه من خلال استدعاء التابع open()‎ كما يلي: var file = FileAccess.open("user://myfile.name", File.READ) ملاحظة: يجب تخزين بيانات المستخدم فقط في المسار user://‎، ويمكننا استخدام المسار res://‎ عند التشغيل من المحرّر، ولكن يصبح هذا المسار للقراءة فقط عند تصدير مشروعنا. الوسيط الثاني الموجود بعد مسار الملف هو راية الوضع Mode Flag ويمكن أن يكون لها أحد الخيارات التالية: FileAccess.READ: مفتوح للقراءة FileAccess.WRITE: مفتوح للكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه Truncate إذا كان موجودًا مسبقًا FileAccess.READ_WRITE: مفتوح للقراءة والكتابة، ولا يقتطع الملف FileAccess.WRITE_READ: مفتوح للقراءة أو الكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه إذا كان موجودًا مسبقًا تخزين البيانات يمكننا حفظ البيانات باستخدام نوع البيانات المحدّد مثل store_float()‎ و store_string()‎ وغير ذلك، أو باستخدام الدالة store_var()‎ المعمَّمة، والتي ستستخدم التسلسل Serialization المُدمَج في جودو لتشفير بياناتك بما في ذلك البيانات المعقدة مثل الكائنات التي سنتحدث عنها لاحقًا. لنبدأ بمثال بسيط لحفظ أعلى نتيجة للاعب، حيث يمكننا كتابة دالة يمكن استدعاؤها كلما احتجنا إلى حفظ النتيجة: var save_path = "user://score.save" func save_score(): var file = FileAccess.open(save_path, FileAccess.WRITE) file.store_var(highscore) نحفظ النتيجة، ولكن يجب تحميلها عند بدء اللعبة كما يلي: func load_score(): if FileAccess.file_exists(save_path): print("file found") var file = FileAccess.open(save_path, FileAccess.READ) highscore = file.get_var() else: print("file not found") highscore = 0 علينا أن لا ننسى التحقق من وجود الملف قبل محاولة القراءة منه، إذ قد لا يكون موجودًا، وإن كان غير موجود، فيمكننا استخدام قيمة افتراضية. كما يمكن استخدام الدالتين store_var()‎ و get_var()‎ عدة مرات حسب حاجتك مع أيّ عدد من القيم. حفظ الموارد تعمل الطريقة السابقة بنجاح عندما نريد أن نحفظ عددًا معينًا من القيم، ولكن يمكننا حفظ بياناتنا في مورد Resource في الحالات الأكثر تعقيدًا كما يفعل جودو الذي يحفظ جميع موارد البيانات الخاصة به على أنها ملفات ‎.tres مثل Animations و TileSets و Shaders وما إلى ذلك حيث يمكننا تطبيق ذلك أيضًا. يمكن حفظ الموارد وتحميلها باستخدام صنفي جودو ResourceSaver و ResourceLoader. لنفترض مثلًا تخزين جميع بيانات الإحصائيات الخاصة بشخصية لعبتنا في مورد كما يلي: extends Resource class_name PlayerData var level = 1 var experience = 100 var strength = 5 var intelligence = 3 var charisma = 2 يمكننا بعد ذلك الحفظ والتحميل كما يلي: func load_character_data(): if ResourceLoader.exists(save_path): return load(save_path) return null func save_character_data(data): ResourceSaver.save(data, save_path) قد تحتوي الموارد على موارد فرعية، لذا يمكن أيضًا تضمين موارد مخزن اللاعب وغير ذلك. هل يمكن تخزين البيانات في ملف JSON قد يخطر في البال سؤال عن إمكانية استخدام صيغة JSON لحفظ البيانات، ولكن يُوصَى بعدم استخدام JSON مع ملفات الحفظ الخاصة بنا. يدعم جودو صيغة JSON، ولكن لا يُعَد حفظ بيانات اللعبة هدف استخدام JSON التي هي صيغة لتبادل البيانات، والغرض منها هو السماح للأنظمة التي تستخدم صيغ بيانات ولغات مختلفة بتبادل البيانات، لذا ستسبّب صيغة JSON قيودًا سلبية عندما يتعلق الأمر بحفظ بيانات اللعبة. إضافة لذلك لا تدعم JSON العديد من أنواع البيانات، إذ لا يوجد نوع البيانات int مقابل نوع البيانات float مثلًا، لذا يجب إجراء الكثير من عمليات التحويل والتحقق لمحاولة حفظ أو تحميل بياناتنا، ويُعَد ذلك أمرًا مرهقًا ويستغرق وقتًا طويلًا. لا ننصح بتضيع الوقت في محاولة كهذه، إذ يمكننا تخزين كائنات جودو الأصيلة مثل العقد والموارد والمشاهد دون أي جهد باستخدام التسلسل المُدمَج في جودو، مما يعني أننا ستستخدم شيفرة برمجية أقل مع وجود أخطاء أقل، ولا يستخدم جودو صيغة JSON لحفظ المشاهد والموارد. الخاتمة تعلمنا في مقال اليوم أساسيات حفظ واسترجاع البيانات المحلية بين جلسات اللعب، وتجدر الإشارة لأن هذا المقال لا يمثل سوى جزء بسيط ممّا يمكنك إنجازه باستخدام FileAccess، لذا ننصح بالاطلاع على توثيق FileAccess للحصول على القائمة الكاملة لتوابعه. ترجمة -وبتصرّف- للقسم Saving/loading data من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: تعرف على مفهوم Delta في تطوير الألعاب الطريقة الصحيحة للتواصل بين العقد في جودو Godot لغات البرمجة المتاحة في جودو Godot إنشاء شخصيات ثلاثية الأبعاد في جودو Godot
  20. شرحنا في المقال السابق مفهوم الفهارس في قاعدة البيانات، ووضحنا أنواعًا مختلفة من الفهارس على قاعدة البيانات، وكانت جميع هذه الفهارس معرًفة باستخدام اسم عمود واحد single column، حيث يتعلق هذا الفهرس بقيم هذا العمود المختار، ولكن تدعم معظم أنظمة قواعد البيانات الفهارس التي تمتد لأكثر من عمود واحد multiple columns، وهذا ما سنوضّحه في هذا المقال، بالإضافة توضيح كيفية سرد وإزالة الفهارس الموجودة مسبقًا. استخدام الفهارس مع أعمدة متعددة توفر الفهارس متعددة الأعمدة طريقةً لتخزين قيم أعمدة متعددة في فهرس واحد، مما يسمح لمحرّك قاعدة البيانات بتنفيذ الاستعلامات بسرعة وكفاءة أكبر باستخدام مجموعة الأعمدة مع بعضها البعض. فالاستعلامات المستخدَمة بصورة متكررة والتي يجب تحسينها للحصول على أداء أفضل تستخدم شروطًا متعددة في تعليمة الترشيح WHERE في أغلب الأحيان، ومن الأمثلة على هذا النوع من الاستعلامات استعلام يطلب من قاعدة البيانات أن تعثر على شخص معين من خلال اسمه الأول والأخير كما يلي: mysql> SELECT * FROM employees WHERE last_name = 'Smith' AND first_name = 'John'; قد تكون الفكرة الأولى لتحسين هذا الاستعلام باستخدام الفهارس هي إنشاء فهرسين، أحدهما في العمود last_name والآخر في العمود first_name، ولكنه ليس الخيار الأفضل لهذه الحالة، حيث فإذا أنشأنا فهرسين منفصلين بهذه الطريقة، فسيعرف MySQL كيفية العثور على جميع الموظفين الذين يحملون اسم Smith مثلًا، وسيعرف أيضًا كيفية العثور على جميع الموظفين الذين يحملون اسم John، ولكنه لن يعرف كيفية العثور على الموظفين الذين يحملون الاسم John Smith. لنوضّح مشكلة وجود فهرسين فرديين من خلال تخيل وجود دليلي هاتف منفصلين، أحدهما مرتب حسب الاسم الأخير والآخر حسب الاسم الأول، ويشبه هذان الدليلان الفهارس التي أنشأناها في المقال السابق في العمودين last_name و first_name على التوالي. يمكنك التعامل مع مشكلة العثور على الاسم John Smith كمستخدمٍ لدليل الهاتف باستخدام ثلاث طرق ممكنة هي: الطريقة الأولى هي استخدم دليل الهاتف المرتب حسب الاسم الأخير للعثور على جميع الأشخاص الذين يحملون الاسم Smith، وتجاهل دليل الهاتف الثاني، ثم يمكن المرور يدويًا على جميع الأشخاص الذين يحملون اسم Smith واحدًا تلو الآخر حتى نجد الاسم John Smith. الطريقة الثانية هي تطبيق الطريقة المعاكسة من خلال استخدام دليل الهاتف المرتب حسب الاسم الأول للعثور على جميع الأشخاص الذين يحملون اسم John، وتجاهل دليل الهاتف الثاني، ثم المرور يدويًا على جميع الأشخاص الذين يحملون اسم John واحدًا تلو الآخر حتى نجد الاسم John Smith. الطريقة الأخيرة هي محاولة استخدام دليلي الهاتف معًا من خلال البحث عن جميع الأشخاص الذين يحملون اسم John وعن جميع الأشخاص الذين يحملون اسم Smith بطريقة منفصلة، وكتابة النتائج المؤقتة، ثم نحاول يدويًا إيجاد تقاطع هاتين المجموعتين الفرعيتين من البيانات بحثًا عن الأشخاص الموجودين في القائمتين الفرديتين. لا تُعَد أي طريقة من الطرق السابقة مثالية، ويوفر MySQL أيضًا خيارات مماثلة عند التعامل مع العديد من الفهارس المنفصلة والاستعلامات التي تطلب أكثر من شرط ترشيح واحد. توجد طريقة أخرى أيضًا تتمثّل باستخدام الفهارس التي تأخذ عدة أعمدة بدلًا من عمود واحد، حيث يمكنك تخيل ذلك كدليل هاتف موضوع ضمن دليل هاتف آخر، إذ سنبحث أولًا عن الاسم الأخير Smith، مما يوجّهنا إلى الدليل الثاني لجميع الأشخاص الذين يحملون اسم Smith بحيث تكون الأسماء مرتبة أبجديًا حسب الاسم الأول، ويمكننا استخدام هذا الدليل للعثور على الاسم John بسرعة. إنشاء فهرس متعدد الأعمدة يمكن إنشاء فهرس متعدد الأعمدة في MySQL للأسماء الأخيرة والأسماء الأولى في الجدول employees من خلال تنفيذ التعليمة التالية: mysql> CREATE INDEX names ON employees(last_name, first_name); تختلف التعليمة CREATE INDEX في هذه الحالة بعض الشيء، حيث ستحتوي على عمودين هما: last_name ثم first_name بين قوسين بعد اسم الجدول employees، مما يؤدي إلى إنشاء فهرس متعدد الأعمدة مع هذين العمودين، ويُعَد ترتيب الأعمدة في تعريف الفهرس مهمًا. تعرض قاعدة البيانات الرسالة التالية التي تؤكّد إنشاء الفهرس بنجاح: الخرج Query OK, 0 rows affected (0.024 sec) Records: 0 Duplicates: 0 Warnings: 0 نستخدم الآن استعلام SELECT للعثور على الصفوف التي يتطابق فيها الاسم الأول مع John والاسم الأخير مع Smith كما يلي: mysql> SELECT * FROM employees WHERE last_name = 'Smith' AND first_name = 'John'; وتكون النتيجة صفًا واحدًا يحتوي على موظف اسمه John Smith: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) نستخدم الآن استعلام مع أمر EXPLAIN التالي للتحقق من استخدام الفهرس: mysql> EXPLAIN SELECT * FROM employees WHERE last_name = 'Smith' AND first_name = 'John'; وستكون طريقة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+------+---------------+-------+---------+-------------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+-------+---------+-------------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | ref | names | names | 406 | const,const | 1 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+-------+---------+-------------+------+----------+-------+ 1 row in set, 1 warning (0.00 sec) في هذه الحالة استخدمت قاعدة البيانات الفهرس names، ومسحت صفًا واحدًا، لذا لم تمر على الجدول أكثر مما تحتاج إليه. يحتوي العمود Extra على العبارة Using index condition التي تعني أن MySQL يمكنه إكمال الترشيح باستخدام الفهرس فقط، حيث يوفّر الترشيح -وفقًا للأسماء الأولى والأخيرة باستخدام الفهرس متعدد الأعمدة الذي يمتد بين هذين العمودين لقاعدة البيانات- طريقةً مباشرة وسريعة للعثور على النتائج المطلوبة. لنشاهد الآن ما سيحدث إذا حاولنا العثور على جميع الموظفين الذين يحملون اسم Smith دون الترشيح وفقًا للاسم الأول مع تعريف الفهرس في العمودين، ولنشغّل الاستعلام المعدَّل التالي: mysql> SELECT * FROM employees WHERE last_name = 'Smith'; وستظهر النتائج التالية: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 20 | Abigail | Smith | FGH890 | 155000 | | 17 | Daniel | Smith | WXY901 | 140000 | | 1 | John | Smith | ABC123 | 60000 | | 5 | Michael | Smith | MNO345 | 80000 | +-------------+------------+-----------+---------------+--------+ 4 rows in set (0.000 sec) نلاحظ وجود أربع موظفين يحملون الاسم الأخير Smith. ننتقل الآن إلى طريقة تنفيذ الاستعلام باستخدام التعليمة التالية: mysql> EXPLAIN SELECT * FROM employees WHERE last_name = 'Smith'; وستكون طريقة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+------+---------------+-------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+-------+---------+-------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | ref | names | names | 203 | const | 4 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+-------+---------+-------+------+----------+-------+ 1 row in set, 1 warning (0.01 sec) نلاحظ إعادة 4 صفوف هذه المرة، حيث يوجد أكثر من موظف يحمل هذا الاسم الأخير، ولكن يوضّح جدول طريقة التنفيذ أن قاعدة البيانات استخدمت الفهرس متعدد الأعمدة names لإجراء هذا الاستعلام، ومسحت 4 صفوف فقط، وهو العدد الدقيق المُعاد. مرّرنا في الاستعلامات السابقة العمود المُستخدَم لترشيح النتائج last_name أولًا في تعليمة CREATE INDEX، وسنرشّح الآن الجدول employees وفق العمود first_name، وهو العمود الثاني في قائمة الأعمدة لهذا الفهرس متعدد الأعمدة، لذا ننفّذ الآن الاستعلام التالي: mysql> SELECT * FROM employees WHERE first_name = 'John'; وسيظهر الخرج التالي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) لننتقل الآن لعرض طريقة تنفيذ الاستعلام كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE first_name = 'John'; وسيظهر الخرج التالي: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | NULL | NULL | NULL | NULL | 20 | 10.00 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) تحتوي النتائج المُعادة على موظف واحد دون استخدام أي فهرس هذه المرة، ومسحَت قاعدة البيانات الجدول بالكامل كما يوضح التعليق Using where في العمود Extra، بالإضافة إلى 20 صفًا ممسوحًا. لم تستخدم قاعدة البيانات الفهرس في هذه الحالة بسبب ترتيب الأعمدة المُمرَّرة إلى التعليمة CREATE INDEX عند إنشاء الفهرس لأول مرة: last_name, first_name، إذ لا يمكن لقاعدة البيانات استخدام الفهرس إلا إذا استخدم الاستعلام العمود الأول أو العمودين الأول والثاني، ولا يمكنها دعم الاستعلامات مع الفهرس عند عدم استخدام العمود الأول من تعريف الفهرس. إذا أنشأنا فهرسًا لأعمدة متعددة، فيمكن لقاعدة البيانات استخدام هذا الفهرس لتسريع الاستعلامات التي تتضمن جميع الأعمدة المفهرسَة أو ذات البادئة المتزايدة اليسارية لجميع الأعمدة المفهرسَة، فمثلًا يمكن استخدام فهرس متعدد الأعمدة يتضمن الأعمدة a و b و c لتسريع الاستعلامات التي تتضمن جميع الأعمدة الثلاثة، والاستعلامات التي تتضمن العمودين الأولين فقط، أو حتى الاستعلامات التي تتضمن العمود الأول فقط، ولكن لن يساعد الفهرس في الاستعلامات التي تتضمن العمود الأخير فقط c أو العمودين الأخيرين b و c. يمكن استخدام فهرس واحد متعدد الأعمدة لتسريع الاستعلامات المختلفة للجدول نفسه من خلال اختيار الأعمدة المُضمَّنة في الفهرس بعناية وترتيبها، فمثلًا إذا افترضنا أن نبحث عن الموظفين باستخدام الاسم الأول والأخير أو الاسم الأخير فقط، فسيضمن الترتيب المُقدَّم للأعمدة في الفهرس names أن الفهرس سيسرّع جميع الاستعلامات ذات الصلة. استخدمنا في هذا القسم فهرسًا متعدد الأعمدة وتعلّمنا ترتيب الأعمدة عند تحديد مثل هذا الفهرس، وسنتعلّم في الفقرات التالية كيفية إدارة الفهارس الموجودة مسبقًا. سرد وإزالة الفهارس الموجودة مسبقًا أنشأنا في الأقسام السابقة فهارس جديدة، بما أن الفهارس لها أسماء وتُعرَّف لجداول معينة، فيمكننا أيضًا سردها ومعالجتها عند الحاجة، حيث يمكن سرد جميع الفهارس التي أنشأناها سابقًا للجدول employees من خلال تنفيذ التعليمة التالية: mysql> SHOW INDEXES FROM employees; وسيكون الخرج مشابهًا لما يلي: الخرج +-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression | +-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ | employees | 0 | device_serial | 1 | device_serial | A | 20 | NULL | NULL | YES | BTREE | | | YES | NULL | | employees | 1 | salary | 1 | salary | A | 20 | NULL | NULL | YES | BTREE | | | YES | NULL | | employees | 1 | names | 1 | last_name | A | 16 | NULL | NULL | YES | BTREE | | | YES | NULL | | employees | 1 | names | 2 | first_name | A | 20 | NULL | NULL | YES | BTREE | | | YES | NULL | +-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+ 4 rows in set (0.01 sec) قد يختلف الخرج بعض الشيء اعتمادًا على إصدار MySQL الخاص بنا، ولكنه سيتضمن جميع الفهارس مع أسمائها والأعمدة المستخدمة لتعريف الفهرس والمعلومات التي تجعله فريدًا وتفاصيل أخرى لتعريف الفهرس. يمكن حذف الفهارس الموجودة مسبقًا من خلال استخدام تعليمة SQL التالية: DROP INDEX فإن لم نعد نرغب في فرض جعل العمود device_serial فريدًا، فلن تكون هناك حاجة إلى الفهرس device_serial بعد الآن، وسننفّذ الأمر التالي لحذفه: mysql> DROP INDEX device_serial ON employees; device_serial هو اسم الفهرس و employees هو الجدول الذي عرّفنا الفهرس له، وستؤكد قاعدة البيانات حذف الفهرس كما يلي: الخرج Query OK, 0 rows affected (0.018 sec) Records: 0 Duplicates: 0 Warnings: 0 قد تتغير أنماط الاستعلامات النموذجية بمرور الوقت، وقد تظهر أنواع استعلامات جديدة في بعض الأحيان، لذا قد نحتاج إلى إعادة تقييم الفهارس التي نستخدمها أو إنشاء فهارس جديدة أو حذف الفهارس غير المستخدمة لتجنب تناقص أداء قاعدة البيانات من خلال تحديثها باستمرار. يمكننا إدارة الفهارس في قاعدة بيانات موجودة مسبقًا باستخدام أوامر CREATE INDEX و DROP INDEX من خلال اتباع أفضل الممارسات لإنشاء الفهارس عندما تصبح ضرورية ومفيدة. الخلاصة تعلّمنا في هذا المقال كيف يمكن تعريف فهارس متعددة الأعمدة وكيف يمكن للفهارس أن تؤثر على الاستعلامات عند استخدام أكثر من عمود واحد في شرط الترشيح وكيفية سرد وإزالة الفهارس الموجودة مسبقًا، وقد ركزنا على أمثلة بسيطة توضح أساسيات استخدام الفهارس فقط، ولكن يمكننا دعم الاستعلامات الأكثر تعقيدًا من خلال الفهارس عند فهم كيفية اختيار MySQL للفهارس المُستخدَمة ومتى يستخدمها، لذا يمكن الرجوع لتوثيق MySQL للفهارس لمزيد من المعلومات. ترجمة -وبتصرف- للجزء الثاني من مقال How To Use Indexes in MySQL لصاحبيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: مقدمة إلى الفهارس Indexes في SQL الفهارس Indexes في SQL إنشاء فهرس CREATE INDEX حذف الفهرس DROP INDEX تعديل الفهرس ALTER INDEX
  21. يمكن استخدام قواعد البيانات العلاقية Relational Databases للعمل مع بيانات من جميع الأحجام بما في ذلك قواعد البيانات الكبيرة التي تحتوي على ملايين الصفوف، وتوفّر لنا لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- طريقة موجزة ومباشرة للعثور على صفوف معينة في جداول قاعدة البيانات وفق معايير محددة، ولكن مع تزايد الحجم سيصبح تحديد موقع صفوف معينة في قواعد البيانات أكثر صعوبة ويشبه البحث عن إبرة في كومة قش! تصعّب قدرة قواعد البيانات على قبول مجموعة واسعة من شروط الاستعلام على محرّك قاعدة البيانات توقّع الاستعلامات الأكثر شيوعًا، إذ يجب أن يكون المحرّك مستعدًا لتحديد موقع الصفوف بكفاءة في جداول قاعدة البيانات بغض النظر عن حجمها، ولكن بطبيعة الحالة سيسوء أداء البحث مع زيادة حجم البيانات وسيصعب العثور على النتائج التي تتطابق مع الاستعلام بسرعة. في هذه الحالة يمكن لمسؤول قواعد البيانات استخدام مفهوم الفهارس Indexes لمساعدة محرّك قاعدة البيانات على تسريع البحث وتحسين أدائه، حيث سنتعلم في هذا المقال مفهوم الفهارس وكيفية إنشائها للاستفادة منها في الاستعلام من قاعدة البيانات. مستلزمات العمل يجب أن يكون لدينا حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- مستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذي صلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة التالية من المقال معرفة أساسية بتنفيذ استعلامات SELECT لاسترجاع البيانات من قاعدة البيانات كما هو موضّح في مقال الاستعلام عن السجلات من الجداول في SQL ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن الفهارس ليست جزءًا من صيغة SQL المعيارية، لذا قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لنتمكن من التدرب على استخدام الفهارس، وسنشرح في القسم التالي تفاصيل حول الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية، والتي سنستخدمها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل بخادم MySQL وسننشئ قاعدة بيانات تجريبية لاتباع الأمثلة الواردة في هذا المقال. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نتصل بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات باسم indexes: mysql> CREATE DATABASE indexes; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات indexes من خلال تنفيذ تعليمة USE التالية: $ USE indexes; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، وسننشئ جدولًا تجريبيًا ضمنها، حيث سنستخدم في هذا المقال قاعدة بيانات افتراضية للموظفين لتخزين تفاصيل الموظفين الحاليين وأجهزة عملهم. سيحتوي الجدول employees على بيانات بسيطة حول الموظفين في قاعدة البيانات، والتي سنمثّلها باستخدام الأعمدة التالية: employee_id: معرّف الموظف، نوع بياناته int، وسيكون هذا العمود المفتاح الرئيسي Primary Key للجدول first_name: الاسم الأول لكل موظف، نوع بياناته varchar بحد أقصى 50 محرفًا last_name: لاسم الأخير لكل موظف، ونمثّله باستخدام نوع بياناته varchar بحد أقصى 50 محرفًا device_serial: الرقم التسلسلي لحاسوب الموظف، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 15 محرفًا salary: راتب كل موظف، ونمثّله باستخدام نوع البيانات int الذي يخزّن البيانات العددية ننشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE employees ( mysql> employee_id int, mysql> first_name varchar(50), mysql> last_name varchar(50), mysql> device_serial varchar(15), mysql> salary int mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول employees ببعض البيانات التجريبية من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO employees VALUES mysql> (1, 'John', 'Smith', 'ABC123', 60000), mysql> (2, 'Jane', 'Doe', 'DEF456', 65000), mysql> (3, 'Bob', 'Johnson', 'GHI789', 70000), mysql> (4, 'Sally', 'Fields', 'JKL012', 75000), mysql> (5, 'Michael', 'Smith', 'MNO345', 80000), mysql> (6, 'Emily', 'Jones', 'PQR678', 85000), mysql> (7, 'David', 'Williams', 'STU901', 90000), mysql> (8, 'Sarah', 'Johnson', 'VWX234', 95000), mysql> (9, 'James', 'Brown', 'YZA567', 100000), mysql> (10, 'Emma', 'Miller', 'BCD890', 105000), mysql> (11, 'William', 'Davis', 'EFG123', 110000), mysql> (12, 'Olivia', 'Garcia', 'HIJ456', 115000), mysql> (13, 'Christopher', 'Rodriguez', 'KLM789', 120000), mysql> (14, 'Isabella', 'Wilson', 'NOP012', 125000), mysql> (15, 'Matthew', 'Martinez', 'QRS345', 130000), mysql> (16, 'Sophia', 'Anderson', 'TUV678', 135000), mysql> (17, 'Daniel', 'Smith', 'WXY901', 140000), mysql> (18, 'Mia', 'Thomas', 'ZAB234', 145000), mysql> (19, 'Joseph', 'Hernandez', 'CDE567', 150000), mysql> (20, 'Abigail', 'Smith', 'FGH890', 155000); ستستجيب قاعدة البيانات برسالة النجاح التالية: الخرج Query OK, 20 rows affected (0.010 sec) Records: 20 Duplicates: 0 Warnings: 0 ملاحظة: مجموعة البيانات هنا ليست كبيرة بما يكفي لتوضيح تأثير الفهارس على الأداء مباشرة، فالهدف منها هنا توضيح لكيفية استخدام الفهارس في MySQL لتقييد عدد الصفوف التي نجتازها لإجراء الاستعلامات والحصول على النتائج المطلوبة. نحن الآن جاهزون لمتابعة هذا المقال والبدء باستخدام الفهارس في MySQL. ما هي الفهارس Indexes يجب أن تمر قاعدة البيانات على جميع الصفوف الموجودة في الجدول واحدًا تلو الآخر عند تنفيذ استعلام على قاعدة بيانات MySQL، فمثلًا قد نرغب في البحث عن الاسم الأخير للموظفين المتطابق مع الاسم Smith أو جميع الموظفين الذين يتقاضون راتبًا أعلى من 100000 دولار، حيث سيُفحَص كل صف في الجدول واحدًا تلو الآخر للتحقق مما إذا كان يتطابق مع الشرط. إذا كان الصف متطابقًا مع الشرط، فسيُضاف إلى قائمة الصفوف المُعادة، وإن لم يكن كذلك، فسيمسح MySQL الصفوف اللاحقة حتى يستعرض الجدول بالكامل. هذه الطريقة للعثور على الصفوف المطابقة فعّالة، ولكنها قد تصبح بطيئة وتستهلك كثيرًا من الموارد عند زيادة حجم الجدول، لذا قد لا تكون مناسبة للجداول الكبيرة أو الاستعلامات التي تتطلب وصولًا متكررًا أو سريعًا إلى البيانات. يمكن حل مشكلات الأداء المتعلقة بالجداول والاستعلامات الكبيرة باستخدام الفهارس، وهي هياكل بيانات فريدة تخزّن مجموعة فرعية مرتبة من البيانات فقط بحيث تكون منفصلة عن صفوف الجدول، وتسمح لمحرّك قاعدة البيانات بالعمل بسرعة وكفاءة أكبر عند البحث عن القيم أو الترتيب وفق حقل معين أو مجموعة من الحقول. لنستخدم الآن الجدول employees، فأحد الاستعلامات النموذجية التي يمكن تنفيذها هو العثور على الموظفين باستخدام اسمهم الأخير. إن لم نستخدم الفهارس، فسيسترجع MySQL كل موظف من الجدول ويتحقق من تطابق الاسم الأخير مع الاستعلام، وإذا استخدمنا فهرسًا ما، فسيحتفظ بقائمة منفصلة من الأسماء الأخيرة، والتي تحتوي فقط على مؤشّرات إلى صفوف الموظفين المحدَّدين في الجدول الرئيسي، ثم سيستخدم هذا الفهرس لاسترجاع النتائج دون مسح الجدول بأكمله. يمكن تشبيه الفهارس بدليل الهاتف، فإذا أردنا تحديد موقع شخص اسمه John Smith في هذا الدليل، ننتقل أولًا إلى الصفحة الصحيحة التي تسرد الأشخاص الذين تبدأ أسماؤهم بالحرف S، ثم نبحث في الصفحات عن الأشخاص الذين تبدأ أسماؤهم بالحرفين Sm، وبذلك يمكن استبعاد العديد من الإدخالات بسرعة، مع العلم أنها لا تتطابق مع الشخص الذي نبحث عنه. تعمل هذه العملية بنجاح لأن البيانات في دليل الهاتف مرتبة أبجديًا، وهو أمر نادر الحدوث مع البيانات المخزَّنة مباشرةً في قاعدة البيانات. يمثّل الفهرس في محرّك قاعدة البيانات غرضًا مشابهًا لدليل الهاتف، حيث يحتفظ بالمراجع المرتبة أبجديًا إلى البيانات، وبالتالي يساعد قاعدة البيانات في العثور على الصفوف المطلوبة بسرعة. لاستخدام الفهارس فوائد متعددة، وأكثرها شيوعًا هو تسريع استعلامات WHERE الشرطية، وفرز البيانات باستخدام تعليمات ORDER BY بسرعة أكبر، وفرض أن تكون القيم فريدة، لكن من ناحية أخرى قد يؤدي استخدام الفهارس إلى تراجع أداء قاعدة البيانات في بعض الظروف، فهي مصممة الفهارس لتسريع استرجاع البيانات وتُنفَّذ باستخدام هياكل بيانات إضافية مخزَّنة مع بيانات الجدول، ويجب تحديث هذه الهياكل عند كل تغيير في قاعدة البيانات، مما قد يؤدي إلى إبطاء أداء استعلامات INSERT و UPDATE و DELETE. لكن إذا كان لدينا مجموعات بيانات كبيرة تتغير كثيرًا، فستتفوق الفوائد الناتجة عن السرعة المُحسَّنة لاستعلامات SELECT أحيانًا على الأداء الأبطأ الملحوظ للاستعلامات التي تكتب البيانات في قاعدة البيانات. يُفضَّل إنشاء الفهارس عند وجود حاجة واضحة إليها فقط مثل الوقت الذي يبدأ فيه أداء التطبيق في الانخفاض. نحتاج لأن نضع في الاعتبار الاستعلامات التي تُنفَّذ بشكل متكرر وتستغرق وقتًا أطول عند اختيار الفهارس التي سننشئها، ونبني الفهارس بناءً على شروط الاستعلام التي ستستفيد منها أكثر من غيرها. ملاحظة: نركز في هذا المقال على شرح فهارس قاعدة البيانات في MySQL وتوضيح تطبيقاتها الشائعة وأنواعها، حيث يدعم محرّك قاعدة البيانات عدة سيناريوهات أكثر تعقيدًا لاستخدام الفهارس لزيادة أداء قاعدة البيانات، لكن هذا خارج نطاق هذا المقال. ويمكن مطالعة توثيق MySQL الرسمي حول الفهارس للحصول على معلومات وافية عن مميزات فهارس قاعدة البيانات. ستنشئ في الأقسام التالية فهارس من أنواع مختلفة لمجموعة من السيناريوهات، وسنتعلم كيفية التحقق من استخدام الفهارس في الاستعلام، وكيفية إزالة الفهارس عند الحاجة. استخدام فهارس العمود الواحد Single-Column فهرس العمود الواحد هو أحد أكثر أنواع الفهارس شيوعًا ووضوحًا، حيث يمكن استخدامه لتحسين أداء الاستعلام، ويساعد هذا النوع من الفهارس قاعدة البيانات على تسريع الاستعلامات التي ترشّح مجموعة البيانات بناءً على قيم عمود واحد. يمكن للفهارس التي أنشأناها على عمود واحد تسريع العديد من الاستعلامات الشرطية التي تستخدم المطابقات التامة باستخدام المعامل = والمقارنات باستخدام ‎>‎ أو ‎<‎. لا توجد فهارس في قاعدة البيانات التجريبية التي أنشأناها في خطوة سابقة. سنختبر أولًا كيفية تعامل قاعدة البيانات مع استعلامات SELECT للجدول employees عند استخدام التعليمة WHERE لطلب مجموعة فرعية من البيانات من الجدول فقط قبل إنشاء الفهرس. لنفترض أننا نريد العثور على الموظفين الذين راتبهم يساوي 100000 دولار أمريكي تمامًا من خلال تنفيذ الاستعلام التالي: mysql> SELECT * FROM employees WHERE salary = 100000; تطلب التعليمة WHERE مطابقة تامة للموظفين الذين يتطابق راتبهم مع القيمة المطلوبة، وستستجيب قاعدة البيانات كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 9 | James | Brown | YZA567 | 100000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) ملاحظة: استجابت قاعدة البيانات استجابةً آنية تقريبًا للاستعلام كما يظهر الخرج السابق، فلن يؤثر استخدام الفهارس بوضوح على أداء الاستعلام مع وجود عدد قليل من الصفوف في قاعدة البيانات، ولكن سنلاحظ تغييرات كبيرة في زمن تنفيذ الاستعلام في حال تنفيذه على مجموعات البيانات الكبيرة. لا يمكن معرفة كيفية تعامل محرّك قاعدة البيانات مع مسألة العثور على الصفوف المطابقة في الجدول بالاعتماد على خرج الاستعلام فقط، ولكن يوفّر MySQL طريقة لمعرفة الطريقة التي ينفّذ بها المحرّك الاستعلام باستخدام التعليمة EXPLAIN، حيث يمكننا مثلًا الوصول إلى طريقة تنفيذ الاستعلام SELECT من خلال تنفيذ الأمر التالي: mysql> EXPLAIN SELECT * FROM employees WHERE salary = 100000; يخبر الأمر EXPLAIN نظام MySQL بتشغيل استعلام SELECT، ويعرض معلومات حول كيفية إجراء الاستعلام داخليًا إلى جانب إعادة النتائج، وستكون نتيجة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | NULL | NULL | NULL | NULL | 20 | 10.00 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) توضّح الأعمدة في جدول الخرج السابق العديد من جوانب تنفيذ الاستعلام، وقد يحتوي الخرج على أعمدة إضافية بناءً على إصدار MySQL، وفيما يلي أهم هذه المعلومات: يسرد possible_keys الفهارس التي اعتمدها MySQL للاستخدام، حيث لا يوجد فهارس في حالتنا NULL يمثل key الفهرس الذي قرّر MySQL استخدامه عند تنفيذ الاستعلام، حيث لم نستخدم أي فهرس في مثالنا NULL. يحدد rows عدد الصفوف التي يجب على MySQL تحليلها قبل إعادة النتائج، وتبلغ قيمته 20 في مثالنا وهو يمثّل عدد جميع الصفوف الممكنة في الجدول، مما يعني أنه يجب على MySQL مسح جميع الصفوف في الجدول employees للعثور على الصف الوحيد المُعاد يعرض Extra معلومات إضافية تصف خطة الاستعلام، حيث تعني Using where في مثالنا أن قاعدة البيانات رشّحت النتائج مباشرة من الجدول باستخدام التعليمة WHERE تجدر الإشارة لأنه يجب على قاعدة البيانات مسح 20 صفًا لاسترجاع صف واحد في حالة عدم وجود فهرس، وإذا احتوى الجدول على ملايين الصفوف، فيجب على MySQL المرور عليها واحدًا تلو الآخر، مما يؤدي إلى ضعف أداء الاستعلام. ملاحظة: تعرض إصدارات MySQL الأحدث العبارة ‎1 row in set, 1 warning‎ في الخرج عند استخدام التعليمة EXPLAIN، بينما تعرض إصدارات MySQL الأقدم وقواعد البيانات المتوافقة مع MySQL العبارة ‎1 row in set، ولا يُعَد التحذير علامة على وجود مشكلة، حيث يستخدم MySQL آلية التحذيرات الخاصة به لتوفير مزيد من المعلومات الموسَّعة حول خطة الاستعلام. يُعَد هذا الاستخدام لهذه المعلومات الإضافية خارج نطاق هذا المقال، حيث يمكنك معرفة المزيد حول هذا السلوك في صفحة تنسيق خرج التعليمة EXPLAIN المُوسَّع في توثيق MySQL. استخدم استعلام SELECT الذي نفّذته سابقًا شرط المساواة WHERE salary = 100000، ولكن لنتحقق مما إذا كانت قاعدة البيانات ستتصرف بطريقة مماثلة مع شرط المقارنة، ونجرب استرجاع الموظفين الذين راتبهم أقل من 70000: mysql> SELECT * FROM employees WHERE salary < 70000; أعادت قاعدة البيانات هذه المرة صفين John Smith و Jane Doe كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | | 2 | Jane | Doe | DEF456 | 65000 | +-------------+------------+-----------+---------------+--------+ 8 rows in set (0.000 sec) ولكن إذا استخدمنا التعليمة EXPLAIN لفهم تنفيذ الاستعلام كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE salary < 70000; فسنلاحظ أن الجدول مطابق تقريبًا للاستعلام السابق: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | NULL | NULL | NULL | NULL | 20 | 33.33 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) مسح MySQL جميع الصفوف 20 في الجدول للعثور على الصفوف التي طلبتها باستخدام تعليمة WHERE في الاستعلام كما هو الحال مع الاستعلام السابق. يُعَد عدد الصفوف المُعادة في الخرج السابق صغيرًا مقارنة بعدد جميع الصفوف في الجدول، ولكن يجب على محرّك قاعدة البيانات إنجاز الكثير من العمل للعثور عليها. يمكن حل هذه المشكلة من خلال إنشاء فهرس للعمود salary، والذي سيخبر MySQL بالحفاظ على هيكل بيانات إضافي ومُحسَّن، وخاصةً لبيانات العمود salary من الجدول employees، لذا ننفّذ الاستعلام التالي: mysql> CREATE INDEX salary ON employees(salary); تتطلب صيغة التعليمة CREATE INDEX ما يلي: اسم الفهرس وهو salary في مثالنا، ويجب أن يكون اسم الفهرس فريدًا في الجدول الواحد ويمكن تكراره بجداول مختلفة في قاعدة البيانات اسم الجدول الذي أنشأنا الفهرس له، وهو employees في مثالنا قائمة الأعمدة التي أنشأنا الفهرس لها، حيث استخدمنا في مثالنا عمودًا واحدًا بالاسم salary لبناء الفهرس قد يظهر الخطأ التالي: ERROR 1142 (42000): INDEX command denied to user 'user'@'host' for table 'employees'‎ عند تنفيذ الأمر CREATE INDEX بناءً على أذونات مستخدم MySQL، حيث يمكن منح أذونات INDEX للمستخدم من خلال تسجيل الدخول إلى MySQL كمستخدم جذر وتنفيذ الأوامر التالية مع تعديل اسم مستخدم MySQL والمضيف حسب الحاجة: mysql> GRANT INDEX on *.* TO 'user'@'localhost'; mysql> FLUSH PRIVILEGES; نسجّل الخروج كمستخدم جذر ونسجّل الدخول مرة أخرى كمستخدم عادي بعد تحديث أذونات المستخدم، ثم نعيد تشغيل التعليمة CREATE INDEX، ستؤكّد الآن قاعدة البيانات إنشاء الفهرس بنجاح كما يلي: الخرج Query OK, 0 rows affected (0.024 sec) Records: 0 Duplicates: 0 Warnings: 0 نجرّب تكرار الاستعلامات السابقة للتحقق مما إذا كان هناك أي تغيير عند استخدام الفهرس، لذا نبدأ باسترجاع الموظف الذي يتقاضى راتبًا قدره 100000 بالضبط كما يلي: mysql> SELECT * FROM employees WHERE salary = 100000; وستبقى النتيجة نفسها مع إعادة الموظف James Brown فقط كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 9 | James | Brown | YZA567 | 100000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) وإذا طلبنا من MySQL شرحَ كيفية تعامله مع الاستعلام، فسيعرض بعض الاختلافات عمّا سبق، لذا ننفّذ تعليمة EXPLAIN كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE salary = 100000; وسيكون الخرج هذه المرة كما يلي: الخرج +----+-------------+-----------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | ref | salary | salary | 5 | const | 1 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+--------+---------+-------+------+----------+-------+ 1 row in set, 1 warning (0.00 sec) يصرّح MySQL أنه قرّر استخدام المفتاح الذي اسمه salary من المفتاح الوحيد الموضح في العمود possible_keys، وهذا المفتاح هو الفهرس الذي أنشأناه. يعرض العمود rows الآن القيمة 1 بدلًا من 20، حيث تجنّبت قاعدة البيانات مسح جميع الصفوف في قاعدة البيانات ويمكنها إعادة الصف المطلوب مباشرة لأنها استخدمت الفهرس. لا يذكر العمود Extra الآن العبارة Using WHERE، لأن التكرار على الجدول الرئيسي والتحقق من أن كل صف يحقق شرط الاستعلام لم يكن ضروريًا لإجراء الاستعلام. وكما ذكرنا سابقًا لن نلاحظ تأثير استخدام الفهرس جدًا مع مجموعة بيانات تجريبية صغيرة، ولكن تطلّب الأمر من قاعدة البيانات عملًا أقل بكثير لاسترجاع النتيجة وسيكون تأثير هذا التغيير كبيرًا على مجموعة بيانات أكبر. نجرّب إعادة تشغيل الاستعلام الثاني واسترجاع الموظفين الذين راتبهم أقل من 70000 للتحقق من استخدام الفهرس، لذا نفّذ الاستعلام التالي: mysql> SELECT * FROM employees WHERE salary < 70000; نلاحظ إعادة بيانات John Smith و Jane Doe أيضًا كما يلي: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | | 2 | Jane | Doe | DEF456 | 65000 | +-------------+------------+-----------+---------------+--------+ 8 rows in set (0.000 sec) ولكن إذا استخدمنا تعليمة EXPLAIN كما يلي: mysql> EXPLAIN SELECT * FROM employees WHERE salary < 70000; فسيكون الجدول مختلفًا عن التنفيذ السابق للاستعلام نفسه كما يلي: الخرج +----+-------------+-----------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | employees | NULL | range | salary | salary | 5 | NULL | 2 | 100.00 | Using index condition | +----+-------------+-----------+------------+-------+---------------+--------+---------+------+------+----------+-----------------------+ 1 row in set, 1 warning (0.00 sec) يخبرنا العمود key أن MySQL استخدم الفهرس لإجراء الاستعلام، ويخبرنا العمود rows بتحليل صفين فقط لإعادة النتيجة. يحتوي العمود Extra الآن على العبارة Using index condition، مما يعني أن MySQL أجرى ترشيحًا باستخدام الفهرس في هذه الحالة ثم استخدم الجدول الأساسي فقط لاسترجاع الصفوف المطابقة فعليًا. ملاحظة: قد يقرر MySQL عدم استخدام الفهرس في بعض الأحيان حتى في حالة وجود الفهرس وإمكانية استخدامه، فمثلًا إذا نفذنا الأمر التالي: mysql> EXPLAIN SELECT * FROM employees WHERE salary < 140000; فستكون خطة التنفيذ كما يلي: الخرج +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | employees | NULL | ALL | salary | NULL | NULL | NULL | 20 | 80.00 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+------+----------+-------------+ 1 row in set, 1 warning (0.00 sec) يعني وجود عمود key الفارغ الذي له القيمة NULL أن MySQL قرر عدم استخدام الفهرس، والذي يمكن تأكيده من خلال الصفوف العشرين الممسوحة بالرغم من إدراج الفهرس salary في العمود possible_keys. يحلل مخطِّط استعلام قاعدة البيانات كل استعلام مقابل للفهارس المحتملة لتحديد أسرع مسار للتنفيذ، فإذا كانت تكلفة الوصول إلى الفهرس أكبر من فائدة استخدامه مثل إعادة الاستعلام جزءًا كبيرًا من بيانات الجدول الأصلية، فيمكن لقاعدة البيانات أن تقرر أنه من الأسرع إجراء مسح كامل للجدول فعليًا. توضّح التعليقات في العمود Extra مثل Using index condition أو Using where كيفية تنفيذ محرّك قاعدة البيانات للاستعلام بمزيد من التفصيل، فقد تختار قاعدة البيانات طريقة أخرى لتنفيذ الاستعلام وقد يكون لدينا خرج مع عدم وجود التعليق Using index condition أو أي تعليق آخر اعتمادًا على السياق. لا يعني ذلك عدم استخدام الفهرس استخدامًا صحيحًا، ولكنه يعني أن قاعدة البيانات قرّرت أن الطريقة الأخرى للوصول إلى الصفوف ستكون أفضل في الأداء. أنشأنا واستخدمنا في هذا القسم فهارس مؤلفة من عمود واحد لتحسين أداء استعلامات SELECT التي تعتمد على الترشيح لعمود واحد، وسنتعرّف في القسم التالي على كيفية استخدام الفهارس لضمان أن تكون القيم فريدة في عمود معين. استخدام الفهارس الفريدة لمنع تكرار البيانات أحد الاستخدامات الشائعة للفهارس هو استرجاع البيانات بسرعة من خلال مساعدة محرّك قاعدة البيانات على إجراء عمل أقل لتحقيق النتيجة نفسها كما وضّحنا سابقًا، وهناك استخدام آخر وهو ضمان عدم تكرار البيانات في جزء الجدول الذي عرّفنا الفهرس له، وهذا ما يفعله الفهرس الفريد Unique Index. إن تجنب القيم المكررة ضروري لضمان سلامة البيانات سواءً من وجهة نظر منطقية أو تقنية، فمثلًا لا ينبغي أن يكون هناك شخصان يستخدمان نفس رقم الضمان الاجتماعي، ولا ينبغي لنظام عبر الإنترنت أن يسمح لمستخدمين متعددين أن يسجّلوا باستخدام اسم المستخدم أو عنوان البريد الإلكتروني نفسه. في حالة جدولنا employees لا ينبغي أن يحتوي حقل الرقم التسلسلي على قيمٍ مكررة وإذا كان الأمر كذلك، فهذا قد يتسبب في منح أكثر من موظف الحاسوب نفسه، ففي هذا الجدول يمكن بسهولة إدخال موظفين جدد مع أرقام تسلسلية مكررة. لنحاول إدخال موظف آخر مع رقم تسلسلي لجهاز قيد الاستخدام كما يلي: mysql> INSERT INTO employees VALUES (21, 'Sammy', 'Smith', 'ABC123', 65000); ستدرج قاعدة البيانات هذا الصف وتعلمنا بنجاح العملية كما يلي: الخرج Query OK, 1 row affected (0.009 sec) فإذا استعلمنا عن الموظفين باستخدام الحاسوب ذي الرقم التسلسلي ABCD123 كما يلي: mysql> SELECT * FROM employees WHERE device_serial = 'ABC123'; فسنحصل على شخصين مختلفين كما توضح النتيجة التالية: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | | 21 | Sammy | Smith | ABC123 | 65000 | +-------------+------------+-----------+---------------+--------+ 2 rows in set (0.000 sec) هذا ليس سلوكًا متوقعًا لإبقاء قاعدة بيانات employees صالحة. لذا سنتراجع عن هذا التغيير من خلال حذف الصف الأخير الذي أنشأناه كما يلي: mysql> DELETE FROM employees WHERE employee_id = 21; يمكنك التأكد من ذلك من خلال إعادة تشغيل استعلام SELECT السابق كما يلي: mysql> SELECT * FROM employees WHERE device_serial = 'ABC123'; وبالتالي أصبح الموظف John Smith المستخدم الوحيد للجهاز الذي رقمه التسلسلي ABC123 مرة أخرى: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) لننشئ الآن فهرسًا فريدًا للعمود device_serial لحماية قاعدة البيانات من مثل هذه الأخطاء من خلال تنفيذ التعليمة التالية: mysql> CREATE UNIQUE INDEX device_serial ON employees(device_serial); توجِّه إضافة الكلمة المفتاحية UNIQUE عند إنشاء الفهرس قاعدةَ البيانات للتأكد من عدم تكرار القيم في العمود device_serial، حيث تؤدي الفهارس الفريدة إلى التحقق من جميع الصفوف الجديدة المضافة إلى الجدول مقابل الفهرس لتحديد ما إذا كانت قيمة العمود تتوافق مع القيد أم لا. وستؤكد قاعدة البيانات إنشاء الفهرس كما يلي: الخرج Query OK, 0 rows affected (0.021 sec) Records: 0 Duplicates: 0 Warnings: 0 نتحقق الآن من إمكانية إضافة إدخال مكرر إلى الجدول من خلال تشغيل استعلام INSERT من جديد: mysql> INSERT INTO employees VALUES (21, 'Sammy', 'Smith', 'ABC123', 65000); ستظهر رسالة الخطأ التالية هذه المرة: الخرج ERROR 1062 (23000): Duplicate entry 'ABC123' for key 'device_serial' يمكن التحقق من عدم إضافة الصف الجديد إلى الجدول باستخدام استعلام SELECT مرة أخرى: mysql> SELECT * FROM employees WHERE device_serial = 'ABC123'; وسيُعاد صف واحد فقط هذه المرة: الخرج +-------------+------------+-----------+---------------+--------+ | employee_id | first_name | last_name | device_serial | salary | +-------------+------------+-----------+---------------+--------+ | 1 | John | Smith | ABC123 | 60000 | +-------------+------------+-----------+---------------+--------+ 1 row in set (0.000 sec) تعمل الفهارس الفريدة على الحماية من الإدخالات المكررة، وهي أيضًا فهارس وظيفية بالكامل لتسريع الاستعلامات. ويستخدم محرّك قاعدة البيانات الفهارس الفريدة باستخدام الطريقة نفسها في الخطوة السابقة، حيث يمكننا التحقق من ذلك من خلال تنفيذ التعليمة التالية: mysql> EXPLAIN SELECT * FROM employees WHERE device_serial = 'ABC123'; وستكون نتيجة التنفيذ مشابهة لما يلي: الخرج +----+-------------+-----------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | employees | NULL | const | device_serial | device_serial | 63 | const | 1 | 100.00 | NULL | +----+-------------+-----------+------------+-------+---------------+---------------+---------+-------+------+----------+-------+ 1 row in set, 1 warning (0.00 sec) يظهَر الفهرس device_serial في العمودين possible_keys و key، مما يؤكّد استخدام الفهرس عند تنفيذ الاستعلام. بهذا تعلمنا استخدام الفهارس الفريدة Unique Index للحماية من البيانات المكررة في قاعدة البيانات، وسنستخدم في القسم التالي الفهارس التي تمتد إلى أكثر من عمود واحد. الخلاصة تعلّمنا في هذا المقال ما هي الفهارس واستعرضنا أمثلة متعددة على فهارس العمود الواحد المستخدمة لتسريع استرجاع البيانات من خلال استعلامات SELECT الشرطية، أو للحفاظ على جعل بيانات العمود فريدة، وسنشرح في المقال التالي المزيد حول الفهارس ونوضح كيفية تعريف فهارس متعددة الأعمدة Indexes on Multiple Columns وحالات استخدامها، كما ننصح بالاطلاع على سلسلة تعلم SQL للمزيد حول التعامل مع لغة SQL. ترجمة -وبتصرف- للمقال How To Use Indexes in MySQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا الفهارس Indexes في SQL نظرة سريعة على لغة الاستعلامات الهيكلية SQL فهم قواعد البيانات العلاقية تحسين أداء قواعد بيانات SQL للمطورين
  22. سنشرح في هذا المقال من سلسلة دليل جودو مفهوم Delta في مجال صناعة الألعاب، ونوضح كيفية استخدامه. يُعَد معامل دلتا delta أو "زمن دلتا" مفهومًا يُساء فهمه كثيرًا في تطوير الألعاب، لذا سنشرح في هذا المقال كيفية استخدامه وأهمية الحركة المستقلة عن معدل الإطارات وأمثلة عملية لاستخدامه في محرّك الألعاب جودو Godot. ليكن لدينا عقدة Sprite تتحرك عبر الشاشة. إذا كان عرض الشاشة 600 بكسل ونريد أن نعبر الشخصية الرسومية Sprite الشاشة خلال 5 ثوانٍ، فيمكننا استخدام العملية الحسابية التالية لإيجاد السرعة اللازمة لذلك: 600 pixels / 5 seconds = 120 pixels/second سنحرّك الشخصية الرسومية في كل إطار باستخدام الدالة ‎_process()‎، بحيث إذا شُغِّلت اللعبة بمعدل 60 إطارًا في الثانية، فيمكننا إيجاد الحركة لكل إطار باستخدام العملية الحسابية التالية: 120 pixels/second * 1/60 second/frame = 2 pixels/frame ملاحظة: كما نلاحظ، أن وِحدات المقادير متناسقة في جميع العمليات الحسابية السابقة، لذا لا بد من الانتباه دائمًا إليها لتجنب الوقوع في الأخطاء. تكون الشيفرة البرمجية الضرورية كما يلي: extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(2, 0) func _process(delta): $Sprite.position += movement نشغّل الشيفرة البرمجية السابقة وسنجد أن الصورة تعبر الشاشة خلال 5 ثوانٍ. تحصل المشكلة إذا كان هناك شيء آخر يشغَل وقت الحاسوب، والذي يسمى بالتأخير Lag، الذي يكون له عدة أسباب، حيث يمكن أن يكون السبب هو الشيفرة البرمجية التي نستخدمها أو حتى التطبيقات الأخرى التي تعمل على الحاسوب. وإذا حدث التأخير، فقد يؤدي ذلك إلى زيادة طول الإطار. إذا تخيلنا مثلًا أن معدل الإطارات انخفض إلى النصف بحيث يستغرق كل إطار 1/30 بدلًا من 1/60 من الثانية. فمعنى هذا أن الأمر سيستغرق ضعف الوقت حتى تصل الشخصية الرسومية إلى طرف الشاشة عند التحرك بمعدل 2 بكسل لكل إطار. ستؤدي حتى التقلبات الصغيرة في معدل الإطارات إلى سرعة حركة غير متناسقة، وإذا كانت هذه الشخصية الرسومية رصاصة أو جسمًا سريع الحركة، فلن نرغب في إبطائه بهذه الطريقة؛ إذ يجب أن تكون الحركة مستقلة عن معدل الإطارات. إصلاح مشكلة معدل الإطارات تتضمن الدالة ‎_process()‎ تلقائيًا عند استخدامها معاملًا بالاسم delta يمرّره المحرّك كما في الدالة ‎_physics_process()‎ التي تُستخدَم في الشيفرة البرمجية المتعلقة بالفيزياء. المعامل delta هو قيمة عشرية تمثل الوقت المستغرق منذ الإطار السابق، والذي سيكون 1/60 أو 0.0167 ثانية تقريبًا. يمكننا التوقف عن القلق بشأن مقدار تحريك كل إطار من خلال استخدام المعامل delta، إذ سنحتاج للاهتمام فقط بالسرعة المطلوبة بالبكسلات في الثانية، والتي هي 120 من العمليات الحسابية السابقة. سيعطينا ضرب قيمة delta الخاصة بالمحرك بهذا العدد عددَ البكسلات التي يجب تحريكها في كل إطار، وسيُعدَّل هذا العدد تلقائيًا عند تقلّب زمن الإطار. # ‫60 إطار في الثانية 120 pixels/second * 1/60 second/frame = 2 pixels/frame # ‫30 إطار في الثانية 120 pixels/second * 1/30 second/frame = 4 pixels/frame وكما نلاحظ، يبدو أنه إذا انخفض معدل الإطارات إلى النصف (أي تضاعف زمن الإطار)، فيجب أن تتضاعف أيضًا الحركة لكل إطار للحفاظ على السرعة المطلوبة، ولهذا سنعدّل الشيفرة البرمجية كما يلي: extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(120, 0) func _process(delta): $Sprite.position += movement * delta يكون زمن الانتقال متناسقًا عند التشغيل بمعدل 30 إطارًا في الثانية كما يلي: إذا أصبح معدل الإطارات منخفضًا جدًا، فلن تكون الحركة سلسةً بعد الآن، ولكن يبقى الزمن كما هو. استخدام معامل دلتا مع معادِلات الحركة إذا كانت الحركة التي نريد العمل عليها أكثر تعقيدًا، فسيبقى المفهوم كما هو مع إبقاء الوحدة بالثواني وليس بالإطارات، والضرب بمعامل delta لكل إطار. ملاحظة: يُعَد التعامل بالبكسلات والثواني أسهل بكثير لأنه يتعلق بكيفية قياس هذه الكميات في العالم الحقيقي، فمثلًا الجاذبية Gravity هي 100 بكسل/ثانية/ثانية، لذا ستتحرك الكرة بسرعة 200 بكسل/ثانية بعد سقوطها لمدة ثانيتين، وإذا استخدمنا وحدة الإطارات، فيجب استخدام التسارع Acceleration بوحدة البكسل/إطار/إطار، ولكننا سنجد أن هذه الوحدة غير مألوفة. إذا طبّقنا الجاذبية مثلًا، فإنها تمثّل التسارع، بحيث ستزيد السرعة بمقدارٍ معين في كل إطار، وستغير السرعة موضع العقدة كما هو الحال في المثال السابق؛ وهنا سنضبط قيم delta و target_fps في الشيفرة البرمجية التالية لمعرفة النتائج: extends Node2D # التسارع بالبكسل/ثانية/ثانية var gravity = Vector2(0, 120) # التسارع بالبكسل/إطار/إطار var gravity_frame = Vector2(0, .033) # السرعة بالبكسل/ثانية أو بالبكسل/إطار var velocity = Vector2.ZERO var use_delta = false var target_fps = 60 func _ready(): Engine.target_fps = target_fps func _process(delta): if use_delta: velocity += gravity * delta $Sprite.position += velocity * delta else: velocity += gravity_frame $Sprite.position += velocity وكما هو ظاهر، فقد ضربنا القيمة المحدثة في الخطوة الزمنية لكل إطار لتحديث السرعة velocity والموضع position، إذ يجب ضرب أي كمية مُحدَّثة في كل إطار بقيمة delta لضمان تغيرها بحيث تكون مستقلة عن معدل الإطارات. استخدام الدوال الحركية Kinematic استخدمنا Sprite للتبسيط في الأمثلة السابقة، مع تحديث الموضع position في كل إطار. إذا استخدمنا جسمًا حركيًا Kinematic ثنائي الأبعاد أو ثلاثي الأبعاد، فسنحتاج لاستخدام أحد توابع الحركة الخاصة به بدلًا من ذلك، خاصةً في حالة استخدام التابع move_and_slide()‎؛ إذ قد يحدث بعض الارتباك لأنه يستخدم متجه السرعة وليس الموضع، وهذا يعني أننا لن نضرب السرعة بقيمة delta لإيجاد المسافة، إذ تنجز الدالة ذلك نيابةً عنا، ولكن يجب تطبيقها على أيّ عمليات حسابية أخرى مثل التسارع كما في المثال التالي: # الشيفرة البرمجية لحركة الشخصية الرسومية‫: velocity += gravity * delta position += velocity * delta # الشيفرة البرمجية لحركة الجسم الحركي‫: velocity += gravity * delta move_and_slide() إن لم نستخدم قيمة delta عند تطبيق التسارع على السرعة، فسيكون التسارع عرضةً للتقلبات في معدل الإطارات، وقد يكون لذلك تأثير أكثر دقةً على الحركة؛ إذ سيكون غير متناسق مع وجود صعوبة في ملاحظته. ملاحظة: يجب أيضًا تطبيق قيمة delta على أي كميات أخرى مثل الجاذبية والاحتكاك وغير ذلك عند استخدام التابع move_and_slide()‎. ختامًا بهذا نكون قد تعرفنا على مفهوم delta في مجال تطوير الألعاب وكيفية استخدامه، وسنتعرف في المقال التالي على كيفية حفظ واسترجاع البيانات المحلية بين جلسات اللعب. ترجمة -وبتصرّف- للقسم Understanding delta من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: الطريقة الصحيحة للتواصل بين العقد في جودو Godot دليلك الشامل إلى برمجة الألعاب مطور الألعاب: من هو وما هي مهامه
  23. سننتعرف في مقال اليوم على صيغة ملفات GGUF التي تستخدم لتخزين النماذج للاستدلال Inference باستخدام مكتبة GGML والمكتبات الأخرى التي تعتمد عليها مثل llama.cpp أو whisper.cpp الشهيرتين. يدعم مستودع Hub هذه الصيغة مع ميزات تسمح بالفحص السريع للموترات Tensors والبيانات الوصفية داخل الملف. صُمِّمت هذه الصيغة بوصفها "صيغة ملف واحد"، حيث يحتوي الملف الواحد عادةً على كلٍّ من سمات الضبط Configuration Attributes ومفردات المرمِّز Tokenizer Vocabulary وسمات أخرى، بالإضافة إلى جميع الموترات المُراد تحميلها في النموذج. تأتي هذه الملفات بصيغ مختلفة وفقًا لنوع تكميم Quantization الملف، لذا اطّلع على بعض منها. استخدامات صيغة GGUF يفيد استخدام صيغة GGUF في العديد من الجوانب تشمل: تخزين النماذج بكفاءة حيث تسهل ملفات GGUF تخزين جميع المعلومات المتعلقة بالنموذج في ملف واحد، مما يبسط عملية تحميل وتشغيل النماذج في بيئات مختلفة تدعم صيغة GGUF أنواعًا مختلفة من التكميم مثل F32، Q4_K، و Q6_K، مما يساعد على تقليل حجم النماذج مع الحفاظ على أدائها تتكامل مع مكتبات التحويل حيث يمكن استخدام ملفات GGUF مع مكتبة المحولات transformers لتحميل النماذج والمرمزين مباشرةً، مما يسهل عملية الاستدلال أو التدريب المخصص دعم صيغة GGUF ضمن مكتبة المحولات Transformers لقد أُضفيت القدرة على تحميل ملفات gguf ضمن مكتبة المحوِّلات transformers لتقديم مزيد من إمكانات التدريب أو الصقل Fine-tuning لنماذج gguf قبل تحويل هذه النماذج مرةً أخرى إلى صيغة gguf لاستخدامها ضمن نظام ggml البيئي، ويجب إلغاء تكميم النموذج إلى صيغة fp32 عند تحميله قبل تحميل الأوزان المُراد استخدامها في إطار عمل PyTorch. ملاحظة: لا يزال الدعم في مراحله الأولى والمساهمات مرحَّبٌ بها لترسيخ هذا الدعم على أنواع التكميم وبنى النماذج المختلفة. سنوضّح فيما يلي بنى النماذج وأنواع التكميم المدعومة حاليًا. أنواع التكميم المدعومة تُحدَّد أنواع التكميم المدعومة الأولية وفقًا لملفات التكميم الشائعة التي جرت مشاركتها على مستودع Hub وهي: F32 Q2_K Q3_K Q4_0 Q4_K Q5_K Q6_K Q8_0 اطّلع على مثال من محلّل بايثون Python الممتاز ‎99991/pygguf‎ لإلغاء تكميم Dequantize الأوزان. ملاحظة: التكميم هو عملية ضغط تقلل من حجم النموذج عن طريق تقريب القيم داخل النموذج إلى نطاقات أقل دقة، مما يقلل من متطلبات التخزين والذاكرة. بنى النماذج المدعومة بنى النماذج المدعومة حاليًا هي البنى الشائعة جدًا على مستودع Hub، وهي: LLaMa Mistral Qwen2 مثال لاستخدام صيغة ملفات GGUF يمكننا تحميل ملفات gguf في مكتبة transformers من خلال تحديد الوسيط gguf_file لتوابع from_pretrained لكلٍّ من المرمِّزات والنماذج. وفيما يلي كيفية تحميل المرمِّز والنموذج، حيث يمكن تحميلهما من الملف نفسه: from transformers import AutoTokenizer, AutoModelForCausalLM model_id = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF" filename = "tinyllama-1.1b-chat-v1.0.Q6_K.gguf" tokenizer = AutoTokenizer.from_pretrained(model_id, gguf_file=filename) model = AutoModelForCausalLM.from_pretrained(model_id, gguf_file=filename) وبهذا يمكننا الآن الوصول إلى الإصدار الكامل غير المكمَّم من النموذج في نظام PyTorch البيئي، والذي يمكن دمجه مع مجموعة كبيرة من الأدوات الأخرى. يُوصَى باستخدام ملف convert-hf-to-gguf.py من نموذج llama.cpp للتحويل مرةً أخرى إلى ملف gguf. نوضّح فيما يلي كيفية إكمال السكربت السابق لحفظ النموذج وتصديره مرةً أخرى إلى صيغة gguf: tokenizer.save_pretrained('directory') model.save_pretrained('directory') !python ${path_to_llama_cpp}/convert-hf-to-gguf.py ${directory} ترجمة -وبتصرّف- للقسم GGUF and interaction with Transformers من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: استكشاف الأخطاء وإصلاحها في مكتبة المحولات Transformers تعرف على مكتبة المحوّلات Transformers من منصة Hugging Face تثبيت مكتبة المحوّلات Transformers
  24. بعد أن تعرفنا في المقال السابق من سلسلة دليل جودو على كيفية ترتيب معالجة العقد والتنقل في شجرة المشاهد في محرك الألعاب جودو Godot، سنتعرف في هذا المقال على الطريقة السليمة للتواصل بين العقد Nodes ونوضح المشاكل التي قد تحدث عند القيام بممارسات غير متوافقة مع الطريقة الصحيحة المنصوح بها. كما هو معروف، إذا أصبحت لدينا مشاهد ونسخ متعددة مع عدد كبير من العقد، فسيصبح مشروعنا معقدًا؛ وعندها قد نكتب شيفرة برمجية تشبه ما يلي: get_node("../../SomeNode/SomeOtherNode") get_parent().get_parent().get_node("SomeNode") get_tree().get_root().get_node("SomeNode/SomeOtherNode") وهنا إذا كتبنا الكود بهذا الشكل، فسنجد أن مثل مراجع References هذه العقد ستنكسر بكل سهولة، بحيث قد يتسبب إحداث تغيير بسيط في شجرة المشاهد في جعل مراجع العقد غير صالحة؛ ولهذا السبب، لا يجب أن يكون الاتصال بين العقد والمشاهد معقدًا. يجب أن تدير العقد أبناءها وليس العكس؛ حيث إذا استخدمنا الدالة get_parent()‎ أو get_node("..")‎، فيُحتمَل أننا نتجه إلى مشكلة ما، إذ تكون مثل هذه المسارات للعقد سهلة الكسر. سنذكر ففيما يلي المشاكل الرئيسية الثلاث في هذا الترتيب: لا يمكن اختبار مشهد بطريقة مستقلة؛ فإذا شغّلنا المشهد بمفرده أو في مشهد اختبار لا يحتوي على إعداد العقدة نفسه، فستسبّب الدالة get_node()‎ عطلًا لا يمكننا تغيير الأشياء بسهولة، لأننا إذا قرّرنا إعادة ترتيب أو تصميم الشجرة، فلن تكون المسارات صالحةً مجددًا يبدأ ترتيب الجاهزية من الأبناء أولًا حتى الوصول إلى الأب أخيرًا، وهذا يعني فشل محاولة الوصول إلى خاصية الأب في التابع ‎_ready()‎ الخاصة بالعقدة، لأن الأب غير جاهزٍ بعد ملاحظة: يُنصح بالاطلاع على المقال السابق للحصول على شرح لكيفية دخول العقد إلى الشجرة وكيف تصبح جاهزة. لا بد من توفر إمكانية إنشاء نسخة لعقدة أو مشهد في أيّ مكان في اللعبة التي ننشؤها، مع عدم افتراض أيّ شيء حول ما سيكون عليه الأب. سنستخدم أمثلة مفصلة لاحقًا، ولكن سنتحدث الآن عن القاعدة الذهبية لتواصل العقد، والتي هي: "استدعاء للأسفل، وإشارة Signal للأعلى". تشير هذه القاعدة إلى أنه في حال استدعَت العقدة ابنًا، بمعنى تنقلها إلى أسفل الشجرة، فسيكون استدعاء الدالة get_node()‎ مناسبًا؛ وإذا كانت العقدة بحاجة إلى التواصل مع أعلى الشجرة، فيجب استخدام إشارة لذلك. يؤدي استخدام هذه القاعدة عند تصميم إعداد المشهد الخاص بنا إلى الحصول على مشروع قابل للصيانة ومنظم جيدًا، وسنتجنب بذلك استخدام مسارات العقد المعقدة التي تؤدي إلى مشاكل. لنلقِ الآن نظرةً على كلٍّ من هذه الاستراتيجيات مع بعض الأمثلة. استخدام الدالة get_node()‎ يعبُر استدعاء الدالة get_node()‎ شجرة المشاهد باستخدام مسار معين للعثور على العقدة المسماة. مثال عن استخدام get_node()‎ ليكن لدينا الضبط التالي: يجب أن يُعلِم السكربت الموجود في العقدة Player العقدةَ AnimatedSprite2D بالرسوم المتحركة التي تُشغَّل بناءً على حركة اللاعب، بحيث تعمل الدالة get_node()‎ في هذه الحالة بنجاح كما يلي: extends CharacterBody2D func _process(delta): if speed > 0: get_node("AnimatedSprite2D").play("run") else: get_node("AnimatedSprite2D").play("idle") ملاحظة: يمكننا استخدام المحرف $ في لغة GDScript كاختصار لاستدعاء get_node()‎، لذا بإمكاننا كتابة ‎$AnimatedSprite2D مباشرةً. طريقة أفضل للاستدعاء تتمثل سلبية الطريقة السابقة في تحديد مسار العقدة، حيث إذا تغير هذا المسار لاحقًا، فيجب تعديل الشيفرة البرمجية أيضًا؛ ولهذا يمكننا استخدام الميزة ‎@export لتحديد عقدة مباشرةً كما يلي: extends CharacterBody2D @export var animation : AnimatedSprite2D func _process(delta): if speed > 0: animation.play("run") else: animation.play("idle") يمكننا باستخدام هذه الطريقة إسناد قيمةٍ إلى المتغير مباشرةً في الفاحص Inspector من خلال اختيار العقدة. استخدام الإشارات Signals يجب استخدام الإشارات لاستدعاء الدوال في العقد الموجودة في مستوًى أعلى من الشجرة أو الموجودة على المستوى نفسه (أي العقد الأشقاء Siblings). يمكننا توصيل إشارة في المحرّر للعقد الموجودة قبل بدء اللعبة في أغلب الأحيان أو في الشيفرة البرمجية للعقد التي تنشِئ نسخةً منها في وقت التشغيل، وتكون صيغة توصيل الإشارة كما يلي: signal_name.connect(target_node.target_function) عند تجربة الاتصال بعقدة شقيقة، قد يبدو أننا سنحتاج إلى مسارات للعقدة، مثل المسار ‎../Sibling، ولكن كما نرى، ذلك يخالف القاعدة السابقة. ولتفادي أي خلط، لا بد من التأكد دائمًا من أن الأب المشترك هو الذي يجري الاتصالات، نظرًا لتمكن عقدة الأب التي تُعَد أبًا مشتركًا لعقدتي الإشارة والاستقبال من تحديد مكانهما وأنها ستكون جاهزة بعدهما باتباع قاعدة الاستدعاء في الشجرة من الأعلى للأسفل. مثال عن استخدام الإشارات يُعَد تحديث واجهة المستخدم UI الخاصة بك حالة استخدام شائعة جدًا للإشارات، لأننا نريد مثلًا تحديث عرض Label أو ProgressBar كلما اختلف المتغير health الخاص باللاعب، ولكن تكون عقد واجهة المستخدم الخاصة بك منفصلة تمامًا عن اللاعب إذ لا يعرف اللاعب شيئًا عن مكان هذه العقد وكيفية العثور عليها. ليكن لدينا إعداد المثال التالي: يمكننا ملاحظة أن واجهة المستخدم هي نسخة من مشهد، لأننا نعرض العقد المضمَّنة فقط، وهو المكان الذي نرى فيه أشياء مثل get_node("../UI/VBoxContainer/HBoxContainer/Label).text = str(health)‎ التي نريد تجنبها، لذا يصدر اللاعب بدلًا من ذلك إشارة health_changed كلما أضاف أو فقد جزءًا من صحته، ويجب إرسال هذه الإشارة إلى الدالة update_health()‎ الخاصة بواجهة المستخدم UI، والتي تتولّى ضبط قيمة Label. سنستخدم الشيفرة البرمجية التالية في سكربت Player كلما تغيرت صحة اللاعب: health_changed.emit(health) لدينا ما يلي في سكربت واجهة المستخدم UI: onready var label = $VBoxContainer/HBoxContainer/Label func update_health(value): label.text = str(value) كل ما نحتاجه الآن هو توصيل الإشارة بالدالة، والمكان المثالي ذلك موجود في سكربت World الذي يمثّل الأب المشترك للعقدتين ويعرف مكانهما: func _ready(): $Player.health_changed.connect($UI.update_health) استخدام المجموعات تُعَد المجموعات طريقةً أخرى لفك الارتباط بين عقدتين، وخاصةً عندما تكون لدينا الكثير من الكائنات المتشابهة التي تطبّق الشيء نفسه؛ إذ يمكن إضافة عقدة إلى أيّ عددٍ من المجموعات، كما يمكن تغيير العضوية ديناميكيًا في أيّ وقت باستخدام الدالتين add_to_group()‎ و remove_from_group()‎. من المفاهيم الخاطئة الشائعة حول المجموعات أنها نوع من الكائنات أو المصفوفات التي تحتوي على مراجعٍ للعقد، ولكن المجموعات هي نظام وسم Tagging System، في حين تكون العقدة ضمن مجموعة عند إسناد وسمٍ من هذه المجموعة إليها. تتعقّب شجرة المشهد SceneTree الوسوم ويكون لديها دوال مثل الدالة get_nodes_in_group()‎ للمساعدة في العثور على جميع العقد التي يكون لها وسم معين. مثال عن استخدام المجموعات لتكن لدينا لعبة إطلاق نار فضائية مثل لعبة غالاغا Galaga، مع وجود الكثير من الأعداء الذين يطيرون حول الشخصية الرئيسية، وقد يكون للأعداء أنواع وسلوكيات مختلفة، ونريد إضافة ترقية للقنبلة الذكية Smart Bomb التي تدمّر جميع الأعداء على الشاشة عند تنشيطها. هنا يمكننا الأمر باستخدام المجموعات بأقل قدر من الشيفرة البرمجية. سنحتاج أولًا إلى إضافة جميع الأعداء إلى المجموعة enemies في المحرّر باستخدام تبويب العقدة Node: يمكننا أيضًا إضافة عقد إلى المجموعة في السكربت الخاص بنا كما يلي: func _ready(): add_to_group("enemies") لنفترض أن لكل عدو الدالة explode()‎ التي تتعامل مع ما يحدث عندما يموت، مثل تشغيل رسوم متحركة أو توليد عناصر متساقطة وما إلى ذلك. يمكننا الآن تنفيذ دالة القنبلة الذكية smart bomb الخاصة بنا كما يلي بعد أن أصبح كل عدو في مجموعته: func activate_smart_bomb(): get_tree().call_group("enemies", "explode") استخدام خاصية المالك owner owner هي خاصية عقدة Node التي تُضبَط تلقائيًا عند حفظ مشهد. تُضبَط هذه الخاصية لكل عقدة في هذا المشهد على العقدة الجذر للمشهد، مما يوفر طريقةً ملائمةً لتوصيل إشارات العقد الأبناء بالعقدة الرئيسية. مثال عن استخدام الخاصية owner سنحصل في أغلب الأحيان على تسلسل هرمي عميق ومتداخل من الحاويات وعناصر التحكم في واجهة مستخدم معقدة، وتصدر العقد التي يتفاعل معها المستخدم مثل عقدة Button إشاراتٍ. قد نرغب في ربط هذه الإشارات بالسكربت الموجود على عقدة الجذر لواجهة المستخدم. ليكن لدينا مثلًا الإعداد التالي: يحتوي السكربت الموجود على العقدة الجذر CenterContainer على الدالة التالية التي نريد استدعاءها عند الضغط على أيّ زر: extends CenterContainer func _on_button_pressed(button_name): print(button_name, " was pressed") تُعَد الأزرار هنا نسخًا من مشهد Button، وتمثّل كائنًا قد يحتوي على شيفرة برمجية ديناميكية تضبط نص الزر أو خاصيات أخرى؛ أو قد تكون لدينا أزرار تُضاف أو تُزال ديناميكيًا من الحاوية وفقًا لحالة اللعبة، ولكن ما نحتاجه لتوصيل إشارة الزر هو ما يلي: extends Button func _ready(): pressed.connect(owner._on_button_pressed.bind(name)) وكما هو ظاهر، ستبقى العقدة CenterContainer هي المالك owner بغض النظر عن المكان الذي تضع فيه الأزرار في الشجرة إذا أضفتَ مزيدًا من الحاويات مثلًا. ختامًا بهذا نكون قد تعرفنا على كيفية تحقيق تواصل سليم بين العقد Nodes في محرك الألعاب جودو Godot، وسنتابع في المقال التالي من هذه السلسلة في شرح مفهوم جديد من عالم الألعاب وهو delta. ترجمة -وبتصرّف- للقسم Node communication من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: ترتيب معالجة العقد والتنقل في شجرة المشاهد في Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot تعرف على واجهة محرك الألعاب جودو Godot
  25. عند العمل مع قواعد البيانات العلاقية Relational Databases ولغة الاستعلام البنيوية SQL فإننا نجري معظم العمليات على البيانات الناتجة عن استعلامات منفَّذة صراحةً مثل استعلامات SELECT أو INSERT أو UPDATE. لكن يمكننا توجيه قواعد بيانات SQL لتنفيذ إجراءات مُعرَّفة مسبقًا تلقائيًا في كل مرة يقع فيها حدث معين باستخدام القوادح أو محفّزات التنفيذ Triggers. يمكننا مثلًا استخدام هذه القوادح للاحتفاظ بسجل يتضمن جميع تعليمات الحذف DELETE بحيث نحفظ بعد كل عملية حدث تقع تفاصيل هذه العملية ومن قام بها ومتى، كما يمكن استخدامها لتحديث البيانات التراكمية مثل المجموع أو المتوسط حيث يمكننا تحديث هذه البيانات الإحصائية كلما جرت عملية إضافة أو تحديث على البيانات الموجودة. سنستخدم في هذا المقال قوادح SQL مختلفة لتنفيذ الإجراءات تلقائيًا عندما ندرج الصفوف أو نحدثها أو نحذفها. مستلزمات العمل يجب توفر حاسوب يشغّل نظام إدارة قواعد بيانات علاقية RDBMS مستند إلى لغة SQL. وقد اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم ذي صلاحيات مسؤول مختلف عن المستخدم الجذر، وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو، ومقال كيفية تثبيت توزيعة أوبنتو من لينكس بأبسط طريقة نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة التالية من المقال معرفة أساسية بتنفيذ استعلامات SELECT و INSERT و UPDATE و DELETE لمعالجة البيانات في قاعدة البيانات كما هو موضح في مقال كيفية الاستعلام عن السجلات من الجداول في SQL وكيفية إدراج البيانات في SQL وتحديث البيانات في لغة الاستعلام البنيوية SQL وحذف البيانات في لغة الاستعلام البنيوية SQL معرفة أساسية باستخدام الاستعلامات المتداخلة كما هو موضَّح في مقال كيفية استخدام الاستعلامات المتداخلة في لغة SQL المعرفة الأساسية باستخدام الدوال الرياضية التجميعية كما هو موضَّح في مقال كيفية استخدام التعابير الرياضية والدوال التجميعية في SQL ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL. فلا يفرض معيار SQL صيغةً للقوادح Triggers أو طريقة صارمة لتحقيقها بالرغم من أنها تُعَد جزءًا من هذا المعيار، لذا يختلف تقديمها من قاعدة البيانات إلى أخرى، وتستخدم الأوامر الموضَّحة في هذا المقال صيغة قاعدة بيانات MySQL وقد لا تعمل على محرّكات قواعد البيانات الأخرى. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لنتمكّن من التدرب على استخدام المحفّزات، وفي القسم التالي نوضح تفاصيل الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية لاستخدامها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل في هذا القسم بخادم MySQL وننشئ قاعدة بيانات تجريبية لتطبيق الأمثلة الواردة في هذا المقال، حيث سنستخدم قاعدة بيانات تحفظ مجموعة هدايا تذكارية افتراضية، ونخزّن كل تفاصيل الهدايا التذكارية المملوكة حاليًا، وقيمتها الإجمالية المتاحة ونحتاج للتأكد من أن إجراء حذف الهدايا التذكارية سيُحفَظ في سجل دائم يوضح كافة تفاصيل عملية الحذف. إذا كان نظام قاعدة بيانات SQL الخاص بنا يعمل على خادم بعيد، نحتاج للاتصال بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم collectibles: mysql> CREATE DATABASE collectibles; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات collectibles من خلال تنفيذ تعليمة USE التالية: $ USE collectibles; وسيظهر الخرج التالي: Database changed اخترنا قاعدة البيانات، وسننشئ عدة جداول تجريبية ضمنها، حيث سيحتوي الجدول collectibles على بيانات مبسَّطة عن الهدايا التذكارية الموجودة في قاعدة البيانات، ويتضمن الجدول الأعمدة التالية: name: يخزّن اسم كل هدية تذكارية، ويستخدم نوع البيانات varchar بحد أقصى 50 محرفًا value: يخزّن قيمة الهدية التذكارية، ويستخدم نوع البيانات decimal بحد أقصى 5 قيم قبل الفاصلة العشرية وقيمتين بعدها أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE collectibles ( mysql> name varchar(50), mysql> value decimal(5, 2) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: Query OK, 0 rows affected (0.00 sec) سنسمّي الجدول الثاني باسم collectibles_stats وسنستخدمه لتتبّع القيمة المتراكمة لجميع الهدايا التذكارية في المجموعة، وسيحتوي هذا الجدول على صف واحد من البيانات مع الأعمدة التالية: count: يحتوي عدد الهدايا التذكارية المملوكة، ونمثّله باستخدام نوع البيانات int value: يخزّن القيمة المتراكمة لجميع الهدايا باستخدام نوع البيانات decimal بحد أقصى 5 قيم قبل الفاصلة العشرية وقيمتين بعدها أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE collectibles_stats ( mysql> count int, mysql> value decimal(5, 2) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: Query OK, 0 rows affected (0.00 sec) سنسمي الجدول الثالث والأخير بالاسم collectibles_archive، والذي سيتتبّع جميع الهدايا التذكارية المحذوفة من المجموعة لضمان عدم اختفائها أبدًا، وسيحتوي على بيانات مشابهة للجدول collectibles مع تاريخ الإزالة، وسيستخدم الأعمدة التالية: name: يحتوي اسم كل هدية تذكارية محذوفة، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 50 محرفًا value: يخزّن قيمة الهدايا التذكارية لحظة الحذف باستخدام نوع البيانات decimal بحد أقصى 5 قيم قبل الفاصلة العشرية وقيمتين بعدها removed_on: يخزّن تاريخ ووقت الحذف لكل هدية تذكارية مؤرشفة باستخدام نوع البيانات timestamp باستخدام القيمة الافتراضية NOW()‎ التي تعني التاريخ الحالي لإدراج صف جديد في هذا الجدول أنشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE collectibles_archive ( mysql> name varchar(50), mysql> value decimal(5, 2), mysql> removed_on timestamp DEFAULT CURRENT_TIMESTAMP mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول بنجاح: Query OK, 0 rows affected (0.00 sec) نحمّل بعد ذلك الجدول collectibles_stats بالبيانات الأولية لمجموعة الهدايا التذكارية من خلال تنفيذ عملية INSERT INTO التالية: mysql> INSERT INTO collectibles_stats SELECT COUNT(name), SUM(value) FROM collectibles; تضيف عملية INSERT INTO السابقة صفًا واحدًا إلى الجدول collectibles_stats مع القيم المحسوبة باستخدام الدوال التجميعية Aggregate Functions لحساب عدد الصفوف في الجدول collectibles ولجمع قيم جميع الهدايا التذكارية باستخدام العمود value والدالة SUM. يشير الخرج التالي إلى إضافة الصف بنجاح: Query OK, 1 row affected (0.002 sec) Records: 1 Duplicates: 0 Warnings: 0 يمكننا التحقق من ذلك من خلال تنفيذ تعليمة SELECT التالية مع الجدول collectibles_stats: mysql> SELECT * FROM collectibles_stats; لا توجد هدايا تذكارية في قاعدة البيانات حتى الآن، لذا يكون العدد الأولي للعناصر هو 0 وتكون القيمة المتراكمة هي NULL كما يلي: +-------+-------+ | count | value | +-------+-------+ | 0 | NULL | +-------+-------+ 1 row in set (0.000 sec) نحن الآن جاهزون لمتابعة هذا المقال والبدء باستخدام القوادح Triggers في MySQL. فهم Triggers القوادح Triggers هي تعليمات مُعرَّفة من أجل جدول معين تنفّذها قاعدة البيانات تلقائيًا في كل مرة يقع فيها حدث محدد في هذا الجدول، ويمكننا استخدامها لضمان تنفيذ بعض الإجراءات بتناسق في كل مرة نُنفَّذ فيها تعليمة معينة مع الجدول بدلًا من أن يحتاج مستخدمو قاعدة البيانات لتنفيذها يدويًا. يُعرَّف كل Trigger مرتبط بجدول باسم يحدده المستخدم، وشرطين لتوجيه محرك قاعدة البيانات وإعلامه بالوقت المناسب لتنفيذ القادح، ويمكن تجميع هذين الشرطين ضمن فئتين منفصلتين هما: حدث قاعدة البيانات: يمكن تنفيذ القوادح عند تشغيل تعليمات INSERT أو UPDATE أو DELETE مع الجدول وقت الحدث: يمكن تنفيذ القوادح أيضًا قبل BEFORE أو بعد AFTER التعليمة المحدَّدة يؤدي الجمع بين مجموعتي الشروط السابقتين لستة احتمالات منفصلة للقوادح التي تُنفَّذ تلقائيًا في كل مرة يتحقق فيها الشرط المشترك. القوادح التي تحدث قبل تنفيذ التعليمة التي تحقق الشرط هي BEFORE INSERT و BEFORE UPDATE و BEFORE DELETE، ويمكن استخدامها لمعالجة البيانات والتحقق من صحتها قبل إدراجها أو تحديثها في الجدول أو لحفظ تفاصيل الصف المحذوف لأغراض التدقيق أو الأرشفة. المحفّزات التي تحدث بعد تنفيذ التعليمة التي تحقق الشرط هي AFTER INSERT و AFTER UPDATE و AFTER DELETE، ويمكن استخدامها لتحديث القيم الملخَّصة في جدول منفصل بناءً على الحالة النهائية لقاعدة البيانات بعد التعليمة. يمكن تنفيذ إجراءات مثل التحقق من صحة بيانات الدخل، ومعالجتها، أو أرشفة الصف المحذوف، إذ تسمح قاعدة البيانات بالوصول إلى قيم البيانات من داخل القوادح، ويمكن استخدام البيانات المدرَجة حديثًا فقط بالنسبة لقوادح INSERT، ويمكن الوصول إلى كلٍّ من البيانات الأصلية والمُحدَّثة بالنسبة لمحفّزات UPDATE، وتكون بيانات الصف الأصلية فقط متاحة للاستخدام بالنسبة لمحفّزات DELETE نظرًا لعدم وجود بيانات جديدة للإشارة إليها. يمكن الوصول للبيانات المُستخدَمة في جسم القادح ضمن السجل OLD بالنسبة للبيانات الموجودة حاليًا في قاعدة البيانات والسجل NEW بالنسبة للبيانات التي سيحفظها الاستعلام، ويمكن الإشارة إلى الأعمدة الفردية باستخدام الصيغة OLD.column_name و OLD.column_name. يوضّح المثال التالي الصيغة العامة لتعليمة SQL المُستخدَمة لإنشاء قادح جديد: mysql> CREATE TRIGGER trigger_name trigger_condition mysql> ON table_name mysql> FOR EACH ROW mysql> trigger_actions; لنشرح التعليمة السابقة بالتفصيل: CREATE TRIGGER: اسم تعليمة SQL المُستخدَمة لإنشاء قادح جديد في قاعدة البيانات trigger_name: هو الاسم الذي يحدّده المستخدم للقادح، ويصف دوره مثل استخدام أسماء الجداول وأسماء الأعمدة لوصف معناها ON table_name: نخبر قاعدة البيانات بأن القادح يجب أن يراقب الأحداث التي تحدث في الجدول table_name trigger_condition: أحد الاختيارات الستة المُحتملة التي تحدد متى يجب تشغيل القادح مثل BEFORE INSERT. FOR EACH ROW: تخبر قاعدة البيانات بأنه يجب تشغيل القادح لكل صف يتأثر بالحدث. تدعم بعض قواعد البيانات أنماطًا إضافية للتنفيذ مختلف عن النمط FOR EACH ROW، ولكن تشغيل التعليمات من جسم القادح لكل صف متأثر بالتعليمة التي تسبّبت في تنفيذ القادح هو الخيار الوحيد في حال MySQL trigger_actions: جسم القادح الذي يحدّد ما يحدث عند تنفيذه، وهو تعليمة SQL واحدة، ويمكن تضمين تعليمات متعددة في جسم القادح لإجراء عمليات معقدة باستخدام الكلمات المفتاحية BEGIN و END لتضمين التعليمات ضمن كتلة، ولكن ذلك خارج نطاق هذا المقال اطّلع على التوثيق الرسمي للمحفّزات لمعرفة المزيد حول الصيغة المُستخدمَة لتعريف القوادح. سننشئ في القسم التالي أمثلة على قوادح تعالج البيانات قبل إجراء عمليتي INSERT و UPDATE. معالجة البيانات باستخدام محفزات BEFORE INSERT و BEFORE UPDATE سنستخدم في هذا القسم قوادح لمعالجة البيانات قبل تنفيذ تعليمات INSERT و UPDATE، حيث سنستخدم المحفّزات في للتأكّد من أن جميع الهدايا التذكارية في قاعدة البيانات تستخدم أسماءً بحروف كبيرة لتحقيق التناسق. في حال لم نستخدم قوادح سيتوجب علينا تذكّر استخدام أسماء الهدايا التذكارية بحروف كبيرة لكل تعليمة INSERT و UPDATE، وإذا نسينا، فستحتفظ قاعدة البيانات بالبيانات كما هي وهذا قد يؤدي إلى حدوث أخطاء محتملة في مجموعة البيانات. لنبدأ بإدخال مثال لعنصر من الهدايا التذكارية بالاسم spaceship model وبقيمة 12.50 دولار، وسنكتب اسم العنصر بحروف صغيرة لتوضيح المشكلة. لننفّذ التعليمة التالية: mysql> INSERT INTO collectibles VALUES ('spaceship model', 12.50); تؤكد الرسالة التالية إضافة العنصر: Query OK, 1 row affected (0.009 sec) يمكننا التحقق من إدراج الصف من خلال تنفيذ استعلام SELECT التالي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | spaceship model | 12.50 | +-----------------+-------+ 1 row in set (0.000 sec) حُفِظ هذا العنصر كما هو مع كتابة اسمه بحروف صغيرة فقط. لنتأكد من كتابة جميع الهدايا التذكارية اللاحقة بحروف كبيرة دائمًا من خلال إنشاء قادح باسم BEFORE INSERT لمعالجة البيانات المُمرَّرة إلى قاعدة البيانات قبل حدوثها. نشغّل الآن التعليمة التالية: mysql> CREATE TRIGGER uppercase_before_insert BEFORE INSERT mysql> ON collectibles mysql> FOR EACH ROW mysql> SET NEW.name = UPPER(NEW.name); ينشئ الأمر السابق قادح باسم uppercase_before_insert، والذي سيُنفَّذ قبل كافة تعليمات INSERT في الجدول collectibles. تُنفَّذ التعليمة الموجودة في القادحSET NEW.name = UPPER(NEW.name)‎ لكل صف مدرج، ويسند أمر SQL الذي هو SET القيمة الموجودة على الجانب الأيمن إلى الجانب الأيسر، حيث يمثل NEW.name قيمة العمود name الذي ستحفظه تعليمة الإدراج. نحوّل حالة الحروف للقيمة التي ستُحفَظ في قاعدة البيانات من خلال تطبيق الدالة UPPER على اسم الهدية وإسناده مرة أخرى لقيمة العمود. ملاحظة: قد تظهر رسالة خطأ مشابهة للخطأ التالي عند تشغيل الأمر CREATE TRIGGER. ERROR 1419 (HY000): You do not have the SUPER privilege, and binary logging is enabled (you might want to use the less safe log_bin_trust_function_creators variable) التسجيل الثنائي Binary Logging هو آلية تُسجل كل التعديلات التي تتم على قاعدة البيانات مثل إضافة أو تعديل أو حذف بيانات في سجل ثنائي، ويحتوي هذا السجل على أحداث Events تصف التعديلات التي حدثت وتحفظها بتنسيق ثنائي يمكن معالجته، وهذا التسجيل يكون مفعَّلًا افتراضيًا في محرّك قاعدة بيانات MySQL وذلك بدءًا من الإصدار MySQL 8، حيث يتعقّب التسجيل الثنائي جميع تعليمات SQL التي تعدّل محتويات قاعدة البيانات في صيغة أحداث محفوظة تَصِف هذه التعديلات، وتُستخدَم هذه السجلات في النسخ المتماثل Replication لقاعدة البيانات للحفاظ على مزامنة النسخ المتماثلة لقاعدة البيانات وأثناء استعادة البيانات في الوقت المناسب. لكن لا يسمح MySQL بإنشاء القوادح Triggers والإجراءات المخزَّنة Stored Procedures في حال تفعيل تسجيل التعديلات أو الأحداث بصيغة ثنائية كإجراء احترازي لضمان سلامة البيانات وتكاملها في بيئات النسخ المتماثل Replication، ولكن فهم كيفية تأثير القوادح والإجراءات المُخزَّنة على النسخ المتماثل خارج نطاق هذا المقال. يمكننا تجاوز القيود التي يفرضها MySQL عند تفعيل التسجيل الثنائي Binary Logging، وذلك لأغراض التعلم أو الاختبار في بيئة محلية على جهازنا الشخصي، ولكن لن يستمر هذا الإعداد الذي عدّلناه بالعمل وسيعود للقيمة الأصلية عند إعادة تشغيل خادم MySQL. يمكن تجاوز الإعداد الافتراضي من خلال تعديل الإعدادات الخاصة بـ MySQL كما يلي: mysql> SET GLOBAL log_bin_trust_function_creators = 1; يتحكم الإعداد log_bin_trust_function_creators بإمكانية الوثوق بالمستخدمين الذين ينشئون القوادح والدوال المخزنة بحيث لا ينشئون قوادح تتسبب في كتابة أحداث غير آمنة في السجل الثنائي. تكون قيمة الإعداد الافتراضية هي 0، مما يسمح للمستخدمين الذين يتمتعون بصلاحيات مميزة فقط بإنشاء قوادح في البيئة التي فعّلنا فيها تسجيل التعديلات أو الأحداث بصيغة ثنائية، وإذا عدّلنا القيمة إلى 1، فسنثق بأيّ مستخدم ينشئ تعليمات CREATE TRIGGER لفهم النتائج. جرى الآن تحديث الإعداد، لنعمل إذًا على تسجيل الخروج كمستخدم جذر، وتسجيل الدخول مرة أخرى كمستخدم عادي، ونعيد تشغيل تعليمة CREATE TRIGGER. ويمكن الاطلاع على توثيق MySQL الرسمي: السجل الثنائي و تسجيل التعديلات أو الأحداث بصيغة ثنائية للبرنامج المخزن، كما يمكن مطالعة مقال كيفية إعداد النسخ المتماثل في MySQL لمعرفة المزيد حول تسجيل التعديلات أو الأحداث بصيغة ثنائية والنسخ المتماثل في MySQL وارتباطه بالقوادح. ملاحظة: قد نتلقى خطأً عند تنفيذ أمر CREATE TRIGGER اعتمادًا على أذونات مستخدم MySQL الخاصة بنا ERROR 1142 (42000): TRIGGER command denied to user 'user'@'host' for table 'collectibles'‎ لحل هذا الخطأ يمكن منح أذونات TRIGGER للمستخدم الخاص بنا من خلال تسجيل الدخول إلى MySQL كمستخدم جذر، وتنفيذ الأوامر التالية مع وضع اسم مستخدم MySQL والمضيف حسب الحاجة: mysql> GRANT TRIGGER on *.* TO 'user'@'localhost'; mysql> FLUSH PRIVILEGES; نحدّث أذونات المستخدم، ثم نسجّل الخروج كمستخدم جذر، ونسجّل الدخول مرة أخرى كمستخدم عادي، ونعيد تشغيل التعليمة CREATE TRIGGER، وسيطبع MySQL الرسالة التالية للتأكد من إنشاء القادح بنجاح: Query OK, 1 row affected (0.009 sec) نحاول الآن إدراج مجموعة هدايا تذكارية جديدة باستخدام وسيط بحروف صغيرة مع استعلام INSERT كما يلي: mysql> INSERT INTO collectibles VALUES ('aircraft model', 10.00); ثم نتحقّق من الصفوف الناتجة في الجدول collectibles كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | spaceship model | 12.50 | | AIRCRAFT MODEL | 10.00 | +-----------------+-------+ 2 rows in set (0.000 sec) يشير الإدخال الجديد هذه المرة إلى أن AIRCRAFT MODEL -مع جميع حروفه الكبيرة- يختلف عن الإدخال الذي حاولنا إدراجه، حيث شُغِّل القادح في الخلفية وحوّل حالة الحروف قبل حفظ الصف في قاعدة البيانات. يحمي القادح جميع الصفوف الجديدة لضمان حفظ الأسماء بحروف كبيرة، ولكن لا يزال من الممكن حفظ البيانات غير المقيَّدة التي تستخدم تعليمات UPDATE، حيث يمكن حماية تعليمات UPDATE باستخدام التأثير نفسه، إذًا لننشئ قادحًا آخر كما يلي: mysql> CREATE TRIGGER uppercase_before_update BEFORE UPDATE mysql> ON collectibles mysql> FOR EACH ROW mysql> SET NEW.name = UPPER(NEW.name); يكمن الفرق بين القادحين في المعايير، فالقادح هنا BEFORE UPDATE، مما يعني تنفيذه في كل مرة تُنفَّذ فيها تعليمة UPDATE مع الجدول، ويؤثر ذلك على الصفوف الموجودة في كل تحديث، بالإضافة إلى الصفوف الجديدة التي يؤثر عليها القادح السابق. سيعطي MySQL تأكيدًا بإنشاء القادح بنجاح كما يلي: Query OK, 0 row affected (0.009 sec) يمكن التحقق من سلوك القادح الجديد من خلال تحديث قيمة سعر spaceship model كما يلي: mysql> UPDATE collectibles SET value = 15.00 WHERE name = 'spaceship model'; ترشّح تعليمة WHERE الصف المراد تحديثه حسب الاسم، وتغيّر تعليمة SET القيمة إلى 15.00، وسيظهر الخرج التالي، مما يؤكد أن التعليمة قد غيّرت صفًا واحدًا: Query OK, 1 row affected (0.002 sec) Rows matched: 1 Changed: 1 Warnings: 0 لنتحقّق الآن من الصفوف الناتجة في الجدول collectibles كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | SPACESHIP MODEL | 15.00 | | AIRCRAFT MODEL | 10.00 | +-----------------+-------+ 2 rows in set (0.000 sec) أصبح الاسم الآن SPACESHIP MODEL بالإضافة إلى تحديث السعر إلى 15.00 باستخدام التعليمة المُنفَّذة. إذا شغّلنا تعليمة UPDATE، فسيُنفَّذ القادح، مما يؤثر على القيم الموجودة في الصف المُحدَّث، مع تحويل عمود الاسم إلى حروف كبيرة قبل الحفظ. أنشأنا في هذا القسم قادحين Triggersيعملان قبل استعلامات INSERT وقبل استعلامات UPDATE لجعل البيانات ملائمةً قبل حفظها في قاعدة البيانات، وسنستخدم في القسم التالي محفّزات BEFORE DELETE لنسخ الصفوف المحذوفة في جدول منفصل للأرشفة. استخدام قوادح BEFORE DELETE قد نرغب في أرشفة البيانات بدلاً من حذفها نهائيًا من قاعدة البيانات، خاصةً إذا كنا بحاجة للاحتفاظ بسجل لهذه البيانات في المستقبل، فإذ أنشأنا في بداية هذا المقال جدول آخر باسم collectibles_archive لتعقّب جميع الهدايا التذكارية المحذوفة من المجموعة وأرشفتها. سنستخدم قادح يُنفَّذ قبل تنفيذ تعليمات الحذف DELETE. نتحقّق أولًا مما إذا كان جدول الأرشيف فارغًا بالكامل من خلال تنفيذ التعليمة التالية: mysql> SELECT * FROM collectibles_archive; وسيظهر الخرج التالي، مما يؤكد أن الجدول collectibles_archive فارغ: Empty set (0.000 sec) إذا نفّذنا استعلام DELETE مع الجدول collectibles، فيمكن حذف أيّ صفٍ من الجدول دون أيّ أثر. يمكن معالجة ذلك من خلال إنشاء قادح يُنفَّذ قبل جميع استعلامات DELETE مع الجدول collectibles، والغرض من هذا القادح هو حفظ نسخة من الكائن المحذوف في جدول الأرشيف قبل حدوث الحذف. لنشغّل الآن الأمر التالي: mysql> CREATE TRIGGER archive_before_delete BEFORE DELETE mysql> ON collectibles mysql> FOR EACH ROW mysql> INSERT INTO collectibles_archive (name, value) VALUES (OLD.name, OLD.value); يُسمَّى هذا القادح باسم archive_before_delete ويحدث قبل أيّ استعلامات DELETE مع الجدول collectibles. ستُنفَّذ تعليمة INSERT لكل صفٍ سيُحذف، بينما تدرج تعليمة INSERT صفًا جديدًا في الجدول collectibles_archive مع قيم البيانات المأخوذة من السجل OLD، وهو السجل المقرّر حذفه، حيث يصبح OLD.name هو العمود name ويصبح OLD.value العمود value. وتؤكد قاعدة البيانات إنشاء هذا القادح كما يلي: Query OK, 0 row affected (0.009 sec) نحاول حذف إحدى الهدايا التذكارية من الجدول collectibles الرئيسي مع استخدام القادح كما يلي: mysql> DELETE FROM collectibles WHERE name = 'SPACESHIP MODEL'; ويؤكّد الخرج التالي نجاح تشغيل الاستعلام السابق: Query OK, 1 row affected (0.004 sec) لنسرد الآن جميع الهدايا التذكارية كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +----------------+-------+ | name | value | +----------------+-------+ | AIRCRAFT MODEL | 10.00 | +----------------+-------+ 1 row in set (0.000 sec) حذفنا SPACESHIP MODEL ولم يَعُد موجودًا في الجدول مع بقاء AIRCRAFT MODEL، ولكن يجب تسجيل هذا الحذف في الجدول collectibles_archive باستخدام القادح الذي أنشأناه مسبقًا، إذًا لنتحقق من ذلك، ولننفّذ استعلامًا آخر كما يلي: mysql> SELECT * FROM collectibles_archive; وسيظهر الخرج التالي: +-----------------+-------+---------------------+ | name | value | removed_on | +-----------------+-------+---------------------+ | SPACESHIP MODEL | 15.00 | 2022-11-20 11:32:01 | +-----------------+-------+---------------------+ 1 row in set (0.000 sec) لاحظ القادح عملية الحذف في هذا الجدول تلقائيًا، مع تعبئة أعمدة name و value بالبيانات من الصف المحذوف، ولم يضبط القادح العمود الثالث removed_on صراحةً، لذلك سيأخذ القيمة الافتراضية المُحدَّدة أثناء إنشاء الجدول، أي تاريخ إنشاء أيّ صف جديد، مما يؤدي دائمًا إلى إضافة تعليق توضيحي لكل إدخال مُضاف بمساعدة القادح مع تاريخ الحذف. يمكننا الآن مع وجود هذا القادح التأكد من أن جميع استعلامات DELETE ستؤدي إلى إدخال سجلٍ في الجدول collectibles_archive مع ترك معلومات حول الهدايا التذكارية المملوكة مسبقًا. سنستخدم في القسم التالي القوادح التي تُنفَّذ بعد تعليمات تحديث الجدول الذي يحتوي على القيم المُجمَّعة بناءً على جميع الهدايا التذكارية. استخدام محفزات AFTER INSERT و AFTER UPDATE و AFTER DELETE استخدمنا في القسمين السابقين القوادح المُنفَّذة قبل التعليمات الرئيسية لتنفيذ العمليات المستندة إلى البيانات الأصلية قبل تحديث قاعدة البيانات، وسنحدّث في هذا القسم جدول البيانات التلخيصية بالعدد والقيمة المتراكمة لجميع الهدايا التذكارية المحدَّثة دائمًا باستخدام القوادح المُنفَّذة بعد التعليمات المطلوبة، وبذلك سنكون متأكدين من أن بيانات الجدول تأخذ في الحسبان الحالة الحالية لقاعدة البيانات. لنبدأ الآن بفحص الجدول collectibles_stats كما يلي: mysql> SELECT * FROM collectibles_stats; لم نضف معلومات إلى هذا الجدول بعد، لذا يكون عدد عناصر الهدايا التذكارية المملوكة هو 0، والقيمة التراكمية هي NULL: +-------+-------+ | count | value | +-------+-------+ | 0 | NULL | +-------+-------+ 1 row in set (0.000 sec) لا توجد قوادح لهذا الجدول، وبالتالي لم تؤثر الاستعلامات الصادرة مسبقًا لإدراج الهدايا التذكارية وتحديثها على هذا الجدول. نريد ضبط القيم في صفٍ واحد من الجدول collectibles_stats لتقديم معلومات مُحدَّثة حول عدد الهدايا التذكارية وقيمتها الإجمالية، حيث نتأكد من تحديث محتويات الجدول بعد كل عملية إدراج INSERT أو تحديث UPDATE أو حذف DELETE من خلال إنشاء ثلاثة قوادح منفصلة وتنفيذها بعد الاستعلام المقابل لها. لننشئ أولًا القادح AFTER INSERT: mysql> CREATE TRIGGER stats_after_insert AFTER INSERT mysql> ON collectibles mysql> FOR EACH ROW mysql> UPDATE collectibles_stats mysql> SET count = ( mysql> SELECT COUNT(name) FROM collectibles mysql> ), value = ( mysql> SELECT SUM(value) FROM collectibles mysql> ); سمّينا القادح بالاسم stats_after_insert وسيُنفَّذ بعد AFTER كل استعلام INSERT في الجدول collectibles مع تشغيل تعليمة UPDATE في جسم القادح. يؤثر استعلام UPDATE على جدول البيانات التلخيصية ويضبط العمودين count و value على القيم التي تعيدها الاستعلامات المتداخلة كما يلي: SELECT COUNT(name) FROM collectibles: يحصل على عدد الهدايا التذكارية SELECT SUM(value) FROM collectibles: يحصل على القيمة الإجمالية لجميع الهدايا التذكارية وتؤكّد قاعدة البيانات إنشاء القادح كما يلي: Query OK, 0 row affected (0.009 sec) نجرّب الآن إعادة إدراج spaceship model المحذوف مسبقًا في الجدول collectibles للتحقق من التحديث الصحيح لجدول البيانات التلخيصية كما يلي: mysql> INSERT INTO collectibles VALUES ('spaceship model', 15.00); وتطبع قاعدة البيانات رسالة النجاح التالية: Query OK, 1 row affected (0.009 sec) يمكن سرد جميع الهدايا التذكارية المملوكة باستخدام الاستعلام التالي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | AIRCRAFT MODEL | 10.00 | | SPACESHIP MODEL | 15.00 | +-----------------+-------+ 2 rows in set (0.000 sec) يوجد نوعان من عناصر الهدايا التذكارية التي تبلغ قيمتها الإجمالية 25.00. لنفحص الآن جدول البيانات التلخيصية بعد العنصر الذي أدرجناه حديثًا من خلال تنفيذ الاستعلام التالي: mysql> SELECT * FROM collectibles_stats; سيسرد الجدول هذه المرة عدد جميع عناصر الهدايا التذكارية المملوكة التي هي 2 وقيمتها التراكمية 25.00، والتي تطابق الخرج السابق: +-------+-------+ | count | value | +-------+-------+ | 2 | 25.00 | +-------+-------+ 1 row in set (0.000 sec) يُنفَّذ القادح stats_after_insert بعد استعلام INSERT ويحدّث الجدول collectibles_stats بالبيانات الحالية count و value للمجموعة، وتُجمَع إحصائيات محتويات المجموعة بأكملها، وليس الإدخال الأخير فقط. تحتوي المجموعة الآن على عنصرين spaceship model و aircraft model، لذا يسرد جدول البيانات التلخيصية عنصرين مع قيمتهما الإجمالية، وبالتالي ستؤدي إضافة أيّ عنصر هدية تذكارية جديد إلى الجدول collectibles إلى تحديث جدول البيانات التلخيصية بالقيم الصحيحة، ولكن لن يؤثر تحديث العناصر الموجودة أو حذف الهدايا التذكارية على جدول البيانات التلخيصية أبدًا، لذا سننشئ قادحين إضافيين لإجراء عمليات متطابقة ولكن تحفّزها أحداث مختلفة كما يلي: mysql> CREATE TRIGGER stats_after_update AFTER UPDATE mysql> ON collectibles mysql> FOR EACH ROW mysql> UPDATE collectibles_stats mysql> SET count = ( mysql> SELECT COUNT(name) FROM collectibles mysql> ), value = ( mysql> SELECT SUM(value) FROM collectibles mysql> ); mysql> mysql> CREATE TRIGGER stats_after_delete AFTER DELETE mysql> ON collectibles mysql> FOR EACH ROW mysql> UPDATE collectibles_stats mysql> SET count = ( mysql> SELECT COUNT(name) FROM collectibles mysql> ), value = ( mysql> SELECT SUM(value) FROM collectibles mysql> ); أنشأنا قادحين جديدين هما: stats_after_update و stats_after_delete، وسيُنفّذان مع الجدول collectible_stats عند تنفيذ تعليمة UPDATE أو DELETE في الجدول collectibles. يؤدي الإنشاء الناجح لهذين القادحين إلى طباعة الخرج التالي: Query OK, 0 row affected (0.009 sec) لنحدّث الآن قيمة السعر لإحدى الهدايا التذكارية كما يلي: mysql> UPDATE collectibles SET value = 25.00 WHERE name = 'AIRCRAFT MODEL'; ترشّح تعليمة WHERE الصف المراد تحديثه حسب الاسم، وتغيّر تعليمة SET القيمة إلى 25.00. يؤكّد الخرج التالي أن التعليمة غيّرت صفًا واحدًا: Query OK, 1 row affected (0.002 sec) Rows matched: 1 Changed: 1 Warnings: 0 لنتحقق مرة أخرى من محتويات جدول البيانات التلخيصية بعد التحديث كما يلي: mysql> SELECT * FROM collectibles_stats; وسيسرد العمود value الآن القيمة 40.00، وهي القيمة الصحيحة بعد التحديث: +-------+-------+ | count | value | +-------+-------+ | 2 | 40.00 | +-------+-------+ 1 row in set (0.000 sec) الخطوة الأخيرة هي التحقق من أن جدول البيانات التلخيصية سيظهِر حذف إحدى الهدايا التذكارية بطريقة صحيحة، فلنحاول حذف العنصر aircraft model كما يلي: mysql> DELETE FROM collectibles WHERE name = 'AIRCRAFT MODEL'; ويؤكد الخرج التالي تشغيل الاستعلام بنجاح: Query OK, 1 row affected (0.004 sec) لنسرد الآن جميع الهدايا التذكارية كما يلي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | SPACESHIP MODEL | 15.00 | +-----------------+-------+ 1 row in set (0.000 sec) لاحظ بقاء العنصر SPACESHIP MODEL فقط. لنتحقق الآن من القيم الموجودة في جدول البيانات التلخيصية كما يلي: mysql> SELECT * FROM collectibles_stats; وسيظهر الخرج التالي: +-------+-------+ | count | value | +-------+-------+ | 1 | 15.00 | +-------+-------+ 1 row in set (0.000 sec) يعرض العمود count الآن هدية تذكارية واحدة فقط في الجدول الرئيسي، والقيمة الإجمالية هي 15.00، وهي مطابقة لقيمة العنصر SPACESHIP MODEL. تعمل المحفّزات الثلاثة السابقة مع بعضها البعض بعد استعلامات INSERT و UPDATE و DELETE للحفاظ على مزامنة جدول الملخص مع القائمة الكاملة للهدايا التذكارية. سنتعلم في القسم التالي كيفية معالجة القوادح الموجودة مسبقًا مع قاعدة البيانات. سرد وحذف القوادح Triggers أنشأنا في الأقسام السابقة قوادح جديدة، ولكن يمكنك أيضًا سردها ومعالجتها عند الحاجة لأنها كائنات مسمَّاة ومُعرَّفة في قاعدة البيانات مثل الجداول، حيث يمكنك سرد جميع القوادح من خلال تنفيذ التعليمة SHOW TRIGGERS كما يلي: mysql> SHOW TRIGGERS; وسيتضمن الخرج جميع القوادح مع أسمائها وحدث التحفيز مع الوقت قبل BEFORE أو بعد AFTER تنفيذ التعليمة، بالإضافة إلى التعليمات التي تشكّل جزءًا من جسم القادح والتفاصيل الشاملة الأخرى لتعريفه كما يلي: +-------------------------+--------+--------------+--------(...)+--------+(...) | Trigger | Event | Table | Statement | Timing |(...) +-------------------------+--------+--------------+--------(...)+--------+(...) | uppercase_before_insert | INSERT | collectibles | SET (...)| BEFORE |(...) | stats_after_insert | INSERT | collectibles | UPDATE (...)| AFTER |(...) | uppercase_before_update | UPDATE | collectibles | SET (...)| BEFORE |(...) | stats_after_update | UPDATE | collectibles | UPDATE (...)| AFTER |(...) | archive_before_delete | DELETE | collectibles | INSERT (...)| BEFORE |(...) | stats_after_delete | DELETE | collectibles | UPDATE (...)| AFTER |(...) +-------------------------+--------+--------------+--------(...)+--------+(...) 6 rows in set (0.001 sec) يمكن حذف القوادح الموجودة مسبقًا من خلال استخدام تعليمات SQL التي هي DROP TRIGGER، فمثلًا إن لم نعد نرغب بفرض الحروف الكبيرة على أسماء الهدايا التذكارية، ولم نعد بحاجة إلى القادحين uppercase_before_insert و uppercase_before_update، فيمكننا إزالتهما من خلال تنفيذ الأمرين التالية: mysql> DROP TRIGGER uppercase_before_insert; mysql> DROP TRIGGER uppercase_before_update; ويستجيب MySQL برسالة النجاح التالية: Query OK, 0 rows affected (0.004 sec) لنجرّب الآن إضافة هدية تذكارية جديدة بحروف صغيرة كما يلي: mysql> INSERT INTO collectibles VALUES ('ship model', 10.00); وستؤكد قاعدة البيانات ذلك كما يلي: Query OK, 1 row affected (0.009 sec) يمكننا التحقق من إدراج الصف من خلال تنفيذ استعلام SELECT التالي: mysql> SELECT * FROM collectibles; وسيظهر الخرج التالي: +-----------------+-------+ | name | value | +-----------------+-------+ | SPACESHIP MODEL | 15.00 | | ship model | 10.00 | +-----------------+-------+ 2 rows in set (0.000 sec) نلاحظ كتابة الهدية التذكارية المضافة حديثًا بحروف صغيرة، إذًا لم يتغيّر الاسم عن الخرج الأصلي، وبالتالي تأكّدنا من أن القادح الذي حوّل حالة الحروف سابقًا لم يَعُد قيد الاستخدام. الخلاصة تعلمنا في هذا المقال ما هي القوادح Triggers في SQL وكيفية استخدامها في MySQL لمعالجة البيانات قبل استعلامات INSERT و UPDATE، وتعلّمنا كيفية استخدام قادح BEFORE DELETE لأرشفة الصف المحذوف في جدول منفصل، بالإضافة إلى استخدام قوادح AFTER بعد التعليمات لإبقاء جدول البيانات التلخيصية مُحدَّثة باستمرار. يمكننا استخدام الدوال لتفريغ بعض عمليات معالجة البيانات والتحقق من صحتها في محرّك قاعدة البيانات، مما يضمن سلامة البيانات أو إخفاء بعض سلوكيات قاعدة البيانات عن المستخدم الذي يستخدم قاعدة البيانات يوميًا، وقد غطّى هذا المقال أساسيات استخدام القوادح لهذا الغرض فقط، ولكن يمكنك أيضًا إنشاء قوادح معقدة تتكوّن من تعليمات متعددة واستخدام المنطق الشرطي لتنفيذ الإجراءات بدقة أكبر. ويمكن مطالعة توثيق MySQL الخاص بالتعامل مع triggers لمزيد من المعلومات. ترجمة -وبتصرف- للمقال How To Use Triggers in MySQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: استخدام الدوال Functions في لغة SQL التعامل مع قواعد البيانات مدخل إلى تصميم قواعد البيانات استخدام الدوال Functions في لغة SQL استخدام العروض Views في لغة الاستعلام البنيوية SQL
×
×
  • أضف...