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

التعامل مع قواعد البيانات


أسامة دمراني

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

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

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

سننظر في هذا المقال من سلسلة تعلم البرمجة في كيفية تخزين البيانات ومعالجتها من خلال حزمة قاعدة البيانات، وقد رأينا سابقًا كيفية استخدام الملفات في تخزين كميات صغيرة من البيانات في برنامج دليل جهات الاتصال الذي مررنا عليه عدة مرات من قبل، غير أن استخدام الملفات يزداد تعقيدًا مع زيادة تعقيد البيانات نفسها، وزيادة حجمها، وتعقيد العمليات التي تُجرى عليها، مثل الفرز والبحث والترشيح filtering وغير ذلك، وتوجد عدة حزم لقواعد البيانات للعناية بإدارة الملفات، وكشف البيانات في شكل أكثر تجريدًا ويسهل التعديل عليه، بعضها عبارة عن مكتبات للشيفرات code libraries تبسط عمليات الملفات التي رأيناها من قبل مثل وحدتي pickle و shelve اللتين تأتيان مع بايثون، لكننا في هذا المقال سندرس حزمًا أكثر قوةً صممت للتعامل مع أحجام كبيرة من البيانات المعقدة، حيث سنشرح الحزمة SQLite، وهي حزمة مجانية مفتوحة المصدر وسهلة التثبيت والاستخدام، ومع هذا فهي قادرة على معالجة حاجات أغلب المبرمجين المبتدئين والمتوسطين أيضًا، ولا يحتاج المبرمج في الغالب إلى حزمة أقوى منها إلا إذا كان يتعامل مع مجموعات كبيرة للغاية من البيانات -ملايين السجلات مثلًا-، وحتى في تلك الحالة نستطيع نقل ما تعلمناه من SQLite إلى الحزمة الجديدة.

يمكن تحميل حزمة SQLite من موقعها، فاختر حزمة سطر الأوامر -أي الأدوات- المناسبة لمنصتك، وبعد التحميل اتبع إرشادات التثبيت الموجودة في الموقع لتثبيت الحزمة.

توجد عدة بيئات تطوير لـ SQLite، غير أننا لا نحتاجها في هذه السلسلة.

سندرس في هذا المقال ما يلي:

  • مفهوم قواعد البيانات وSQL.
  • إنشاء الجداول وإدخال البيانات.
  • استخراج البيانات والتعديل عليها.
  • ربط مجموعات البيانات بعضها ببعض.
  • الوصول إلى SQL من بايثون.

مفاهيم قاعدة البيانات العلائقية

يمكن وصف قواعد البيانات العلائقية relational databases بأنها مجموعة من الجداول، حيث يمكن لخلية في جدول فيها أن تشير إلى صف في جدول آخر، وتسمى الأعمدة في تلك الجداول بالحقول fields، كما تسمى الصفوف بالسجلات records.

سيبدو جدول بيانات الموظفين في إحدى الشركات كما يلي:

EmpID Name HireDate Grade ManagerID
1020304 Hasan Saleh 20030623 Foreman 1020311
1020305 Amin Akbar 20040302 Labourer 1020304
1020307 Ayat Othman 19991125 Labourer 1020304

نلاحظ بعض المصطلحات هنا:

  1. لدينا حقل معرِّف ID فريد يعرف كل صف، ويُعرف باسم المفتاح الأساسي primary key، ويمكن أن يكون لدينا عدة مفاتيح أخرى، لكن سيكون لدينا حقل ID دومًا لتعريف سجل ما، وهذا مفيد إذا كان لموظفين اثنين نفس الاسم مثلًا.
  2. يمكن ربط صف بآخر بأن يحمل حقلٌ ما المفتاحَ الأساسي لصف آخر، فمثلًا يُحدَّد مدير الموظف بواسطة معرف المدير ManagerID الذي هو مرجع إلى حقل EmpID آخر، ويتضح بالنظر إلى البيانات التي لدينا أن لكل من Amin وAyat المدير نفسه وهو Hasan، و Hasan هذا له مدير آخر لكننا لا نرى بياناته في هذا الجزء من الجدول.

ويمكن إنشاء جدول آخر للرواتب Salary مثلًا، فلسنا مقيدين بربط البيانات داخل جدول واحد، ويُربط هذا بالدرجة Grade الخاصة بكل موظف، فنحصل عندها على جدول شبيه بما يلي:

SalaryID Grade Amount
000010 Foreman 60000
000011 Labourer 35000

نستطيع الآن أن نبحث عن درجة موظف ما مثل Hasan، وسنجد أن درجته هي كبير عمال Foreman، وإذا بحثنا في جدول Salary فسنجد أن راتب هذه الدرجة هو 60000$، وإمكانية ربط صفوف الجداول معًا في علاقات هي التي تعطي قواعد البيانات العلائقية اسمها.

توجد قواعد بيانات أخرى مثل قواعد بيانات الشبكات network databases، وقواعد البيانات الهرمية hierarchical databases، وقواعد بيانات الملفات المسطحة flat-file databases، ولكن القواعد العلائقية هي أكثرها شهرةً، رغم أن الاتجاه السائد الآن في معالجة الأحجام الهائلة من البيانات هو NoSQL -أي ليست SQL فقط "Not only SQL"-، وهي قواعد بيانات تبنى في الغالب على هياكل شبكية أو هرمية.

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

لغة الاستعلامات الهيكلية SQL

لغة الاستعلامات الهيكلية أو Structured Query Language - SQL هي أداة قياسية تُستخدم لتعديل قواعد البيانات العلائقية، ويسمى التعبير فيها عادةً استعلامًا query حتى لو لم يجلب أي بيانات، وتتكون SQL من جزأين هما: لغة تعريف البيانات Data Definition Language واختصارًا DDL، وهي مجموعة من الأوامر التي تُستخدم لإنشاء وتعديل هيكل قاعدة البيانات نفسها، وتكون عادةً خاصةً لكل قاعدة بيانات، ويوفر كل مزود قواعد بيانات صيغةً مختلفةً قليلًا لمجموعة أوامر SQL الخاصة بتعريف البيانات، أما الجزء الآخر فهو لغة تعديل البيانات Data Manipulation Language، واختصارًا DML، وهي قياسية أكثر بين قواعد البيانات، وتُستخدم لتعديل محتويات البيانات، وهي التي سنستخدمها غالبًا في التعامل مع قواعد البيانات، لذا سنتعلم بعض أوامر DDL التي تكفينا لإنشاء قاعدة بياناتنا باستخدام التعليمة CREATE، وتدمير جداولها باستخدام التعليمة DROP، ثم ننتقل بعدها إلى ملء الجداول بالبيانات، ثم نجلب تلك البيانات بطرق مختلفة باستخدام أوامر DML مثل INSERT وSELECT وUPDATE وDELETE وغيرها.

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

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

إنشاء الجداول

نستخدم الأمر CREATE لإنشاء جدول في SQL، وهو أمر سهل ويأخذ الصورة التالية:

CREATE TABLE tablename (fieldName, fieldName,....);

نلاحظ أن تعليمات SQL تنتهي بفاصلة منقوطة ;، وهي لا تهتم بمستويات الإزاحة أو المسافات البيضاء، لكننا سنرى اتباع اصطلاح بعينه -مع أنه ليس لازمًا، ولا تهتم SQL له إطلاقًا- ولا نستخدمه إلا لاتساق سير العمل.

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

E:\PROJECTS\SQL> sqlite3 employee.db

سينشئ هذا قاعدة بيانات فارغةً اسمها employee.db، ويتركنا عند محث sqlite>‎ لنكتب أوامر SQL، ثم ننشئ بعض الجداول كما يلي:

sqlite> CREATE TABLE Employee
   ...> (EmpID,Name,HireDate,Grade,ManagerID);
sqlite> CREATE TABLE Salary
   ...> (SalaryID, Grade,Amount);
sqlite>.tables
Employee    Salary
sqlite>

لاحظ أننا نقلنا قائمة الحقول إلى سطر منفصل لتسهيل رؤيتها، وتُرتَّب الحقول هنا بالاسم، وليس لها أي معلومات تعرِّفها -مثل نوع البيانات- وهذا في SQLite وحدها، إذ تطلب أغلب قواعد البيانات الأخرى تحديد النوع مع الاسم، ويمكن تحديد النوع في SQLite، كما سنرى بعد قليل.

ونلاحظ أيضًا أننا تحققنا من عمل تعليمات CREATE باستخدام الأمر ‎.tables لسرد جميع الجداول في قاعدة البيانات، وتحتوي SQLite على العديد من هذه الأوامر المنقوطة، والتي نستخدمها لجلب معلومات عن قاعدة البيانات، وللحصول على قائمة بتلك الأوامر يُستخدم الأمر ‎.help.

يمكننا تحديد قيود على القيم، بالإضافة إلى التصريح عن أنواع البيانات في كل عمود، فمثلًا NOT NULL تعني أن القيمة إلزامية ويجب ملؤها، ونجعل عادةً حقل المفتاح الأساسي غير خالٍ NOT NULL وفريدًا UNIQUE، كما نستطيع تحديد الحقل الذي سيكون المفتاح الأساسي PRIMARY KEY.

سنترك تعريف الجدول الأساسي كما هو، وننتقل إلى التعديل في البيانات نفسها.

إدخال البيانات

أول ما نفعله بعد إنشاء الجداول هو ملؤها بالبيانات، وذلك باستخدام تعليمة INSERT في SQL، والتي لها هيكل أساسي بسيط هو:

INSERT INTO tablename ( column1, column2... ) VALUES ( value1, value2... );

كما توجد لها صيغة أخرى تستخدم استعلامًا لاختيار البيانات من مكان آخر في قاعدة البيانات، لكن هذا مستوىً متقدم ننصح بالقراءة عنه في دليل SQLite.

يمكننا أن ندخِل بعض الصفوف في جدول موظفينا كما يلي:

sqlite> INSERT INTO Employee (EmpID, Name, HireDate, Grade, ManagerID)
   ...> VALUES ('1020304','Hasan Saleh','20030623','Foreman','1020311');
sqlite> INSERT INTO Employee (EmpID, Name, HireDate, Grade, ManagerID)
   ...> VALUES ('1020305','Amin Akbar','20040302','Labourer','1020304');
sqlite> INSERT INTO Employee (EmpID, Name, HireDate, Grade, ManagerID)
   ...> VALUES ('1020307','Ayat Othman','19991125','Labourer','1020304');

وكذلك في جدول الرواتب:

sqlite> INSERT INTO Salary (SalaryID, Grade,Amount)
   ...> VALUES('000010','Foreman','60000');
sqlite> INSERT INTO Salary (SalaryID, Grade,Amount)
   ...> VALUES('000011','Labourer','35000');

وبهذا نكون قد انتهينا من إنشاء جدولين وملئهما بالبيانات الموافقة للقيم الموصوفة في المقدمة أعلاه، وسننتقل الآن إلى إجراء بعض التجارب على البيانات.

استخراج البيانات

تُستخرج البيانات من قاعدة البيانات باستخدام الأمر SELECT في SQL، والذي هو لب SQLite، وهو أعقد الأوامر هيكلًا، لذا سنبدأ بأبسط صورة ثم نضيف مزايا جديدةً أثناء العمل.

ستبدو أبسط صورة ممكنة لتعليمة SELECT كما يلي:

SELECT column1, column2... FROM table1,table2...;

فلاختيار أسماء جميع العاملين نستخدم:

sqlite> SELECT Name FROM Employee;

حيث سنحصل على قائمة بجميع الأسماء في جدول الموظفين، وهي ثلاثة أسماء في حالتنا، لكن إذا كان لدينا قاعدة بيانات كبيرة فسنحصل على معلومات أكثر مما نريد، وسنحتاج إلى تحسين بحثنا بطريقة ما للتحكم في الخرج، وتسمح لنا SQL بفعل ذلك بإضافة الشرط WHERE إلى تعليمة SELECT، كما يلي:

SELECT col1,col2... FROM table1,table2... WHERE condition;

حيث الشرط condition هو تعبير بولياني معقد وعشوائي، ويمكن أن يتضمن تعليمات SELECT متشعبةً داخله، لنستخدم الشرط WHERE لتحسين بحث الأسماء، حيث نريد البحث عن أسماء الموظفين العمال فقط، أي أصحاب الدرجة labourer.

sqlite> SELECT Name 
   ...> FROM Employee
   ...> WHERE Employee.Grade = 'Labourer';

سنحصل الآن على اسمين فقط، ونستطيع توسيع الشرط باستخدام معامِلات بوليانية مثل AND وOR وNOT وغيرها، لاحظ أن استخدام الشرط = في حالة السلسلة النصية مهم، فلم يكن البحث عن labourer لينجح لولاه، وسنرى كيفية حل هذه المشكلة لاحقًا.

كما نلاحظ أننا استخدمنا الصيغة النقطية dot notation في شرط WHERE لإبراز حقل Grade، ولم يكن ذلك ضروريًا في هذه الحالة لأننا نعمل مع جدول واحد، لكن عند وجود عدة جداول محددة فسنحتاج إلى توضيح الجدول الذي ينتمي إليه الحقل، فمثلًا لنغير استعلامنا ليبحث عن أسماء جميع الموظفين الذين يحصلون على راتب أكثر من 50000$، حيث سنحتاج إلى النظر في بيانات كلا الجدولين:

sqlite> SELECT Name, Amount FROM Employee, Salary
   ...> WHERE  Employee.Grade = Salary.Grade
   ...> AND    Salary.Amount > '50000';

نلاحظ استخدام المسافات البيضاء لترتيب شكل الاستعلام، وقد وضعنا شرط FROM هذه المرة في السطر الأول، وهذا أمر تنسيقي بحت يُستخدم لتحسين القراءة، فلغة SQLite لا تهتم بالفراغات.

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

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

لقد حدَّدنا اثنين من أسماء الحقول الفريدة، فإذا أردنا عرض الدرجة الوظيفية Grade التي تظهر في كلا الجدولين، لكنا استخدمنا الصيغة النقطية لتحديد الجدول الذي نريده، كما يلي:

sqlite> SELECT Employee.Grade, Name, Amount 
   ...> FROM Employee, Salary
   etc/...

آخر ما نريد الحديث عنه من مزايا SELECT هي القدرة على تصنيف الخرج، رغم وجود عدة مزايا أخرى يمكن الرجوع إليها في توثيق SQL، فقواعد البيانات تحتفظ بالبيانات بالترتيب الذي يسهل به إيجادها أو بالترتيب الذي أُدخلت به، وفي كلا الحالتين لا يكون هو الترتيب الذي نريد عرض البيانات به، لذا نستخدم الشرط ORDER BY الخاص بتعليمة SELECT لحل هذه المشكلة:

SELECT columns FROM tables WHERE expression ORDER BY columns;

نلاحظ أن شرط ORDER BY الأخير قد يأخذ عدة أعمدة، وهذا يمكننا من الحصول على طلبات الفرز والتصنيف الأولية والثانوية، لنستخدم ذلك الآن للحصول على قائمة بأسماء الموظفين مصنفة وفق تاريخ التوظيف HireDate:

sqlite> SELECT Name FROM Employee
   ...> ORDER BY HireDate;

لم يبقَ إلا ذكر أننا لم نستخدم شرط WHERE هنا، فإذا استخدمناه فسيأتي قبل شرط order by، لذا ورغم أن SQL لا تمانع إذا أهملنا الشرط إلا أنها تدقق كثيرًا في ترتيب الشروط داخل التعليمة.

تعديل البيانات

توجد طريقتان لتعديل البيانات في قواعد البيانات، إما بتغيير محتويات سجل واحد أو مجموعة سجلات، أو بحذف السجلات أو الجدول كاملًا، والحالة الأشهر هي تغيير محتويات سجل موجود بالفعل من خلال الأمر UPDATE في SQL، وأبسط صورة هي:

UPDATE tablename SET column = value WHERE condition;

نستطيع تجربة ذلك في قاعدة البيانات التي لدينا بتغيير راتب كبير العمال foreman إلى 70000$:

sqlite> UPDATE Salary
   ...> SET Amount ='70000'
   ...> WHERE Grade = 'Foreman';

لاحظ أن جميع البيانات التي أدخلناها واخترناها كانت أنواعًا نصيةً string types، لأن SQLite تخزن بياناتها داخليًا في سلاسل نصية، لكنها تدعم عدة أنواع مختلفة من البيانات بما فيها الأعداد، لذا كان بالإمكان تحديد الراتب في صيغة رقمية لتسهيل العمليات الحسابية، وسنرى كيفية فعل ذلك فيما يلي.

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

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

DELETE FROM Tablename WHERE condition

فإذا أردنا حذف Ayat Othman من جدول الموظفين فسنكتب ما يلي:

sqlite> DELETE FROM Employee WHERE Name = 'Ayat Othman';

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

أما لحذف الجدول كله بمحتوياته فنستخدم الأمر DROP، ويجب توخي الحذر الشديد عند استخدام مثل هذه الأوامر التدميرية مثل DELETE وDROP لما لها من آثار قد لا يمكن إصلاحها.

ربط البيانات بين الجداول

تحدثنا عن ربط البيانات بين الجداول من قبل في القسم الخاص بتعليمة SELECT، أما الآن فننتقل إلى جزء مهم في نظرية قواعد البيانات.

قيود البيانات Data Constraints

تمثل الروابط بين الجداول علاقات Relations بين وحدات البيانات التي تعطي قاعدة البيانات العلائقية -مثل SQLite- اسمها، وتحتفظ قاعدة البيانات بالبيانات الخام عن الكيانات، بل تحتفظ بمعلومات عن العلاقات بينها أيضًا.

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

CREATE TABLE Tablename (Column, Column,...);

إلى:

CREATE TABLE Tablename (
ColumnName Type Constraint,
ColumnName Type Constraint,
...);

حيث أغلب القيود:

NOT NULL 
PRIMARY KEY [AUTOINCREMENT] 
UNIQUE 
DEFAULT value 

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

أما المفتاح الرئيسي PRIMARY KEY فيخبر SQLite أن تستخدم هذا العمود مفتاحًا رئيسيًا لعمليات البحث، مما يُحسّن تنفيذ عمليات بحث أسرع.

كما تعني AUTOINCREMENT أن قيمة النوع هي INTEGER ستُسنَد تلقائيًا عند كل عملية إدخال INSERT، وتتزايد القيمة بمقدار واحد، مما يوفر على المبرمج كثيرًا من حيث المحافطة على أعداد مستقلة، ولا تُستخدم الكلمة المفتاحية AUTOINCREMENT حقيقةً، وإنما تكون مضمنةً في تجميعة نوع/قيد بدمج INTEGER PRIMARY KEY، وهذه خاصية غير واضحة في توثيق SQLite إلى الحد الذي يجعلها من أبرز الأسئلة في الأسئلة الشائعة حول SQLite.

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

يصاحب القيد DEFAULT قيمة دومًا، وهذه القيمة هي التي تخبرنا بما ستدخِله SQLite في ذلك الحقل إذا لم يقم المستخدم بذلك صراحةً، وهذا يفيد في أن الأعمدة التي لها القيد DEFAULT لا تكون NULL إلا نادرًا، لأن علينا ضبط القيمة NULL بصراحة إذا أردنا إنشاءها، ونستطيع رؤية مثال سريع على استخدام DEFAULT هنا:

sqlite> CREATE TABLE test
   ...> (id Integer PRIMARY KEY,
   ...> Name NOT NULL,
   ...> Value Integer DEFAULT 42);
sqlite> INSERT INTO test (Name, Value) VALUES ('Alan',24);
sqlite> INSERT INTO test (Name) VALUES ('Heather');
sqlite> INSERT INTO test (Name,Value) VALUES ('Linda', NULL);
sqlite> SELECT * FROM test;
1|Alan|24
2|Heather|42
3|Linda|
sqlite>

نلاحظ هنا كيف تعينت القيمة الافتراضية لحقل value الموافقة للاسم المدخَل Heather، وأن قيمة value للمدخل Linda غير موجودة أو NULL، وهذا اختلاف جوهري بين NOT NULL وDEFAULT، فالأول لن يسمح بقيم NULL افتراضيًا أو صراحةً، والقيد DEFAULT يمنع NULL غير المحددة، لكنه يسمح في نفس الوقت بالإنشاء المتعمد لها.

لقد استخدمنا محرف النجمة * مكان قائمة الحقل في آخر تعليمة SELECT، وهذه طريقة بسيطة لجلب جميع الحقول في الجدول، وهي ممتازة لمثل هذه التجارب، لكن يجب ألا تُستخدم في البرامج العملية لأن أي تغيير في هيكل البيانات سيتسبب في تغيير النتائج أو تعطيل أي شيفرة تعتمد على عدد أو ترتيب الحقول التي طُلبت.

توجد قيود يمكن تطبيقها على الجدول نفسه، لكننا لن نناقشها في هذه السلسلة.

أما النوع الآخر من القيود الذي نستطيع تطبيقه كما ذكرنا من قبل فهو تحديد نوع العمود، وهو يشبه مفهوم الأنواع في لغة البرمجة، ومجموعة الأنواع الصالحة في SQLite هي ما يلي:

  • TEXT.
  • INTEGER.
  • REAL.
  • NUMERIC.
  • BLOB.
  • NULL.

يجب أن تكون هذه الأنواع مفهومةً وواضحةً، باستثناء NUMERIC وBLOB، فالأول يسمح بتخزين أعداد الفاصلة العائمة floating-point numbers والأعداد الصحيحة، أما BLOB فيُستخدم لتخزين البيانات الثنائية، مثل الصور أو المستندات غير النصية، فهو أفضل في تخزين مثل تلك العناصر في ملفات منفصلة مع وجود مرجع reference فقط يشير إليها في قاعدة البيانات.

أما NULL فهو ليس نوعًا حقيقيًا، وإنما يشير إلى أننا لا نحتاج إلى تحديد نوع مطلقًا، فأغلب قواعد البيانات تأتي بمجموعة واسعة من الأنواع بما فيها النوع DATE، لكن SQLite لديها أسلوب غير تقليدي في الأنواع التي تجعل مثل هذه التفاصيل غير مهمة.

اقتباس

أنشئ معيار SQL بواسطة لجنة شُكلت من جميع الجهات المزودة لقواعد البيانات، لذا فإن قائمة الأنواع تشمل جميع الأنواع المختلفة التي يسمح بها هؤلاء المزودون، والتي كانت موجودةً قبل وضع الأنواع القياسية، وتدعم SQLite العديد من تلك الأنواع على مستوى الصياغة syntactical، لكنها في الممارسة العملية تُسمى بأسلوب بديل alias لأفضل نوع مكافئ محلي، لذا فإن النوع VARCHAR في قاعدة Oracle مثلًا هو اسم بديل للنوع TEXT الخاص بـ SQLite، والفكرة هنا هي القدرة على استيراد سكربتات SQL إلى SQLite بأقل قدر ممكن من التغيير.

تطبق أغلب قواعد البيانات الأنواع المحددة بصرامة، لكن SQLite تتبع نهجًا أكثر ديناميكيةً ومرونةً، حيث يكون النوع المحدَّد أشبه بالتلميح أو الإرشاد hint، ويمكن تخزين أي نوع من البيانات في الجدول، وعند تحميل بيانات من نوع مختلف إلى الحقل فإن SQLite ستستخدم النوع المصرَّح عنه لتحاول تحويل البيانات إليه، فإن لم تستطع فستخزنها في صورتها الأصلية، فإذا صُرِّح عن حقل على أنه عددي INTEGER ثم مُررت القيمة النصية '123'، فستحول SQLite السلسلة النصية '123' إلى العدد 123، لكن إذا كانت القيمة النصية TEXT هي 'Amindy' فلن ينجح تحويلها إلى عدد، وستخزنها SQLite في صورتها كما هي في الحقل، وقد يسبب هذا سلوكًا غريبًا إذا لم يكن المبرمج على علم بهذا العيب، أما بقية قواعد البيانات فتعدّ التصريح عن الأنواع قيدًا صارمًا، وتفشل عند تمرير قيمة غير مسموح بها.

نمذجة العلاقات مع القيود

لنر الآن كيف تساعدنا هذه القيود في صنع نماذج للبيانات والعلاقات، من خلال العودة إلى قاعدة البيانات البسيطة ذات الجدولين التي بدأنا المقال بها:

  • الجدول 1:
EmpID Name HireDate Grade ManagerID
1020304 Hasan Saleh 20030623 Foreman 1020311
1020305 Amin Akbar 20040302 Labourer 1020304
1020307 Ayat Othman 19991125 Labourer 1020304
  • الجدول 2:
SalaryID Grade Amount
000010 Foreman 60000
000011 Labourer 35000

ينبغي أن يكون نوع قيمة المعرِّف ID عددًا صحيحًا، أي INTEGER، ويحمل القيد PRIMARY KEY، أما العمود الآخر فيجب أن يكون NOT NULL، باستثناء ManagerID الذي يجب أن يكون عددًا صحيحًا.

ونرى هنا في جدول الرواتب Salary أن معرِّف الراتب SalaryID يجب أن يكون عددًا صحيحًا INTEGER مع قيد PRIMARY KEY، كما يجب أن يكون عمود مقدار الراتب Amount عددًا صحيحًا، وسنطبق القيمة الافتراضية DEFAULT التي مقدارها 10000، وأخيرًا يجب أن يكون العمود Grade مقيدًا بقيد التفرد Unique بما أننا لا نريد أكثر من راتب واحد لكل درجة وظيفية، رغم أن هذه الفكرة غير عملية، لأن الراتب يتغير بعوامل عدة، مثل مدة العمل والدرجة، لكننا سنتجاهل هذا التفصيل الآن لتبسيط الشرح، فلو كان هذا الجدول في حالة حقيقية لسميناه جدول الدرجات وليس الرواتب.

ستبدو SQL المعدلة كما يلي:

sqlite> CREATE TABLE Employee (
   ...> EmpID INTEGER pRIMARY kEY,
   ...> Name NOT NULL,
   ...> HireDate NOT NULL,
   ...> Grade NOT NULL,
   ...> ManagerID INTEGER
   ...> );

sqlite> CREATE TABLE Salary (
   ...> SalaryID INTEGER PRIMARY KEY,
   ...> Grade  UNIQUE,
   ...> Amount INTEGER DEFAULT 10000
   ...> );

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

الأمر الذي تجب الإشارة إليه هنا هو أن تعليمات INSERT التي استخدمناها من قبل لم تعد مناسبةً، فقد أدخلنا قيمنا الخاصة من قبل لحقول ID، أما الآن فهي تُملأ تلقائيًا، فينبغي أن نهملها من البيانات المدرجة، غير أن هذا يفتح الباب لصعوبة جديدة، فكيف نملأ حقل معرِّف المدير managerID إذا كنا لا نعرف المعرف التوظيفي EmpID له؟

والإجابة هي أننا نستخدم تعليمة SELECT متشعبة، وقد رأينا أن ننفذ هذا على مرحلتين باستخدام حقول NULL أولًا، ثم استخدام تعليمة update بعد إنشاء جميع الصفوف، ولتجنب تكرار الكتابة قد وضعنا جميع الأوامر في بضعة ملفات، سميناها employee.sql لأوامر إنشاء الجداول، وemployee.dat لتعليمات الإدراج، وهذا يشبه إنشاء ملف سكربت بايثون ذي الامتداد ‎.py لتوفير كتابة كل الأوامر في محث ‎>>>‎.

سيكون ملف employee.sql كما يلي:

DROP TABLE IF EXISTS Employee;
CREATE TABLE Employee (
EmpID INTEGER PRIMARY KEY,
Name NOT NULL,
HireDate NOT NULL,
Grade NOT NULL,
ManagerID INTEGER
);

DROP TABLE IF EXISTS Salary;
CREATE TABLE Salary (
SalaryID INTEGER PRIMARY KEY,
Grade UNIQUE,
Amount INTEGER DEFAULT 10000
);

نلاحظ هنا أننا أسقطنا الجداول -أي حذفناها- قبل إنشائها، فأمر DROP TABLE الذي ذكرناه من قبل يحذف الجدول وأي بيانات موجودة فيه، وهذا يضمن أن قاعدة البيانات ستكون خاليةً نظيفةً قبل أن ننشئ جدولنا الجديد، كما أضفنا شرط IF EXISTS الذي يمنعنا من محاولة حذف جدول حذف سابقًا.

أما ملف employee.dat فسيكون كما يلي:

INSERT INTO Employee (Name, HireDate, Grade, ManagerID)
    VALUES ('Hasan Saleh','20030623','Foreman', NULL);
INSERT INTO Employee (Name, HireDate, Grade, ManagerID)
    VALUES ('Amin Akbar','20040302','Labourer',NULL);
INSERT INTO Employee (Name, HireDate, Grade, ManagerID)
    VALUES ('Ayat Othman','19991125','Labourer',NULL);

UPDATE Employee
SET ManagerID = (SELECT EmpID 
                 FROM Employee 
                 WHERE Name = 'Hasan Saleh')
WHERE Name = 'Amin Akbar' OR 
      Name = 'Ayat Othman';

INSERT INTO Salary (Grade, Amount)
       VALUES('Foreman','60000');
INSERT INTO Salary (Grade, Amount)
       VALUES('Labourer','35000');

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

ثم نشغل هذه الملفات من محث sqlite كما يلي:

sqlite> .read employee.sql
sqlite> .read employee.dat

تأكد أولًا من حل أي مشاكل تتعلق بمسارات الملفات، إما بتشغيل Sqlite من نفس مجلد سكربتات SQL كما فعلنا هنا، أو بتوفير المسار الكامل إلى السكربت.

والآن لنجرب استعلامًا للتحقق من عمل هذه الملفات كما يجب:

sqlite> SELECT Name FROM Employee
   ...> WHERE Grade IN
   ...> (SELECT Grade FROM Salary WHERE amount >50000)
   ...> ;
Hasan Saleh

يبدو أننا نجحنا هنا، إذ أن Hasan Saleh هو الموظف الوحيد الذي يتقاضى أكثر من 50000$، ونلاحظ أننا استخدمنا الشرط IN مع تعليمة SELECT مضمنة أخرى، وهذه صورة مختلفة عن استعلام مشابه أجريناه سابقًا باستخدام وصلة بين الجداول cross-table join، ورغم أن كلا التقنيتين ستعملان إلا أن طريقة الوصلة أسرع.

العلاقات التعددية بين الجداول

أحد السيناريوهات التي لم نتحدث عنها هو ربط جدولين معًا بعلاقات تعددية، أي يُربط صف في أحد الجدولين بعدة صفوف في الجدول الآخر، في نفس الوقت الذي يمكن ربط صف من الجدول الآخر بعدة صفوف من الجدول الأول، فمثلًا لنفرض أننا نكتب قاعدة بيانات لدعم دار نشر للكتب، حيث سيكون لدينا قائمة من المؤلفين وقائمة من الكتب، وسيكتب كل مؤلف كتابًا أو أكثر، وفي نفس الوقت قد يكون للكتاب الواحد عدة مؤلفين، فكيف نعبر عن هذه العلاقات في قاعدة بيانات؟

الجواب هنا هو تمثيل العلاقة بين الكتب والمؤلفين في جدول مستقل بذاته، ويُدعى هذا الجدول بجدول التقاطع intersection table أو جدول الربط mapping table، وكل صف في ذلك الجدول يمثل علاقةً من النوع كتاب/مؤلف، فقد يكون لكل كتاب عدة علاقات كتاب/مؤلف، لكن لكل علاقة كتابًا واحدًا ومؤلفًا واحدًا، وبذلك نكون قد حولنا علاقة متعدد-متعدد إلى علاقتي واحد-متعدد، وبما أننا نعرف كيف نبني مثل هذه العلاقات باستخدام المعرِّفات، فلنر ذلك عمليًا:

DROP TABLE IF EXISTS author;
CREATE TABLE author (
ID INTEGER PRIMARY KEY,
Name TEXT NOT NULL
);

DROP TABLE IF EXISTS book;
CREATE TABLE book (
ID INTEGER PRIMARY KEY,
Title TEXT NOT NULL
);

DROP TABLE IF EXISTS book_author;
CREATE TABLE book_author (
bookID INTEGER NOT NULL,
authorID INTEGER NOT NULL
);

INSERT INTO author (Name) VALUES ('Jane Austin');
INSERT INTO author (Name) VALUES ('Grady Booch');
INSERT INTO author (Name) VALUES ('Ivar Jacobson');
INSERT INTO author (Name) VALUES ('James Rumbaugh');

INSERT INTO book (Title) VALUES('Pride & Prejudice');
INSERT INTO book (Title) VALUES('Emma');
INSERT INTO book (Title) VALUES('Sense & Sensibility');
INSERT INTO book (Title) VALUES ('Object Oriented Design with Applications');
INSERT INTO book (Title) VALUES ('The UML User Guide');

INSERT INTO book_author (BookID,AuthorID) values (
(SELECT ID FROM book WHERE title = 'Pride & Prejudice'),
(SELECT ID FROM author WHERE Name = 'Jane Austin')
);

INSERT INTO book_author (BookID,AuthorID) VALUES (
(SELECT ID FROM book WHERE title = 'Emma'),
(SELECT ID FROM author WHERE Name = 'Jane Austin')
);

INSERT INTO book_author (BookID,AuthorID) VALUES (
(SELECT ID FROM book WHERE title = 'Sense & Sensibility'),
(SELECT ID FROM author WHERE Name = 'Jane Austin')
);

INSERT INTO book_author (BookID,AuthorID) VALUES (
(SELECT ID FROM book WHERE title = 'Object Oriented Design with Applications'),
(SELECT ID FROM author WHERE Name = 'Grady Booch')
);

INSERT INTO book_author (BookID,AuthorID) VALUES (
(SELECT ID FROM book WHERE title = 'The UML User Guide'),
(SELECT ID FROM author WHERE Name = 'Grady Booch')
);

INSERT INTO book_author (BookID,AuthorID) VALUES (
(SELECT ID FROM book WHERE title = 'The UML User Guide'),
(SELECT ID FROM author WHERE Name = 'Ivar Jacobson')
);

INSERT INTO book_author (BookID,AuthorID) VALUES (
(SELECT ID FROM book WHERE title = 'The UML User Guide'),
(SELECT ID FROM author WHERE Name = 'James Rumbaugh')
);

يمكن أن نجرب الآن بعض الاستعلامات لنرى كيف ستعمل، فمثلًا لنبحث عما نشرته جين أوستن Jane Austin من كتب:

sqlite> SELECT title FROM book, book_author
   ...> WHERE book_author.bookID = book.ID
   ...> AND book_author.authorID = (SELECT ID FROM author 
   ...>                             WHERE name = "Jane Austin");

لعل الأمر صار معقدًا قليلًا، لكن الفكرة ستتضح مع التكرار والتدريب، ولاحظ كيف نحتاج إلى إدراج كل من الجدولين المشار إليهما book وbook_author في قائمة الجداول بعد SELECT، أما الجدول الثالث author فليس موجودًا هناك لأنه مدرج مقابل تعليمة SELECT الخاصة به.

لنجرب الآن بالطريقة المعاكسة، أي لنر من ألف كتاب The UML User Guide:

sqlite> SELECT name FROM author, book_author
   ...> WHERE book_author.authorID = author.ID
   ...> AND book_author.bookID = (SELECT ID FROM book 
   ...>                           WHERE title = "The UML User Guide");

بالنظر إلى تلك الشيفرة سنجد تطابق هيكل الاستعلامين، فلم نغير إلا أسماء الحقل والجدول.

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

إعادة النظر في دليل جهات الاتصال

في دليل جهات الاتصال الذي كتبناه من قبل وبنيناه على الملفات، استخدمنا قاموسًا فيه اسم الشخص هو المفتاح، وعنوانه عنصر بيانات وحيد، ولا بأس بهذا إذا كنا نعرف الاسم الذي نريده، أو إذا أردنا تفاصيل العنوان كلها، لكن ماذا لو أردنا جميع الأسماء الموجودة في مدينة بعينها؟ أو كل من اسمه Hasan؟

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

Field Name Type Constraint
First Name String Primary Key
Last Name String Primary Key
House Number String NOT NULL
Street String NOT NULL
District String  
Town String NOT NULL
Post Code String NOT NULL
Phone Number String NOT NULL

نلاحظ عدة أمور هنا:

  1. لدينا مفتاحان رئيسيان primary keys وهذا غير مسموح به، وسنتعامل معه بعد قليل.
  2. جميع البيانات من نوع TEXT رغم أن House Number قد يكون عددًا صحيحًا INTEGER، إلا أن أرقام المنازل تتضمن أحرفًا، لذا يجب استخدام TEXT.
  3. الحقل الاختياري الوحيد هو الحقل district.
  4. الرمز البريدي محدد الصيغة للغاية، لكنه يختلف وفقًا لكل دولة، وهذا يعني أن علينا أن نجعله من النوع TEXT ليناسب جميع الاحتمالات.
  5. رغم أن رقم الهاتف Phone Number قد يبدو مناسبًا لوضع قيد UNIQUE إلا أن هذا لن يسمح بوجود شخصين يتشاركان نفس رقم الهاتف، وهي حالة محتملة.

بالعودة إلى النقطة الأولى -وجود مفتاحين رئيسيين- وهذا غير مسموح في SQL، لكن نستطيع جمع عمودين معًا في ما يسمى بالمفتاح المركب composite key، والذي يسمح بمعاملتهما مثل قيمة واحدة فيما يخص تعريف الصف، وعلى ذلك يمكن إضافة سطر في نهاية تعليمة create table ليجمع الاسم الأول FirstName وLastName في مفتاح رئيسي واحد، وسيبدو ذلك كما يلي:

CREATE TABLE address (
FirstName NOT NULL,
LastName NOT NULL,
...
PhoneNumber NOT NULL,
PRIMARY KEY (FirstName,LastName)
);

نلاحظ هنا السطر الأخير PRIMARY KEY (FirstName,LastName)‎ الذي يحوي الأعمدة التي نريد استخدامها لتكون مفتاحًا مركبًا، وهو مثال على قيد قائم على الجدول table-based constraint، غير أن هذه الفكرة غير سديدة، فإذا كنا نعرف شخصين بنفس الاسم فلن نستطيع تخزينهما معًا، ولن نخزن إلا واحدًا فقط منهما، وسنتعامل مع هذا بتعريف حقل integer primary key لتعريف جهات اتصالاتنا تعريفًا فريدًا رغم أننا لن نستخدم ذلك في الاستعلامات إلا نادرًا.

نعرف كيفية التصريح عن قيد INTEGER PRIMARY KEY حيث فعلنا ذلك في مثال الموظف، ونستطيع تحويل ذلك مباشرةً إلى سكربت إنشاء بيانات SQLite كما يلي:

-- احذف الجداول إذا كانت موجودة من قبل وأعد إنشاءها.
-- استخدم القيود لتحسين كفاءة البيانات.
DROP TABLE IF EXISTS address;
CREATE TABLE address (
ContactID INTEGER PRIMARY KEY,
First NOT NULL,
Last NOT NULL,
House NOT NULL,
Street NOT NULL,
District,
Town NOT NULL,
PostCode NOT NULL,
Phone NOT NULL
);

السطران الأولان في الشيفرة السابقة ما هما إلا تعليقات، فأي شيء متبوع بشرطتين -- هنا يُعد تعليقًا في SQL، كما في حالة رمز # في بايثون.

نلاحظ أننا لم نعرّف النوع لأن TEXT هو النوع الافتراضي في SQLite، فإذا أردنا تحويل هذا المخطط أو تخطيط الجدول -أو نقله port بالاصطلاح الحاسوبي- إلى قاعدة بيانات أخرى فسيتوجب علينا إضافة بعض المعلومات.

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

First Last House Street District Town PostCode Phone
Mona Akbar 42 Any Street SomePlace MyTown ABC123 01234 567890
Jamil Masoud 17 Any Street SomePlace MyTown ABC234 01234 543129
Yousef Mohammad 9 Crypt Drive Hotspot Metropolis ABC345 01234 456459
Yasein Akbar 42 Any Street SomePlace MyTown ABC123 01234 567890
Yasein Akbar 12A Double Street   AnyTown DEF174 01394 784310
Amal Akbar 12A Double Street   AnyTown DEF174 01394 784310

لدينا الآن بعض البيانات ونريد إجراء التجارب عليها، لنرى كيفية استخدام الإمكانيات الموجودة في SQL لاستخراج البيانات بطرق لم نكن لنحلم بها في مثال القاموس المبني على الملفات في بايثون.

من يعيش في هذا الشارع؟

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

sqlite> SELECT First,Last FROM Address
   ...> WHERE Street = "Any Street";

من يحمل اسم Akbar؟

هذا أيضًا تعبير SELECT/WHERE بسيط في SQL:

sqlite> SELECT First,Last FROM Address
   ...> WHERE Last = "Akbar";

ما هو رقم هاتف Yasein؟

وهذا أيضًا استعلام بسيط إلا أننا سنحصل على عدة نتائج:

sqlite> SELECT First,Last, Phone FROM Address
   ...> WHERE First LIKE "Yas%";

نلاحظ أننا استخدمنا LIKE في شرط WHERE، وهذا يستخدم أسلوب الموازنة الخاص بمحرف البدل wild card، ويتجاهل حالة الأحرف، لاحظ أن رمز محرف البدل في SQL هو % بدلًا من محرف * الشائع، ونتيجةً لهذا نحصل على مطابقة أكثر مرونةً من التساوي الذي يتطلب تطابقًا تامًا، ونلاحظ أننا لو استخدمنا %Y فقط في محرف البدل لحصلنا على Yosef في النتائج أيضًا.

ما هي الأسماء المتكررة؟

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

sqlite> SELECT DISTINCT A.First, A.Last 
   ...> FROM Address AS A, Address AS B
   ...> WHERE A.First = B.First
   ...> AND A.Last = B.Last
   ...> AND NOT A.ContactID = B.ContactID;

نستخدم هنا بعض المزايا الجديدة، حيث نضيف A وB -وهما اسمان بديلان aliases- إلى الجداول في شرط FROM، كما نضيف أسماءً بديلةً للقيم الناتجة أيضًا لتقليل الكتابة، ونستخدم هذه الأسماء عند الإشارة إلى الحقول الناتجة باستخدام الصيغة النقطية المعتادة، يمكن استخدام الاسم البديل في أي استعلام، لكننا مجبرون على استخدامه هنا لأننا نستخدم نفس الجدول Address في المرتين -ومن ثم نضمه إلى نفسه-، لذا نحتاج إلى اسمين بديلين للتمييز بين النسختين في شرط where، كما نضيف الكلمة المفتاحية DISTINCT التي تحذف أي نتائج مكررة.

وخلاصة الفقرات السابقة أن الاستعلام يبحث عن الصفوف التي لها نفس الاسم الأول والأخير، لكن لها معرِّف جهة اتصال ContactID مختلف، ثم يحذف الصفوف المتشابهة قبل عرض النتائج.

لمحث SQLite التفاعلي نفس قوة محث بايثون من حيث القدرة على تطوير استعلامات معقدة مثل هذا، فقد نبدأ باستعلام بسيط ثم نزيد التعقيد لاحقًا، فمثلًا آخر جزء أضفناه إلى الاستعلام الأخير كان كلمة DISTINCT، على الرغم من أنها الكلمة الثانية فيه.

الوصول إلى SQL من بايثون

توفر SQLite واجهة برمجة تطبيقات API تتكون من عدد من الدوال القياسية التي تسمح للمبرمجين بتنفيذ جميع العمليات الممكنة في محث SQL، وقد كُتبت API الخاصة بـ SQLite بلغة C، لكن توجد مغلِّفات لها للغات الأخرى، بما في ذلك بايثون.

الاتصالات Connections

أول ما نحتاج إليه للتعامل مع قواعد البيانات هو الاتصالات، ويأتي الاسم من حقيقة أن أغلب قواعد البيانات ما هي إلا برامج مخدمات server programs تعمل على حاسوب مركزي في مكان ما في الشبكة، ونريد أن نتصل بها، عادةً بواسطة التسجيل باسم مستخدم وكلمة مرور.

ورغم أن SQLite ما هي إلا ملف موجود في نظام الملفات لدينا، إلا أن API تطلب الاتصال به -أي فتحه- للحفاظ على اتساق العمليات.

المؤشرات Cursors

من المهم عند استخدام قاعدة بيانات من داخل برنامج ما أن نعرف كيفية الوصول إلى الصفوف المتعددة التي يُحتمل أن تعيدها تعليمة SELECT، وذلك باستخدام ما يُعرف بمؤشرات SQL أي SQL cursors، والمؤشر هنا يشبه تسلسل بايثون في القدرة على الوصول فيه إلى صف واحد في كل مرة، وعليه فإن استخراج بياناتنا إلى مؤشر ثم استخدام حلقة تكرارية loop للوصول إليه يمكّننا من معالجة تجميعات كبيرة من البيانات.

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

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

أما في البرامج الصغيرة -مثل التي لدينا- فنستطيع نسخ جميع البيانات إلى هيكل بيانات بايثون لحل المشكلة، لكن يجب الانتباه إلى أننا قد نحتاج إلى عدة مؤشرات في البرامج الكبيرة.

الواجهة البرمجية لقواعد البيانات DB API

يمكن قراءة توثيق الإصدار الأخير من واجهة برمجة التطبيقات لقواعد البيانات DB API في بايثون على موقع بايثون في قسم Database Topic Guide، ويجب قراءته بعناية خاصةً عند برمجة قواعد بيانات ذات أهمية باستخدام بايثون.

تثبيت تعريفات SQLite

تأتي تعريفات SQLite في مكتبة بايثون القياسية افتراضيًا، فإذا أردنا استخدام قاعدة بيانات أخرى، مثل SQL Server الخاصة بمايكروسوفت أو MySQL أو Oracle، فسنحتاج إلى تنزيل الوحدات المناسبة لكل منها وتثبيتها، ويمكن الحصول على تعريفات أغلب قواعد البيانات المشهورة من pip أو في ملفات تنفيذية.

سيكون أمر استيراد SQLite كما يلي:

   import sqlite3

استخدام DBI الأساسي

لن نغطي جميع المزايا الموجودة في DBI، لكننا سنذكر ما يكفي ليمكننا من الاتصال بقاعدة بياناتنا، وتنفيذ بعض الاستعلامات، ومعالجة النتائج، وسنختم بإعادة كتابة برنامج دليل جهات الاتصال ليستخدم قاعدة بيانات جهات الاتصال بدلًا من ملف نصي.

>>> db = sqlite3.connect('address.db')

>>> cur = db.cursor()
>>> cur.execute('SELECT * FROM address')

>>> print( cur.fetchall() )

ستكون النتيجة ما يلي:

[(1, 'Mona', 'Akbar', '42', 'Any Street', 'SomePlace', 'MyTown', 'ABC123', '01234 567890'), 
 (2, 'Jamil', 'Masoud', '17', 'Any Street', 'SomePlace', 'MyTown', 'ABC234', '01234 543129'), 
 (3, 'Yousef', 'Mohammad', '9', 'Crypt Drive', 'Hotspot', 'Metropolis', 'ABC345', '01234 456459'),
 (4, 'Yasein', 'Akbar', '42', 'Any Street', 'SomePlace', 'MyTown', 'ABC123', '01234 567890'), 
 (5, 'Yasein', 'Akbar', '12A', 'Double Street', '', 'AnyTown', 'DEF174', '01394 784310')]

تعيد cursor.fetchball()‎ قائمةً من الصفوف tuples، وهذا مشابه لما بدأنا به في مقال مدخل إلى البيانات وأنواعها: أنواع البيانات الأساسية، ونستطيع استخدام هذه القائمة في برنامجنا كما لو قرأناها من ملف مستخدمين قاعدة البيانات آليةً ثابتةً، غير أن قوة قواعد البيانات الحقيقية تكمن في قدرتها على تنفيذ استعلامات معقدة باستخدام SELECT.

دليل جهات الاتصال

لا زال برنامج دليل جهات الاتصال الخاص بنا يعتمد على سطر الأوامر وليست له واجهة رسومية، فإذا أردنا إضافة واجهة مستخدم فيجب أن نعيد هيكلة الشيفرة refactoring لفصل الوظائف عن العرض، على كل سنضيف واجهة ويب للبرنامج.

لن نشرح كل تفصيل في الشيفرة هنا، فيجب أن يستطيع القارئ في هذا المستوى أن يفهمها بنفسه، لكننا سنركز على بعض النقاط ونناقشها.

###############################
# Addressbook.py
#
# Author: A J Gauld
#
''' 
    Build a simple addressbook using 
    the SQLite database and Python 
    DB-API.
'''
###############################

# set up the database and cursor
import sqlite3
dbpath = "D:/DOC/Homepage/Tutor2/sql/"
def initDB(path):
    try: 
        db = sqlite3.connect(path)
        cursor = db.cursor()
    except sqlite3.OperationalError: 
        print( "Failed to connect to database:", path )
        db,cursor = None,None
        raise
    return db,cursor

# Driver functions
def addEntry(book):
    first = input('First name: ') 
    last =  input('Last name: ') 
    house = input('House number: ') 
    street = input('Street name: ') 
    district = input('District name: ') 
    town =  input('City name: ') 
    code =  input('Postal Code: ') 
    phone = input('Phone Number: ') 
    query = '''INSERT INTO Address 
               (First,Last,House,Street,District,Town,PostCode,Phone)
               VALUES (?,?,?,?,?,?,?,?)'''

    try:
       book.execute(query,(first, last, house, street, district, town, code, phone))
    except sqlite3.OperationalError:  
       print( "Insert failed" )
       raise
    return None

def removeEntry(book):
    name  = input("Enter a name: ")
    names = name.split()
    first = names[0]; last = names[-1]
    try:
       book.execute('''DELETE FROM Address 
                    WHERE First LIKE ? 
                    AND Last LIKE ?''',(first,last))
    except sqlite3.OperationalError: 
       print( "Remove failed" )
       raise
    return None

def findEntry(book):
    validFields = ('first','last','house','street',
                   'district','town','postcode','phone')
    field = input("Enter a search field: ")
    value = input("Enter a search value: ")
    if field.lower() in validFields:
       query = '''SELECT first,last,house,street,district,town,postcode,phone
                  FROM Address WHERE %s LIKE ?''' % field
    else: raise ValueError("invalid field name")
    try:
        book.execute(query, (value,) )
        result = book.fetchall()
    except sqlite3.OperationalError: 
       print( "Sorry search failed" )
       raise
    else:
        if result:
           for line in result:
               print( line )
        else: print("No matching data")
    return None


def testDB(database):
    database.execute("SELECT * FROM Address")
    print( database.fetchall() )
    return None

def closeDB(database, cursor):
    try:
       cursor.close()
       database.commit()
       database.close()
    except sqlite3.OperationalError:
       print( "problem closing database..." )
       raise

# User Interface functions
def getChoice(menu):
    print( menu )
    choice = input("Select a choice(1-4): ")
    return choice

def main():
    theMenu = '''
    1) Add Entry
    2) Remove Entry
    3) Find Entry
    4) Test database connection

    9) Quit and save
    '''

    try:
       theDB, theBook = initDB(dbpath + 'address.db')
       while True:
           choice = getChoice(theMenu)
           if choice == '9' or choice.upper() == 'Q':
              break
           if choice == '1' or choice.upper() == 'A':
               addEntry(theBook)
           elif choice == '2' or choice.upper() == 'R':
               removeEntry(theBook)
           elif choice == '3' or choice.upper() == 'F':
               try: findEntry(theBook)
               except: ValueError: print("No such field name"))
           elif choice == '4' or choice.upper() == 'T':
               testDB(theBook)
           else: print( "Invalid choice, try again" )

    except sqlite3.OperationalError:
        print( "Database error, exiting program." )
        # raise
    finally: 
        closeDB(theDB,theBook)

if __name__ == '__main__': main()

نلاحظ عدة أمور هي:

  1. استخدمنا الشرط try/except لالتقاط أي أخطاء في قاعدة البيانات، وبما أن الخطأ هو نوع مخصص معرَّف داخل وحدة sqlite3، فسنحتاج إلى سبقه باسم الوحدة.
  2. استخدمنا الكلمة المفتاحية raise بعد طباعة رسالة الخطأ، فنتج عن ذلك رفع الاستثناء الأصلي إلى المستوى التالي، والذي هو main في حالتنا، حيث التقط وطُبعت رسالة أخرى.
  3. استخدمنا كذلك محرف البدل ? في سلاسل الاستعلامات لتحمل متغيرات البيانات، وهذا يشبه محدِّدات % المستخدمة في صياغة السلاسل النصية، لكن تُدخل القيم هنا جزءًا من تنفيذ الاستعلام بواسطة book.execute، حيث نمرر صف القيم المدرجة وسيطًا ثانيًا، وميزة هذا تكمن في التحقق الأمني من قيم الدخل مما يحسِّن من أمان الشيفرة، فإذا لم نتحقق من الدخل، أو استخدمنا الصياغة القياسية للسلاسل النصية؛ فقد يكتب أحد المستخدمين شيفرةً بدلًا من قيمة الدخل، لتدخل تلك الشيفرة إلى الاستعلام وتخرب قاعدة البيانات، وهذا يُعرف بهجمات الحقن injection attack في دوائر الأمن الرقمي، وهو أحد أكثر الاختراقات الأمنية شهرةً في الويب هذه الأيام.
  4. نستخدم كلًا من اسم الحقل وقيمة البحث حقول إدراج في الدالة findEntry، مما يجعل دالة البحث أكثر تنوعًا، ولولاه لاحتجنا إلى دالة بحث لكل معيار بحث criteria وهذا أمر مرهق جدًا. لكن توجد مشكلة هنا سببها أن آلية معامِلات SQLite تعمل للقيم فقط، وليس لعناصر SQL، مثل أسماء الحقول أو الجداول، ولحل هذا نحتاج إلى استخدام صياغة السلاسل النصية في بايثون لإدراج اسم الحقل، ونحتاج إلى التحقق من أن اسم الحقل هو أحد الأسماء المعرّفة قبل إدراجه في الاستعلام، لنضمن أن الاستعلام آمن، فإذا لم يكن الحقل صالحًا فسنرفع استثناء بايثون قياسي من النوع ValueError، ثم نحتاج إلى التقاط ذلك في دالة main()‎، ونلاحظ أن الاستعلام يستخدم تعبير البحث LIKE الذي يسمح لنا باستغلال خيار محرف البدل % في SQL في سلسلة البحث الخاصة بنا.
  5. تحتوي الدالة closeDB على استدعاء commit، وهذا يجبر قاعدة البيانات على كتابة جميع التغييرات في الجلسة الحالية إلى الملف، ويمكن النظر إليها على أنها تشبه التابع file.flush، فهي تنهي العملية transaction نوعًا ما.
  6. تغلف الدالة main كل شيء داخل بنية try/except/finally، ويلتقط الشرط except الاستثناءات التي رفعتها دوال المستوى الأدنى كما ذكرنا أعلاه، لكن نلاحظ أنه يحوي تعليمة raise التي أُخرجت من التنفيذ بوضع علامة تعليق قبلها، لأن التعقب الخلفي الكامل للخطأ مفيد جدًا في تنقيح الأخطاء رغم كونه غير مفضل للمستخدم من حيث قابلية القراءة، لذلك يمكن إلغاء التعليق من raise التي في المستوى الأعلى أثناء التطوير لنحصل على تعقب خلفي كامل على الشاشة، وبعد حلّ جميع الزلات البرمجية bugs نعيد التعليق إلى تعليمة raise مرةً أخرى لاستعادة العرض النهائي للبرنامج، وهذا الأسلوب ليس مقصورًا على قواعد البيانات وحدها، بل يمكن استخدامه في أي برنامج يحتمل رفع أخطاء كثيرة فيه ولا نريد إظهار ذلك للمستخدمين، لكننا نحن المكورون نريد أن نراها.
  7. يُستخدم الشرط finally في دالة main لضمان إغلاق قاعدة البيانات بأناقة بغض النظر قابلنا أخطاءً أم لا، وهذا يقلل خطر تخريب البيانات.

ملاحظة عن الأمن الرقمي

ذكرنا أعلاه أن استخدام DB API لمحددات ? بدلًا من استخدام % المعتادة كان بداعي الأمان، ويمكن استخدام صياغة السلاسل النصية المعتادة في الشيفرة، وسنجد أنها ستعمل، مما يغري باستخدامها، غير أنه يُفضل عدم فعل ذلك لما نعلم من كثرة الهجمات السيبرانية، فيجب هنا اتباع هذه الإرشادات لتصبح عادات للمبرمج للحفاظ على أمان الشيفرة.

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

كلمة أخيرة

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

المزايا المتقدمة لقواعد البيانات

المفاتيح الخارجية

تحتوي أغلب قواعد البيانات -بما فيها SQLite- على مفاتيح خارجية foreign keys، تسمح لنا بتحديد الروابط أو العلاقات بين الجداول، بحيث تتكون هذه المفاتيح من مفاتيح رئيسية لجداول أخرى، وأحيانًا في قواعد بيانات أخرى. ورغم أننا لم نتحدث كثيرًا عن الروابط بين الجداول إلا أنها من أكثر المزايا شيوعًا في قواعد البيانات العلائقية خاصةً عند زيادة حجم البرامج.

التكامل المرجعي

التكامل المرجعي Referential integrity هو القدرة على عدم السماح بقيم بيانات في عمود إلا إذا كانت موجودةً في مكان آخر، ففي قاعدة بيانات الموظفين مثلًا، كان بإمكاننا تقييد القيمة في حقل Employee.Grade لتسمح بالقيم المعرَّفة في جدول Salary.Grade فقط، وهذه أداة بالغة القوة في الحفاظ على اتساق البيانات عبر قاعدة البيانات، خاصةً عندما تُستخدم القيم مفاتيح لربط جدولين، كما هو الحال في أعمدة grade.

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

الإجراءات المخزنة

الإجراءات المخزنة Stored Procedures هي دوال مكتوبة بلغة برمجة خاصة proprietary programming language يوفرها مزود قاعدة البيانات، وتُخزَّن في قاعدة البيانات، ومزيتها أنها إجراءات مصرَّفة compiled، وبالتالي أسرع كثيرًا من استخدام أوامر SQL المكافئة، كما أنها توفر من استهلاك الإنترنت وحجم البيانات المطلوب، من خلال طلب اسم الدالة والوسطاء فقط ليرسلها برنامج العميل، ولكونها مبنيةً داخل الخادم فهي تسمح لنا ببناء سلوكيات مشتركة -مثل قواعد العمل المعقدة- في قاعدة البيانات حيث يمكن تشاركها بواسطة جميع التطبيقات باستمرار، لكن عيبها أنها خاصة ومغلقة، فإذا أردنا تغيير مزود قاعدة البيانات لدينا فيجب إعادة كتابة جميع الإجراءات المخزنة، في حين أن SQL القياسية ستعمل دون تغيير على أي قاعدة بيانات، ولا تدعم SQLite أي إجراءات مخزنة.

العروض

العروض Views هي جداول افتراضية مكونة من جداول حقيقية أخرى، وقد تكون مجموعةً فرعيةً من البيانات في جدول آخر من أجل تبسيط التصفح، وفي الحالة الأشهر تكون بعض الأعمدة من جدول وبعضها من جدول آخر بحيث يكون الجدولان مرتبطين معًا بمفتاح ما.

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

تُستخدم العروض في الغالب لتقسيم البيانات بحيث يستطيع المستخدم الواحد أن يرى مجموعة البيانات المتعلقة به فقط، وتدعم SQLite العروض، لكننا لم نشرحها هنا.

عمليات الحذف المتتالية

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

المحفزات

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

أما عيبها فهو إمكانية إساءة استخدامها، وشرهها الزائد للموارد، مما يؤدي لإبطاء قاعدة البيانات كثيرًا، لذا يجب استخدامها بحذر، وتدعم SQLite محفزات SQL الأساسية والخالصة.

أنواع البيانات المتقدمة

تسمح بعض قواعد البيانات بتخزين مجموعات متنوعة من أنواع البيانات، فقد يكون لدينا عناوين شبكية network addresses وكائنات ثنائية binary كبيرة يشار إليها اختصارًا باسم BLOBS لملفات الصور وغيرها، إضافةً إلى البيانات العددية وبيانات المحارف والتاريخ والوقت المعتادة، ومن أنواع البيانات المشهورة أيضًا نوع يسمى بالنوع العشري ذي الدقة الثابتة fixed precision decimal type، وهو يُستخدم في البيانات المالية لتجنب أخطاء التقريب الموجودة في أعداد الفاصلة العائمة التقليدية، وتدعم SQLite بيانات BLOB لكنها لا تدعم بقية الأنواع المتقدمة، وللاطلاع على المزيد من الاستخدامات المعقدة لـ SQLite يمكن العودة إلى موقع sqlitetutorial الذي يحتوي على شرح ممتاز يسهل تعلم ما فيه بناءً على ما شرحنا هنا.

خاتمة

في نهاية هذا المقال نرجو أن تكون تعلمت ما يلي:

  • قواعد البيانات تنظم البيانات في جداول.
  • تتكون السجلات من حقول، وتشمل صفوف الجداول.
  • SQL هي لغة تُستخدم في إدارة البيانات.
  • الأوامر الأساسية في لغة SQL هي: CREATE وINSERT وSELECT وUPDATE.
  • توفر لغات البرمجة مغلِّفات SQL للوصول إلى البيانات من البرامج.
  • تخزن المؤشرات نتائج استعلامات SQL في صورة مؤقتة، لكن يمكن الوصول إليها.
  • تُستخدم محدِّدات DB API لإدراج قيم في الاستعلامات، ولا تُستخدم صيغة السلاسل النصية القياسية.

ترجمة -بتصرف- للفصل الرابع والعشرين: Working with Databases من كتاب Learn To Program لصاحبه Alan Gauld.

اقرأ أيضًا


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

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

لا توجد أية تعليقات بعد



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

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

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

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

  Only 75 emoji are allowed.

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

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

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


×
×
  • أضف...