Skip to content

19. استخدام ORM SQLALCHEMY

أوضح الفصل السابق أنه في بعض الحالات يمكن كتابة كود مستقل عن SGBD باستخدام البنية التالية:

Image

في هذا الفصل، سنستخدم ORM (مُخطِّط العلاقات بين الكائنات) [sqlalchemy] للوصول إلى SGBD بطريقة موحدة بغض النظر عن SGBD المستخدم. يتيح ORM أمرين:

  • يسمح لبرنامج نصي بالتفاعل مع SGBD دون إصدار أوامر SQL؛
  • يخفي عن البرنامج النصي الخصائص المميزة لكل SGBD؛

تصبح البنية كما يلي:

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

وستكون شجرة البرامج النصية المدروسة كما يلي:

Image

19.1. تثبيت ORM و [sqlalchemy]

يأتي ORM [sqlalchemy] في شكل حزمة Python يجب تثبيتها في محطة Python:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\databases\sqlalchemy>pip install sqlalchemy
Collecting sqlalchemy
  Downloading SQLAlchemy-1.3.18-cp38-cp38-win_amd64.whl (1.2 MB)
     || 1.2 MB 3.3 MB/s
Installing collected packages: sqlalchemy
Successfully installed sqlalchemy-1.3.18

19.2. البرامج النصية 01: الأساسيات

Image

  • في [1]، البرامج النصية التي سيتم دراستها. ستستخدم هذه البرامج النصية فئات [2] التالية: BaseEntity، MyException، Personne، Utils؛

19.2.1. التكوين

يقوم الملف [config] بتكوين التطبيق على النحو التالي:


def configure():
    # root_dir
    # المسار المطلق المرجعي للمسارات النسبية للتكوين
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # المسارات المطلقة للتبعيات
    absolute_dependencies = [
        # BaseEntity، MyException، Personne، Utils
        f"{root_dir}/classes/02/entities",
    ]

    # يتم تعيين مسار النظام
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # تكوين الفئات
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # تطبيق التكوين
    return {}

تعليقات

  • السطر 8: يتم إضافة المجلد الذي يحتوي على فئات [BaseEntity, MyException, Personne, Utils] إلى مسار Python؛
  • السطران 12-13: يتم تعيين مسار Python Path للتطبيق؛
  • السطران 16-17: ربما نتذكر أن الفئة |BaseEntity| لها سمة فئة تُسمى [excluded_keys]. هذه السمة عبارة عن قائمة نضع فيها خصائص الفئة التي لا نريد أن تظهر في قاموسها (دالة asdict). هنا نستبعد الخاصية [_sa_instance_state] من حالة الفئة [Personne]. سنرى قريبًا السبب؛

19.2.2. البرنامج النصي [démo]

يُظهر البرنامج النصي [démo] أول استخدام لـ ORM [sqlalchemy]:


# يتم استرداد إعدادات التطبيق
import config

config = config.configure()

# عمليات الاستيراد
from sqlalchemy import Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.orm import mapper

from Personne import Personne

# البيانات الوصفية
metadata = MetaData()

# الجدول
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )
# فئة Personne قبل التعيين
personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
print(f"personne1={personne1.__dict__}")

# التعيين
mapper(Personne, personnes_table, properties={
    'id': personnes_table.c.id،
    'الاسم الأول: personnes_table.c.prenom،
    '«اللقب»: personnes_table.c.nom،
    'العمر: personnes_table.c.age
})

# لم يتم تعديل «الشخص 1»
print(f"personne1={personne1.__dict__}")
# تم تعديل فئة «Personne» - تم «إثرائها»
personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
print(f"personne2={personne2.__dict__}")

تعليقات

  • الأسطر 1-4: يتم تكوين التطبيق؛
  • الأسطر 6-10: يتم استيراد الوحدات النمطية اللازمة للبرنامج النصي؛
  • السطر 13: [MetaData] هي فئة تابعة لـ [sqlalchemy]؛
  • الأسطر 15-22: [Table] هي فئة تابعة لـ [sqlalchemy]. وهي تسمح بوصف جدول في قاعدة بيانات. هنا، سنقوم بوصف الجدول [personnes] التابع لقاعدة البيانات MySQL [dbpersonnes] التي تمت دراستها في الفصل |MySQL
    • السطر 16: المعلمة الأولى [personnes] هي اسم الجدول الموصوف؛
    • السطر 16: المعلمة الثانية [metadata] هي المثيل [MetaData] الذي تم إنشاؤه في السطر 13؛
    • الأسطر 17-22: يصف كل معلمة من المعلمات التالية عمودًا من الجدول باستخدام صيغة خاصة بـ [sqlalchemy] ولكنها قريبة من صيغة SQL؛
    • يتم وصف كل عمود باستخدام مثيل من فئة [Column] التابعة لـ [sqlalchemy]؛
      • المعلمة الأولى هي اسم العمود؛
      • المعلمة الثانية هي نوعه؛
      • المعلمات التالية هي معلمات مسماة:
        • السطر 17: [primary_key=True] للإشارة إلى أن العمود [id] هو المفتاح الأساسي للجدول [personnes]؛
        • السطر 18: [nullable=False] للإشارة إلى أن العمود يجب أن يحتوي بالضرورة على قيمة عند إدراج سطر في الجدول؛
    • السطر 21: وأخيرًا، تسمح الفئة [UniqueConstraint] بوصف قيد التفرد. هنا يُشار إلى أن الأعمدة (الاسم، الاسم الأول) يجب أن تكون فريدة في الجدول. تسمح الخاصية المسماة [name] بتسمية هذا القيد. هنا، يجب التمييز بين حالتين:
      • نقوم بوصف جدول موجود. في هذه الحالة، يجب البحث عن اسم القيد في خصائص الجدول (phpMyAdmin أو pgAdmin
      • وصف جدول سيتم إنشاؤه. في هذه الحالة، نضع الاسم الذي نريده؛
  • الأسطر 23-25: نقوم بإنشاء شخص [personne1] ونعرض قاموسه [__dict__]. سنحصل هنا على:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • الأسطر 27-33: نقوم بعملية التعيين، أي ننشئ تطابقًا بين الفئة [Personne] والجدول [personnes]. وهو في الأساس تطابق [propriétés de la classe  colonnes de la table]. تقبل الدالة [mapper] هنا ثلاثة معلمات:
    • السطر 28: المعلمة الأولى هي اسم الفئة التي يتم إجراء التعيين لها؛
    • السطر 28: المعلمة الثانية هي الجدول الذي سيتم ربطها به. وهو الكائن [Table] الذي تم إنشاؤه في السطر 16؛
    • السطر 28: المعلمة الثالثة هنا هي معلمة تُسمى [properties]. وهي عبارة عن قاموس تكون فيه المفاتيح هي خصائص الفئة المُعينة، والقيم هي أعمدة الجدول المُعين. للإشارة إلى العمود X في الجدول [personnes_table]، نكتب [personnes_table.c.X
  • السطران 35-36: يتم إعادة عرض الشخص [personne1] بعد إتمام عملية التعيين. ونلاحظ أنه لم يتغير:
personne1={'_BaseEntity__id': 67, '_Personne__prénom': 'x', '_Personne__nom': 'y', '_Personne__âge': 10}
  • الأسطر 37-39: يتم إنشاء شخص جديد [personne2] وعرضه. عندئذٍ يظهر العرض التالي:
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>, 'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}

نلاحظ أن القاموس [__dict__] قد تعرض لتعديلات جذرية:

  • (تابع)
    • تظهر خاصية جديدة [_sa_instance_state]. ونلاحظ أنها كائن تابع لـ ORM و [sqlalchemy
    • وقد أُزيلت البادئة التي تشير إلى الفئة التي تنتمي إليها من الخصائص الأخرى؛

وبالتالي يمكننا أن نستنتج أن عملية التعيين في الأسطر 27-33 قد عدلت الفئة [Personne].

وعندما نرغب في عرض حالة كائن [Personne]، فإننا لا نرغب عمومًا في الحصول على الخاصية [_sa_instance_state]. فهي موجودة في الواقع فقط من أجل العمليات الداخلية لـ [sqlalchemy]، وعمومًا لا تهمنا. ولهذا السبب كُتب في البرنامج النصي [config] ما يلي:


    # تكوين الفئات
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

19.2.3. البرنامج النصي [main]

سيقوم البرنامج النصي [main] بمعالجة الجدول [personnes] في قاعدة البيانات MySQL [dbpersonnes] من خلال التفاعل مع [sqlalchemy]. لفهم ما يلي، يجب تذكر البنية المستخدمة هنا:

Image

إذا كانت [Database1] هي قاعدة البيانات [dbpersonnes]، نلاحظ أن الارتباط بين البرنامج النصي وهذه القاعدة يمر عبر كيانين:

  • موصل Python إلى SGBD MySQL؛
  • SGBD و MySQL؛

سيتواصل البرنامج النصي [main] مع ORM الذي سيتواصل بدوره مع موصل Python. يتفاعل البرنامج النصي ORM مع هذا الموصل باستخدام الأدوات الموضحة في الفقرتين |MySQL| و|PostgreSQL|، ولا سيما عن طريق إرسال أوامر SQL. لن يستخدم البرنامج النصي [main] أوامر SQL. سيعتمد على واجهة برمجة التطبيقات (API) الخاصة بـ ORM، المكونة من فئات وواجهات.

النص البرمجي [main] هو كما يلي:


# يتم تكوين التطبيق
import config

config = config.configure()

# عمليات الاستيراد
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
from sqlalchemy.exc import IntegrityError, InterfaceError
from sqlalchemy.orm import mapper, sessionmaker

from Personne import Personne

# سلسلة اتصال بقاعدة البيانات MySQL
engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

# البيانات الوصفية
metadata = MetaData()

# الجدول
personnes_table = Table("personnes", metadata,
                        Column('id', Integer, primary_key=True),
                        Column('prenom', String(30), nullable=False),
                        Column("nom", String(30), nullable=False),
                        Column("age", Integer, nullable=False),
                        UniqueConstraint('nom', 'prenom', name='uix_1')
                        )

# التعيين
mapper(Personne, personnes_table, properties={
    'المعرف: personnes_table.c.id،
    'الاسم الأول: personnes_table.c.prenom،
    '«اللقب»: personnes_table.c.nom،
    'العمر: personnes_table.c.age
})

# مصنع الجلسات
Session = sessionmaker()
Session.configure(bind=engine)

session = None
try:
    # جلسة عمل
    session = Session()

    # حذف الجدول [personnes]
    session.execute("drop table if exists personnes")

    # إعادة إنشاء الجدول من التعيين
    metadata.create_all(engine)

    # عملية إدراج
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

    # استعلام
    personnes = session.query(Personne).all()

    # عرض
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # إدخالان آخران، فشل الثاني منهما بسبب تكرار (الاسم الأول، الاسم الأخير)
    session.add(Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 10}))
    session.add(Personne().fromdict({"id": 69, "prénom": "x1", "nom": "y1", "âge": 10}))

    # استعلام
    personnes = session.query(Personne).all()

    # عرض
    print("Liste des personnes ---------")
    for personne in personnes:
        print(personne)

    # التحقق من صحة الجلسة
    session.commit()

except (InterfaceError, IntegrityError) as erreur:
    # عرض
    print(f"L'erreur suivante s'est produite : {erreur}")
    # إلغاء الجلسة الأخيرة
    if session:
        print("rollback...")
        session.rollback()
finally:
    # تحرير موارد الجلسة
    if session:
        session.close()

تعليقات

  • الأسطر 1-4: يتم تكوين التطبيق؛
  • الأسطر 7-9: يتم استيراد سلسلة كاملة من الفئات والواجهات من مكتبة [sqlalchemy]؛
  • السطر 11: يتم استيراد الفئة [Personne
  • السطر 14: سلسلة الاتصال بقاعدة البيانات. وهي تحدد:
    • SGBD المستخدم (mysql
    • موصل Python المستخدم (mysql.connector بدون النقطة)؛
    • المستخدم الذي يقوم بالاتصال (admpersonnes
    • كلمة مروره (nobody
    • الجهاز الذي يوجد عليه SGBD (localhost = الجهاز الذي يوجد عليه البرنامج النصي الذي يتم تنفيذه)؛
    • اسم قاعدة البيانات (dbpersonnes

باستخدام هذه المعلومات، يمكن لـ [sqlalchemy] الاتصال بقاعدة البيانات. تجدر الإشارة إلى أن موصل Python المستخدم يجب أن يكون مثبتًا مسبقًا. لا يقوم [sqlalchemy] بذلك.

  • الأسطر 19-26: وصف الجدول [personnes]؛
  • الأسطر 28-34: التعيين بين الفئة [Personne] والجدول [personnes]؛
  • الأسطر 36-38: تُجرى معظم العمليات [sqlalchemy] ضمن جلسة عمل. ويشبه مفهوم «جلسة العمل» [sqlalchemy] مفهوم «المعاملة» SQL. يتم إنشاء الجلسات انطلاقًا من الفئة [Session] التي تُرجعها الدالة [sessionmaker] في السطر 37؛
  • السطر 38: ترتبط الفئة [Session] بقاعدة البيانات [dbpersonnes] عبر سلسلة الاتصال الموجودة في السطر 14؛
  • السطر 43: يتم إنشاء جلسة عمل. وكما ذكرنا سابقًا، يمكن ربط جلسة العمل بمعاملة؛
  • السطران 45-46: تتيح الطريقة [Session.execute] تنفيذ أمر SQL. وهذا ليس أمرًا شائعًا، حيث سبق أن ذكرنا أن ORM تسمح بتجنب استخدام لغة SQL؛
  • السطران 48-49: تسمح الطريقة [metadata.create_all] بإنشاء جميع الجداول التي تستخدم المثيل [MetaData] الموجود في السطر 17. وليس لدينا سوى واحدة: الجدول [personnes] المُعرَّف في الأسطر 20-26. ستستخدم [sqlalchemy] المعلومات الواردة في هذه الأسطر لإنشاء الجدول. وهنا تكمن الميزة الأولى لـ ORM: فهي تخفي الخصائص المميزة لـ SGBD. في الواقع، قد يختلف ترتيب SQL و[create] اختلافًا كبيرًا من SGBD إلى آخر بسبب أنواع البيانات المحددة للأعمدة. لم يتم توحيد أنواع البيانات. وبالتالي، يختلف الترتيب من سجل لآخر. هنا، بفضل:
    • نصف الجدول الذي نريده بطريقة فريدة؛
    • يتمكن [sqlalchemy] من إنشاء [create] المناسب لـ SGBD الموجود أمامه؛
  • السطر 52: نضيف كائن [Personne] إلى الجلسة. وهذا لا يؤدي إلى إضافته تلقائيًا إلى قاعدة البيانات. في الواقع، يتبع كائن ORM قواعده الخاصة للتزامن مع قاعدة البيانات. وسيسعى دائمًا إلى تحسين عدد الاستعلامات التي يجريها. لنأخذ مثالاً. يضيف البرنامج النصي (add) شخصين (personne1، personne2) إلى الجلسة ثم يقوم بعد ذلك بإجراء استعلام: فهو يريد عرض جميع الأشخاص الموجودين في الجدول. يمكن لـ [sqlalchemy] أن يتصرف على النحو التالي:
    • يمكن إجراء عملية الإضافة لـ [personne1] في الذاكرة. ولا توجد حاجة في الوقت الحالي لإدراجها في قاعدة البيانات؛
    • وينطبق الأمر نفسه على [personne2
    • ثم يأتي الاستعلام من النوع [select]. عندئذٍ يجب استرداد جميع الصفوف من الجدول [personnes]. سيقوم [sqlalchemy] عندئذٍ بإدراج [personne1, personne2] في قاعدة البيانات ثم تنفيذ الاستعلام؛

وبالتالي، سيقوم [sqlalchemy] بإجراء تحسينات غير مرئية للمطور.

  • السطر 56: لإجراء استعلام من النوع [select] (أريد عرض ...)، نستخدم الطريقة [Session.query]. معلمة الطريقة [query] هي الفئة المرتبطة بالجدول الذي يتم الاستعلام عنه. تُرجع هذه الطريقة نوعًا [Query]. تطلب الطريقة [Query.all] جميع الكائنات من النوع [Personne] في الجلسة. ويتم إرجاع جميع الصفوف من الجدول [personnes] إليها، كل منها في شكل كائن من النوع [Personne]. وللقيام بذلك، تستخدم [sqlalchemy] التعيين الذي تم إجراؤه بين الفئة [Personne] والجدول [personnes]. ونتيجة السطر 56 هي قائمة من الكائنات [Personne]؛
  • الأسطر 58-61: يتم عرض عناصر القائمة [personnes]. ونظرًا لأن الفئة [Personne] مشتقة من الفئة [BaseEntity]، فإن الطريقة [Personne.__str__] المستخدمة هنا ضمناً في السطر 61، هي في الواقع الطريقة [BaseEntity.__str__] التي تُرجع السلسلة jSON الخاصة بالكائن المستدعي. وهذه السلسلة هي السلسلة jSON من القاموس [Personne.asdict] (انظر |BaseEntity|). لقد ذكرنا أنه بعد عملية التعيين، سنجد الخاصية [_sa_instance_state] في كل كائن من نوع [Personne]. لكن قيمة هذه الخاصية ليست من النوع [BaseEntity]. لذلك يجب استبعادها من قاموس الفئة [Personne وإلا فسيتعطل العرض. وهذا ما تم تنفيذه في البرنامج النصي [config
  • الأسطر 63-65: نضيف شخصين آخرين يحملان نفس الاسم واللقب. لكن هناك قيد تفرد على اتحاد هذين العمودين. لذا من المفترض أن يحدث خطأ. وهذا ما نسعى إلى رؤيته؛
  • الأسطر 67-68: نطلب مرة أخرى قائمة بجميع الأشخاص الموجودين في قاعدة البيانات؛
  • الأسطر 70-73: ثم يتم عرضها؛
  • السطور 75-76: يتم تثبيت الجلسة (commit). وكما يوحي اسمها، سيتم تثبيت المعاملة الأساسية؛
  • سنلاحظ عند التنفيذ أن الأسطر 67-76 لن يتم تنفيذها بسبب الاستثناء الناتج عن السطر 65. سننتقل بعد ذلك إلى الأسطر 78-84 لمعالجة الاستثناء؛
  • السطر 78: يحدث الاستثناء [InterfaceError] إذا فشل [sqlalchemy] في الاتصال بقاعدة البيانات [dbpersonnes]. تحدث الاستثناء [IntegrityError] في السطر 65؛
  • السطر 80: يتم عرض الخطأ؛
  • الأسطر 82-84: إذا كانت الجلسة موجودة، يتم إلغاؤها. وهذا يعادل إلغاء المعاملة الأساسية؛
  • الأسطر 85-88: في جميع الأحوال، سواء حدث خطأ أم لا، يتم إغلاق الجلسة لتحرير الموارد؛

نتائج التنفيذ هي كما يلي:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/01/main.py
Liste des personnes ---------
{"nom": "y", "prénom": "x", "id": 67, "âge": 10}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y1-x1' for key 'uix_1'
[SQL: INSERT INTO personnes (id, prenom, nom, age) VALUES (%(id)s, %(prenom)s, %(nom)s, %(age)s)]
[parameters: ({'id': 68, 'prenom': 'x1', 'nom': 'y1', 'age': 10}, {'id': 69, 'prenom': 'x1', 'nom': 'y1', 'age': 10})]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...

Process finished with exit code 0
  • السطران 2-3: قائمة الأشخاص بعد الإدراج الأول؛
  • السطر 5: الاستثناء [IntegrityError] الذي حدث عند إضافة شخصين يحملان نفس الاسم واللقب؛
  • السطران 6-7: تجدر الإشارة إلى الأمر SQL الذي فشل. هذا أمر INSERT تم تهيئته: فقد أدخل الأمر [sqlalchemy] الشخصين باستخدام أمر واحد هو INSERT. ونلاحظ هنا أنه حاول تحسين الأوامر SQL التي تم إصدارها؛

والآن دعونا نرى، باستخدام الأمر phpMyAdmin، محتوى الجدول [personnes]:

Image

نلاحظ في [6] أن الجدول فارغ. لا يوجد حتى الشخص الأول الذي أدرجه البرنامج النصي في الجلسة. ويرجع ذلك إلى أن الجلسة كانت تجري ضمن معاملة، وقد تم إلغاء هذه المعاملة في الجملة [except] من البرنامج النصي [main].

لنقم الآن بإجراء التعديل التالي في [main]:


    # إدراج
    session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
    # session.commit()

بعد إضافة شخص في السطر 2، نقوم بإلغاء تعليق السطر 3. ستقوم العملية [session.commit] بالتحقق من صحة المعاملة الأساسية وستبدأ معاملة جديدة. بعد التنفيذ، يكون محتوى الجدول [personnes] كما يلي:

Image

نلاحظ في [6] أن الإدراج الأول قد تم الاحتفاظ به. ويرجع ذلك إلى أنه تم إجراؤه ضمن المعاملة 1، في حين أن الخطأ الذي تلاه وقع ضمن المعاملة 2.

19.3. النصوص البرمجية 02: التعيينات في ملف [sqlalchemy]

Image

البرامج النصية 02 هي نسخة معدلة من البرامج النصية 01. نحاول إجراء أكبر قدر ممكن من التكوينات في [config.py]. نقوم الآن بتكوين بيئة التطبيق [sqlalchemy]:


def configure():
    # المسار المطلق المرجعي للمسارات النسبية للتكوين
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # المسارات المطلقة للتبعيات
    absolute_dependencies = [
        # BaseEntity، MyException، Personne، Utils
        f"{root_dir}/classes/02/entities",
    ]

    # يتم تعيين مسار النظام
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # الاستيرادات
    from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
    from sqlalchemy.orm import mapper, sessionmaker

    # رابط إلى قاعدة بيانات MySQL
    engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")

    # البيانات الوصفية
    metadata = MetaData()

    # الجدول
    personnes_table = Table("personnes", metadata,
                            Column('id', Integer, primary_key=True),
                            Column('prenom', String(30), nullable=False),
                            Column("nom", String(30), nullable=False),
                            Column("age", Integer, nullable=False),
                            UniqueConstraint('nom''prenom', name='uix_1')
                            )

    # التعيين
    from Personne import Personne

    mapper(Personne, personnes_table, properties={
        'المعرف: personnes_table.c.id،
        'الاسم الأول: personnes_table.c.prenom،
        '«اللقب»: personnes_table.c.nom،
        'العمر: personnes_table.c.age
    })

    # مصنع الجلسات
    Session = sessionmaker()
    Session.configure(bind=engine)

    # نضع هذه المعلومات في ملف التكوين
    config = {}
    config["Session"] = Session
    config["metadata"] = metadata
    config["engine"] = engine
    config["personnes_table"] = personnes_table

    # تكوين الفئات
    from Personne import Personne
    Personne.excluded_keys = ['_sa_instance_state']

    # يتم تفعيل التكوين
    return config

تعليقات

  • الأسطر 2-12: تكوين مسار Python؛
  • الأسطر 14-45: تهيئة بيئة [sqlalchemy]؛
  • الأسطر 47-52: يتم إدراج بيئة [sqlalchemy] في قاموس التكوين؛
  • الأسطر 54-56: تهيئة الفئة [Personne]؛

مع هذا التكوين، يصبح النص البرمجي [main] كما يلي:


# يتم تكوين التطبيق
import config

config = config.configure()

# تم تكوين syspath - نقوم بإجراء عمليات الاستيراد
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.exc import FlushError

from Personne import Personne

session = None
try:
    # جلسة عمل
    session = config["Session"]()

    # حذف الجدول [personnes]
    session.execute("drop table if exists personnes")

    # إعادة إنشاء الجدول من التعيين
    config["metadata"].create_all(config["engine"])

    # عمليتا إدراج
    session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
    personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
    session.add(personne)

    # التحقق من صحة عمليتي الإدراج
    session.commit()

    # استعلام واحد
    personnes = session.query(Personne).all()

    # عرض
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # عمليتا إدراج أخريان، فشلت الثانية منهما
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
    session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))

    # استعلام واحد
    personnes = session.query(Personne).all()

    # عرض
    print("Liste des personnes-----------")
    for personne in personnes:
        print(personne)

    # تأكيد الجلسة
    session.commit()

except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
    # عرض
    print(f"L'erreur suivante s'est produite : {erreur}")
    # إلغاء الجلسة الأخيرة
    if session:
        print("rollback...")
        session.rollback()
finally:
    # عرض
    print("Travail terminé...")
    # تحرير موارد الجلسة
    if session:
        session.close()

نتائج التنفيذ هي كما يلي:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/02/main.py
Liste des personnes-----------
{"âge": 10, "nom": "y", "prénom": "x", "id": 1}
{"âge": 7, "nom": "y1", "prénom": "x1", "id": 2}
L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a session.no_autoflush block if this flush is occurring prematurely)
(mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y2-x2' for key 'uix_1'
[SQL: INSERT INTO personnes (prenom, nom, age) VALUES (%(prenom)s, %(nom)s, %(age)s)]
[parameters: {'prenom': 'x2', 'nom': 'y2', 'age': 10}]
(Background on this error at: http://sqlalche.me/e/13/gkpj)
rollback...
Travail terminé...

Process finished with exit code 0

في phpMyAdmin، أصبحت الجدولة [personnes] كما يلي:

Image

الآن، لنلقِ نظرة على الجدول [personnes] الذي تم إنشاؤه بواسطة [sqlalchemy]:

Image

  • في [6]، الأنواع المستخدمة للأعمدة المختلفة؛
  • في [7]، نرى أن العمود [id] له السمة [AUTO_INCREMENT]. وهذا يعني أنه عند إدراج صف في الجدول، إذا لم يكن لهذا الصف قيمة للعمود [id]، فسيتم إنشاء هذه القيمة بواسطة MySQL بطريقة تصاعدية: 1، 2، 3، … تُغني هذه الخاصية عن الحاجة إلى الاهتمام بقيمة المفتاح الأساسي عند إجراء عملية إدراج في الجدول: فنترك MySQL تتولى إنشاؤها؛
  • في [8]، نلاحظ أن العمود [id] هو المفتاح الأساسي؛
  • في [9]، نجد قيد التفرد على الحقول [nom, prenom]؛

19.4. البرامج النصية 03: معالجة كيانات الجلسة [sqlalchemy]

Image

ملف التكوين [config] هو نفسه الموجود في المثال السابق. في البرنامج النصي [main يتم تنفيذ العمليات التقليدية [INSERT, UPDATE, DELETE, SELECT] على الجدول [personnes] باستخدام أساليب [sqlalchemy]:


# تكوين التطبيق
import config

config = config.configure()

# عمليات الاستيراد
from sqlalchemy import func
from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
from sqlalchemy.orm.session import Session
from Personne import Personne

# عرض محتوى الجدول [personnes]
def affiche_table(session: Session):
    print("----------------")
    # استعلام
    personnes = session.query(Personne).all()
    # عرض
    affiche_personnes(personnes)

# يعرض قائمة بالأشخاص
def affiche_personnes(personnes: list):
    print("----------------")
    # عرض
    for personne in personnes:
        print(personne)


# الرئيسية ---------------------------
session = None
try:
    # جلسة
    session = config["Session"]()

    # حذف الجدول [personnes]
    # checkfirst=True: التحقق أولاً من وجود الجدول
    config["personnes_table"].drop(config["engine"], checkfirst=True)

    # إعادة إنشاء الجدول استنادًا إلى التعيين
    config["metadata"].create_all(config["engine"])

    # عمليات الإدراج
    session.add(Personne().fromdict({"prénom": "Pierre", "nom": "Nicazou", "âge": 35}))
    session.add(Personne().fromdict({"prénom": "Géraldine", "nom": "Colou", "âge": 26}))
    session.add(Personne().fromdict({"prénom": "Paulette", "nom": "Girondé", "âge": 56}))

    # عرض محتوى الجلسة
    affiche_table(session)

    # قائمة بالأشخاص مرتبة أبجديًا حسب الأسماء، وفي حالة تساوي الأسماء، مرتبة أبجديًا حسب الأسماء الأولى
    personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())

    # العرض
    affiche_personnes(personnes)

    # قائمة بالأشخاص الذين تتراوح أعمارهم بين [20,40] مرتبة حسب العمر من الأكبر إلى الأصغر
    # ثم حسب الترتيب الأبجدي للأسماء في حالة تساوي الأعمار، وحسب الترتيب الأبجدي للأسماء الأولى في حالة تساوي الأسماء
    personnes = session.query(Personne). \
        filter(Personne.âge >= 20, Personne.âge <= 40). \
        order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())

    # العرض
    affiche_personnes(personnes)

    # إضافة السيدة برونو
    bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
    session.add(bruneau)
    # تعديل عمرها
    bruneau.âge = 47

    # قائمة بالأشخاص الذين يحملون اسم «برونو»
    personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()

    # عرض
    affiche_personnes([personne])

    # حذف السيدة برونو
    session.delete(personne)

    # قائمة الأشخاص الذين يحملون اسم «برونو»
    personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")

    # عرض
    affiche_personnes(personnes)

    # تأكيد الجلسة
    session.commit()

except (DatabaseError, InterfaceError, IntegrityError) as erreur:
    # عرض
    print(f"L'erreur suivante s'est produite : {erreur}")
    # إلغاء الجلسة الأخيرة
    if session:
        session.rollback()

finally:
    # العرض
    print("Travail terminé...")
    # تحرير موارد الجلسة
    if session:
        session.close()

تعليقات

  • الأسطر 20-25: تعرض الدالة [affiche_personnes] عناصر قائمة بالأشخاص؛
  • الأسطر 12-18: تعرض الدالة [affiche_table] محتوى الجدول [personnes
  • الأسطر 34-36: يتم حذف الجدول [personnes]. على عكس الإصدارات السابقة، لا يتم استخدام الأمر SQL بل طريقة [sqlalchemy]:
    • config["personnes_table"] هو الكائن [Table] الذي يصف الجدول [personnes]؛
    • config["engine"] هي سلسلة الاتصال بقاعدة البيانات [dbpersonnes]؛
    • المعلمة المسماة [checkfirst=True] تشترط ألا تتم العملية إلا في حالة وجود الجدول [personnes]؛
  • السطران 38-39: يتم إعادة إنشاء الجدول [personnes
  • الأسطر 41-44: يتم إدراج ثلاثة أشخاص في الجلسة. تجدر الإشارة إلى أنهم لا يُدرجون بالضرورة على الفور في الجدول [personnes]. يعتمد ذلك على استراتيجية [sqlalchemy] التي تهدف إلى تحسين الأداء؛
  • الأسطر 46-47: يتم عرض محتوى الجدول [personnes]. إذا لم تكن عمليات إدراج الأشخاص الثلاثة قد تمت بعد، فإنها تتم الآن بسبب هذا الطلب؛
  • السطران 49-50: مثال على استخدام الطريقة [order_by] التي تسمح بعرض نتائج الاستعلام بترتيب معين. يعرض بناء الجملة [order_by(critère1, critère2)] النتائج أولاً وفقًا لمعيار [critère1]، وعندما تحتوي الأسطر على نفس قيمة [critère1]، يتم فرزها وفقًا لمعيار [critère2]. يمكن وضع عدة معايير على النحو التالي:
  • السطور 55-59: تُقدِّم مفهوم التصفية باستخدام الطريقة [filter]. ويُشكِّل الترميز [filter(critère1, critère2)] علاقة منطقية (AND) بين المعايير المستخدمة؛
  • الأسطر 64-67: تم تسجيل دخول مستخدم جديد؛
  • الأسطر 70-71: مثال آخر على الاستعلام المُصفى. تقوم الدالة [func.lower(param)] بتحويل [param] إلى أحرف صغيرة. وهناك دوال أخرى متاحة مثل [func.xx]. في التعبير الموجود في السطر 71:
    • تُرجع الدالة [session.query.filter] قائمة من العناصر [Personne]؛
    • تُرجع الدالة [session.query.filter.first] العنصر الأول من هذه القائمة؛
  • السطر 77: يتم حذف عنصر من الجلسة؛
  • السطر 86: يتم التحقق من صحة الجلسة؛

نتائج التنفيذ هي كما يلي:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/03/main.py
----------------
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
{"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0
  • الأسطر 4-6: محتوى الجلسة؛
  • الأسطر 8-10: محتوى الجلسة مرتبة حسب الأسماء من الأكبر إلى الأصغر؛
  • الأسطر 12-13: محتوى الجلسة للأشخاص الذين تتراوح أعمارهم بين [20, 40]؛
  • السطر 15: الشخص الذي يحمل اسم «bruneau»؛

في phpMyAdmin، يكون محتوى الجدول [personnes] عند انتهاء التنفيذ كما يلي:

Image

19.5. النصوص البرمجية 04: استخدام قاعدة بيانات [PostgreSQL]

Image

المجلد [04] هو نسخة من المجلد [03]. يتم تغيير شيء واحد فقط، وهو سلسلة الاتصال في الملف [config]:


    # الارتباط بقاعدة بيانات PostgreSQL
    engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")

أصبح هذا السلسلة الآن تشير إلى قاعدة البيانات [dbpersonnes] الخاصة بـ SGBD و [PostgreSQL]. تجدر الإشارة إلى استخدام الموصل [psycopg2]. يجب أن يكون هذا الموصل مثبتًا.

يؤدي تشغيل البرنامج النصي [main] إلى النتائج التالية:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/04/main.py
----------------
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
{"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
----------------
{"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
----------------
Travail terminé...

Process finished with exit code 0

باستخدام الأداة [pgAdmin] (انظر الفقرة |pgAdmin|)، تكون الجدولة [personnes] في الحالة التالية:

Image

تم إنشاء الجدول [personnes] باستخدام الرمز SQL التالي:

Image

  • في [4-5]، نلاحظ أن العمود [id] هو المفتاح الأساسي. ونلاحظ أيضًا أن له قيمة افتراضية هي [mot clé DEFAULT مما يعني أنه في حالة إدراج صف بدون مفتاح أساسي، فسيتم إنشاء هذا المفتاح بواسطة SGBD. وهذا أسلوب شائع: نترك SGBD يقوم بإنشاء المفاتيح الأساسية؛

تُظهر هذه النسخة 05 من البرامج النصية [sqlalchemy] بوضوح سهولة الانتقال من SGBD إلى الأخرى: فقد كان يكفي تغيير سلسلة الاتصال في برنامج نصي للتكوين. لم يتغير أي شيء آخر. إذا قارنا أنواع أعمدة [id, nom, prenom, age] المذكورة أعلاه بأنواع أعمدة الجدول MySQL في المثال |02|، نلاحظ أنها مختلفة. يقوم [sqlalchemy] بتكييفها مع SGBD المستخدم. هذه السهولة في التكيف مع SGBD جديد هي سبب كافٍ لاعتماد [sqlalchemy] أو أي ORM آخر.

19.6. النصوص البرمجية 05: مثال كامل

Image

المثال الذي ندرسه هو إعادة عرض للمثال الذي تمت دراسته في الفقرة |troiscouches-v01|. كان هذا المثال يعرض بنية ثلاثية الطبقات [ui, métier, dao] تتعامل مع كيانات [Classe, Elève, Matière, Note]. كانت الكيانات مكتوبة بشكل ثابت في طبقة [dao]. سنقوم الآن بوضعها في قاعدة بيانات. سنستخدم طبقتين SGBD: MySQL و PostgreSQL.

19.6.1. بنية التطبيق

ستكون بنية التطبيق على النحو التالي:

Image

  • في [1-3]، نجد الطبقات [ui, métier, dao] الموجودة بالفعل في المثال |troiscouches-v01|. أصبحت الطبقة [dao] تتواصل الآن مع الطبقة [ORM
  • يتم تنفيذ الطبقات [1-5] بواسطة كود لغة Python؛

19.6.2. قواعد البيانات

نقوم بإنشاء قاعدة بيانات MySQL باسم [dbecole] مملوكة للمستخدم [admecole] الذي لديه كلمة المرور [mdpecole]. ولذلك، نتبع الإجراء الموضح في الفقرة |إنشاء قاعدة بيانات|:

Image

Image

  • في [1]، قاعدة البيانات [dbecole] بدون الجداول [3]؛
  • إلى [7]، ويتمتع المستخدم [admecole] بجميع الامتيازات على قاعدة البيانات هذه؛

ونفعل الشيء نفسه مع قاعدة البيانات SGBD و PostgreSQL. نقوم بإنشاء قاعدة بيانات باسم [dbecole] مملوكة للمستخدم [admecole] الذي يستخدم كلمة المرور [mdpecole]. وللقيام بذلك، نتبع الإجراء الموضح في الفقرة |إنشاء قاعدة بيانات|:

Image

  • في [1]، قاعدة البيانات [dbecole]؛
  • إلى [2]، والمستخدم [admecole]؛
  • في [3-4]، قاعدة البيانات [dbecole] مملوكة للمستخدم [admecole]؛

19.6.3. الكيانات التي تعالجها التطبيق

في التطبيق |troiscouches v01|، كانت الكيانات التي يتم التعامل معها هي التالية (انظر |الكيانات|). هذه هي الكيانات التي سيتم تخزينها في قواعد البيانات السابقة. لن نقوم بتكرار هذه الكيانات في التطبيق الجديد. سنقوم باستردادها من المكان الذي تم تعريفها فيه بالفعل.

الفئة [Classe]:


# عمليات الاستيراد
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Classe(BaseEntity):
    # السمات المستبعدة من حالة الفئة
    excluded_keys = []

    # خصائص الفئة
    @staticmethod
    def get_allowed_keys() -> list:
        # المعرف: معرف الفئة
        # الاسم: اسم الفئة
        return BaseEntity.get_allowed_keys() + ["nom"]

    # مُسترد
    @property
    def nom(self: object) -> str:
        return self.__nom

    # المعلمات المُعيّنة
    @nom.setter
    def nom(self: object, nom: str):
        # يجب أن يكون الاسم سلسلة أحرف غير فارغة
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")

الفئة [Elève]:


# الاستيرادات
from BaseEntity import BaseEntity
from Classe import Classe
from MyException import MyException

from Utils import Utils


class Elève(BaseEntity):
    # السمات المستبعدة من حالة الفئة
    excluded_keys = []

    # خصائص الفئة
    @staticmethod
    def get_allowed_keys() -> list:
        # id: معرّف الطالب
        # اللقب: لقب الطالب
        # الاسم الأول: الاسم الأول للطالب
        # الصف: الصف الذي ينتمي إليه الطالب
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]

    # المُستردات
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def prénom(self: object) -> str:
        return self.__prénom

    @property
    def classe(self: object) -> Classe:
        return self.__classe

    #  وظائف التعيين
    @nom.setter
    def nom(self: object, nom: str) -> str:
        # يجب أن يكون الاسم سلسلة أحرف غير فارغة
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @prénom.setter
    def prénom(self: object, prénom: str) -> str:
        # الاسم الأول يجب أن يكون سلسلة أحرف غير فارغة
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom
        else:
            raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")

    @classe.setter
    def classe(self: object, value):
        try:
            # يُتوقع نوع «فئة»
            if isinstance(value, Classe):
                self.__classe = value
            # أو نوع dict
            elif isinstance(value,dict):
                self.__classe=Classe().fromdict(value)
            # أو نوع json
            elif isinstance(value,str):
                self.__classe = Classe().fromjson(value)
        except BaseException as erreur:
            raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json. Erreur : {erreur}")

الفئة [Matière]:


# استيرادات
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils


class Matière(BaseEntity):
    # السمات المستبعدة من حالة الفئة
    excluded_keys = []

    # خصائص الفئة
    @staticmethod
    def get_allowed_keys() -> list:
        # id: معرّف المادة
        # الاسم: اسم المادة
        # المعامل: معامل المادة
        return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]

    # مُسترد
    @property
    def nom(self: object) -> str:
        return self.__nom

    @property
    def coefficient(self: object) -> float:
        return self.__coefficient

    # المعلمات المُعيّنة
    @nom.setter
    def nom(self: object, nom: str):
        # يجب أن يكون الاسم سلسلة أحرف غير فارغة
        if Utils.is_string_ok(nom):
            self.__nom = nom
        else:
            raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")

    @coefficient.setter
    def coefficient(self, coefficient: float):
        # يجب أن يكون المعامل عددًا حقيقيًا >=0
        erreur = False
        if isinstance(coefficient, (int, float)):
            if coefficient >= 0:
                self.__coefficient = coefficient
            else:
                erreur = True
        else:
            erreur = True
        # خطأ؟
        if erreur:
            raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")

الفئة [Note]:


# استيرادات
from BaseEntity import BaseEntity
from Elève import Elève
from Matière import Matière
from MyException import MyException


class Note(BaseEntity):
    # السمات المستبعدة من حالة الفئة
    excluded_keys = []

    # خصائص الفئة
    @staticmethod
    def get_allowed_keys() -> list:
        # المعرف: معرف الملاحظة
        # القيمة: الدرجة نفسها
        # الطالب: الطالب (من النوع «طالب») المعني بالدرجة
        # المادة: المادة (من النوع «مادة») التي تتعلق بها الدرجة
        # وبالتالي، فإن كائن «Note» يمثل الدرجة التي حصل عليها الطالب في مادة ما
        return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]

    # وظائف الاسترجاع
    @property
    def valeur(self: object) -> float:
        return self.__valeur

    @property
    def élève(self: object) -> Elève:
        return self.__élève

    @property
    def matière(self: object) -> Matière:
        return self.__matière

    # دالات الاسترجاع
    @valeur.setter
    def valeur(self: object, valeur: float):
        # يجب أن تكون الدرجة عددًا حقيقيًّا يتراوح بين 0 و20
        if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
            self.__valeur = valeur
        else:
            raise MyException(31,
                f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")

    @élève.setter
    def élève(self: object, value):
        try:
            # يُتوقع أن يكون النوع «Elève»
            if isinstance(value, Elève):
                self.__élève = value
            # أو نوع «dict»
            elif isinstance(value, dict):
                self.__élève = Elève().fromdict(value)
            # أو نوع json
            elif isinstance(value, str):
                self.__élève = Elève().fromjson(value)
        except BaseException as erreur:
            raise MyException(32,
                f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")

    @matière.setter
    def matière(self: object, value):
        try:
            # يُتوقع أن يكون النوع «Matière»
            if isinstance(value, Matière):
                self.__matière = value
            # أو نوع dict
            elif isinstance(value, dict):
                self.__matière = Matière().fromdict(value)
            # أو نوع json
            elif isinstance(value, str):
                self.__matière = Matière().fromjson(value)
        except BaseException as erreur:
            raise MyException(33,
                f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur : {erreur}")

19.6.4. التكوين

Image

تم تقسيم التكوين إلى عدة ملفات:

  • التكوين العام في [config.py]: يحدد مسار Python للتطبيق ويقوم بإنشاء مثيلات لطبقات البنية؛
  • تكوين [sqlalchemy] في [config_database]: يقوم بإجراء التعيينات بين الفئات والجداول؛
  • يتم تكوين طبقات التطبيق في الملف [config_layers]؛

الملف [config] هو كما يلي:


def configure(config: dict) -> dict:
    import os

    # الخطوة 1 ---
    # يتم تعيين مسار Python للتطبيق
    # المسار المطلق لمجلد هذا البرنامج النصي
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # المسار المطلق الذي يشير إلى المسارات النسبية للتكوين
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # المسارات المطلقة للتبعيات
    absolute_dependencies = [
        # BaseEntity، MyException
        f"{root_dir}/classes/02/entities",
        # مشروع ثلاثي الطبقات v01
        f"{root_dir}/troiscouches/v01/interfaces",
        f"{root_dir}/troiscouches/v01/services",
        f"{root_dir}/troiscouches/v01/entities",
        # ملفات هذا المشروع
        script_dir,
        f"{script_dir}/../services",
    ]

    # تحديث مسار النظام
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # الخطوة 2 ------
    # تكوين قاعدة البيانات
    import config_database
    config = config_database.configure(config)

    # الخطوة 3 ------
    # إنشاء مثيلات طبقات التطبيق
    import config_layers
    config = config_layers.configure(config)

    # تطبيق التكوين
    return config
  • الأسطر 4-27: إنشاء مسار Python للتطبيق؛
  • الأسطر 29-32: تكوين [sqlalchemy]؛
  • الأسطر 34-37: تكوين طبقات التطبيق؛

الملف [config_database] هو كما يلي:


def configure(config: dict) -> dict:
    # التكوين ['sgbd'] هو اسم SGBD المستخدم
    # mysql: MySQL
    # pgres: PostgreSQL

    # تكوين sqlalchemy
    from sqlalchemy import Table, Column, Integer, MetaData, String, Float, ForeignKey, create_engine

    from sqlalchemy.orm import mapper, relationship, sessionmaker

    # سلاسل الاتصال بقواعد البيانات المستخدمة
    engines = {
        'mysql': "mysql+mysqlconnector://admecole:mdpecole@localhost/dbecole",
        'pgres': "postgresql+psycopg2://admecole:mdpecole@localhost/dbecole"
    }
    # سلسلة اتصال بقاعدة البيانات المستخدمة
    engine = create_engine(engines[config['sgbd']])

    # البيانات الوصفية
    metadata = MetaData()

    # جداول قاعدة البيانات
    tables = {}
    # الفئات المُعينة
    from Classe import Classe
    from Elève import Elève
    from Note import Note
    from Matière import Matière

    # جدول الفئات
    tables['classes'] = classes_table = \
        Table("classes", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              )

    mapper(Classe, tables['classes'], properties={
        'المعرف: classes_table.c.id،
        'الاسم: classes_table.c.nom
    })

    # جدول الطلاب
    tables['élèves'] = élèves_table = \
        Table("élèves", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              Column('prénom', String(30), nullable=False),
              # ينتمي الطالب إلى فصل دراسي
              Column('classe_id', Integer, ForeignKey('classes.id')),
              )
    # التعيين
    mapper(Elève, tables['élèves'], properties={
        'المعرف: élèves_table.c.id،
        'الاسم: élèves_table.c.nom،
        'الاسم الأول: élèves_table.c.prénom،
        'الفصل: relationship(الفصل, backref="الطلاب", lazy="select")
    })

    # جدول المحتويات
    tables['matières'] = matières_table = \
        Table("matières", metadata,
              Column('id', Integer, primary_key=True),
              Column('nom', String(30), nullable=False),
              Column('coefficient', Float, nullable=False)
              )
    # التعيين
    mapper(Matière, tables['matières'], properties={
        'id': matières_table.c.id,
        'الاسم: matières_table.c.nom،
        "coefficient": matières_table.c.coefficient
    })

    # جدول الدرجات
    tables['notes'] = notes_table = \
        Table("notes", metadata,
              Column('id', Integer, primary_key=True),
              Column('valeur', Float, nullable=False),
              # الدرجة هي درجة أحد الطلاب
              Column('élève_id', Integer, ForeignKey('élèves.id')),
              # الدرجة هي درجة مادة دراسية
              Column('matière_id', Integer, ForeignKey('matières.id')),
              )

    # التعيين
    mapper(Note, tables['notes'], properties={
        'المعرف: notes_table.c.id،
        'القيمة: notes_table.c.valeur،
        'الطالب': relationship(الطالب, backref="notes", lazy="select"),
        '«المادة»: relationship(Matière, backref="notes", lazy="select")
    })

    # تكوين الكيانات [BaseEntity]
    Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
    Classe.excluded_keys = ['_sa_instance_state', 'élèves']
    Matière.excluded_keys = ['_sa_instance_state', 'notes']
    Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']

    # مصنع الجلسات
    Session = sessionmaker()
    Session.configure(bind=engine)

    # جلسة عمل
    session = Session()

    # يتم تسجيل بعض المعلومات في قاموس التكوين
    config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}

    # يتم تفعيل التكوين
    return config

تعليقات

  • الأسطر 1-4: تستقبل الدالة [configure] قاموسًا كمعلمة. ولا يتم استخدام سوى المفتاح [sgbd]. وتكون قيمتها [mysql] إذا كانت قاعدة البيانات هي MySQL، و[pgres] إذا كانت قاعدة البيانات هي PostgreSQL؛
  • الأسطر 6-9: استيراد عناصر من [sqlalchemy]. يقوم البرنامج النصي [config_database] بإجراء التعيينات بين جداول قاعدة البيانات [dbecole] وكيانات [Classes, Elève, Matière, Note]. في الجدول، يتم تغليف بيانات الكيان في سطر واحد. أما في كود Python، فهي مغلفة في كائن. ومن هنا جاء الاسم ORM (Object Relational Mapper): حيث يقوم البرنامج ORM بإجراء ربط بين الأسطر في قاعدة البيانات العلائقية والكائنات. في هذا التطبيق، لدينا أربع كيانات [Classe, Elève, Matière, Note] سيتم ربطها بأربع جداول [classes, élèves, matières, notes]. لاحظ أن أسماء الجداول قد تحتوي على أحرف ذات علامات تشكيل؛
  • السطور 11-17: سلسلة الاتصال بقاعدة البيانات المستخدمة. وتعتمد هذه السلسلة على العنصر config[‘sgbd’]؛
  • الأسطر 24-28: كيانات التطبيق التي ستخضع لعملية التعيين [sqlalchemy]. عند تنفيذ هذه الأسطر، سيكون مسار Python قد تم تحديده مسبقًا بواسطة البرنامج النصي [config]؛
  • الأسطر 30-40: التعيين بين الكيان [Classe] والجدول [classes]؛
  • الأسطر 30-35: يتم تعريف الجدول [classes] باستخدام الفئة [Table] من [sqlalchemy]. ونشير إلى أن هذا الجدول يحتوي على عمودين:
    • العمود [id] الذي يمثل المفتاح الأساسي وهو رقم الفئة، السطر 33؛
    • العمود [nom] الذي يحتوي على اسم الفئة، السطر 34؛
  • السطران 31-32: لاحظ أن صيغة x=y=z صحيحة في لغة Python: يتم تعيين قيمة z إلى y ثم قيمة y إلى x؛
  • الأسطر 37-40: يتم سرد التوافقات بين أعمدة الجدول [classes] وخصائص الكيان [Classe]؛
  • الأسطر 42-57: التعيين بين الكيان [Elève] والجدول [élèves]؛
  • الأسطر 51-57: تم تعريف الجدول [élèves] باستخدام الفئة [Table] التابعة لـ [sqlalchemy]. ونشير إلى أن هذا الجدول يحتوي على أربعة أعمدة:
    • العمود [id] الذي يمثل المفتاح الأساسي وهو رقم الطالب، السطر 45؛
    • العمود [nom] الذي يحتوي على اسم الطالب، السطر 46؛
    • العمود [prénom] الذي يحتوي على الاسم الأول للطالب، السطر 47. لاحظ أن اسم العمود قد يحتوي على أحرف ذات علامات تشكيل؛
    • السطر 49، العمود [classe_id] الذي سيحتوي على رقم الفصل الذي ينتمي إليه الطالب. وهذا ما يُسمى بالمفتاح الأجنبي. [élèves.classe_id] هو مفتاح أجنبي (ForeignKey) في العمود [classes.id]. وهذا يعني أن قيمة [élèves.classe_id] يجب أن تكون موجودة في العمود [classes.id]؛
  • السطور 51-57: يتم سرد التوافقات بين أعمدة الجدول [élèves] وخصائص الكيان [Elève]:
    • السطور 53-55 سهلة الفهم؛
    • أما السطر 56 فهو أكثر تعقيدًا: فهو يحدد قيمة الخاصية [Elève.classe] على أنها تُحسب من خلال علاقة المفتاح الأجنبي التي تربط بين الجدولين [élèves] و [classes]. معلمات الدالة [relationship] هي كما يلي:
      • [Classe]: هذا هو اسم الكيان الذي ترتبط به الكيان [Elève] بعلاقة مفتاح أجنبي. ويجب أن تتجسد هذه العلاقة في الجدول [élèves] من خلال وجود مفتاح أجنبي في الجدول [classes]. ونحن نعلم أن هذا المفتاح موجود؛
      • [backref="élèves"]: اسم خاصية سيتم إضافتها إلى الكيان [Classe]. وسيكون [Classe.élèves] هو قائمة بجميع طلاب الفصل. يجب ألا تكون هذه الخاصية موجودة مسبقًا. إذا كانت موجودة بالفعل، فيجب ببساطة اختيار اسم آخر هنا لـ [backref]. لا يتعين على المطور إدارة هذه الخاصية. ستقوم [sqlalchemy] بذلك. عليه فقط أن يعلم بوجودها، بعد أن تمت إضافتها بواسطة [sqlalchemy]، وأنه يمكنه استخدامها في كوده؛
      • [lazy=’select’]: هذا يعني أن ORM لا يجب أن يسعى إلى إعطاء قيمة للخاصية [Elève.classe] على الفور. ولا يجب أن يبحث عن قيمتها إلا عندما يطلبها الكود صراحةً. وبالتالي:
      • إذا طلب الكود قائمة بجميع الطلاب، فسيتم إرجاعهم ولكن لن يتم حساب خاصيتهم [classe]؛
      • وبعد ذلك بقليل، يهتم الكود بطالب معين [e] ويشير إلى فصله [e.classe]. سيؤدي هذا الإشارة إذن إلى إجبار [sqlalchemy] على إجراء استعلام في قاعدة البيانات لجلب فصل الطالب، وذلك بطريقة شفافة بالنسبة للمطور؛
      • كما يهدف إدراج [lazy=’select’] إلى تجنب الاستعلامات غير الضرورية في قاعدة البيانات؛
  • السطر 56: عندما يسترد ORM صفًّا من الجدول [élèves]، فإنه يسترد المعلومات الواردة في [id, nom, prénom, classe_id]. ومن هناك، يجب عليه إنشاء كائن «طالب» (id، الاسم، الاسم الأول، الفصل). بالنسبة لخصائص [id, nom, prénom لا يمثل ذلك أي صعوبة. أما بالنسبة للخاصية [classe]، فالأمر أكثر تعقيدًا. فقيمتها عبارة عن مرجع لكائن من النوع [Classe]. إلا أن كائن ORM لا يحتوي إلا على معلومة واحدة هي [élèves.classe_id]. ونظرًا لأن [élèves.classe_id] يمثل مفتاحًا خارجيًا في العمود [classes.id]، فإننا نطلب منه هنا استخدام هذه العلاقة لاسترداد السطر ذي الرقم التعريفي id=[élèves.classe_id] (وهو موجود بالضرورة) وإنشاء الكائن [Classe] المطلوب من قبل الخاصية [Elève.classe] انطلاقًا من هذا السطر؛
  • الأسطر 59-71: التعيين بين الكيان [Matière] والجدول [matières]؛
  • الأسطر 59-65: تعريف الجدول [sqlalchemy] المسمى [matières]؛
  • الأسطر 66-71: يتم سرد التوافقات بين أعمدة الجدول [matières] وخصائص الكيان [Matière]. لا توجد صعوبات هنا؛
  • الأسطر 73-90: التعيين بين الكيان [Note] والجدول [notes]؛
  • الأسطر 73-82: تعريف الجدول [sqlalchemy] المسمى [notes]. ويحتوي على مفتاحين خارجيين:
    • السطر 79، يستمد العمود [notes.élève_id] قيمه من العمود [élèves.id]]. ويجسد هذا المفتاح الأجنبي حقيقة أن الملاحظة هي ملاحظة تابعة لطالب معين؛
    • السطر 81، العمود [notes.matière_id] يأخذ قيمه من العمود [matières.id]. هذا المفتاح الخارجي يجسد حقيقة أن الدرجة هي درجة في مادة معينة؛
  • الصفوف 84-90: التعيين بين الكيان [Note] والجدول [notes]:
    • السطر 88: يجب أن تكون قيمة الخاصية [Note.élève] مثيلًا من النوع [Elève]. لا تحتوي ORM في سطر الجدول [notes] إلا على العمود [notes.élève_id] الذي يشير إلى العمود [élèves.id]. يُطلب هنا استخدام علاقة المفتاح الأجنبي هذه لاسترداد المثيل [Elève] الذي تتوفر لدينا درجته. من ناحية أخرى، ستقوم [relationship(Elève, backref="notes", …)] بإنشاء الخاصية الجديدة [Elève.notes] التي ستكون قائمة درجات الطالب. يجب ألا تكون هذه الخاصية موجودة مسبقًا في الفئة [Elève]؛
    • السطر 89: يجب أن تكون قيمة الخاصية [Note.matière] مثيلًا من النوع [Matière]. لا تحتوي ORM في سطر الجدول [notes] إلا على العمود [notes.matière_id] الذي يشير إلى العمود [matières.id]. ويُطلب هنا استخدام علاقة المفتاح الأجنبي هذه لاسترداد المثيل [Matière] الذي تتوفر لدينا ملاحظته. من ناحية أخرى، سيقوم [relationship(Matière, backref="notes", …)] بإنشاء الخاصية الجديدة [Matière.notes] التي ستكون قائمة الملاحظات في المادة. يجب ألا تكون هذه الخاصية موجودة مسبقًا في الفئة [Matière]؛
  • الأسطر 92-96: نحدد لكل كيان مشتق من [BaseEntity] قائمة الخصائص التي يجب استبعادها من قاموس خصائص الكيان (BaseEntity.asdict). لقد رأينا أن [sqlalchemy] أضافت الخاصية [_sa_instance_state] إلى جميع الكيانات المُعَدَّلة. ولا نريدها في قاموس الخصائص. من ناحية أخرى، رأينا أن عمليات التعيين السابقة أضافت خصائص جديدة إلى الكيانات:
    • [Elève.notes]: جميع درجات الطالب؛
    • [Classe.élèves]: جميع طلاب الفصل؛
    • [Matière.notes]: جميع درجات المادة؛

بشكل عام، لا نريد إضافة هذه الخصائص إلى حالة الكيان. ففي الواقع، حساب قيمتها ينطوي على تكلفة SQL، وغالبًا ما تكون هذه القيمة غير مفيدة. لذا، إذا استدعينا الطالب الذي يحمل الاسم «X»:

  • (تابع)
    • فإن ORM ستُرجع كيانًا [Elève(id, nom, prénom, classe, notes)]. وبسبب [lazy=’select’]، لن يتم حساب الخصائص [classe, notes] المرتبطة بالمفاتيح الخارجية للقاعدة؛
    • والآن إذا قمت بعرض السلسلة jSON الخاصة بهذا الطالب، فمن المعروف أنها ستكون السلسلة jSON من قاموس [asdict] الخاص بالكيان. إذا كانت الخصائص [classe] و [notes] موجودة فيها، فسيضطر [sqlalchemy] إلى إجراء استعلامات SQL لحساب قيمهما. وهذا مكلف. وإذا أمكن تجنب هذه الاستعلامات، فهذا أفضل؛
    • هنا، قمنا باستبعاد جميع الخصائص المرتبطة بمفتاح أجنبي؛
  • الأسطر 98-100: إنشاء مثيل وتكوين كائن [Session factory] (factory=مصنع). يُستخدم الكائن [Session] لإنشاء جلسات [sqlalchemy] مرتبطة بمعاملات؛
  • الأسطر 102-103: إنشاء جلسة عمل sqlalchemy
  • السطر 106: يتم وضع بعض عناصر التكوين [sqlalchemy] في القاموس العام لتكوين التطبيق؛
  • السطر 109: يتم إرجاع هذا القاموس؛

يقوم الملف [config_layers] بتكوين طبقات التطبيق:


def configure(config: dict) -> dict:
    # إنشاء مثيل لطبقة [dao]
    from DatabaseDao import DatabaseDao
    dao = DatabaseDao(config)

    # إنشاء مثيل للطبقة [métier]
    from Métier import Métier
    métier = Métier(dao)

    # إنشاء مثيل للطبقة [ui]
    from Console import Console
    ui = Console(métier)

    # إدراج الطبقات في التكوين
    config['dao'] = dao
    config['métier'] = métier
    config['ui'] = ui

    # يتم إعادة التكوين
    return config
  • السطر 1: تستقبل الدالة [configure] قاموس التكوين العام للتطبيق؛
  • الأسطر 2-12: يتم إنشاء مثيلات لطبقات التطبيق؛
  • الأسطر 15-17: يتم وضع مراجع الطبقات في التكوين العام؛
  • السطر 20: يتم إرجاع التكوين الجديد؛

19.6.5. الطبقة [dao] - 1

Image

يجب أن نفهم هنا أن الطبقة [dao] [3] تتواصل معORM [sqlalchemy] [4] التي تم تكوينها كما هو موضح في الفقرة السابقة. من بين الطبقات الثلاث [ui, métier, dao] للتطبيق |troiscouches v01|، يجب إعادة كتابة الطبقة [dao] فقط. أما الطبقات [ui, métier] فسيتم الاحتفاظ بها.

تم وضع تنفيذ الطبقة [dao] في المجلد [services]:

Image

[InterfaceDatabaseDao] هي واجهة الطبقة [dao]:


from abc import ABC, abstractmethod

from InterfaceDao import InterfaceDao


class InterfaceDatabaseDao(InterfaceDao, ABC):

    # تهيئة قاعدة البيانات
    @abstractmethod
    def init_database(self, data: dict):
        pass
  • السطر 6: واجهة [InterfaceDatabaseDao] مشتقة في آن واحد من الفئة [ABC] لتكون فئة مجردة، ومن واجهة [InterfaceDao] الخاصة بمشروع |troiscouches v01|؛
  • الأسطر 8-11: تُضاف الطريقة [init_database] إلى الطرق الموروثة من [InterfaceDao]. وستكون مهمتها تهيئة قاعدة البيانات ببيانات القاموس [data] التي يتم تمريرها إليها كمعلمة في السطر 10؛

تجدر الإشارة إلى أن واجهة [InterfaceDao] كانت كما يلي:


# عمليات الاستيراد
from abc import ABC, abstractmethod

# واجهة Dao
from Elève import Elève


class InterfaceDao(ABC):
    # قائمة الفصول
    @abstractmethod
    def get_classes(self: object) -> list:
        pass

    # قائمة الطلاب
    @abstractmethod
    def get_élèves(self: object) -> list:
        pass

    # قائمة المواد الدراسية
    @abstractmethod
    def get_matières(self: object) -> list:
        pass

    # قائمة الدرجات
    @abstractmethod
    def get_notes(self: object) -> list:
        pass

    # قائمة درجات الطالب
    @abstractmethod
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        pass

    # البحث عن طالب باستخدام معرّفه
    @abstractmethod
    def get_élève_by_id(self: object, élève_id: int) -> Elève:
        pass

وفيما يلي تنفيذ الطبقة [dao]:


from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note


class DatabaseDao(InterfaceDatabaseDao):

    def __init__(self, config: dict):
        # قاعدة البيانات = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

  • السطر 11: تقوم الفئة [DatabaseDao] بتنفيذ الواجهة [InterfaceDatabaseDao]؛
  • الأسطر 13-16: منشئ الفئة. يتلقى كمعلمة قاموس تكوين التطبيق؛
  • السطر 15: يتم تخزين التكوين [sqlalchemy]؛
  • السطر 16: يتم تخزين الجلسة [sqlalchemy] التي سيتم من خلالها التعامل مع قاعدة البيانات؛
  • السطر 18: تقوم الطريقة [init_database] بتهيئة قاعدة البيانات باستخدام قاموس [data]؛

يتم تنفيذ القاموس [data] بواسطة البرنامج النصي [data.py] التالي:


def configure():
    from Classe import Classe
    from Elève import Elève
    from Matière import Matière
    from Note import Note

    # يتم إنشاء مثيلات للفئات
    classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
    classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
    classes = [classe1, classe2]
    # المواد الدراسية
    matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
    matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
    matières = [matière1, matière2]
    # الطلاب
    élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
    élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
    élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
    élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
    élèves = [élève11, élève21, élève32, élève42]
    # درجات الطلاب في المواد المختلفة
    note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
    note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
    note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
    note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
    note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
    note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
    note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
    note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
    notes = [note1, note2, note3, note4, note5, note6, note7, note8]
    # يتم تجميع المجموعة
    data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
    # يتم عرض البيانات
    return data
  • السطر 34: القاموس الذي سيتم تمريره إلى الأسلوب [init_database]. يتكون هذا القاموس من المفاتيح التالية (السطر 32):
    • [élèves]: قائمة الطلاب؛
    • [classes]: قائمة الفصول؛
    • [matières]: قائمة المواد الدراسية؛
    • [notes]: قائمة درجات جميع الطلاب في جميع المواد الدراسية؛

لنعد إلى الأسلوب [init_database]:


def init_database(self, data: dict):
        # تكوين قاعدة البيانات
        database = self.database
        engine = database['engine']
        metadata = database['metadata']
        tables = database['tables']

        try:
            # حذف الجداول الموجودة
            # checkfirst=True: التحقق أولاً من وجود الجدول
            tables["notes"].drop(engine, checkfirst=True)
            tables["matières"].drop(engine, checkfirst=True)
            tables["élèves"].drop(engine, checkfirst=True)
            tables["classes"].drop(engine, checkfirst=True)

            # إعادة إنشاء الجداول بناءً على التعيين
            metadata.create_all(engine)

            # ملء الجداول
            session = self.session

            # الفئات
            classes = data["classes"]
            for classe in classes:
                session.add(classe)

            # المواد الدراسية
            matières = data["matières"]
            for matière in matières:
                session.add(matière)

            # الطلاب
            élèves = data["élèves"]
            for élève in élèves:
                session.add(élève)

            # الدرجات
            notes = data["notes"]
            for note in notes:
                session.add(note)

            # التثبيت
            session.commit()
        except (DatabaseError, InterfaceError, IntegrityError) as erreur:
            # إلغاء الجلسة
            if session:
                session.rollback()
            # إعادة توجيه الاستثناء
            raise MyException(23, f"{erreur}")
  • الأسطر 3-6: يتم استرداد المعلومات من تكوين قاعدة البيانات؛
  • الأسطر 9-14: رأينا أن التكوين [sqlalchemy] قد ربط أربع كيانات بأربع جداول [élèves, matières, classes, notes]. نبدأ بحذف هذه الجداول إن وجدت؛
  • الأسطر 16-17: نُعيد إنشاء الجداول الأربعة التي قمنا بحذفها للتو؛
  • الأسطر 22-25: نضع جميع الفئات في الجلسة؛
  • الأسطر 27-30: نضيف جميع المواد الدراسية إلى الدورة؛
  • الأسطر 32-35: نضع جميع الطلاب في الدورة الدراسية؛
  • الأسطر 37-40: نضيف جميع الدرجات إلى الدورة الدراسية؛
  • لإجراء هذه الإضافات، اتبعنا ترتيبًا معينًا. بدأنا بالكيانات التي لا ترتبط بكيانات أخرى، وانتهينا بتلك التي ترتبط بها. وهكذا، عند إضافة الطلاب إلى الدورة الدراسية، تكون الفصول التي ينتمون إليها موجودة بالفعل في الدورة الدراسية؛
  • السطر 43: تم التحقق من صحة الجلسة [sqlalchemy]. بعد هذه العملية، نكون على يقين من أن جميع البيانات الموجودة في الجلسة قد تمت مزامنتها مع قاعدة البيانات. وبعبارة أوضح، فقد تم إدراجها في الجداول. وقد تم ذلك بفضل عمليات التعيين التي تم إجراؤها في تكوين [sqlalchemy]. يعرف [sqlalchemy] كيفية تخزين كل كيان في الجداول. كما قام [sqlalchemy] بإنشاء المفاتيح الخارجية التي قد تحتوي عليها الجداول؛
  • الأسطر 44-49: في حالة مواجهة مشكلة، يتم إلغاء جلسة [sqlalchemy]، وفي السطر 49، يتم إثارة استثناء؛

19.6.6. تهيئة قاعدة البيانات

Image

يقوم البرنامج النصي [main_init_database] بتهيئة قاعدة البيانات بمحتوى البرنامج النصي [data.py]. وفيما يلي كوده:


# في انتظار معلمة mysql أو pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# يتم تكوين التطبيق
import config
config = config.configure({'sgbd': sgbd})

# تم تكوين syspath - يمكن إجراء عمليات الاستيراد
from MyException import MyException

# يتم استرداد البيانات المراد إدخالها في قاعدة البيانات
import data
data = data.configure()

# يتم استرداد الطبقة [dao]
dao = config["dao"]

# ----------- الرئيسي
try:
    # إنشاء وتهيئة جداول قاعدة البيانات
    dao.init_database(data)
except MyException as ex:
    # يتم عرض الخطأ
    print(f"L'erreur suivante s'est produite : {ex}")
finally:
    # تحرير الموارد التي استخدمها التطبيق
    import shutdown
    shutdown.execute(config)
# النهاية
print("Travail terminé...")
  • الأسطر 1-11: ينتظر البرنامج النصي معلمة [mysql] أو [pgres] حسب ما إذا كنا نريد تهيئة قاعدة بيانات MySQL أو PostgreSQL؛
  • الأسطر 13-15: تم تكوين التطبيق لـ SGBD الذي تم تمريره كمعلمة؛
  • الأسطر 20-22: يتم استرداد البيانات المراد إدخالها في قاعدة البيانات؛
  • السطر 25: تم بالفعل إنشاء مثيل لطبقة [dao] ويمكن الوصول إليها في إعدادات التطبيق؛
  • السطر 30: يتم تهيئة قاعدة البيانات؛
  • الأسطر 34-37: سواء حدث خطأ أم لا، يتم تحرير موارد التطبيق باستخدام الوحدة النمطية [shutdown]؛

وحدة [shutdown.py] هي كما يلي:


def execute(config: dict):
    # تحرير الموارد التي استخدمها التطبيق
    sqlalchemy_session = config['database']['session']
    if sqlalchemy_session:
        sqlalchemy_session.close()

تقوم الدالة [shutdown.execute] بإغلاق الجلسة [sqlalchemy] المستخدمة لتهيئة قاعدة البيانات.

نقوم بإنشاء تكوين تشغيل أول (انظر |تكوين التشغيل|) لتنفيذ [main_init_database] مع SGBD و MySQL:

Image

نتائج تنفيذ هذا التكوين هي كما يلي في phpMyAdmin:

Image

Image

Image

بالنسبة لـ SGBD و [PostgreSQL]، نستخدم التكوين التالي للتنفيذ:

Image

عند التنفيذ، تكون النتائج في [pgAdmin] كما يلي:

Image

Image

Image

تجدر الإشارة إلى السهولة التي تم بها تغيير SGBD.

19.6.7. الطبقة [dao] – 2

نعود إلى الفئة [DatabaseDao] التي تنفذ الطبقة [dao]. لم نعرض حتى الآن سوى تنفيذ الأسلوب [init_database]. نعرض الآن تنفيذ الأساليب الأخرى:


from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Classe import Classe
from Elève import Elève
from InterfaceDatabaseDao import InterfaceDatabaseDao
from Matière import Matière
from MyException import MyException
from Note import Note


class DatabaseDao(InterfaceDatabaseDao):

    def __init__(self, config: dict):
        # قاعدة البيانات = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
        self.database = config['database']
        self.session = self.database['session']

    def init_database(self, data: dict):
        

    # قائمة بجميع الفئات
    def get_classes(self: object) -> list:
        # الاستعلام
        return self.session.query(Classe).all()

    # قائمة بجميع الطلاب
    def get_élèves(self: object) -> list:
        # استعلام
        return self.session.query(Elève).all()

    # قائمة بجميع المواد الدراسية
    def get_matières(self: object) -> list:
        # الاستعلام
        return self.session.query(Matière).all()

    # قائمة درجات جميع الطلاب
    def get_notes(self: object) -> list:
        # الاستعلام
        return self.session.query(Note).all()

    # قائمة درجات طالب معين
    def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
        # البحث عن الطالب - يتم إصدار استثناء إذا لم يكن موجودًا
        # يُسمح لها بالصعود
        élève = self.get_élève_by_id(élève_id)
        # يتم استرداد درجاته (التحميل المؤجل)
        notes = élève.notes
        # يتم إرجاع قاموس
        return {"élève": élève, "notes": notes}

    # تم تحديد الطالب برقمه
    def get_élève_by_id(self, élève_id: int) -> Elève:
        # نبحث عن الطالب
        élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
        # هل تم العثور عليه؟
        if élèves:
            return élèves[0]
        else:
            raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")

    # طالب تم تحديده بواسطة اسمه
    def get_élève_by_name(self, élève_name: str) -> Elève:
        # نبحث عن الطالب
        élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
        # هل تم العثور عليه؟
        if élèves:
            return élèves[0]
        else:
            raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")

    # فصل تم تحديده برقمه
    def get_classe_by_id(self, classe_id: int) -> Classe:
        # نبحث عن الفصل
        classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
        # هل تم العثور عليها؟
        if classes:
            return classes[0]
        else:
            raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")

    # فصل محدد برقمه
    def get_classe_by_name(self, classe_name: str) -> Classe:
        # نبحث عن الفصل
        classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
        # هل تم العثور عليها؟
        if classes:
            return classes[0]
        else:
            raise MyException(14, f"La classe de nom {classe_name} n'existe pas")

    # مادة تم تحديدها برقمها
    def get_matière_by_id(self, matière_id: int) -> Matière:
        # نبحث عن المادة
        matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
        # هل تم العثور عليها؟
        if matières:
            return matières[0]
        else:
            raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")

    # مادة تم تحديدها برقمها
    def get_matière_by_name(self, matière_name: str) -> Matière:
        # نبحث عن المادة
        matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
        # هل تم العثور عليها؟
        if matières:
            return matières[0]
        else:
            raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
  • الأسطر 21-24: يجب أن تعرض الطريقة [get_classes] قائمة الفصول الدراسية بالمدرسة. في السطر 20، نستخدم استعلامًا سبق أن تناولناه؛
  • الأسطر 26-39: ثلاث طرق أخرى مشابهة للحصول على قوائم الطلاب والمواد الدراسية والدرجات؛
  • الأسطر 51-59: يجب أن تعرض الدالة [get_élève_by_id] طالبًا محددًا برقمه. وتطلق استثناءً إذا كان هذا الطالب غير موجود؛
    • السطر 54: نستخدم استعلامًا مُصفَّىً. نحصل على قائمة فارغة أو تحتوي على عنصر واحد؛
    • السطر 57: إذا لم تكن القائمة المستردة فارغة، يتم إرجاع العنصر الأول من القائمة؛
    • وإلا، في السطر 59، يتم إثارة استثناء؛
  • الأسطر 41-49: يجب أن تُرجع الطريقة [get_notes_for_élève_by_id] درجات طالب محدد برقمه:
    • السطر 45، نستخدم الطريقة [get_élève_by_id] للحصول على كيان «الطالب» الخاص بالطالب؛
    • في السطر 47، نستخدم الخاصية [Elève.notes] التي تم إنشاؤها بواسطة التعيين بين الكيان [Note] والجدول [notes] (انظر الفقرة |تكوين SQLAlchemy|) والتي تمثل درجات الطالب؛
    • السطر 49: يتم إرجاع قاموس؛
  • الأسطر 61-109: سلسلة من الطرق المماثلة التي تتيح:
    • البحث عن تلميذ باسمه، الأسطر 61-69؛
    • البحث عن فصل دراسي، الأسطر 71-89؛
    • البحث عن مادة دراسية، الأسطر 91-109؛

19.6.8. النص البرمجي [main_joined_queries]

Image

يُسمى البرنامج النصي [main_joined_queries] بهذا الاسم لأنه يهدف إلى تسليط الضوء على الاستعلامات التي يقوم بها [sqlalchemy] ضمناً لاسترداد المعلومات الموجودة في عدة جداول. يتم إجراء هذه الاستعلامات، التي تكون مخفية عن المبرمج، في كل مرة يتم فيها ربط خاصية ما لكيان ما بالدالة [relationship] في تعيين الكيان. على سبيل المثال:


    # التعيين
    mapper(Note, tables['notes'], properties={
        'المعرف: notes_table.c.id،
        'القيمة: notes_table.c.valeur،
        'الطالب': relationship(الطالب, backref="notes", lazy="select"),
        'المادة': relationship(المادة, backref="الدرجات", lazy="select")
    })

فيما يلي، التعيين بين الكيان [Note] والجدول [notes]:

  • السطر 5، عندما يُطلب السمة [élève] الخاصة بكيان [Note] للمرة الأولى، سيتم البحث عنها في الجدول [élèves] عبر استعلام SQL. وطالما لم يتم طلب هذه الخاصية، فإنها تظل غير محددة (التحميل المتأخر). وبمجرد الحصول عليها، تظل قيمتها مخزنة في ذاكرة ORM. وعندما تتم الإشارة إليها للمرة الثانية، سيقوم ORM بتسليم قيمتها على الفور دون الحاجة إلى استعلام جديد SQL. كل هذا يتم بشكل شفاف بالنسبة للمطور؛
  • وينطبق الأمر نفسه على الخاصية العكسية [Elève.notes] (backref)، السطر 5؛
  • وينطبق الأمر نفسه على الخاصية [Note.matière] وخاصيتها العكسية [Matière.notes] (backref)، السطر 6؛

النص البرمجي [main_joined_queries] هو كما يلي:


# يُتوقع معلمة mysql أو pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# يتم تكوين التطبيق
import config
config = config.configure({"sgbd": sgbd})

# تم تكوين syspath - يمكن إجراء عمليات الاستيراد
from MyException import MyException

# الطبقة [dao]
dao = config["dao"]
try:
    # الطالب حسب المعرف
    print("élève id=11 -----------")
    élève = dao.get_élève_by_id(11)
    print(f"élève={élève}")
    # فصل الطالب (التحميل المؤجل)
    classe = élève.classe
    print(f"classe de l'élève : {classe}")
    # الطلاب من نفس الفصل (التحميل المؤجل)
    print("élèves dans la même classe :")
    for élève in classe.élèves:
        print(f"élève={élève}")

    # طالب حسب اسمه
    print("élève nom='nom2' -----------")
    print(f"élève={dao.get_élève_by_name('nom2')}")
    # فصله (التحميل المؤجل)
    print(f"classe de l'élève : {élève.classe}")

    # درجات الطالب
    print("notes de l'élève id=11 -----------")
    # الطالب أولاً
    élève = dao.get_élève_by_id(11)
    # ثم درجاته (التحميل المتأخر)
    for note in élève.notes:
        # الدرجة
        print(f"note={note}, "
              # المادة التي تتعلق بها الدرجة (التحميل المتأخر)
              f"matière={note.matière}")

    # طلاب الفصل
    print("élèves de la classe nom='classe1' -----------")
    # أولاً الفصل
    classe = dao.get_classe_by_name('classe1')
    # ثم الطلاب (التحميل المتأخر)
    for élève in classe.élèves:
        print(élève)

    # نفس الشيء بالنسبة لـ [classe2]
    print("élèves de la classe de nom 'classe2' -----------")
    classe = dao.get_classe_by_name('classe2')
    for élève in classe.élèves:
        print(élève)

    # الدرجات في مادة ما
    print("matière de nom='matière1' -----------")
    # أولاً المادة
    matière = dao.get_matière_by_name('matière1')
    print(f"matière={matière}")
    # ثم الدرجات في هذه المادة (التحميل المتأخر)
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(note)

    # نفس الشيء بالنسبة للمادة 2
    print("matière de nom='matière2' -----------")
    matière = dao.get_matière_by_name('matière2')
    print(f"matière={matière}")
    print("Notes dans la matière : ")
    for note in matière.notes:
        print(f"note={note}")
except MyException as ex1:
    # يتم عرض الخطأ
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # يتم عرض الخطأ
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # يتم تحرير الموارد
    import shutdown
    shutdown.execute(config)

التعليقات كافية لفهم الكود.

نقوم بإنشاء إعدادات تشغيل لـ MySQL:

Image

نتائج التنفيذ هي كما يلي:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/05/main/main_joined_queries.py mysql
élève id=11 -----------
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
classe de l'élève : {"nom": "classe1", "id": 1}
élèves dans la même classe :
élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élève nom='nom2' -----------
élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
classe de l'élève : {"nom": "classe1", "id": 1}
notes de l'élève id=11 -----------
note={"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}, matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}, matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
élèves de la classe nom='classe1' -----------
{"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
{"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
élèves de la classe de nom 'classe2' -----------
{"classe_id": 2, "nom": "nom3", "prénom": "prénom3", "id": 32}
{"classe_id": 2, "nom": "nom4", "prénom": "prénom4", "id": 42}
matière de nom='matière1' -----------
matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
Notes dans la matière : 
{"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}
{"matière_id": 1, "valeur": 12.0, "élève_id": 21, "id": 2}
{"matière_id": 1, "valeur": 14.0, "élève_id": 32, "id": 3}
{"matière_id": 1, "valeur": 16.0, "élève_id": 42, "id": 4}
matière de nom='matière2' -----------
matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
Notes dans la matière : 
note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}
note={"matière_id": 2, "valeur": 8.0, "élève_id": 21, "id": 6}
note={"matière_id": 2, "valeur": 10.0, "élève_id": 32, "id": 7}
note={"matière_id": 2, "valeur": 12.0, "élève_id": 42, "id": 8}

Process finished with exit code 0

لفهم هذه النتائج، يجب أن نتذكر أننا استبعدنا بعض الخصائص من قاموس الكيانات (انظر |التكوين|):


    # تكوين الكيانات [BaseEntity]
    Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
    Classe.excluded_keys = ['_sa_instance_state', 'élèves']
    Matière.excluded_keys = ['_sa_instance_state', 'notes']
    Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']

وبالتالي، عند كتابة [print(f"élève={élève}")] في السطر 26 من الكود، يشير السطر 1 أعلاه إلى أن خصائص ['_sa_instance_state', 'notes', 'classe'] لن يتم عرضها. وهذا ما نراه في السطر 3 من النتائج. يتم عرض جميع الخصائص الأخرى. وهكذا، في السطر 3 نفسه، نكتشف خاصية جديدة هي [classe_id] لم تكن موجودة في البداية في الكيان [Elève]. تتوافق هذه الخاصية مباشرةً مع العمود [classe_id] في الجدول [élèves]. وبالتالي، أضافت [sqlalchemy] الخصائص التالية إلى الكيان [Elève]: [classe_id, _sa_instance_state, notes]. يجب أن نكون على دراية بذلك، لا سيما لأنها يجب ألا تكون موجودة مسبقًا في الكيان المُعَدَّل.

تعد الخصائص المستبعدة من قاموس الكيانات مهمة. فإذا لم يتم، على سبيل المثال، استبعاد الخصائص [notes, élève] من الكيان [Elève]، فإن العملية [print(f"élève={élève}")] ستعرضها، وبالتالي، كما تم شرحه للتو، ستؤدي إلى استعلامات SQL ضمنية (التحميل المتأخر) لاسترداد قيم هذه الخصائص. وإذا كانت القائمة المعروضة، كما في هذه الحالة، قائمة بالطلاب، فإن العمليات الضمنية SQL تُجرى لكل طالب على حدة. وقد يكون هذا، من ناحية، غير ضروري، ومن ناحية أخرى، مكلفًا بالتأكيد من حيث وقت التنفيذ.

لتنفيذ البرنامج النصي باستخدام قاعدة PostgreSQL، نقوم بإنشاء تكوين التنفيذ التالي:

Image

يُعطي التنفيذ نفس النتائج التي يتم الحصول عليها باستخدام MySQL.

19.6.9. البرنامج النصي [main_stats_for_élève]

Image

البرنامج النصي [main_stats_for_élève] هو نفسه الذي تم استخدامه سابقًا في تطبيق |troiscouches v01]. وكان يُسمى آنذاك [main]. وهو تطبيق وحدة التحكم يتيح الحصول على بعض المؤشرات المتعلقة بدرجات الطالب: [moyenne pondérée, min, max, liste]. ويندرج ضمن البنية التالية:

Image

في هذه البنية الطبقية، لم يتم تغيير سوى الطبقة [dao] بين تطبيق |troiscouches v01| وهذا التطبيق. ونظرًا لأن الطبقة الجديدة [dao] تتوافق مع واجهة [InterfaceDao] الخاصة بالطبقة القديمة [dao]، فلا داعي لتغيير الطبقات [ui, métier]. وبالتالي، يمكن الاستمرار في استخدام الطبقات المحددة في تطبيق |troiscouches v01|.

يقوم البرنامج النصي [main_stats_for_élève] بتنفيذ الطبقة [main] من المخطط أعلاه بالطريقة التالية:


# في انتظار معلمة mysql أو pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# يتم تكوين التطبيق
import config
config = config.configure({'sgbd': sgbd})

# تم تكوين syspath - يمكن إجراء عمليات الاستيراد
from MyException import MyException

# الطبقة [ui]
ui = config["ui"]
try:
    # تنفيذ الطبقة [ui]
    ui.run()
except MyException as ex1:
    # يتم عرض الخطأ
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # يتم عرض الخطأ
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # تحرير الموارد
    import shutdown
    shutdown.execute(config)
  • السطر 20: يتم استرداد مرجع للطبقة [ui] من تكوين التطبيق؛
  • السطر 24: يتم فتح مربع الحوار مع المستخدم باستخدام الطريقة الوحيدة للطبقة [ui]؛

قد يكون تكوين التشغيل لـ PostgreSQL كما يلي:

Image

فيما يلي مثال على التنفيذ باستخدام هذا التكوين:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/05/main/main_stats_for_élève.py pgres
Numéro de l'élève (>=1 et * pour arrêter) : 11
Elève={"prénom": "prénom1", "id": 11, "classe_id": 1, "nom": "nom1"}, notes=[10.0 6.0], max=10.0, min=6.0, moyenne pondérée=7.33
Numéro de l'élève (>=1 et * pour arrêter) : 1
L'erreur suivante s'est produite : MyException[11, L'élève d'identifiant 1 n'existe pas]
Numéro de l'élève (>=1 et * pour arrêter) : *

Process finished with exit code 0