18. Написання коду, незалежного від SGBD
Раніше ми бачили, що в деяких випадках можна просто перенести код Python, написаний для SGBD та MySQL, на код, написаний для SGBD та PostgreSQL. У цьому розділі ми покажемо, як систематизувати цей підхід. Пропонована архітектура виглядає наступним чином:

Бажано, щоб вибір коннектора, а отже, і SGBD, здійснювався через конфігурацію і не вимагав переписання скрипта. Нагадаємо, що це можливо лише в тих випадках, коли скрипт не використовує власницькі розширення SGBD.
Структура скриптів буде такою:

Скрипти [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 все відбувається так, ніби була виконана така інструкція:
Це дозволить нам написати у рядку 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