Skip to content

18. كتابة كود مستقل عن SGBD

لقد رأينا سابقًا أنه في بعض الحالات كان من الممكن الترحيل ببساطة من كود Python المكتوب لـ SGBD و MySQL إلى كود مكتوب لـ SGBD و PostgreSQL. في هذا الفصل، نوضح كيفية تنظيم هذا النهج. وتصبح البنية المقترحة كما يلي:

Image

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

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

Image

تستند البرامج النصية [any_xx] إلى البرامج النصية التي تمت دراستها سابقًا لـ SGBD و MySQL و PostgreSQL. لن نعيد استعراضها جميعًا. سنركز على البرنامج النصي [any_04] الذي يُعد الأكثر تعقيدًا. نذكر أن هذا البرنامج النصي يُنفذ الأوامر SQL الواردة في الملف [data/commandes.sql] التالي:


# حذف الجدول [personnes]
drop table if exists personnes
# إنشاء جدول الأشخاص
create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))
# إدراج شخصين
insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)
insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)
# عرض الجدول
select prenom, nom, age from personnes
# خطأ متعمد
xx
# إدراج ثلاثة أشخاص
insert into personnes(id, prenom, nom, age) values (3, 'Pierre','Nicazou',35)
insert into personnes(id, prenom, nom, age) values (4, 'Geraldine','Colou',26)
insert into personnes(id, prenom, nom, age) values (5, 'Paulette','Girond',56)
# عرض الجدول
select prenom, nom, age from personnes
# قائمة الأشخاص مرتبة أبجديًا حسب الأسماء، وفي حالة تساوي الأسماء مرتبة أبجديًا حسب الأسماء الأولى
select nom,prenom from personnes order by nom asc, prenom desc
# قائمة بالأشخاص الذين تتراوح أعمارهم بين [20,40] مرتبة حسب العمر من الأكبر إلى الأصغر
# ثم حسب الترتيب الأبجدي للأسماء في حالة تساوي الأعمار، وحسب الترتيب الأبجدي للأسماء الأولى في حالة تساوي الأسماء
select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
# إضافة السيدة برونو
insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)
# تحديث عمرها
update personnes set age=47 where nom='Bruneau'
# قائمة بالأشخاص الذين يحملون اسم «برونو»
select nom,prenom,age from personnes where nom='Bruneau'
# حذف السيدة برونو
delete from personnes where nom='Bruneau'
# قائمة بالأشخاص الذين يحملون اسم «برونو»
select nom,prenom,age from personnes where nom='Bruneau'

لقد قمنا بتعديل السطر 2 بحيث يتصرف الأمر بنفس الطريقة مع SGBD وMySQL وPostgreSQL في حالة عدم وجود الجدول [personnes].

يتم تكوين البرنامج النصي [any_04] بواسطة البرنامج النصي [config.py] التالي:


def configure():
    import os

    # المسار المطلق لمجلد هذا البرنامج النصي
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # تكوين مجلدات syspath
    absolute_dependencies = [
        # المجلدات المحلية
        f"{script_dir}/shared",
    ]

    # تحديد مسار syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # تكوين التطبيق
    config = {
        # أنظمة إدارة قواعد البيانات المدعومة
        "sgbds": {
            "mysql": {
                # موصل نظام إدارة قواعد البيانات
                "sgbd_connector""mysql.connector",
                # اسم الوحدة النمطية التي تضم وظائف إدارة نظام إدارة قواعد البيانات
                "sgbd_fonctions""any_module",
                # بيانات اعتماد الاتصال
                "user""admpersonnes",
                "password""nobody",
                "host""localhost",
                "database""dbpersonnes"
            },
            "postgresql": {
                # موصل نظام إدارة قواعد البيانات
                "sgbd_connector""psycopg2",
                # اسم الوحدة التي تضم وظائف إدارة نظام إدارة قواعد البيانات
                "sgbd_fonctions""any_module",
                # معرفات الاتصال
                "user""admpersonnes",
                "password""nobody",
                "host""localhost",
                "database""dbpersonnes"
            }
        },
        # ملف الأوامر SQL
        "commands_filename"f"{script_dir}/data/commandes.sql"
    }
    # مسار النظام (syspath) للتطبيق
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # يتم إجراء التهيئة
    return config

يكمن الجديد في الأسطر 18-43:

  • السطر 20: [sgbds] عبارة عن قاموس يحتوي على مفتاحين هما [mysql] في السطر 21 و[postgresql] في السطر 32؛
  • القيمة المرتبطة بهذه المفاتيح هي قاموس يقدم العناصر التي تسمح بالاتصال بـ SGBD:
  • الأسطر 21-32: عناصر الاتصال بـ SGBD و MySQL؛
    • السطر 23: موصل Python المطلوب استخدامه؛
    • السطر 25: الوحدة النمطية التي تحتوي على الدوال المشتركة؛
    • الأسطر 26-30: معرّفات الاتصال؛
  • الأسطر 32-41: نفس العناصر للاتصال بـ SGBD و PostgreSQL؛

النص البرمجي [any_04] الذي ينفذ ملف الأوامر SQL [data/commandes.sql] هو التالي:


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

config = config.configure()

# تم تكوين مسار النظام (syspath) - يمكن الآن إجراء عمليات الاستيراد
import importlib
import sys

# التحقق من صحة صيغة الاستدعاء
# argv[0] sgbd_name صحيح / خطأ
# يلزم وجود 3 معلمات
args = sys.argv
erreur = len(args) != 3
if not erreur:
    # نسترد المعلمتين اللتين تهماننا
    sgbd_name = args[1].lower()
    with_transaction = args[2].lower()
    # التحقق من المعلمتين
    erreur = (with_transaction != "true" and with_transaction != "false") \
             or sgbd_name not in config["sgbds"].keys()
# خطأ؟
if erreur:
    print(f"syntaxe : {args[0]} (1) sgbd_name (2) true / false")
    sys.exit()

# تكوين sgbd_name
sgbd_config = config["sgbds"][sgbd_name]
# موصل sgbd_name
sgbd_connector = importlib.import_module(sgbd_config["sgbd_connector"])
# مكتبة الوظائف
lib = importlib.import_module(sgbd_config["sgbd_fonctions"])


# حساب نص للعرض
with_transaction = with_transaction == "true"
if with_transaction:
    texte = "avec transaction"
else:
    texte = "sans transaction"

# العرض
commands_filename=config['commands_filename']
print("--------------------------------------------------------------------")
print(f"Exécution du fichier SQL {commands_filename} {texte}")
print("--------------------------------------------------------------------")

# تنفيذ الأوامر SQL من الملف
connexion = None
try:
    # تسجيل الدخول
    connexion = sgbd_connector.connect(
        host=sgbd_config['host'],
        user=sgbd_config['user'],
        password=sgbd_config['password'],
        database=sgbd_config['database'])
    # تنفيذ ملف الأوامر SQL
    erreurs = lib.execute_file_of_commands(sgbd_connector, connexion, commands_filename, suivi=True, arrêt=False,
                                           with_transaction=with_transaction)
except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # إغلاق الاتصال
    if connexion:
        connexion.close()

# عرض عدد الأخطاء
print("--------------------------------------------------------------------")
print(f"Exécution terminée")
print("--------------------------------------------------------------------")
print(f"Il y a eu {len(erreurs)} erreur(s)")
# عرض الأخطاء
for erreur in erreurs:
    print(erreur)

تعليقات

  • الأسطر 1-4: يتم استرداد تكوين التطبيق [config]؛
  • الأسطر 10-21: يتم استدعاء البرنامج النصي بمعلمتين [sgbd_name with_transaction]:
    • [sgbd_name]: اسم ملف SGBD المراد استخدامه؛
    • [with_transaction]: True إذا كنا نريد تنفيذ ملف الأوامر SQL ضمن معاملة، و False بخلاف ذلك؛
  • الأسطر 10-25: يتم استرداد المعلمات والتحقق منها؛
  • السطر 28: تكوين ملف SGBD المختار؛
  • السطر 30: يتم استيراد موصل ملف SGBD المختار. ويُستخدم في ذلك المكتبة [importlib] (السطر 7) التي تسمح باستيراد وحدة نمطية اسمها موجود في متغير. نتيجة عملية [importlib.import_module] هي وحدة. وبالتالي، بعد السطر 30، تسير الأمور كما لو أن الأمر الذي تم تنفيذه كان:
import sgbd_connector

وهذا سيسمح لنا بكتابة السطر 52 [sgbd_connector.connect] حيث نستخدم الدالة [connect] من الوحدة النمطية [sgbd_connector]. يجب أن نتذكر هنا أن [sgbd_connector] هي إما [mysql.connector] أو [psycopg2]. وتحتوي هاتان الوحدتان على الدالة [connect]. وبالمثل، في السطر 60، يمكن كتابة [sgbd_connector.InterfaceError, sgbd_connector.DatabaseError].

  • السطر 32: يتم استيراد الوحدة النمطية التي تحتوي على الوظائف المستخدمة في البرنامج النصي؛
  • السطر 58: يتم تنفيذ الدالة [execute_file_of_commands] من وحدة الدوال المستخدمة في البرنامج النصي. مقارنة بالإصدارات السابقة، تحتوي توقيع هذه الدالة على معلمة إضافية، وهي المعلمة الأولى. يتم تمرير موصل Python [sgbd_connector] إلى الدالة لتستخدمه؛
  • بخلاف هذه النقاط، يظل البرنامج النصي [any_04] كما كان في الإصدارات السابقة؛

مكتبة الدوال [any_module] هي كما يلي:


# ---------------------------------------------------------------------------------

def afficher_infos(curseur):
    


# ---------------------------------------------------------------------------------
def execute_list_of_commands(sgbd_connector, connexion, sql_commands: list,
                             suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
    

    # عمليات التهيئة
    curseur = None
    connexion.autocommit = not with_transaction
    erreurs = []
    try:
        # طلب مؤشر
        curseur = connexion.cursor()
        # تنفيذ sql_commands SQL الموجودة في sql_commands
        # يتم تنفيذها واحدة تلو الأخرى
        for command in sql_commands:
            # يتم حذف المسافات في بداية ونهاية الأمر الحالي
            command = command.strip()
            # هل لدينا أمر فارغ أم تعليق؟ إذا كان الجواب نعم، ننتقل إلى الأمر التالي
            if command == '' or command[0] == "#":
                continue
            # تنفيذ الأمر الحالي
            error = None
            try:
                curseur.execute(command)
            except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
                error = erreur
            # هل حدث خطأ؟
            


# ---------------------------------------------------------------------------------
def execute_file_of_commands(sgbd_connector, connexion, sql_filename: str,
                             suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
    

    # معالجة الملف SQL
    try:
        # فتح الملف للقراءة
        file = open(sql_filename, "r")
        # المعالجة
        return execute_list_of_commands(sgbd_connector, connexion, file.readlines(), suivi, arrêt, with_transaction)
    except BaseException as erreur:
        # يتم إرجاع جدول الأخطاء
        return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
    finally:
        pass

تم استخدام المعلمة [sgbd_connector] في السطر 31 لتحديد نوع الاستثناءات التي تم اعتراضها.

يؤدي تنفيذ البرنامج النصي [any_04] باستخدام المعلمات [mysql false] إلى النتائج التالية:


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/anysgbd/any_04.py mysql false
--------------------------------------------------------------------
Exécution du fichier SQL C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\databases\anysgbd/data/commandes.sql sans transaction
--------------------------------------------------------------------
[drop table if exists personnes] : Exécution réussie
nombre de lignes modifiées : 0
[create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age integer not null, unique (nom,prenom))] : Exécution réussie
nombre de lignes modifiées : 0
[insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)] : Exécution réussie
nombre de lignes modifiées : 1
[insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)] : Exécution réussie
nombre de lignes modifiées : 1
[select prenom, nom, age from personnes] : Exécution réussie
prenom, nom, age,
*****************
('Paul', 'Langevin', 48)
('Sylvie', 'Lefur', 70)
*****************
xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xx' at line 1)
[insert into personnes(id, prenom, nom, age) values (3, 'Pierre','Nicazou',35)] : Exécution réussie
nombre de lignes modifiées : 1
[insert into personnes(id, prenom, nom, age) values (4, 'Geraldine','Colou',26)] : Exécution réussie
nombre de lignes modifiées : 1
[insert into personnes(id, prenom, nom, age) values (5, 'Paulette','Girond',56)] : Exécution réussie
nombre de lignes modifiées : 1
[select prenom, nom, age from personnes] : Exécution réussie
prenom, nom, age,
*****************
('Paul', 'Langevin', 48)
('Sylvie', 'Lefur', 70)
('Pierre', 'Nicazou', 35)
('Geraldine', 'Colou', 26)
('Paulette', 'Girond', 56)
*****************
[select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
nom, prenom,
************
('Colou', 'Geraldine')
('Girond', 'Paulette')
('Langevin', 'Paul')
('Lefur', 'Sylvie')
('Nicazou', 'Pierre')
************
[select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] : Exécution réussie
nom, prenom, age,
*****************
('Nicazou', 'Pierre', 35)
('Colou', 'Geraldine', 26)
*****************
[insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)] : Exécution réussie
nombre de lignes modifiées : 1
[update personnes set age=47 where nom='Bruneau'] : Exécution réussie
nombre de lignes modifiées : 1
[select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
nom, prenom, age,
*****************
('Bruneau', 'Josette', 47)
*****************
[delete from personnes where nom='Bruneau'] : Exécution réussie
nombre de lignes modifiées : 1
[select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
nom, prenom, age,
*****************
*****************
--------------------------------------------------------------------
Exécution terminée
--------------------------------------------------------------------
Il y a eu 1 erreur(s)
xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xx' at line 1)

Process finished with exit code 0