Skip to content

18. نوشتن کد مستقل از SGBD

ما قبلاً دیده‌ایم که در برخی موارد می‌توان کد پایتون نوشته شده برای 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
# ایجاد جدول «people»
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'

ما خط ۲ را اصلاح کرده‌ایم تا فرمان برای 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": {
                # اتصال‌دهنده DBMS
                "sgbd_connector""mysql.connector",
                # نام ماژولی که شامل توابع مدیریت DBMS است
                "sgbd_fonctions""any_module",
                # اطلاعات اعتبارنامه‌ی اتصال
                "user""admpersonnes",
                "password""nobody",
                "host""localhost",
                "database""dbpersonnes"
            },
            "postgresql": {
                # اتصال‌دهنده DBMS
                "sgbd_connector""psycopg2",
                # نام ماژولی که شامل توابع مدیریت DBMS است
                "sgbd_fonctions""any_module",
                # اطلاعات اعتبارسنجی اتصال
                "user""admpersonnes",
                "password""nobody",
                "host""localhost",
                "database""dbpersonnes"
            }
        },
        # فایل فرمان SQL
        "commands_filename"f"{script_dir}/data/commandes.sql"
    }
    #مسیر سیستمی برنامه
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #پیکربندی اعمال شده است
    return config

عناصر جدید در خطوط ۱۸–۴۳ یافت می‌شوند:

  • خط ۲۰: [sgbds] یک دیکشنری با دو کلید است: [mysql] در خط ۲۱ و [postgresql] در خط ۳۲؛
  • مقدار مرتبط با این کلیدها یک دیکشنری است که شامل عناصر لازم برای اتصال به یک SGBD می‌باشد:
  • خطوط ۲۱–۳۲: اجزای یک اتصال به SGBD MySQL;
    • خط ۲۳: کانکتور پایتون مورد استفاده؛
    • خط ۲۵: ماژولی که شامل توابع مشترک است؛
    • خطوط ۲۶–۳۰: اعتبارنامه‌های اتصال؛
  • خطوط ۳۲–۴۱: همان جزئیات برای یک اتصال به SGBD PostgreSQL;

اسکریپت [any_04] که فایل دسته‌ای SQL [data/commandes.sql] را اجرا می‌کند به شرح زیر است:


# راه‌اندازی برنامه
import config

config = config.configure()

#مسیر سیستم پیکربندی شده است – اکنون می‌توان واردات را انجام داد
import importlib
import sys

# بررسی نحوی فراخوانی
#argv[0] sgbd_name true / false
#سه پارامتر لازم است
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)

توضیحات

  • خطوط ۱–۴: پیکربندی برنامه [config] بازیابی می‌شود؛
  • خطوط ۱۰–۲۱: اسکریپت با دو پارامتر [sgbd_name with_transaction] فراخوانی می‌شود:
    • [sgbd_name]: نام SGBD که باید استفاده شود؛
    • [with_transaction]: اگر می‌خواهید اسکریپت SQL را در یک تراکنش اجرا کنید، مقدار True و در غیر این صورت False را برمی‌گرداند؛
  • خطوط ۱۰–۲۵: پارامترها بازیابی و بررسی می‌شوند؛
  • خط ۲۸: پیکربندی SGBD انتخاب‌شده؛
  • خط ۳۰: کانکتور برای SGBD انتخاب‌شده وارد می‌شود. برای این کار، از کتابخانه [importlib] (خط ۷) استفاده می‌شود که امکان وارد کردن ماژولی را فراهم می‌کند که نام آن در یک متغیر ذخیره شده است. نتیجهٔ عملیات [importlib.import_module] یک ماژول است. بنابراین، پس از خط ۳۰، همه چیز چنان پیش می‌رود که گویی دستور اجرا شده این بوده است:
import sgbd_connector

این به ما اجازه می‌دهد تا در خط ۵۲، [sgbd_connector.connect] را بنویسیم، که در آن از تابع [connect] از ماژول [sgbd_connector] استفاده می‌کنیم. مهم است که به خاطر داشته باشیم [sgbd_connector] یا [mysql.connector] است یا [psycopg2]. هر دوی این ماژول‌ها حاوی تابع [connect] هستند. به همین ترتیب، در خط ۶۰، می‌توانید [sgbd_connector.InterfaceError, sgbd_connector.DatabaseError] را بنویسید.

  • خط ۳۲: ماژولی که شامل توابع مورد استفاده در اسکریپت است، وارد می‌شود؛
  • خط ۵۸: تابع [execute_file_of_commands] از ماژول تابع مورد استفاده توسط اسکریپت اجرا می‌شود. در مقایسه با نسخه‌های قبلی، امضای این تابع یک پارامتر اضافی، یعنی پارامتر اول، دارد. کانکتور پایتون [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()
            #آیا دستور خالی است یا نظر (comment)؟ اگر چنین است، به دستور بعدی برو
            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] در خط ۳۱ برای مشخص کردن نوع استثناءهای دریافت‌شده استفاده شد.

اجرای اسکریپت [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