19. Використання ORM SQLALCHEMY
У попередньому розділі було показано, що в деяких випадках можна писати код, незалежний від SGBD, який використовується з такою архітектурою:

У цьому розділі ми будемо використовувати ORM (об’єктно-реляційний мапер) [sqlalchemy] для уніфікованого доступу до SGBD незалежно від того, який SGBD використовується. ORM дає дві можливості:
- воно дозволяє скрипту взаємодіяти з SGBD без відправлення команд SQL;
- він приховує від скрипта особливості кожного SGBD;
Архітектура виглядає наступним чином:
Тепер скрипт відокремлений від коннекторів за допомогою ORM. Він взаємодіє з ORM за допомогою класів і методів. Він не виконує код SQL. Це робить ORM за допомогою коннекторів, до яких він підключений. Він приховує від скрипта особливості цих коннекторів. Тому код скрипта не залежить від зміни коннектора (а отже, і від SGBD);
Структура досліджуваних скриптів буде такою:

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: основи

- у [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, Особа, Утиліти
f"{root_dir}/classes/02/entities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# налаштування класів
from Personne import Personne
Personne.excluded_keys = ['_sa_instance_state']
# застосовуємо конфігурацію
return {}
Коментарі
- рядок 8: додаємо до Python Path папку, що містить класи [BaseEntity, MyException, Personne, Utils];
- рядки 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__}")
# клас «Особа» було змінено — його «розширено»
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]. Щоб зрозуміти подальший хід подій, слід згадати архітектуру, яка тут використовується:

Якщо [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={
'id': 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 до іншого через типи даних, задані для стовпців. Уніфікації типів даних не відбулося. Тому порядок [create] варіюється від одного SGBD до іншого. Тут, завдяки [sqlalchemy]:
- ми однозначно описуємо потрібну нам таблицю;
- [sqlalchemy] самостійно генерує [create], який підходить до SGBD, що знаходиться перед ним;
- рядок 52: до сесії додається об’єкт [Personne]. Це не означає, що він автоматично додається до бази даних. Справа в тому, що ORM дотримується власних правил синхронізації з базою даних. Він завжди намагатиметься оптимізувати кількість запитів, які він виконує. Розглянемо приклад. Скрипт додає (add) двох осіб (особа1, особа2) до сесії, а потім виконує запит: він хоче переглянути всіх осіб, присутніх у таблиці. [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|). Ми зазначали, що після маппінгу у кожному об’єкті [Personne] буде знайдено властивість [_sa_instance_state]. Однак значення цієї властивості не є типом [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]:

У [6] видно, що таблиця порожня. Там немає навіть першої особи, яку скрипт додав до сесії. Це пов’язано з тим, що сесія відбувалася в рамках транзакції, яка була скасована у клаузулі [except] скрипта [main].
Тепер внесімо наступну зміну в [main]:
# вставка
session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
# session.commit()
Після додавання особи у рядку 2 ми знімаємо коментар із рядка 3. Операція [session.commit] підтвердить базову транзакцію, і розпочнеться нова транзакція. Після виконання вміст таблиці [personnes] є таким:

У таблиці [6] видно, що перше вставлення було збережено. Це пов’язано з тим, що воно було виконано в рамках транзакції 1, а помилка, що сталася згодом, — у рамках транзакції 2.
19.3. Скрипти 02: відповідності [sqlalchemy]

Скрипти 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",
]
# встановлюємо syspath
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={
'id': 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 Path;
- рядки 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] набула такого вигляду:

Тепер розглянемо таблицю [personnes], згенеровану [sqlalchemy]:

- у [6] — типи, використані для різних стовпців;
- у [7] видно, що стовпець [id] має атрибут [AUTO_INCREMENT]. Це означає, що під час вставки рядка в таблицю, якщо цей рядок не має значення для стовпця [id], воно буде згенеровано MySQL послідовно: 1, 2, 3, … Ця властивість дозволяє нам не турбуватися про значення первинного ключа під час вставки даних у таблицю: ми дозволяємо MySQL генерувати його;
- у [8] бачимо, що стовпець [id] є первинним ключем;
- у [9] ми бачимо обмеження унікальності для полів [nom, prenom];
19.4. Скрипти 03: робота з сутностями сесії [sqlalchemy]

Файл конфігурації [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)] створює логічний ET (AND) між використовуваними критеріями;
- рядки 64–67: підключено нового користувача;
- рядки 70–71: ще один приклад відфільтрованого запиту. Функція [func.lower(param)] перетворює [param] на малі літери. Існують також інші доступні функції, позначені як [func.xx]. У виразі в рядку 71:
- [session.query.filter] повертає список об’єктів [Personne];
- [session.query.filter.first] повертає перший елемент цього списку;
- рядок 77: видаляється елемент із сесії;
- рядок 86: сесія підтверджується;
Результати виконання такі:
- рядки 4–6: вміст сесії;
- рядки 8–10: вміст сесії в порядку спадання імен;
- рядки 12–13: вміст сесії для осіб, вік яких знаходиться в інтервалі [20, 40];
- рядок 15: особа з іменем «bruneau»;
У phpMyAdmin вміст таблиці [personnes] наприкінці виконання є таким:

19.5. Скрипти 04: використання бази даних [PostgreSQL]

Файл [04] є копією файлу [03]. Змінюється лише одне — рядок підключення у файлі [config]:
# посилання на базу даних PostgreSQL
engine = create_engine("postgresql+psycopg2://admpersonnes:nobody@localhost/dbpersonnes")
Відтепер цей рядок підключення вказує на базу даних [dbpersonnes] з SGBD [PostgreSQL]. Зверніть увагу на використання коннектора [psycopg2]. Він має бути встановлений.
Виконання скрипта [main] дає такі результати:
За допомогою інструменту [pgAdmin] (див. параграф |pgAdmin|) таблиця [personnes] має такий стан:

Таблиця [personnes] була згенерована за допомогою наступного коду SQL:

- у [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: повний приклад

Розглянутий приклад є повторенням прикладу, розглянутого в розділі |troiscouches-v01|. У цьому прикладі була представлена трирівнева архітектура [ui, métier, dao], яка оперувала сутностями [Classe, Elève, Matière, Note]. Елементи були жорстко закодовані в одному шарі [dao]. Тепер ми розміщуємо їх у базі даних. Ми будемо використовувати два шари SGBD: MySQL та PostgreSQL.
19.6.1. Архітектура додатка
Архітектура додатка буде такою:

- у [1-3] містяться шари [ui, métier, dao], які вже були присутні у прикладі |troiscouches-v01|. Шар [dao] тепер взаємодіє з шаром [ORM];
- шари [1-5] реалізовані за допомогою коду Python;
19.6.2. Бази даних
Ми створюємо базу даних MySQL з назвою [dbecole], що належить користувачеві [admecole] з паролем [mdpecole]. Для цього ми дотримуємося процедури, описаної в розділі |створення бази даних|:


- у [1] — база [dbecole] без таблиць [3];
- у [7] користувач [admecole] має всі права на цю базу даних;
Те саме робимо з SGBD та PostgreSQL. Ми створюємо базу даних з назвою [dbecole], що належить користувачеві [admecole] з паролем [mdpecole]. Для цього дотримуємося процедури, описаної в розділі |створення бази даних|:

- у [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:
# id: ідентифікатор класу
# назва: назва класу
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:
# id: ідентифікатор оцінки
# значення: сама оцінка
# учень: учень (типу «Учень»), якого стосується оцінка
# предмет: предмет (типу «Предмет»), до якого відноситься оцінка
# отже, об’єкт «Оцінка» — це оцінка учня з певного предмета
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:
# очікується тип «Учень»
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:
# очікується тип «Предмет»
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. Конфігурація

Конфігурація була розділена на кілька файлів:
- загальна конфігурація у файлі [config.py]: вона встановлює Python Path додатка та створює екземпляри рівнів архітектури;
- конфігурація [sqlalchemy] у файлі [config_database]: вона здійснює зіставлення класів та таблиць;
- шари додатка налаштовуються у файлі [config_layers];
Файл [config] має такий вигляд:
def configure(config: dict) -> dict:
import os
# крок 1 ---
# встановлюємо Python Path для додатка
# абсолютний шлях до папки цього скрипта
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",
]
# оновлення syspath
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 Path додатка;
- рядки 29–32: налаштування [sqlalchemy];
- рядки 34–37: налаштування шарів додатка;
Файл [config_database] має такий вигляд:
def configure(config: dict) -> dict:
# config['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={
'id': 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={
'id': é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={
'id': notes_table.c.id,
'значення': notes_table.c.valeur,
'учень': relationship(Учень, backref="notes", lazy="select"),
'предмет': relationship(Предмет, backref="оцінки", 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 Path вже буде встановлено скриптом [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] як таке, що обчислюється за допомогою відношення (relationship) зовнішнього ключа, яке пов’язує таблиці [é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], тут вказується, що слід використати цю залежність, щоб отримати з таблиці [classes] рядок з ідентифікатором 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

Тут слід розуміти, що шар [dao] [3] взаємодіє зORM [sqlalchemy] [4], налаштованим так, як описано в попередньому абзаці. З трьох шарів [ui, métier, dao] додатка |troiscouches v01| переписати потрібно лише шар [dao]. Шари [ui, métier] зберігаються.
Реалізація шару [dao] була розміщена в папці [services]:

[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: до методів, успадкованих від [InterfaceDao], додається метод [init_database]. Його завданням буде ініціалізація бази даних за допомогою даних зі словника [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):
# database = {"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. Ініціалізація бази даних

Скрипт [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:

Результати виконання цієї конфігурації у phpMyAdmin такі:



Для SGBD та [PostgreSQL] ми використовуємо таку конфігурацію виконання:

Під час виконання результати у файлі [pgAdmin] є такими:



Варто відзначити, з якою легкістю вдалося змінити 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):
# database = {"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]

Скрипт [main_joined_queries] має таку назву, оскільки його метою є висвітлення запитів, які неявним чином виконує [sqlalchemy] для отримання інформації з декількох таблиць. Ці запити, приховані від програміста, виконуються щоразу, коли властивість сутності пов’язується з функцією [relationship] у маппінгу сутності. Наприклад:
# картування
mapper(Note, tables['notes'], properties={
'id': 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. Доки ця властивість не була запитана, вона залишається невизначеною (lazy load). Після її отримання її значення зберігається в пам’яті 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}, "
# предмет, з якого виставлено оцінку (lazy loading)
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:

Результати виконання такі:
Щоб зрозуміти ці результати, слід пам’ятати, що ми виключили певні властивості зі словника сутностей (див. |конфігурація|):
# конфігурація сутностей [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, створюємо таку конфігурацію виконання:

Виконання дає ті самі результати, що й із MySQL.
19.6.9. Скрипт [main_stats_for_élève]

Скрипт [main_stats_for_élève] — це той самий, що вже використовувався в додатку |troiscouches v01]. Тоді він називався [main]. Це консольний додаток, що дозволяє отримати певні показники щодо оцінок учня: [moyenne pondérée, min, max, liste]. Він вписується в таку архітектуру:

У цій багатошаровій архітектурі між версією додатка |troiscouches v01| та цією версією було змінено лише шар [dao]. Оскільки новий шар [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 виглядатиме так:

Ось приклад виконання з цією конфігурацією:
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