Введение
SQL-цикл основного плана (статьи 393-442) разбирал реляционные БД глубоко, преимущественно с прицелом на C++/QtSql — в этой статье разберём, как тот же QtSql доступен из Python, и рассмотрим альтернативный, более питонический путь — использование SQLAlchemy (популярной Python ORM, концептуально аналогичной идеям статьи 436 SQL-цикла) в сочетании с Qt-моделями.
Концепция
QtSql (статья 437 SQL-цикла, QSqlQuery/QSqlTableModel) доступен из PySide6 практически без изменений в подходе — та же логика подключения, параметризованных запросов (статья 433 SQL-цикла) и моделей применима из Python с естественными синтаксическими отличиями. Альтернативный подход — использование SQLAlchemy, значительно более развитой и идиоматичной для Python ORM (по сравнению с относительно ограниченными возможностями QSqlTableModel именно как ORM-инструмента), с последующей «ручной» интеграцией результатов SQLAlchemy-запросов в Qt-модель (QAbstractListModel, статья 504), что даёт более богатый, питонический интерфейс работы с данными при сохранении возможности отображения через стандартные виджеты/QML-виды Qt.
Пример кода: Работа с БД в PySide6
# QtSql из Python — прямой аналог C++ паттернов статьи 433/437 SQL-цикла
from PySide6.QtSql import QSqlDatabase, QSqlQuery
db = QSqlDatabase.addDatabase("QPSQL")
db.setHostName("localhost")
db.setDatabaseName("mydb")
db.setUserName("app_readwrite")
db.setPassword("secure_password")
if not db.open():
print(f"Ошибка подключения: {db.lastError().text()}")
query = QSqlQuery()
query.prepare("SELECT id, name FROM customers WHERE id = :id")
query.bindValue(":id", 5) # параметризованный запрос — защита от SQL-инъекций (статья 433 SQL-цикла)
query.exec()
while query.next():
print(query.value("id"), query.value("name"))
# SQLAlchemy — более идиоматичный для Python подход, с ручной интеграцией в Qt-модель
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class Customer(Base):
__tablename__ = "customers"
id = Column(Integer, primary_key=True)
name = Column(String)
engine = create_engine("postgresql://app_readwrite:password@localhost/mydb")
Session = sessionmaker(bind=engine)
session = Session()
customers = session.query(Customer).filter(Customer.id == 5).all()
for customer in customers:
print(customer.id, customer.name)
# Интеграция результатов SQLAlchemy в QAbstractListModel для отображения через Qt (статья 504)
from PySide6.QtCore import QAbstractListModel, Qt, QModelIndex
class CustomerListModel(QAbstractListModel):
NameRole = Qt.UserRole + 1
def __init__(self, session, parent=None):
super().__init__(parent)
self._session = session
self._customers = []
self.refresh()
def refresh(self):
self.beginResetModel()
self._customers = self._session.query(Customer).all() # SQLAlchemy-запрос
self.endResetModel()
def rowCount(self, parent=QModelIndex()) -> int:
return len(self._customers)
def data(self, index: QModelIndex, role: int):
if role == self.NameRole:
return self._customers[index.row()].name
return None
def roleNames(self) -> dict:
return {self.NameRole: b"name"}
Пояснения к коду
QtSql-пример показывает, что весь паттерн параметризованных запросов из SQL-цикла переносится на Python почти без изменений — синтаксис bindValue(":id", 5) идентичен по смыслу C++ версии, и защита от SQL-инъекций (статья 433 SQL-цикла) работает точно так же. SQLAlchemy-пример показывает принципиально иной, значительно более «ORM-овый» подход (статья 436 SQL-цикла) — session.query(Customer).filter(...) даёт богатый, объектно-ориентированный язык запросов, выразительно превосходящий по гибкости относительно простой QSqlTableModel, но требующий, как показывает CustomerListModel, отдельного, ручного «моста» для интеграции с системой моделей Qt — refresh() явно перезагружает данные из SQLAlchemy-сессии и через beginResetModel()/endResetModel() уведомляет подключённые Qt-виды о полном обновлении содержимого.
Подводные камни
- Смешивание
QtSqlи SQLAlchemy для одновременного доступа к одной и той же БД из разных частей одного приложения без явного понимания, как они взаимодействуют (или не взаимодействуют) с точки зрения управления соединениями и транзакциями — обе библиотеки независимо управляют собственными соединениями с БД, и без явной координации (или осознанного выбора единственного, согласованного способа доступа к БД для всего приложения) могут возникнуть проблемы с конкурентным доступом, блокировками (статья 409 SQL-цикла) или просто избыточным, неэффективным количеством одновременных соединений. - Использование
beginResetModel()/endResetModel()для каждого, даже минимального изменения данных (как в примереrefresh(), перезагружающем все данные целиком) вместо более точечногоbeginInsertRows()/dataChanged()для случаев, где известно, что изменилась лишь часть данных — полный reset модели менее эффективен и может вызвать заметное визуальное «мерцание» в подключённых видах по сравнению с точечным уведомлением именно о реально изменившейся части данных, и для частых, небольших изменений стоит явно различать эти случаи, выбирая более точный метод уведомления там, где это практически возможно. - Игнорирование того, что SQLAlchemy-сессия (
session) обычно не является потокобезопасной для одновременного использования из нескольких потоков — для PySide6-приложений, использующих фоновые потоки (QThread, статья 505) для выполнения запросов к БД, требуется явная, осторожная архитектура работы с сессиями SQLAlchemy (отдельная сессия для каждого потока, или явная синхронизация доступа к общей сессии), аналогично общим принципам безопасного использования общих данных между потоками из основного цикла статей про многопоточность. - Отсутствие явного закрытия соединений
QtSql/сессий SQLAlchemy при завершении работы с ними — аналогично подводным камням пула соединений из статьи 438 SQL-цикла, накопление неосвобождённых соединений (особенно для приложений, многократно открывающих новые подключения за время своей работы) может привести к достижению лимита одновременных соединений на стороне сервера БД, и явное, аккуратное управление временем жизни соединений/сессий — важная часть надёжной реализации работы с БД независимо от выбранной конкретной библиотеки.