جدول المحتويات
قواعد البيانات عبر الإكسير
مقدمة
يمكن استعمال ربط تطبيقات عكاشة مع قواعد البيانات مثل sqlite (دعمها مدمج في بايثون) أو mysql عبر حزم بايثون لهذا الغرض (مثل pymysql)
لكن في هذه الحالة يكون عليك متابعة عدد من الأمور مثل:
- تكون محصور في قاعدة بيانات بعينها (مثلا سيكون من الصعب نقل الكود من SQLite إلى MySQL أو PostgreSQL)
- عليك القيام يدويا بتحسين تعابير SQL
- في sqlite لا تستطيع التحدّث مع اتصال فتحته في thread آخر ممّا يعني أنك قد تحتاج لعمل اتصال لكل طلب.
لذا هناك بديل لهذه الطريقة اسمه SQLAlchemy وحوله بُني الإكسير لتسهيله.
الإكسير عبارة عن ORM أي Object-relational mapping حيث تتحوّل الجداول والصفوف وعلاقاتها إلى كائنات.
تدعم SQLAlchemy عددا كبيرا من قواعد البيانات من بينها:
- Sqlite
- MySQL
- Oracle
- Postgresql
إنّ الإكسير يقوم بالكثير من التحسينات فهو مثلا يعمل اتصال واحد مع قاعدة البيانات إلاّ في حالة Sqlite يقوم بعمل اتصال لكل thread
الإكسير من خارج عكاشة
عمل المخطط
لنفترض أن لدينا قاعدة بيانات للكتب تتكوّن من جدول للمؤلفين وجدول للكتب وجدول للناشرين وجدول للتصنيفات من خلال keywords.
جدول الكتب يتكوّن من الحقول التالية:
- معرف رقمي فريد تلقائي
- اسم الكتاب وهو نص قصير
- وصف الكتاب وهو نص حر طويل
- عام النشر
- معرف ISBN فريد ومفهرس
- المؤلّفون وهي علاقة كثير لكثير many to many مع جدول المؤلفين يعني ممكن أن يكون أكثر من مؤلف لنفس الكتاب أو للمؤلف الواحد أكثر من كتاب
- الناشر وهي علاقة كثير لواحد أي قد يكون لأكثر من كتاب ناشر واحد
- التصنيفات وهي علاقة كثير لكثير.
جدول المؤلفون حيث يتكوّن من الحقول التالية:
- معرف رقمي فريد تلقائي
- اسم المؤلف وهو نص
- عام الميلاد
- عام الوفاة أو صفر للأحياء
- الكتب وهي علاقة كثير لكثير (عكس علاقة المؤلفون في جدول الكتب)
جدول الناشرون
- معرف رقمي فريد تلقائي
- اسم الناشر وهو نص
- الكتب وهي علاقة واحد لكثير (عكس علاقة الناشر في جدول الكتب)
جدول التصنيفات بالكلمات المفتاحية
- معرف رقمي
- الكلمة
- الكتب وهي علاقة كثير لكثير
تنفيذ المخطط
لنكتب وحدة module تُعرّف هذه القاعدة كما يلي ونحفظها في ملف باسم bookstoreModel.py
from elixir import * class Book(Entity): using_options(tablename="book") n = Field(Integer, primary_key=True, autoincrement=True) title = Field(Unicode(30), index=True) description = Field(UnicodeText) year = Field(Integer) isbn = Field(String(16), index=True, unique=True) authors = ManyToMany('Author', lazy=False) publisher = ManyToOne('Publisher', lazy=False) keywords = ManyToMany('Keywords', lazy=False) class Author(Entity): using_options(tablename="author") n = Field(Integer, primary_key=True, autoincrement=True) name = Field(Unicode(30), index=True) details = Field(UnicodeText) year_of_birth = Field(Integer, index=True) year_of_death = Field(Integer, index=True, default=0) books = ManyToMany('Book', lazy=True) class Publisher(Entity): using_options(tablename="publisher") n = Field(Integer, primary_key=True, autoincrement=True) name = Field(Unicode(30), index=True) books = OneToMany('Book', lazy=True) class Keywords(Entity): using_options(tablename="keywords") n = Field(Integer, primary_key=True, autoincrement=True) name = Field(Unicode(30), index=True) books = ManyToMany('Book', lazy=True)
في نفس المجلد افتح بايثون التفاعلي واكتب فيه
from bookstoreModel import Book, Author, Publisher, Keywords import elixir elixir.metadata.bind='sqlite:///db.sqlite' elixir.setup_all() elixir.create_all()
الأمر الأخير create_all يعمل على إنشاء قاعدة البيانات (نستدعيه مرة واحدة حيث لاحقا نكتفي ب setup_all) وهي في مثالنا نتحدث مع قاعدة بيانات من نوع sqlite وهي موجودة في الملف db.sqlite لاحظ سطر bind
يمكنك طبعا استعمال mysql وتقديم اسم مستخدم وكلمة سر واسم القاعدة …إلخ مثلا
elixir.metadata.bind='mysql://user:pass@localhost/mytestdb'
لكن دعونا نتابع مستخدمين ملف sqlite وليس mysql. الآن اخرج من مفسر بايثون التفاعلي وانظر محتويات ملف db.sqlite عبر الأمر التالي:
sqlite3 db.sqlite .dump | less
ستلاحظ مخطط قاعدة البيانات schema دون مدخلات لأنها لا تزال خالية.
لنعد إلى مفسر بايثون التفاعلي ونفتح قاعدة البيانات بالأوامر
from bookstoreModel import Book, Author, Publisher, Keywords import elixir elixir.metadata.bind='sqlite:///db.sqlite' elixir.setup_all()
في مراحل الاختبار يمكنك إظهار عمليات SQL حتى تراها بعينك وذلك بكتابة السطر
elixir.metadata.bind.echo = True
إضافة المدخلات
لنقم الآن بإدخال بعض الحقول في قواعد البيانات. ولنبدأ بعمل حقل في جدول الكلمات المفتاحية وذلك عبر الأمر
kw_s=Keywords(name=u"science") kw_a=Keywords(name=u"art") p1=Publisher(name=u"My Press") p2=Publisher(name=u"Nashir2") elixir.session.commit()
السطر الأول يعمل كائن من نوع Keywords ويعين خصائصه والتي هي name ويعطيها قيمة science وهذا يعني عمل صف في جدول keywords قيمة العمود name فيه هي science. يتم حفظ هذا الكائن في المتغير kw_s وهذا غير إلزامي لكنني فعلته لأني قد استخدمه لاحقا.
السطر الثاني عمل كائنا آخر وبالتالي صف آخر بقيمة أخرى. ثم علمنا ناشرين بنفس الطريقة. ولو قمت الآن بفتح قاعدة البيانات لن تجد فيها شيء ذلك أن الإكسير يجمع العمليات إلى أن تطلب منه إرسالها محسنة عبر الدالة elixir.session.commit.
لنضف الآن عدد من المؤلفين
a1=Author(name=u"Oqlah ibn Khalaf", details=u"short story pioneer in Balama", year_of_birth=1975) a2=Author(name=u"John Random Hacker", details=u"fictional author from cyberspace", year_of_birth=1980) elixir.session.commit()
لنضف الآن أحد الكتب
b1=Book(title=u"The twilight of Balama", description=u"a short story.", year=2010, isbn="0-1234-5678-9", publisher=p1) b1.authors.append(a1) b1.keywords.append(kw_a) b2=Book(title=u"Facts you don't know", description=u"true stories from a fake author", year=2010, isbn="1-1234-5678-9", publisher=p2) b2.authors.append(a2) b2.keywords.append(kw_s) elixir.session.commit()
لاحظ استخدام append لإضافة العلاقات. طبعا يمكنك استخدام الاستعلامات لتوليد الكائنات التي تمررها إلى append عوضا عن حفظ الكائنات في متغيرات
الاستعلام والتعديل
أغلق جلسة بايثون التفاعلي السابقة ولنبدأ واحدة جديدة ونكتب بدايتنا التقليدية
from bookstoreModel import Book, Author, Publisher, Keywords import elixir elixir.metadata.bind='sqlite:///db.sqlite' elixir.setup_all()
لنستعلم عن شيء له معرف فريد أو فهرس فريد مثلا
b=Book.get_by(isbn="1-1234-5678-9") print b.title
لاحظ استخدام get_by على الفئة نفسها دون كائن منها (يعرف هذا class method أو static method) مع تمرير لها معرف isbn للحصول على كائن الكتاب. وباملناسبة يمكننا التعديل على الكائن مباشرة حيث تعكس تلك التعديلات على قاعدة البيانات فور عمل commit مثلا لنفرض أني أريد زيادة علامة تعجب في نهاية العنوان
b.title+=u"!" elixir.session.commit()
إن العلاقات تعمل بشكل تلقائي يعني لو استعلمنا عن الكتب تحت تصنيف art يمكننا طباعة أسماءها وأسماء مؤلفيها
kw=Keywords.get_by(name="art") for b in kw.books: print "book: ",b.title,", authors: " for a in b.authors: print b.name, print "."
الاستعلام والفلترة
بطريقة مشابهة لنفرض أننا نريد كل الكتب التي طبعت بعد 1960
books=Book.query.filter(Book.year>1960) for b in books: print b.title, "@", b.year
يمكننا أيضا تركيب الفلاتر وراء بعضها كذلك يمكننا ترتيب النتائج وأخذ أول كذا نتيجة
books=Book.query.filter(Book.year>1960).order_by(elixir.sqlalchemy.desc(Book.year)).limit(10) for b in books: print b.title, "@", b.year