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 true / false
# потрібно 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