Разработка менеджера баз данных при помощи SQLAlchemy

В данном учебном пособии рассматривается пример создания простейшего менеджера баз данных на языке программирования Python с применением библиотеки SQLAlchemy. В качестве основной СУБД используется российская РЕД База Данных
Преимущества использования SQLAlchemy перед низкоуровневым драйвером:
Библиотека firebird-driver предоставляет низкоуровневый драйвер для работы с базой данных РЕД База Данных, позволяющий выполнять SQL-запросы напрямую. В то же время SQLAlchemy предоставляет мощный объектно-реляционный маппинг (ORM) и гибкий интерфейс для работы с базой данных. Это позволяет писать более чистый и поддерживаемый код, не заботясь о синтаксисе SQL, а также упрощает миграции, управление транзакциями и создание сложных запросов. SQLAlchemy дополнительно обеспечивает поддержку различных СУБД, что делает приложение более гибким. При работе SQLAlchemy опирается на функционал драйвера.
Firebird-driver больше подходит для узкоспециализированных задач, где нужна максимальная производительность или доступ к специфичным возможностям РЕД База Данных (например, для работы с процедурами/триггерами на низком уровне).
Создание проекта в VSCode
Прежде всего необходимо создать каталог, в котором будет располагаться проект.
В Linux:
Открыть терминал и выполнить:
mkdir db_manager_project
cd db_manager_project
Далее откроем этот каталог в Visual Studio Code (VSCode)
code .
Теперь следует убедиться, что в VSCode установлены необходимые расширения для запуска и отладки проектов Python, поэтому перейдите во вкладку Extensions (Ctrl + Shift + X) и в поиске введите Python. У вас должны быть установлены расширения показанные на рисунке.

Настройка виртуального окружения
Для удобной работы с виртуальным окружением создадим файл requirements.txt в который запишем
sqlalchemy
sqlalchemy_schemadisplay
PyQt5
sqlalchemy-firebird
firebird.driver
eralchemy
pydot
Это библиотеки которые будут использоваться в нашем проекте.
Для создания виртуального окружения воспользуемся быстрым менеджером пакетов uv. Установить его в систему можно с сайта производителя. Рекомендуемый способ установки uv с помощью автономного установщика:
Для Windows:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
Для macOS и Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
Также можно использовать классический способ установки uv через pip из PYPI:
pip install uv
Проверка установленной версии uv:
uv --version
Для работы с менеджером пакетов uv в VS Code нужно настроить проект и указать редактору путь к созданному виртуальному окружению.
Откройте терминал в VS Code или системный терминал и создайте новый проект:
cd db_manager_project
uv init --no-package
--no-package создает структуру проекта в виде плоского каталога. Это можно использовать для приложений, которые не планируется распространять в качестве пакета. В директории проекта появится каталог виртуальной среды .venv для вашего проекта. В нее надо добавить нужные нам пакеты:
uv add --requirements requirements.txt
Нужные для проекта пакеты будут добавлены из подготовленного нами ранее файла requirements.txt.
Синхронизируйте окружение (uv автоматически создаст папку .venv и зафиксирует зависимости в uv.lock):
uv sync
VS Code обычно сам определяет виртуальное окружение в папке .venv, но если этого не произошло, привяжите его вручную командой Python: Select Interpreter.
Для запуска скриптов используйте встроенный терминал VS Code или команду uv run:
uv run main.py
Обзор установленных библиотек
В данном проекте используются следующие библиотеки:
- SQLAlchemy: Позволяет работать с базой данных через ORM, создавая объектную модель для таблиц и записей, что упрощает работу с данными.\
- sqlalchemy_schemadisplay: Инструмент для генерации схем базы данных.
- PyQt5: Фреймворк для создания графического интерфейса (GUI) на Python.
- sqlalchemy-firebird: Драйвер для работы с БД РЕД База Данных через SQLAlchemy.
- firebird-driver: Низкоуровневый драйвер для подключения к РЕД База Данных, используется для создания базы и прямых операций.
- eralchemy: Генерация диаграмм «сущность-связь» (ER-диаграмм) из модели данных.
- pydot: Библиотека для создания графов, используется для визуализации ER-диаграммы.
Создание программы
Для начала создадим в корне проекта файл main.py (он будет являться точкой входа в нашу программу).
Подключим библиотеки в main.py
import os
import sys
import pydot
from PyQt5 import QtWidgets, QtGui, QtCore
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey, exc
from sqlalchemy.orm import sessionmaker
from firebird.driver import connect, create_database
Класс для централизованного хранение всех параметров подключения
Создадим класс конфигурации базы данных. Он обеспечивает централизованное хранение параметров подключения для всех случаев, поддерживаемых в приложении.
# -------------------------------------------------------------------
# Класс конфигурации базы данных
# -------------------------------------------------------------------
class DatabaseConfig:
"""Централизованное хранение параметров подключения"""
def __init__(self):
# Параметры по умолчанию
self.db_type = "RedDatabase"
self.host = "localhost"
self.port = 3050
self.user = "sysdba"
self.password = "masterkey"
self.charset = "UTF8"
self.sql_dialect = 3
self.dbname = "db_tut.fdb"
# Базовая папка для БД
self.db_folder = "/var/_db"
# Раздельные имена файлов для разных типов БД
self.firebird_dbname = "db_tut.fdb"
self.sqlite_dbname = "db_tut.sqlite"
# Активный путь к БД (зависит от типа)
self.db_file = os.path.join(self.db_folder, self.firebird_dbname)
# Флаг создания новой БД
self.create_if_not_exists = True
def get_db_path(self):
"""Получение полного пути к БД в формате Firebird"""
return f"{self.host}:{self.db_file}"
def get_default_db_file(self, db_type):
"""Получение пути к БД по умолчанию для указанного типа"""
if db_type == "SQLite":
return os.path.join(self.db_folder, self.sqlite_dbname)
else:
return os.path.join(self.db_folder, self.firebird_dbname)
def ensure_db_folder_exists(self):
"""Создание директории для БД, если она не существует"""
if not os.path.exists(self.db_folder):
os.makedirs(self.db_folder)
def create_database_if_needed(self):
"""Создание базы данных, если она не существует"""
# Для SQLite база создаётся автоматически при подключении
if self.db_type == "SQLite":
self.ensure_db_folder_exists()
return True, "SQLite база будет создана при подключении"
# Для RDB/firebird создаём базу явно с firebird-driver
self.ensure_db_folder_exists()
if not os.path.exists(self.db_file) and self.create_if_not_exists:
try:
create_database(
database=self.get_db_path(),
user=self.user,
password=self.password,
charset=self.charset,
overwrite=False
)
print(f"База данных создана: {self.db_file}")
return True, "База данных успешно создана"
except Exception as e:
return False, f"Ошибка создания БД: {str(e)}"
return True, "База данных уже существует"
def get_connection(self):
"""Создание подключения к БД (для SQLAlchemy creator)"""
return connect(
database=self.get_db_path(),
user=self.user,
password=self.password
)
def create_engine(self):
"""Создание SQLAlchemy engine в зависимости от типа БД"""
if self.db_type == "SQLite":
# Для SQLite используем стандартный диалект
self.ensure_db_folder_exists()
return create_engine(f"sqlite:///{self.db_file}")
else:
# Для Firebird используем firebird-driver
return create_engine(
"firebird+firebird://",
creator=self.get_connection
)
def update_from_params(self, params):
"""Обновление конфигурации из параметров диалога"""
if params["type"] == "RedDatabase":
self.db_type = "RedDatabase"
self.host = params["host"]
self.port = int(params["port"])
self.db_file = params["db_file"]
self.user = params["user"]
self.password = params["password"]
self.create_if_not_exists = params["create_new"]
elif params["type"] == "SQLite":
self.db_type = "SQLite"
self.db_file = params["db_file"]
В данном примере все пути к директориям соответствуют правилам Linux, например, RED OS. Внесите соответствующие изменения для Windows. Такие параметры подключения или создания БД РЕД База Данных, как sql_dialect, page_size, port должны быть указаны в конфигураторе firebird-driver, если они отличаются от значений по умолчанию.
Теперь все параметры подключения хранятся в одном месте (DatabaseConfig), и нет дублирования кода для создания БД и подключения. Конфигурация передаётся в диалоги - параметры берутся из одного источника.
Создадим класс DatabaseManager
Класс для работы с базой данных с использованием SQLAlchemy. Создает SQLAlchemy engine с использованием функции‑обёртки (через параметр creator).
Он осуществляет отображение метаданных, создаёт таблицы, получает записи, обновляет и удаляет их. При этом используется firebird-driver для непосредственного подключения к РЕД База Данных через SQLAlchemy (через параметр creator).
# -------------------------------------------------------------------
# Database Manager
# -------------------------------------------------------------------
class DatabaseManager:
def __init__(self, config):
"""Инициализация менеджера базы данных"""
self.config = config
self.engine = self._create_engine()
self.metadata = MetaData()
self.refresh_metadata()
self.Session = sessionmaker(bind=self.engine)
def _create_engine(self):
"""Создание engine в зависимости от типа БД"""
return self.config.create_engine()
def reconnect(self, new_config):
"""Переподключение с новой конфигурацией"""
self.config = new_config
if self.engine:
self.engine.dispose()
self.engine = self._create_engine()
self.refresh_metadata()
def refresh_metadata(self):
"""Обновление метаданных из базы данных"""
try:
# Метод reflect() заполняет объект MetaData информацией о таблицах, найденных в базе данных.
# Это позволяет динамически работать со структурами БД без явного объявления схемы.
self.metadata.reflect(bind=self.engine)
except exc.SQLAlchemyError as e:
self.show_error("Ошибка обновления метаданных", str(e))
def show_error(self, title, message):
"""Отображение диалога с ошибкой"""
msg = QtWidgets.QMessageBox()
msg.setIcon(QtWidgets.QMessageBox.Critical)
msg.setText(message)
msg.setWindowTitle(title)
msg.exec_()
def create_table(self, table_name, columns, id_name='id', autoinc=True):
"""Создание новой таблицы"""
try:
cols = []
# Создаем первичный ключ (ID) с возможным автоинкрементом,
# который будет служить уникальным идентификатором записи
if autoinc:
cols.append(Column(id_name, Integer, primary_key=True, autoincrement=True))
else:
cols.append(Column(id_name, Integer, primary_key=True))
for col in columns:
if col['type'] == 'ForeignKey':
fk = ForeignKey(f"{col['related_table']}.{col['related_column']}")
cols.append(Column(col['name'], Integer, fk, nullable=col.get('nullable', True)))
else:
if col['type'] == 'String':
cols.append(Column(col['name'], String(255), nullable=col.get('nullable', True)))
else:
cols.append(Column(col['name'], Integer, nullable=col.get('nullable', True)))
Table(table_name, self.metadata, *cols).create(self.engine)
# После создания обновляем метаданные, чтобы новые таблицы отразились в объекте metadata
self.refresh_metadata()
return True, "Таблица успешно создана"
except exc.SQLAlchemyError as e:
return False, f"Ошибка создания таблицы: {str(e)}"
def get_tables(self):
"""Получение списка таблиц"""
# Возвращает список имен таблиц, найденных в объекте metadata.
# Это удобно для динамического отображения доступных таблиц в интерфейсе.
return list(self.metadata.tables.keys())
def get_records(self, table_name):
"""Получение всех записей из таблицы"""
try:
# Получаем объект таблицы по имени из объекта metadata
table = self.metadata.tables[table_name]
# Выполняем SQL-запрос SELECT * с использованием метода
# select() для извлечения всех записей
with self.engine.connect() as conn:
results = conn.execute(table.select()).fetchall()
return results, None
except exc.SQLAlchemyError as e:
return None, str(e)
def insert_record(self, table_name, data):
"""Вставка новой записи"""
try:
table = self.metadata.tables[table_name]
with self.engine.begin() as conn:
conn.execute(table.insert().values(**data))
return True, "Запись успешно добавлена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка добавления записи: {str(e)}"
def delete_record(self, table_name, record_id):
"""Удаление записи по первичному ключу"""
try:
table = self.metadata.tables[table_name]
# Получаем имя первичного ключа для определения нужного столбца
pk_column = list(table.primary_key.columns)[0]
stmt = table.delete().where(pk_column == record_id)
with self.engine.begin() as conn:
conn.execute(stmt)
return True, "Запись удалена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка удаления записи: {str(e)}"
def update_record(self, table_name, record_id, data):
"""Обновление записи"""
try:
table = self.metadata.tables[table_name]
pk_column = list(table.primary_key.columns)[0]
# Формируем запрос UPDATE, передавая новые значения через values()
update_stmt = table.update().where(pk_column == record_id).values(**data)
with self.engine.begin() as conn:
conn.execute(update_stmt)
return True, "Запись обновлена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка обновления записи: {str(e)}"
def drop_table(self, table_name):
"""Удаление таблицы"""
try:
# Получаем объект таблицы для удаления
table = self.metadata.tables[table_name]
table.drop(self.engine)
# Обновляем метаданные, чтобы удаленная таблица не отображалась в интерфейсе
self.refresh_metadata()
return True, f"Таблица '{table_name}' успешно удалена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка удаления таблицы: {str(e)}"
def get_pk_column_name(self, table_name):
"""Получение имени первичного ключа таблицы"""
table = self.metadata.tables[table_name]
pk_columns = list(table.primary_key.columns)
return pk_columns[0].name if pk_columns else None
Пояснение методов класса:
-
__init__(self, engine)\ Инициализирует объект DatabaseManager: сохраняет объект подключения (engine), создает объект MetaData, загружает метаданные, а также создает фабрику сессий и активную сессию для работы с базой данных.
-
refresh_metadata(self)\ Обновляет объект MetaData, вызывая метод reflect(), который считывает структуру (таблицы и столбцы) из базы данных. В случае ошибки отображает сообщение через show_error.
-
show_error(self, title, message)\ Отображает окно сообщения с ошибкой, используя QtWidgets.QMessageBox. Позволяет пользователю увидеть сообщение об ошибке с заданным заголовком.
-
create_table(self, table_name, columns, id_name='id', autoinc=True)\ Создает новую таблицу в базе данных. Сначала добавляет первичный ключ с автоинкрементом (если задано), затем обрабатывает список дополнительных столбцов (включая внешние ключи) и создает таблицу через SQLAlchemy. После создания обновляет метаданные.
-
get_tables(self)\ Возвращает список имен таблиц, полученных из объекта MetaData. Это полезно для динамического отображения таблиц в интерфейсе.
-
get_records(self, table_name)\ Извлекает все записи из указанной таблицы, выполняя запрос SELECT * с использованием SQLAlchemy. Возвращает список записей или сообщение об ошибке.
-
delete_record(self, table_name, record_id)\ Удаляет запись из таблицы, используя значение первичного ключа для поиска нужной записи. Формирует запрос DELETE и выполняет его в транзакции.
-
update_record(self, table_name, record_id, data)\ Обновляет запись в таблице, устанавливая новые значения для столбцов, переданных в виде словаря. Формирует запрос UPDATE и выполняет его в транзакции.
-
drop_table(self, table_name)\ Удаляет указанную таблицу из базы данных, затем обновляет метаданные, чтобы таблица исчезла из интерфейса. Возвращает сообщение об успехе или ошибке.
Данный класс инкапсулирует основные операции с базой данных, обеспечивая удобный интерфейс для создания, изменения, удаления таблиц и записей, а также для получения информации о структуре базы данных.
Создадим класс CreateTableDialog
Класс для создания диалогового окна, которое позволяет пользователю задать имя таблицы, настроить первичный ключ и добавить дополнительные столбцы (в том числе с внешними ключами) для будущей таблицы в базе данных.
# -------------------------------------------------------------------
# Диалог создания таблицы
# -------------------------------------------------------------------
class CreateTableDialog(QtWidgets.QDialog):
def __init__(self, db_manager, parent=None):
super().__init__(parent)
self.db_manager = db_manager
self.setWindowTitle("Создать новую таблицу")
self.setMinimumWidth(500)
self.columns = []
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout()
id_layout = QtWidgets.QHBoxLayout()
self.id_name = QtWidgets.QLineEdit("id")
self.autoinc_check = QtWidgets.QCheckBox("Автоинкремент")
self.autoinc_check.setChecked(True)
id_layout.addWidget(QtWidgets.QLabel("Имя столбца ID:"))
id_layout.addWidget(self.id_name)
id_layout.addWidget(self.autoinc_check)
layout.addLayout(id_layout)
self.table_name = QtWidgets.QLineEdit()
layout.addWidget(QtWidgets.QLabel("Имя таблицы:"))
layout.addWidget(self.table_name)
self.columns_scroll = QtWidgets.QScrollArea()
self.columns_widget = QtWidgets.QWidget()
self.columns_layout = QtWidgets.QVBoxLayout(self.columns_widget)
self.columns_scroll.setWidget(self.columns_widget)
self.columns_scroll.setWidgetResizable(True)
layout.addWidget(QtWidgets.QLabel("Столбцы:"))
layout.addWidget(self.columns_scroll)
btn_layout = QtWidgets.QHBoxLayout()
self.add_column_btn = QtWidgets.QPushButton("Добавить столбец")
self.add_column_btn.clicked.connect(self.add_column)
btn_layout.addWidget(self.add_column_btn)
self.add_fk_btn = QtWidgets.QPushButton("Добавить внешний ключ")
self.add_fk_btn.clicked.connect(self.add_foreign_key)
btn_layout.addWidget(self.add_fk_btn)
layout.addLayout(btn_layout)
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(self.validate)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
self.add_column()
def add_column(self, fk_data=None):
widget = ColumnWidget(fk_data)
self.columns_layout.addWidget(widget)
def add_foreign_key(self):
dialog = FkConfigDialog(self.db_manager, self)
if dialog.exec_():
self.add_column(fk_data=dialog.get_data())
def validate(self):
if not self.table_name.text().strip():
QtWidgets.QMessageBox.critical(self, "Ошибка", "Имя таблицы обязательно")
return
columns = []
for i in range(self.columns_layout.count()):
widget = self.columns_layout.itemAt(i).widget()
if widget and isinstance(widget, ColumnWidget):
col_data = widget.get_data()
if col_data:
columns.append(col_data)
if not columns:
QtWidgets.QMessageBox.critical(self, "Ошибка", "Необходимо добавить хотя бы один столбец")
return
self.columns = columns
self.accept()
Пояснение методов класса:
-
__init__(self, db_manager, parent=None)\ Инициализирует диалоговое окно для создания новой таблицы. Сохраняет менеджер базы данных, устанавливает заголовок окна и минимальную ширину, а также создаёт интерфейс с полями для ввода имени таблицы, настройки первичного ключа и добавления столбцов с помощью вызова метода init_ui(self).
-
add_column(self, fk_data=None)\ Добавляет новый виджет для настройки столбца. Если переданы данные для внешнего ключа (fk_data), то виджет создается с предварительной настройкой внешнего ключа.
-
add_foreign_key(self)\ Открывает диалоговое окно для настройки внешнего ключа (с использованием класса FkConfigDialog). Если пользователь успешно завершает диалог, данные о внешнем ключе передаются методу add_column для добавления нового столбца с внешним ключом.
-
validate(self)\ Проверяет, заполнены ли обязательные поля (например, имя таблицы) и добавлены ли столбцы. Если проверка не пройдена, выводит сообщение об ошибке. Если данные корректны, сохраняет список столбцов и завершает диалог с результатом "ОК", что позволяет дальнейшую обработку введенных данных для создания таблицы.
Данный класс инкапсулирует логику взаимодействия с пользователем для создания новой таблицы, обеспечивая удобный и наглядный интерфейс для задания параметров таблицы перед её созданием в базе данных.
Создадим класс ColumnWidget
Виджет для настройки одного столбца таблицы. Позволяет пользователю задать имя столбца и выбрать его тип (String, Integer или ForeignKey). Если переданы данные внешнего ключа (fk_data), виджет настраивается для работы с внешним ключом, отображая соответствующую информацию.
# -------------------------------------------------------------------
# Виджет столбца
# -------------------------------------------------------------------
class ColumnWidget(QtWidgets.QWidget):
def __init__(self, fk_data=None):
super().__init__()
self.fk_data = fk_data
self.init_ui()
def init_ui(self):
layout = QtWidgets.QHBoxLayout()
self.name_edit = QtWidgets.QLineEdit()
self.type_combo = QtWidgets.QComboBox()
self.type_combo.addItems(["String", "Integer", "ForeignKey"])
self.nullable_check = QtWidgets.QCheckBox("Nullable")
self.nullable_check.setChecked(True)
if self.fk_data:
self.type_combo.setCurrentText("ForeignKey")
self.name_edit.setText(self.fk_data['name'])
self.type_combo.setEnabled(False)
self.fk_label = QtWidgets.QLabel(
f"-> {self.fk_data['related_table']}.{self.fk_data['related_column']}"
)
layout.addWidget(self.fk_label)
layout.addWidget(QtWidgets.QLabel("Имя:"))
layout.addWidget(self.name_edit)
layout.addWidget(QtWidgets.QLabel("Тип:"))
layout.addWidget(self.type_combo)
layout.addWidget(self.nullable_check)
self.setLayout(layout)
def get_data(self):
name = self.name_edit.text().strip()
if not name:
return None
col_type = self.type_combo.currentText()
data = {
'name': name,
'type': col_type,
'nullable': self.nullable_check.isChecked()
}
if col_type == "ForeignKey" and self.fk_data:
data.update(self.fk_data)
return data
Пояснение методов класса:
-
__init__(self, fk_data=None)\ Инициализирует виджет столбца. Создает элементы управления для ввода имени и выбора типа столбца. Если переданы данные для внешнего ключа (fk_data), виджет настраивается таким образом, что тип автоматически устанавливается в "ForeignKey", имя заполняется из fk_data, выбор типа блокируется, и отображается метка с информацией о связанном столбце.
-
get_data(self)\ Собирает введенные данные: имя столбца и выбранный тип. Если имя пустое, возвращает None. В случае, если столбец является внешним ключом, объединяет данные из fk_data с основными данными. Возвращает словарь с параметрами столбца, который может использоваться для создания таблицы.
Создадим класс FkConfigDialog
Диалоговое окно для настройки параметров внешнего ключа. Позволяет пользователю выбрать связанную таблицу и столбец, а также задать имя нового столбца, который будет внешним ключом.
# -------------------------------------------------------------------
# Диалог настройки внешнего ключа
# -------------------------------------------------------------------
class FkConfigDialog(QtWidgets.QDialog):
def __init__(self, db_manager, parent=None):
super().__init__(parent)
self.db_manager = db_manager
self.setWindowTitle("Настройка внешнего ключа")
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout()
self.table_combo = QtWidgets.QComboBox()
self.column_combo = QtWidgets.QComboBox()
self.name_edit = QtWidgets.QLineEdit()
self.db_manager.refresh_metadata()
self.table_combo.addItems(self.db_manager.get_tables())
self.table_combo.currentIndexChanged.connect(self.update_columns)
form = QtWidgets.QFormLayout()
form.addRow("Имя столбца:", self.name_edit)
form.addRow("Связанная таблица:", self.table_combo)
form.addRow("Связанный столбец:", self.column_combo)
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addLayout(form)
layout.addWidget(button_box)
self.setLayout(layout)
self.update_columns()
def update_columns(self):
self.column_combo.clear()
table_name = self.table_combo.currentText()
if table_name and table_name in self.db_manager.metadata.tables:
table = self.db_manager.metadata.tables[table_name]
self.column_combo.addItems([c.name for c in table.columns])
def get_data(self):
return {
'name': self.name_edit.text().strip(),
'related_table': self.table_combo.currentText(),
'related_column': self.column_combo.currentText()
}
Пояснение методов класса:
-
__init__(self, db_manager, parent=None)\ Инициализирует диалоговое окно для настройки внешнего ключа. Загружает метаданные из базы данных, заполняет комбобокс выбора таблиц, а также устанавливает связь изменения выбора таблицы с обновлением списка столбцов.
-
update_columns(self)\ Обновляет содержимое комбобокса для выбора столбца, основываясь на выбранной таблице. Получает список столбцов из метаданных, соответствующих выбранной таблице, и заполняет ими комбобокс.
-
get_data(self)\ Собирает данные, введенные пользователем (имя столбца, выбранная связанная таблица и столбец) и возвращает их в виде словаря. Эти данные затем используются для создания столбца с внешним ключом.
Класс инкапсулирует логику взаимодействия с пользователем для задания параметров внешнего ключа, что позволяет затем использовать эти настройки при создании нового столбца таблицы с внешним ключом.
Создадим класс EditRecordDialog
Диалоговое окно для редактирования записи в выбранной таблице. Позволяет пользователю изменить значения полей записи (за исключением первичного ключа) и сохранить изменения в базе данных через DatabaseManager.
# -------------------------------------------------------------------
# Диалог редактирования записи
# -------------------------------------------------------------------
class EditRecordDialog(QtWidgets.QDialog):
def __init__(self, db_manager, table_name, record_data):
super().__init__()
self.db_manager = db_manager
self.table_name = table_name
self.record_data = dict(record_data._mapping)
self.record_id = self.record_data[self.db_manager.get_pk_column_name(table_name)]
self.init_ui()
def init_ui(self):
self.setWindowTitle(f"Редактирование записи в {self.table_name}")
layout = QtWidgets.QVBoxLayout()
table = self.db_manager.metadata.tables[self.table_name]
self.inputs = {}
for col in table.columns:
if col.primary_key:
continue
row = QtWidgets.QHBoxLayout()
lbl = QtWidgets.QLabel(col.name)
value = self.record_data.get(col.name, '')
le = QtWidgets.QLineEdit(str(value) if value is not None else '')
if isinstance(col.type, Integer):
le.setValidator(QtGui.QIntValidator())
self.inputs[col.name] = le
row.addWidget(lbl)
row.addWidget(le)
layout.addLayout(row)
btn_save = QtWidgets.QPushButton("Сохранить изменения")
btn_save.clicked.connect(self.save_changes)
layout.addWidget(btn_save)
self.setLayout(layout)
def save_changes(self):
data = {}
table = self.db_manager.metadata.tables[self.table_name]
for col_name, le in self.inputs.items():
column = table.columns[col_name]
value = le.text().strip()
if not value:
if column.nullable:
data[col_name] = None
continue
else:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Поле {col_name} обязательно"
)
return
if isinstance(column.type, Integer):
try:
data[col_name] = int(value)
except ValueError:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Неверное число в {col_name}"
)
return
else:
data[col_name] = value
success, message = self.db_manager.update_record(
self.table_name, self.record_id, data
)
if success:
QtWidgets.QMessageBox.information(self, "Успех", message)
self.accept()
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
Пояснение методов класса:
-
__init__(self, db_manager, table_name, record_data)\ Инициализирует диалоговое окно для редактирования записи. Сохраняет ссылку на менеджер базы данных, имя таблицы и идентификатор редактируемой записи, после чего вызывает метод init_ui для создания пользовательского интерфейса.
-
init_ui(self, record_data)\ Создает интерфейс диалога: устанавливает заголовок окна, создает поля ввода для каждого столбца записи (кроме первичного ключа), заполняет их текущими значениями, устанавливает валидаторы для числовых полей и размещает кнопку сохранения изменений.
-
save_changes(self)\ Собирает данные из полей ввода, выполняет преобразование значений в нужные типы (например, для целочисленных столбцов), и вызывает метод update_record из класса DatabaseManager для обновления записи в базе данных. В случае успеха выводит информационное сообщение и закрывает диалог, иначе – выводит сообщение об ошибке.
Класс инкапсулирует логику редактирования записи, позволяя пользователю изменять данные в удобном графическом интерфейсе, а затем сохранять их в базе данных с помощью SQLAlchemy.
Создадим класс DataEntryWindow
Окно для ввода новой записи в выбранную таблицу базы данных. Позволяет пользователю заполнить форму, где для каждого столбца (за исключением первичного ключа) создаются поля ввода. После заполнения формы данные проверяются и добавляются в базу данных через SQLAlchemy.
# -------------------------------------------------------------------
# Окно ввода новой записи
# -------------------------------------------------------------------
class DataEntryWindow(QtWidgets.QDialog):
def __init__(self, db_manager, table_name):
super().__init__()
self.db_manager = db_manager
self.table_name = table_name
self.init_ui()
def init_ui(self):
self.setWindowTitle(f"Добавить запись в {self.table_name}")
layout = QtWidgets.QVBoxLayout()
try:
table = self.db_manager.metadata.tables[self.table_name]
self.inputs = {}
for column in table.columns:
if column.primary_key:
continue
row = QtWidgets.QHBoxLayout()
lbl = QtWidgets.QLabel(column.name)
le = QtWidgets.QLineEdit()
if isinstance(column.type, Integer):
le.setValidator(QtGui.QIntValidator())
self.inputs[column.name] = le
row.addWidget(lbl)
row.addWidget(le)
layout.addLayout(row)
btn_add = QtWidgets.QPushButton("Добавить запись")
btn_add.clicked.connect(self.add_record)
layout.addWidget(btn_add)
self.setLayout(layout)
except KeyError:
QtWidgets.QMessageBox.critical(self, "Ошибка", "Таблица не найдена")
self.reject()
def add_record(self):
data = {}
table = self.db_manager.metadata.tables[self.table_name]
for col_name, le in self.inputs.items():
text = le.text().strip()
column = table.columns[col_name]
if not text:
if column.nullable:
data[col_name] = None
continue
else:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Поле {col_name} обязательно"
)
return
if isinstance(column.type, Integer):
try:
data[col_name] = int(text)
except ValueError:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Неверное число в {col_name}"
)
return
else:
data[col_name] = text
success, message = self.db_manager.insert_record(self.table_name, data)
if success:
QtWidgets.QMessageBox.information(self, "Успех", message)
self.accept()
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
Пояснение методов класса:
-
__init__(self, db_manager, table_name)\ Инициализирует окно ввода новой записи. Сохраняет менеджер базы данных и имя таблицы, в которую нужно добавить запись, после чего вызывает метод init_ui для создания интерфейса окна.
-
init_ui(self)\ Создает интерфейс окна ввода. Формирует форму, состоящую из полей ввода для каждого столбца таблицы (за исключением первичного ключа). Если таблица не найдена в метаданных, выводит сообщение об ошибке и закрывает окно.
-
add_record(self)\ Собирает данные, введенные пользователем, из всех полей ввода. Проводит проверку на заполненность полей и корректное преобразование данных к нужным типам (например, для целочисленных столбцов). Затем, используя SQLAlchemy, выполняет вставку новой записи в базу данных. В случае успешной вставки выводит сообщение об успехе и закрывает окно, иначе – сообщение об ошибке.
Класс инкапсулирует процесс ввода новой записи, обеспечивая удобный графический интерфейс для пользователя и интегрируя проверку данных с операциями вставки в базу данных через SQLAlchemy.
Создадим класс ConnectDatabaseDialog
Диалоговое окно для подключения к базе данных. Позволяет пользователю выбрать тип базы данных (РЕД База Данных или SQLite) и задать соответствующие параметры подключения (например, хост, порт, файл базы данных, учетные данные). В зависимости от выбранного типа отображаются разные настройки через виджет‑переключатель (stacked widget).
# -------------------------------------------------------------------
# Диалог подключения к базе данных
# -------------------------------------------------------------------
class ConnectDatabaseDialog(QtWidgets.QDialog):
def __init__(self, config, parent=None):
super().__init__(parent)
self.config = config
self.setWindowTitle("Подключение к базе данных")
self.setMinimumWidth(400)
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout()
self.db_type_combo = QtWidgets.QComboBox()
self.db_type_combo.addItems(["RedDatabase", "SQLite"])
self.db_type_combo.currentIndexChanged.connect(self.switch_db_type)
layout.addWidget(QtWidgets.QLabel("Тип базы данных:"))
layout.addWidget(self.db_type_combo)
self.stack = QtWidgets.QStackedWidget()
self.stack.addWidget(self.reddatabase_widget())
self.stack.addWidget(self.sqlite_widget())
layout.addWidget(self.stack)
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
# Устанавливаем текущий тип БД
self.db_type_combo.setCurrentText(self.config.db_type)
# Обновляем пути в соответствии с типом БД
self.update_db_paths()
def reddatabase_widget(self):
widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self.fb_host = QtWidgets.QLineEdit(self.config.host)
self.fb_port = QtWidgets.QLineEdit(str(self.config.port))
self.fb_db_file = QtWidgets.QLineEdit()
browse_btn = QtWidgets.QPushButton("Обзор")
browse_btn.clicked.connect(self.browse_fb_db)
h_layout = QtWidgets.QHBoxLayout()
h_layout.addWidget(self.fb_db_file)
h_layout.addWidget(browse_btn)
self.fb_user = QtWidgets.QLineEdit(self.config.user)
self.fb_password = QtWidgets.QLineEdit(self.config.password)
self.fb_password.setEchoMode(QtWidgets.QLineEdit.Password)
self.fb_create_new = QtWidgets.QCheckBox("Создать новую базу, если не существует")
self.fb_create_new.setChecked(self.config.create_if_not_exists)
layout.addRow("Хост:", self.fb_host)
layout.addRow("Порт:", self.fb_port)
layout.addRow("Файл базы данных:", h_layout)
layout.addRow("Пользователь:", self.fb_user)
layout.addRow("Пароль:", self.fb_password)
layout.addRow("", self.fb_create_new)
widget.setLayout(layout)
return widget
def sqlite_widget(self):
widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self.sqlite_db_file = QtWidgets.QLineEdit()
browse_btn = QtWidgets.QPushButton("Обзор")
browse_btn.clicked.connect(self.browse_sqlite_db)
h_layout = QtWidgets.QHBoxLayout()
h_layout.addWidget(self.sqlite_db_file)
h_layout.addWidget(browse_btn)
layout.addRow("Файл базы данных:", h_layout)
widget.setLayout(layout)
return widget
def update_db_paths(self):
"""Обновление путей к БД в соответствии с типом"""
# Устанавливаем правильные пути по умолчанию для каждого типа
self.fb_db_file.setText(self.config.get_default_db_file("RedDatabase"))
self.sqlite_db_file.setText(self.config.get_default_db_file("SQLite"))
# Если конфигурация уже содержит путь, используем его
if self.config.db_type == "RedDatabase":
self.fb_db_file.setText(self.config.db_file)
elif self.config.db_type == "SQLite":
self.sqlite_db_file.setText(self.config.db_file)
def switch_db_type(self, index):
self.stack.setCurrentIndex(index)
def browse_fb_db(self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Выбрать файл базы данных", "",
"RedDatabase DB Files (*.fdb);;Все файлы (*)"
)
if filename:
self.fb_db_file.setText(filename)
def browse_sqlite_db(self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Выбрать файл базы данных", "",
"SQLite DB Files (*.sqlite *.db);;Все файлы (*)"
)
if filename:
self.sqlite_db_file.setText(filename)
def get_params(self):
db_type = self.db_type_combo.currentText()
if db_type == "RedDatabase":
return {
"type": "RedDatabase",
"host": self.fb_host.text().strip(),
"port": self.fb_port.text().strip(),
"db_file": self.fb_db_file.text().strip(),
"user": self.fb_user.text().strip(),
"password": self.fb_password.text().strip(),
"create_new": self.fb_create_new.isChecked()
}
else:
return {
"type": "SQLite",
"db_file": self.sqlite_db_file.text().strip()
}
Пояснение методов класса:
-
__init__(self, parent=None)\ Инициализирует диалог подключения к базе данных. Создает основной интерфейс, включающий комбобокс для выбора типа базы данных (РЕД База Данных или SQLite), переключаемый виджет (stacked widget) для отображения соответствующих настроек подключения и стандартный набор кнопок (ОК/Отмена).
-
reddatabase_widget(self)\ Создает виджет с настройками подключения для базы данных РЕД База Данных. Включает поля ввода для хоста, порта, файла базы данных, имени пользователя, пароля и чекбокс для создания новой базы данных, если она не существует. Виджет возвращается для добавления в переключаемый контейнер.
-
sqlite_widget(self)\ Создает виджет с настройками подключения для базы данных SQLite. Включает поле ввода для файла базы данных с кнопкой для выбора файла. Виджет возвращается для добавления в переключаемый контейнер.
-
switch_db_type(self, index)\ Переключает отображение настроек подключения, выбирая нужный виджет из переключаемого контейнера в зависимости от выбранного типа базы данных (индекс в комбобоксе).
-
browse_fb_db(self)\ Открывает диалог выбора файла для базы данных РЕД База Данных. При выборе файла обновляет текстовое поле, где указан путь к файлу базы данных.
-
browse_sqlite_db(self)\ Открывает диалог выбора файла для базы данных SQLite. При выборе файла обновляет текстовое поле с путем к файлу базы данных.
-
get_params(self)\ Собирает и возвращает параметры подключения, введенные пользователем, в виде словаря. Если выбран тип "RedDatabase", возвращаются параметры: тип, хост, порт, файл базы данных, имя пользователя, пароль и флаг создания новой базы; если выбран SQLite – возвращается только тип и путь к файлу базы данных.
Данный класс инкапсулирует логику ввода параметров подключения к базе данных, позволяя пользователю легко выбирать между различными типами баз данных и задавать соответствующие настройки через удобный графический интерфейс.
Создадим класс ERDiagramDialog
Диалоговое окно для отображения ER-диаграммы базы данных. Диаграмма генерируется с использованием библиотеки pydot на основе метаданных, полученных через SQLAlchemy. Это позволяет визуально представить связи между таблицами, отображая имена таблиц, их столбцы и связи по внешним ключам.
# -------------------------------------------------------------------
# Диалог отображения ER-диаграммы
# -------------------------------------------------------------------
class ERDiagramDialog(QtWidgets.QDialog):
def __init__(self, db_manager, parent=None):
super().__init__(parent)
self.setWindowTitle("ER-диаграмма базы данных")
self.resize(800, 600)
self.db_manager = db_manager
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout(self)
graph = pydot.Dot(graph_type='digraph')
for table_name, table in self.db_manager.metadata.tables.items():
label = f"<<TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'>"
label += f"<TR><TD COLSPAN='1'><B>{table_name}</B></TD></TR>"
for col in table.columns:
label += f"<TR><TD>{col.name}</TD></TR>"
label += "</TABLE>>"
node = pydot.Node(table_name, shape="plaintext", label=label)
graph.add_node(node)
for col in table.columns:
for fk in col.foreign_keys:
parent_table = fk.column.table.name
edge = pydot.Edge(table_name, parent_table, label=col.name)
graph.add_edge(edge)
try:
png_str = graph.create_png()
pixmap = QtGui.QPixmap()
pixmap.loadFromData(png_str)
label = QtWidgets.QLabel()
label.setPixmap(pixmap)
label.setAlignment(QtCore.Qt.AlignCenter)
scroll = QtWidgets.QScrollArea()
scroll.setWidget(label)
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
except Exception as e:
QtWidgets.QMessageBox.critical(
self, "Ошибка",
f"Не удалось построить диаграмму: {str(e)}\n\n"
f"Убедитесь, что Graphviz установлен и добавлен в PATH"
)
Создадим класс MainWindow
Главное окно приложения для управления базой данных. Оно предоставляет пользователю интерфейс для отображения списка таблиц, просмотра и редактирования записей, а также для выполнения операций создания и удаления таблиц, подключения к базе данных и отображения ER-диаграммы.
# -------------------------------------------------------------------
# Главное окно приложения
# -------------------------------------------------------------------
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, db_manager):
super().__init__()
self.db_manager = db_manager
self.selected_table = None
self.init_ui()
self.update_table_list()
def init_ui(self):
self.setWindowTitle("Менеджер базы данных")
self.setGeometry(100, 100, 1000, 600)
menubar = self.menuBar()
table_menu = menubar.addMenu('Таблица')
new_table_action = QtWidgets.QAction('Создать новую таблицу', self)
new_table_action.triggered.connect(self.show_create_table_dialog)
table_menu.addAction(new_table_action)
drop_table_action = QtWidgets.QAction('Удалить таблицу', self)
drop_table_action.triggered.connect(self.delete_table_dialog)
table_menu.addAction(drop_table_action)
db_menu = menubar.addMenu("База данных")
connect_action = QtWidgets.QAction("Подключиться к базе данных", self)
connect_action.triggered.connect(self.connect_to_database)
db_menu.addAction(connect_action)
er_action = QtWidgets.QAction("ER-диаграмма", self)
er_action.triggered.connect(self.show_er_diagram)
db_menu.addAction(er_action)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
layout = QtWidgets.QHBoxLayout(central_widget)
left_panel = QtWidgets.QVBoxLayout()
self.table_list = QtWidgets.QListWidget()
self.table_list.itemClicked.connect(self.load_table_data)
left_panel.addWidget(self.table_list)
self.btn_add_record = QtWidgets.QPushButton("Добавить запись")
self.btn_add_record.clicked.connect(self.open_add_record_window)
left_panel.addWidget(self.btn_add_record)
self.btn_refresh = QtWidgets.QPushButton("Обновить")
self.btn_refresh.clicked.connect(self.refresh_table)
left_panel.addWidget(self.btn_refresh)
layout.addLayout(left_panel, 1)
self.table_view = QtWidgets.QTableWidget()
layout.addWidget(self.table_view, 3)
def update_table_list(self):
self.table_list.clear()
self.db_manager.refresh_metadata()
self.table_list.addItems(self.db_manager.get_tables())
def refresh_table(self):
if self.selected_table:
item = QtWidgets.QListWidgetItem(self.selected_table)
self.load_table_data(item)
def show_create_table_dialog(self):
dialog = CreateTableDialog(self.db_manager, self)
if dialog.exec_():
id_name = dialog.id_name.text().strip()
autoinc = dialog.autoinc_check.isChecked()
columns = [col for col in dialog.columns if col is not None]
success, message = self.db_manager.create_table(
dialog.table_name.text().strip(),
columns,
id_name=id_name,
autoinc=autoinc
)
if success:
self.update_table_list()
QtWidgets.QMessageBox.information(self, "Успех", message)
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
def delete_table_dialog(self):
tables = self.db_manager.get_tables()
if not tables:
QtWidgets.QMessageBox.information(self, "Информация", "Нет таблиц для удаления")
return
table, ok = QtWidgets.QInputDialog.getItem(
self, "Удалить таблицу", "Выберите таблицу для удаления:", tables, 0, False
)
if ok and table:
confirm = QtWidgets.QMessageBox.question(
self, "Подтверждение удаления",
f"Вы уверены, что хотите удалить таблицу '{table}'?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if confirm == QtWidgets.QMessageBox.Yes:
success, message = self.db_manager.drop_table(table)
if success:
self.update_table_list()
QtWidgets.QMessageBox.information(self, "Успех", message)
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
def connect_to_database(self):
dialog = ConnectDatabaseDialog(self.db_manager.config, self)
if dialog.exec_():
params = dialog.get_params()
# Создаём новую конфигурацию
new_config = DatabaseConfig()
new_config.update_from_params(params)
# Создаём БД если нужно (только для Firebird)
if new_config.db_type == "RedDatabase":
success, message = new_config.create_database_if_needed()
if not success:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
return
# Переподключаемся
try:
self.db_manager.reconnect(new_config)
self.update_table_list()
QtWidgets.QMessageBox.information(self, "Успех", "Подключение установлено")
except Exception as e:
QtWidgets.QMessageBox.critical(self, "Ошибка подключения", str(e))
def show_er_diagram(self):
dialog = ERDiagramDialog(self.db_manager, self)
dialog.exec_()
def load_table_data(self, item):
self.selected_table = item.text()
self.table_view.clearContents()
self.table_view.setRowCount(0)
self.table_view.setColumnCount(0)
records, error = self.db_manager.get_records(self.selected_table)
if error:
QtWidgets.QMessageBox.critical(self, "Ошибка", error)
return
try:
table = self.db_manager.metadata.tables[self.selected_table]
pk_col_name = self.db_manager.get_pk_column_name(self.selected_table)
self.table_view.setColumnCount(len(table.columns) + 2)
headers = [col.name for col in table.columns] + ["", ""]
self.table_view.setHorizontalHeaderLabels(headers)
self.table_view.setRowCount(len(records))
for row_idx, row in enumerate(records):
for col_idx, value in enumerate(row):
self.table_view.setItem(
row_idx, col_idx,
QtWidgets.QTableWidgetItem(str(value) if value is not None else 'NULL')
)
pk_value = row._mapping[pk_col_name] if pk_col_name else row[0]
btn_delete = QtWidgets.QPushButton("Удалить")
btn_delete.clicked.connect(
lambda _, pk=pk_value: self.delete_record(pk)
)
self.table_view.setCellWidget(row_idx, len(table.columns), btn_delete)
btn_edit = QtWidgets.QPushButton("Редактировать")
btn_edit.clicked.connect(
lambda _, r=row: self.edit_record(r)
)
self.table_view.setCellWidget(row_idx, len(table.columns) + 1, btn_edit)
except KeyError:
QtWidgets.QMessageBox.critical(self, "Ошибка", "Таблица не найдена")
def delete_record(self, record_id):
confirm = QtWidgets.QMessageBox.question(
self, "Подтверждение удаления",
"Вы уверены, что хотите удалить эту запись?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if confirm == QtWidgets.QMessageBox.Yes:
success, message = self.db_manager.delete_record(
self.selected_table, record_id
)
if success:
self.refresh_table()
QtWidgets.QMessageBox.information(self, "Успех", message)
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
def edit_record(self, record_data):
dialog = EditRecordDialog(self.db_manager, self.selected_table, record_data)
if dialog.exec_() == QtWidgets.QDialog.Accepted:
self.refresh_table()
def open_add_record_window(self):
if self.selected_table:
dialog = DataEntryWindow(self.db_manager, self.selected_table)
if dialog.exec_():
self.refresh_table()
def closeEvent(self, event):
if self.db_manager and self.db_manager.engine:
self.db_manager.engine.dispose()
event.accept()
Пояснение методов класса:
-
__init__(self, db_manager)\ Инициализирует главное окно, устанавливая менеджер базы данных, вызывая метод для создания интерфейса (init_ui) и обновляя список таблиц.
-
init_ui(self)\ Создает и настраивает элементы интерфейса главного окна. Формирует меню для управления таблицами и базой данных, а также располагает левую панель (список таблиц и кнопки для добавления/обновления записей) и центральное окно для отображения данных.
-
update_table_list(self)\ Обновляет список таблиц в левом списке. Очищает список, обновляет метаданные через db_manager и заполняет список именами таблиц.
-
refresh_table(self)\ Если выбрана таблица, обновляет ее данные, вызывая метод load_table_data.
-
show_create_table_dialog(self)\ Открывает диалог создания новой таблицы. После успешного ввода параметров и создания таблицы обновляет список таблиц и выводит сообщение об успехе или ошибке.
-
delete_table_dialog(self)\ Открывает диалог выбора таблицы для удаления. После подтверждения вызывает метод drop_table из db_manager, обновляет список таблиц и выводит сообщение о результате операции.
-
connect_to_database(self)\ Открывает диалог подключения к базе данных. На основании введенных параметров создает новый SQLAlchemy engine (с поддержкой РЕД База Данных через firebird-driver или SQLite), обновляет db_manager, список таблиц и выводит сообщение об успехе или ошибке подключения.
-
show_er_diagram(self)\ Открывает диалоговое окно для отображения ER-диаграммы базы данных.
-
load_table_data(self, item)\ Загружает данные выбранной таблицы в центральное окно. Очищает и заполняет QTableWidget записями, добавляя кнопки для удаления и редактирования каждой записи.
-
delete_record(self, record_id)\ Запрашивает подтверждение удаления записи и, при подтверждении, вызывает метод удаления записи из db_manager, обновляет данные таблицы и выводит сообщение о результате.
-
edit_record(self, record_data)\ Открывает диалог редактирования записи для выбранной записи. После успешного редактирования обновляет данные таблицы.
-
open_add_record_window(self)\ Открывает окно ввода новой записи для выбранной таблицы. После добавления новой записи обновляет отображение данных в таблице.
Данный класс обеспечивает полный графический интерфейс для управления базой данных, позволяя пользователю выполнять основные операции: создание, удаление и редактирование таблиц и записей, а также подключаться к базе данных и просматривать ER-диаграмму.
Основная функция main()
Основная функция приложения, которая выполняет следующие задачи:
-
Проверяет наличие каталога и файла базы данных по умолчанию (РЕД База Данных в папке db). Если база не существует, создает её с помощью firebird-driver.
-
Инициализирует экземпляр DatabaseManager для управления базой данных.
-
Создает и запускает графическое приложение с использованием PyQt5, открывая главное окно (MainWindow).
-
Обрабатывает исключения, выводя критическое сообщение об ошибке при невозможности запуска приложения.
# -------------------------------------------------------------------
# Основная функция
# -------------------------------------------------------------------
def main():
app = QtWidgets.QApplication(sys.argv)
try:
# Создаём единую конфигурацию
config = DatabaseConfig()
# Создаём БД если нужно (только для Firebird)
if config.db_type == "RedDatabase":
success, message = config.create_database_if_needed()
if not success:
QtWidgets.QMessageBox.critical(None, "Ошибка", message)
sys.exit(1)
print(f"Подключение к {config.host} для пользователя {config.user} создано успешно.")
# Инициализируем менеджер базы данных с конфигурацией
db_manager = DatabaseManager(config)
window = MainWindow(db_manager)
window.show()
sys.exit(app.exec_())
except Exception as e:
QtWidgets.QMessageBox.critical(
None, "Критическая ошибка",
f"Не удалось запустить приложение: {str(e)}"
)
sys.exit(1)
if __name__ == '__main__':
main()
Пояснение шагов функции:
-
Проверка и создание базы данных\ Определяется каталог db и путь к файлу базы данных database.fdb. Если каталог не существует, он создается. Если файла базы данных нет, вызывается функция firebird-driver create_database(), которая создает новую базу данных РЕД База Данных с заданными параметрами (хост, порт, пользователь, пароль и sql_dialect).
-
Инициализация DatabaseManager\ Создается объект DatabaseManager, который будет использоваться для выполнения операций с базой данных (создание таблиц, получение записей, обновление, удаление и т.д.) через SQLAlchemy.
-
Запуск приложения PyQt5\ Создается экземпляр QApplication, затем главное окно (MainWindow) и отображается. Функция sys.exit(app.exec_()) обеспечивает корректное завершение приложения после закрытия окна.
-
Обработка исключений\ Если в процессе запуска происходит ошибка, она перехватывается, и пользователю показывается критическое сообщение об ошибке через QtWidgets.QMessageBox, после чего приложение завершается с кодом ошибки.
Данная функция является точкой входа в приложение, обеспечивая подготовку окружения для работы с базой данных и запуск графического интерфейса пользователя.
Итог по созданным классам
- DatabaseConfig
Он обеспечивает централизованное хранение параметров подключения для всех случаев, поддерживаемых в приложении.
-
DatabaseManager Этот класс инкапсулирует основные операции с базой данных через SQLAlchemy. Он отвечает за загрузку метаданных (таблиц и столбцов), создание таблиц, извлечение записей, обновление и удаление данных. Благодаря параметру creator он использует firebird-driver для непосредственного подключения к базе данных РЕД База Данных.
-
CreateTableDialog Диалоговое окно, позволяющее пользователю создавать новые таблицы. В нём можно задать имя таблицы, настроить первичный ключ (с автоинкрементом) и добавить дополнительные столбцы (включая внешние ключи) с помощью виджетов ввода.
-
ColumnWidget Виджет для задания параметров одного столбца. Пользователь может ввести имя столбца и выбрать его тип (String, Integer или ForeignKey). При наличии настроек внешнего ключа виджет автоматически переключается в соответствующий режим.
-
FkConfigDialog Диалог для настройки параметров внешнего ключа. Позволяет выбрать связанную таблицу и столбец, а также задать имя столбца, который будет использоваться в качестве внешнего ключа.
-
EditRecordDialog Диалоговое окно для редактирования существующей записи в таблице. Генерирует форму с полями ввода для каждого столбца (кроме первичного ключа) и позволяет сохранить внесённые изменения через вызов методов DatabaseManager.
-
DataEntryWindow Окно для ввода новой записи в выбранную таблицу. Создает форму для заполнения значений всех столбцов (без первичного ключа) и выполняет проверку введенных данных перед вставкой записи в базу.
-
ConnectDatabaseDialog Диалог подключения к базе данных, позволяющий выбрать тип СУБД (RedDatabase или SQLite) и задать соответствующие параметры подключения, такие как хост, порт, файл базы данных, а также учетные данные.
-
ERDiagramDialog Диалог для отображения ER-диаграммы базы данных. С использованием библиотеки pydot на основе метаданных создается граф, который визуально демонстрирует таблицы, их столбцы и связи (внешние ключи).
-
MainWindow Главное окно приложения, которое объединяет весь функционал. Здесь реализовано меню для управления таблицами и базой данных, отображается список таблиц, данные выбранной таблицы (с возможностью редактирования и удаления записей), а также реализованы вызовы диалогов для создания таблиц, ввода новых записей, подключения к базе и отображения ER-диаграммы.
-
main() Точка входа в приложение. Функция настраивает дефолтное подключение к базе данных (создавая её при необходимости), создает SQLAlchemy engine, инициализирует DatabaseManager, запускает приложение PyQt5 и открывает главное окно.
Пример работы программы
На этом разработка программы завершена. Эта программа демонстрирует, как можно объединить мощь SQLAlchemy для работы с данными и гибкость PyQt5 для создания удобных пользовательских интерфейсов. Теперь, имея прочную основу, вы можете расширять функциональность приложения, добавлять новые возможности, улучшать визуальную составляющую и интегрировать дополнительные функции для более эффективного управления базами данных.




Полный код программы
import os
import sys
import pydot
from PyQt5 import QtWidgets, QtGui, QtCore
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey, exc
from sqlalchemy.orm import sessionmaker
from firebird.driver import connect, create_database
# -------------------------------------------------------------------
# Класс конфигурации базы данных
# -------------------------------------------------------------------
class DatabaseConfig:
"""Централизованное хранение параметров подключения"""
def __init__(self):
# Параметры по умолчанию
self.db_type = "RedDatabase"
self.host = "localhost"
self.port = 3050
self.user = "sysdba"
self.password = "masterkey"
self.charset = "UTF8"
self.sql_dialect = 3
# Базовая папка для БД
self.db_folder = "/var/_db"
# Раздельные имена файлов для разных типов БД
self.firebird_dbname = "db_tut.fdb"
self.sqlite_dbname = "db_tut.sqlite"
# Активный путь к БД (зависит от типа)
self.db_file = os.path.join(self.db_folder, self.firebird_dbname)
# Флаг создания новой БД
self.create_if_not_exists = True
def get_db_path(self):
"""Получение полного пути к БД в формате Firebird"""
return f"{self.host}:{self.db_file}"
def get_default_db_file(self, db_type):
"""Получение пути к БД по умолчанию для указанного типа"""
if db_type == "SQLite":
return os.path.join(self.db_folder, self.sqlite_dbname)
else:
return os.path.join(self.db_folder, self.firebird_dbname)
def ensure_db_folder_exists(self):
"""Создание директории для БД, если она не существует"""
if not os.path.exists(self.db_folder):
os.makedirs(self.db_folder)
def create_database_if_needed(self):
"""Создание базы данных, если она не существует"""
# Для SQLite база создаётся автоматически при подключении
if self.db_type == "SQLite":
self.ensure_db_folder_exists()
return True, "SQLite база будет создана при подключении"
# Для Firebird создаём базу явно
self.ensure_db_folder_exists()
if not os.path.exists(self.db_file) and self.create_if_not_exists:
try:
create_database(
database=self.get_db_path(),
user=self.user,
password=self.password,
charset=self.charset,
overwrite=False
)
print(f"База данных создана: {self.db_file}")
return True, "База данных успешно создана"
except Exception as e:
return False, f"Ошибка создания БД: {str(e)}"
return True, "База данных уже существует"
def get_connection(self):
"""Создание подключения к БД (для SQLAlchemy creator)"""
return connect(
database=self.get_db_path(),
user=self.user,
password=self.password
)
def create_engine(self):
"""Создание SQLAlchemy engine в зависимости от типа БД"""
if self.db_type == "SQLite":
# Для SQLite используем стандартный диалект
self.ensure_db_folder_exists()
return create_engine(f"sqlite:///{self.db_file}")
else:
# Для Firebird используем firebird-driver
return create_engine(
"firebird+firebird://",
creator=self.get_connection
)
def update_from_params(self, params):
"""Обновление конфигурации из параметров диалога"""
if params["type"] == "RedDatabase":
self.db_type = "RedDatabase"
self.host = params["host"]
self.port = int(params["port"])
self.db_file = params["db_file"]
self.user = params["user"]
self.password = params["password"]
self.create_if_not_exists = params["create_new"]
elif params["type"] == "SQLite":
self.db_type = "SQLite"
self.db_file = params["db_file"]
# -------------------------------------------------------------------
# Database Manager
# -------------------------------------------------------------------
class DatabaseManager:
def __init__(self, config):
"""Инициализация менеджера базы данных"""
self.config = config
self.engine = self._create_engine()
self.metadata = MetaData()
self.refresh_metadata()
self.Session = sessionmaker(bind=self.engine)
def _create_engine(self):
"""Создание engine в зависимости от типа БД"""
return self.config.create_engine()
def reconnect(self, new_config):
"""Переподключение с новой конфигурацией"""
self.config = new_config
if self.engine:
self.engine.dispose()
self.engine = self._create_engine()
self.refresh_metadata()
def refresh_metadata(self):
"""Обновление метаданных из базы данных"""
try:
self.metadata.reflect(bind=self.engine)
except exc.SQLAlchemyError as e:
self.show_error("Ошибка обновления метаданных", str(e))
def show_error(self, title, message):
"""Отображение диалога с ошибкой"""
msg = QtWidgets.QMessageBox()
msg.setIcon(QtWidgets.QMessageBox.Critical)
msg.setText(message)
msg.setWindowTitle(title)
msg.exec_()
def create_table(self, table_name, columns, id_name='id', autoinc=True):
"""Создание новой таблицы"""
try:
cols = []
if autoinc:
cols.append(Column(id_name, Integer, primary_key=True, autoincrement=True))
else:
cols.append(Column(id_name, Integer, primary_key=True))
for col in columns:
if col['type'] == 'ForeignKey':
fk = ForeignKey(f"{col['related_table']}.{col['related_column']}")
cols.append(Column(col['name'], Integer, fk, nullable=col.get('nullable', True)))
else:
if col['type'] == 'String':
cols.append(Column(col['name'], String(255), nullable=col.get('nullable', True)))
else:
cols.append(Column(col['name'], Integer, nullable=col.get('nullable', True)))
Table(table_name, self.metadata, *cols).create(self.engine)
self.refresh_metadata()
return True, "Таблица успешно создана"
except exc.SQLAlchemyError as e:
return False, f"Ошибка создания таблицы: {str(e)}"
def get_tables(self):
"""Получение списка таблиц"""
return list(self.metadata.tables.keys())
def get_records(self, table_name):
"""Получение всех записей из таблицы"""
try:
table = self.metadata.tables[table_name]
with self.engine.connect() as conn:
results = conn.execute(table.select()).fetchall()
return results, None
except exc.SQLAlchemyError as e:
return None, str(e)
def insert_record(self, table_name, data):
"""Вставка новой записи"""
try:
table = self.metadata.tables[table_name]
with self.engine.begin() as conn:
conn.execute(table.insert().values(**data))
return True, "Запись успешно добавлена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка добавления записи: {str(e)}"
def delete_record(self, table_name, record_id):
"""Удаление записи по первичному ключу"""
try:
table = self.metadata.tables[table_name]
pk_column = list(table.primary_key.columns)[0]
stmt = table.delete().where(pk_column == record_id)
with self.engine.begin() as conn:
conn.execute(stmt)
return True, "Запись удалена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка удаления записи: {str(e)}"
def update_record(self, table_name, record_id, data):
"""Обновление записи"""
try:
table = self.metadata.tables[table_name]
pk_column = list(table.primary_key.columns)[0]
update_stmt = table.update().where(pk_column == record_id).values(**data)
with self.engine.begin() as conn:
conn.execute(update_stmt)
return True, "Запись обновлена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка обновления записи: {str(e)}"
def drop_table(self, table_name):
"""Удаление таблицы"""
try:
table = self.metadata.tables[table_name]
table.drop(self.engine)
self.refresh_metadata()
return True, f"Таблица '{table_name}' успешно удалена"
except exc.SQLAlchemyError as e:
return False, f"Ошибка удаления таблицы: {str(e)}"
def get_pk_column_name(self, table_name):
"""Получение имени первичного ключа таблицы"""
table = self.metadata.tables[table_name]
pk_columns = list(table.primary_key.columns)
return pk_columns[0].name if pk_columns else None
# -------------------------------------------------------------------
# Диалог создания таблицы
# -------------------------------------------------------------------
class CreateTableDialog(QtWidgets.QDialog):
def __init__(self, db_manager, parent=None):
super().__init__(parent)
self.db_manager = db_manager
self.setWindowTitle("Создать новую таблицу")
self.setMinimumWidth(500)
self.columns = []
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout()
id_layout = QtWidgets.QHBoxLayout()
self.id_name = QtWidgets.QLineEdit("id")
self.autoinc_check = QtWidgets.QCheckBox("Автоинкремент")
self.autoinc_check.setChecked(True)
id_layout.addWidget(QtWidgets.QLabel("Имя столбца ID:"))
id_layout.addWidget(self.id_name)
id_layout.addWidget(self.autoinc_check)
layout.addLayout(id_layout)
self.table_name = QtWidgets.QLineEdit()
layout.addWidget(QtWidgets.QLabel("Имя таблицы:"))
layout.addWidget(self.table_name)
self.columns_scroll = QtWidgets.QScrollArea()
self.columns_widget = QtWidgets.QWidget()
self.columns_layout = QtWidgets.QVBoxLayout(self.columns_widget)
self.columns_scroll.setWidget(self.columns_widget)
self.columns_scroll.setWidgetResizable(True)
layout.addWidget(QtWidgets.QLabel("Столбцы:"))
layout.addWidget(self.columns_scroll)
btn_layout = QtWidgets.QHBoxLayout()
self.add_column_btn = QtWidgets.QPushButton("Добавить столбец")
self.add_column_btn.clicked.connect(self.add_column)
btn_layout.addWidget(self.add_column_btn)
self.add_fk_btn = QtWidgets.QPushButton("Добавить внешний ключ")
self.add_fk_btn.clicked.connect(self.add_foreign_key)
btn_layout.addWidget(self.add_fk_btn)
layout.addLayout(btn_layout)
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(self.validate)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
self.add_column()
def add_column(self, fk_data=None):
widget = ColumnWidget(fk_data)
self.columns_layout.addWidget(widget)
def add_foreign_key(self):
dialog = FkConfigDialog(self.db_manager, self)
if dialog.exec_():
self.add_column(fk_data=dialog.get_data())
def validate(self):
if not self.table_name.text().strip():
QtWidgets.QMessageBox.critical(self, "Ошибка", "Имя таблицы обязательно")
return
columns = []
for i in range(self.columns_layout.count()):
widget = self.columns_layout.itemAt(i).widget()
if widget and isinstance(widget, ColumnWidget):
col_data = widget.get_data()
if col_data:
columns.append(col_data)
if not columns:
QtWidgets.QMessageBox.critical(self, "Ошибка", "Необходимо добавить хотя бы один столбец")
return
self.columns = columns
self.accept()
# -------------------------------------------------------------------
# Виджет столбца
# -------------------------------------------------------------------
class ColumnWidget(QtWidgets.QWidget):
def __init__(self, fk_data=None):
super().__init__()
self.fk_data = fk_data
self.init_ui()
def init_ui(self):
layout = QtWidgets.QHBoxLayout()
self.name_edit = QtWidgets.QLineEdit()
self.type_combo = QtWidgets.QComboBox()
self.type_combo.addItems(["String", "Integer", "ForeignKey"])
self.nullable_check = QtWidgets.QCheckBox("Nullable")
self.nullable_check.setChecked(True)
if self.fk_data:
self.type_combo.setCurrentText("ForeignKey")
self.name_edit.setText(self.fk_data['name'])
self.type_combo.setEnabled(False)
self.fk_label = QtWidgets.QLabel(
f"-> {self.fk_data['related_table']}.{self.fk_data['related_column']}"
)
layout.addWidget(self.fk_label)
layout.addWidget(QtWidgets.QLabel("Имя:"))
layout.addWidget(self.name_edit)
layout.addWidget(QtWidgets.QLabel("Тип:"))
layout.addWidget(self.type_combo)
layout.addWidget(self.nullable_check)
self.setLayout(layout)
def get_data(self):
name = self.name_edit.text().strip()
if not name:
return None
col_type = self.type_combo.currentText()
data = {
'name': name,
'type': col_type,
'nullable': self.nullable_check.isChecked()
}
if col_type == "ForeignKey" and self.fk_data:
data.update(self.fk_data)
return data
# -------------------------------------------------------------------
# Диалог настройки внешнего ключа
# -------------------------------------------------------------------
class FkConfigDialog(QtWidgets.QDialog):
def __init__(self, db_manager, parent=None):
super().__init__(parent)
self.db_manager = db_manager
self.setWindowTitle("Настройка внешнего ключа")
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout()
self.table_combo = QtWidgets.QComboBox()
self.column_combo = QtWidgets.QComboBox()
self.name_edit = QtWidgets.QLineEdit()
self.db_manager.refresh_metadata()
self.table_combo.addItems(self.db_manager.get_tables())
self.table_combo.currentIndexChanged.connect(self.update_columns)
form = QtWidgets.QFormLayout()
form.addRow("Имя столбца:", self.name_edit)
form.addRow("Связанная таблица:", self.table_combo)
form.addRow("Связанный столбец:", self.column_combo)
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addLayout(form)
layout.addWidget(button_box)
self.setLayout(layout)
self.update_columns()
def update_columns(self):
self.column_combo.clear()
table_name = self.table_combo.currentText()
if table_name and table_name in self.db_manager.metadata.tables:
table = self.db_manager.metadata.tables[table_name]
self.column_combo.addItems([c.name for c in table.columns])
def get_data(self):
return {
'name': self.name_edit.text().strip(),
'related_table': self.table_combo.currentText(),
'related_column': self.column_combo.currentText()
}
# -------------------------------------------------------------------
# Диалог редактирования записи
# -------------------------------------------------------------------
class EditRecordDialog(QtWidgets.QDialog):
def __init__(self, db_manager, table_name, record_data):
super().__init__()
self.db_manager = db_manager
self.table_name = table_name
self.record_data = dict(record_data._mapping)
self.record_id = self.record_data[self.db_manager.get_pk_column_name(table_name)]
self.init_ui()
def init_ui(self):
self.setWindowTitle(f"Редактирование записи в {self.table_name}")
layout = QtWidgets.QVBoxLayout()
table = self.db_manager.metadata.tables[self.table_name]
self.inputs = {}
for col in table.columns:
if col.primary_key:
continue
row = QtWidgets.QHBoxLayout()
lbl = QtWidgets.QLabel(col.name)
value = self.record_data.get(col.name, '')
le = QtWidgets.QLineEdit(str(value) if value is not None else '')
if isinstance(col.type, Integer):
le.setValidator(QtGui.QIntValidator())
self.inputs[col.name] = le
row.addWidget(lbl)
row.addWidget(le)
layout.addLayout(row)
btn_save = QtWidgets.QPushButton("Сохранить изменения")
btn_save.clicked.connect(self.save_changes)
layout.addWidget(btn_save)
self.setLayout(layout)
def save_changes(self):
data = {}
table = self.db_manager.metadata.tables[self.table_name]
for col_name, le in self.inputs.items():
column = table.columns[col_name]
value = le.text().strip()
if not value:
if column.nullable:
data[col_name] = None
continue
else:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Поле {col_name} обязательно"
)
return
if isinstance(column.type, Integer):
try:
data[col_name] = int(value)
except ValueError:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Неверное число в {col_name}"
)
return
else:
data[col_name] = value
success, message = self.db_manager.update_record(
self.table_name, self.record_id, data
)
if success:
QtWidgets.QMessageBox.information(self, "Успех", message)
self.accept()
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
# -------------------------------------------------------------------
# Окно ввода новой записи
# -------------------------------------------------------------------
class DataEntryWindow(QtWidgets.QDialog):
def __init__(self, db_manager, table_name):
super().__init__()
self.db_manager = db_manager
self.table_name = table_name
self.init_ui()
def init_ui(self):
self.setWindowTitle(f"Добавить запись в {self.table_name}")
layout = QtWidgets.QVBoxLayout()
try:
table = self.db_manager.metadata.tables[self.table_name]
self.inputs = {}
for column in table.columns:
if column.primary_key:
continue
row = QtWidgets.QHBoxLayout()
lbl = QtWidgets.QLabel(column.name)
le = QtWidgets.QLineEdit()
if isinstance(column.type, Integer):
le.setValidator(QtGui.QIntValidator())
self.inputs[column.name] = le
row.addWidget(lbl)
row.addWidget(le)
layout.addLayout(row)
btn_add = QtWidgets.QPushButton("Добавить запись")
btn_add.clicked.connect(self.add_record)
layout.addWidget(btn_add)
self.setLayout(layout)
except KeyError:
QtWidgets.QMessageBox.critical(self, "Ошибка", "Таблица не найдена")
self.reject()
def add_record(self):
data = {}
table = self.db_manager.metadata.tables[self.table_name]
for col_name, le in self.inputs.items():
text = le.text().strip()
column = table.columns[col_name]
if not text:
if column.nullable:
data[col_name] = None
continue
else:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Поле {col_name} обязательно"
)
return
if isinstance(column.type, Integer):
try:
data[col_name] = int(text)
except ValueError:
QtWidgets.QMessageBox.critical(
self, "Ошибка", f"Неверное число в {col_name}"
)
return
else:
data[col_name] = text
success, message = self.db_manager.insert_record(self.table_name, data)
if success:
QtWidgets.QMessageBox.information(self, "Успех", message)
self.accept()
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
# -------------------------------------------------------------------
# Диалог подключения к базе данных
# -------------------------------------------------------------------
class ConnectDatabaseDialog(QtWidgets.QDialog):
def __init__(self, config, parent=None):
super().__init__(parent)
self.config = config
self.setWindowTitle("Подключение к базе данных")
self.setMinimumWidth(400)
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout()
self.db_type_combo = QtWidgets.QComboBox()
self.db_type_combo.addItems(["RedDatabase", "SQLite"])
self.db_type_combo.currentIndexChanged.connect(self.switch_db_type)
layout.addWidget(QtWidgets.QLabel("Тип базы данных:"))
layout.addWidget(self.db_type_combo)
self.stack = QtWidgets.QStackedWidget()
self.stack.addWidget(self.reddatabase_widget())
self.stack.addWidget(self.sqlite_widget())
layout.addWidget(self.stack)
button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
# Устанавливаем текущий тип БД
self.db_type_combo.setCurrentText(self.config.db_type)
# Обновляем пути в соответствии с типом БД
self.update_db_paths()
def reddatabase_widget(self):
widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self.fb_host = QtWidgets.QLineEdit(self.config.host)
self.fb_port = QtWidgets.QLineEdit(str(self.config.port))
self.fb_db_file = QtWidgets.QLineEdit()
browse_btn = QtWidgets.QPushButton("Обзор")
browse_btn.clicked.connect(self.browse_fb_db)
h_layout = QtWidgets.QHBoxLayout()
h_layout.addWidget(self.fb_db_file)
h_layout.addWidget(browse_btn)
self.fb_user = QtWidgets.QLineEdit(self.config.user)
self.fb_password = QtWidgets.QLineEdit(self.config.password)
self.fb_password.setEchoMode(QtWidgets.QLineEdit.Password)
self.fb_create_new = QtWidgets.QCheckBox("Создать новую базу, если не существует")
self.fb_create_new.setChecked(self.config.create_if_not_exists)
layout.addRow("Хост:", self.fb_host)
layout.addRow("Порт:", self.fb_port)
layout.addRow("Файл базы данных:", h_layout)
layout.addRow("Пользователь:", self.fb_user)
layout.addRow("Пароль:", self.fb_password)
layout.addRow("", self.fb_create_new)
widget.setLayout(layout)
return widget
def sqlite_widget(self):
widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
self.sqlite_db_file = QtWidgets.QLineEdit()
browse_btn = QtWidgets.QPushButton("Обзор")
browse_btn.clicked.connect(self.browse_sqlite_db)
h_layout = QtWidgets.QHBoxLayout()
h_layout.addWidget(self.sqlite_db_file)
h_layout.addWidget(browse_btn)
layout.addRow("Файл базы данных:", h_layout)
widget.setLayout(layout)
return widget
def update_db_paths(self):
"""Обновление путей к БД в соответствии с типом"""
# Устанавливаем правильные пути по умолчанию для каждого типа
self.fb_db_file.setText(self.config.get_default_db_file("RedDatabase"))
self.sqlite_db_file.setText(self.config.get_default_db_file("SQLite"))
# Если конфигурация уже содержит путь, используем его
if self.config.db_type == "RedDatabase":
self.fb_db_file.setText(self.config.db_file)
elif self.config.db_type == "SQLite":
self.sqlite_db_file.setText(self.config.db_file)
def switch_db_type(self, index):
self.stack.setCurrentIndex(index)
def browse_fb_db(self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Выбрать файл базы данных", "",
"RedDatabase DB Files (*.fdb);;Все файлы (*)"
)
if filename:
self.fb_db_file.setText(filename)
def browse_sqlite_db(self):
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Выбрать файл базы данных", "",
"SQLite DB Files (*.sqlite *.db);;Все файлы (*)"
)
if filename:
self.sqlite_db_file.setText(filename)
def get_params(self):
db_type = self.db_type_combo.currentText()
if db_type == "RedDatabase":
return {
"type": "RedDatabase",
"host": self.fb_host.text().strip(),
"port": self.fb_port.text().strip(),
"db_file": self.fb_db_file.text().strip(),
"user": self.fb_user.text().strip(),
"password": self.fb_password.text().strip(),
"create_new": self.fb_create_new.isChecked()
}
else:
return {
"type": "SQLite",
"db_file": self.sqlite_db_file.text().strip()
}
# -------------------------------------------------------------------
# Диалог отображения ER-диаграммы
# -------------------------------------------------------------------
class ERDiagramDialog(QtWidgets.QDialog):
def __init__(self, db_manager, parent=None):
super().__init__(parent)
self.setWindowTitle("ER-диаграмма базы данных")
self.resize(800, 600)
self.db_manager = db_manager
self.init_ui()
def init_ui(self):
layout = QtWidgets.QVBoxLayout(self)
graph = pydot.Dot(graph_type='digraph')
for table_name, table in self.db_manager.metadata.tables.items():
label = f"<<TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'>"
label += f"<TR><TD COLSPAN='1'><B>{table_name}</B></TD></TR>"
for col in table.columns:
label += f"<TR><TD>{col.name}</TD></TR>"
label += "</TABLE>>"
node = pydot.Node(table_name, shape="plaintext", label=label)
graph.add_node(node)
for col in table.columns:
for fk in col.foreign_keys:
parent_table = fk.column.table.name
edge = pydot.Edge(table_name, parent_table, label=col.name)
graph.add_edge(edge)
try:
png_str = graph.create_png()
pixmap = QtGui.QPixmap()
pixmap.loadFromData(png_str)
label = QtWidgets.QLabel()
label.setPixmap(pixmap)
label.setAlignment(QtCore.Qt.AlignCenter)
scroll = QtWidgets.QScrollArea()
scroll.setWidget(label)
scroll.setWidgetResizable(True)
layout.addWidget(scroll)
except Exception as e:
QtWidgets.QMessageBox.critical(
self, "Ошибка",
f"Не удалось построить диаграмму: {str(e)}\n\n"
f"Убедитесь, что Graphviz установлен и добавлен в PATH"
)
# -------------------------------------------------------------------
# Главное окно приложения
# -------------------------------------------------------------------
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, db_manager):
super().__init__()
self.db_manager = db_manager
self.selected_table = None
self.init_ui()
self.update_table_list()
def init_ui(self):
self.setWindowTitle("Менеджер базы данных")
self.setGeometry(100, 100, 1000, 600)
menubar = self.menuBar()
table_menu = menubar.addMenu('Таблица')
new_table_action = QtWidgets.QAction('Создать новую таблицу', self)
new_table_action.triggered.connect(self.show_create_table_dialog)
table_menu.addAction(new_table_action)
drop_table_action = QtWidgets.QAction('Удалить таблицу', self)
drop_table_action.triggered.connect(self.delete_table_dialog)
table_menu.addAction(drop_table_action)
db_menu = menubar.addMenu("База данных")
connect_action = QtWidgets.QAction("Подключиться к базе данных", self)
connect_action.triggered.connect(self.connect_to_database)
db_menu.addAction(connect_action)
er_action = QtWidgets.QAction("ER-диаграмма", self)
er_action.triggered.connect(self.show_er_diagram)
db_menu.addAction(er_action)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
layout = QtWidgets.QHBoxLayout(central_widget)
left_panel = QtWidgets.QVBoxLayout()
self.table_list = QtWidgets.QListWidget()
self.table_list.itemClicked.connect(self.load_table_data)
left_panel.addWidget(self.table_list)
self.btn_add_record = QtWidgets.QPushButton("Добавить запись")
self.btn_add_record.clicked.connect(self.open_add_record_window)
left_panel.addWidget(self.btn_add_record)
self.btn_refresh = QtWidgets.QPushButton("Обновить")
self.btn_refresh.clicked.connect(self.refresh_table)
left_panel.addWidget(self.btn_refresh)
layout.addLayout(left_panel, 1)
self.table_view = QtWidgets.QTableWidget()
layout.addWidget(self.table_view, 3)
def update_table_list(self):
self.table_list.clear()
self.db_manager.refresh_metadata()
self.table_list.addItems(self.db_manager.get_tables())
def refresh_table(self):
if self.selected_table:
item = QtWidgets.QListWidgetItem(self.selected_table)
self.load_table_data(item)
def show_create_table_dialog(self):
dialog = CreateTableDialog(self.db_manager, self)
if dialog.exec_():
id_name = dialog.id_name.text().strip()
autoinc = dialog.autoinc_check.isChecked()
columns = [col for col in dialog.columns if col is not None]
success, message = self.db_manager.create_table(
dialog.table_name.text().strip(),
columns,
id_name=id_name,
autoinc=autoinc
)
if success:
self.update_table_list()
QtWidgets.QMessageBox.information(self, "Успех", message)
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
def delete_table_dialog(self):
tables = self.db_manager.get_tables()
if not tables:
QtWidgets.QMessageBox.information(self, "Информация", "Нет таблиц для удаления")
return
table, ok = QtWidgets.QInputDialog.getItem(
self, "Удалить таблицу", "Выберите таблицу для удаления:", tables, 0, False
)
if ok and table:
confirm = QtWidgets.QMessageBox.question(
self, "Подтверждение удаления",
f"Вы уверены, что хотите удалить таблицу '{table}'?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if confirm == QtWidgets.QMessageBox.Yes:
success, message = self.db_manager.drop_table(table)
if success:
self.update_table_list()
QtWidgets.QMessageBox.information(self, "Успех", message)
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
def connect_to_database(self):
dialog = ConnectDatabaseDialog(self.db_manager.config, self)
if dialog.exec_():
params = dialog.get_params()
# Создаём новую конфигурацию
new_config = DatabaseConfig()
new_config.update_from_params(params)
# Создаём БД если нужно (только для Firebird)
if new_config.db_type == "RedDatabase":
success, message = new_config.create_database_if_needed()
if not success:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
return
# Переподключаемся
try:
self.db_manager.reconnect(new_config)
self.update_table_list()
QtWidgets.QMessageBox.information(self, "Успех", "Подключение установлено")
except Exception as e:
QtWidgets.QMessageBox.critical(self, "Ошибка подключения", str(e))
def show_er_diagram(self):
dialog = ERDiagramDialog(self.db_manager, self)
dialog.exec_()
def load_table_data(self, item):
self.selected_table = item.text()
self.table_view.clearContents()
self.table_view.setRowCount(0)
self.table_view.setColumnCount(0)
records, error = self.db_manager.get_records(self.selected_table)
if error:
QtWidgets.QMessageBox.critical(self, "Ошибка", error)
return
try:
table = self.db_manager.metadata.tables[self.selected_table]
pk_col_name = self.db_manager.get_pk_column_name(self.selected_table)
self.table_view.setColumnCount(len(table.columns) + 2)
headers = [col.name for col in table.columns] + ["", ""]
self.table_view.setHorizontalHeaderLabels(headers)
self.table_view.setRowCount(len(records))
for row_idx, row in enumerate(records):
for col_idx, value in enumerate(row):
self.table_view.setItem(
row_idx, col_idx,
QtWidgets.QTableWidgetItem(str(value) if value is not None else 'NULL')
)
pk_value = row._mapping[pk_col_name] if pk_col_name else row[0]
btn_delete = QtWidgets.QPushButton("Удалить")
btn_delete.clicked.connect(
lambda _, pk=pk_value: self.delete_record(pk)
)
self.table_view.setCellWidget(row_idx, len(table.columns), btn_delete)
btn_edit = QtWidgets.QPushButton("Редактировать")
btn_edit.clicked.connect(
lambda _, r=row: self.edit_record(r)
)
self.table_view.setCellWidget(row_idx, len(table.columns) + 1, btn_edit)
except KeyError:
QtWidgets.QMessageBox.critical(self, "Ошибка", "Таблица не найдена")
def delete_record(self, record_id):
confirm = QtWidgets.QMessageBox.question(
self, "Подтверждение удаления",
"Вы уверены, что хотите удалить эту запись?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if confirm == QtWidgets.QMessageBox.Yes:
success, message = self.db_manager.delete_record(
self.selected_table, record_id
)
if success:
self.refresh_table()
QtWidgets.QMessageBox.information(self, "Успех", message)
else:
QtWidgets.QMessageBox.critical(self, "Ошибка", message)
def edit_record(self, record_data):
dialog = EditRecordDialog(self.db_manager, self.selected_table, record_data)
if dialog.exec_() == QtWidgets.QDialog.Accepted:
self.refresh_table()
def open_add_record_window(self):
if self.selected_table:
dialog = DataEntryWindow(self.db_manager, self.selected_table)
if dialog.exec_():
self.refresh_table()
def closeEvent(self, event):
if self.db_manager and self.db_manager.engine:
self.db_manager.engine.dispose()
event.accept()
# -------------------------------------------------------------------
# Основная функция
# -------------------------------------------------------------------
def main():
app = QtWidgets.QApplication(sys.argv)
try:
# Создаём единую конфигурацию
config = DatabaseConfig()
# Создаём БД если нужно (только для Firebird)
if config.db_type == "RedDatabase":
success, message = config.create_database_if_needed()
if not success:
QtWidgets.QMessageBox.critical(None, "Ошибка", message)
sys.exit(1)
print(f"Подключение к {config.host} для пользователя {config.user} создано успешно.")
# Инициализируем менеджер базы данных с конфигурацией
db_manager = DatabaseManager(config)
window = MainWindow(db_manager)
window.show()
sys.exit(app.exec_())
except Exception as e:
QtWidgets.QMessageBox.critical(
None, "Критическая ошибка",
f"Не удалось запустить приложение: {str(e)}"
)
sys.exit(1)
if __name__ == '__main__':
main()
Заключение
Использование SQLAlchemy в этом приложении дает преимущества по сравнению с чистым firebird-driver по нескольким ключевым причинам:
-
- Универсальность (поддержка нескольких СУБД)
SQLAlchemy позволяет работать с Firebird и SQLite через единый API. В коде это реализовано через
create_engine()с разными диалектами (firebird+firebird://иsqlite:///). На чистомfirebird-driverпришлось бы писать отдельные классы-обёртки для каждой БД.
- Универсальность (поддержка нескольких СУБД)
SQLAlchemy позволяет работать с Firebird и SQLite через единый API. В коде это реализовано через
-
- Динамическая работа со схемой через
MetaDataМетодself.metadata.reflect(bind=self.engine)автоматически подгружает структуру всех таблиц из БД. Это позволяет:
- Создавать таблицы "на лету" через
Table(...).create(engine) - Получать список столбцов, типов и FK без ручных SQL-запросов
- Генерировать ER-диаграммы на основе метаданных
С "чистым"
firebird-driverпришлось бы вручную парсить системные таблицы (RDB$RELATIONS,RDB$RELATION_FIELDSи т.д.).
- Динамическая работа со схемой через
-
- Защита от SQL-инъекций
SQLAlchemy автоматически экранирует параметры в запросах:
В
firebird-driverпришлось бы вручную подставлять параметры черезcursor.execute(sql, params).
conn.execute(table.delete().where(pk_column == record_id)) conn.execute(table.insert().values(**data)) - Защита от SQL-инъекций
SQLAlchemy автоматически экранирует параметры в запросах:
В
-
- Управление транзакциями
Контекстные менеджеры
with self.engine.begin() as conn:автоматически делаютcommitпри успехе иrollbackпри ошибке. Вfirebird-driverэто нужно делать вручную.
- Управление транзакциями
Контекстные менеджеры
-
- ORM-возможности
sessionmakerпозволяет работать с записями как с Python-объектами, а не как с кортежами. Это упрощает редактирование и вставку данных.
- ORM-возможности
-
- Абстракция от диалекта SQL
SQLAlchemy сам генерирует корректный SQL для конкретной БД (например, синтаксис автоинкремента в Firebird и SQLite отличается). На
firebird-driverавипришлось бы писать отдельные запросы для каждой СУБД.
- Абстракция от диалекта SQL
SQLAlchemy сам генерирует корректный SQL для конкретной БД (например, синтаксис автоинкремента в Firebird и SQLite отличается). На
-
Итог\ SQLAlchemy превращает приложение из "клиента для Firebird" в универсальный менеджер БД, который легко расширять (добавить PostgreSQL, MySQL) без переписывания кода.
Дата последнего изменения: 07.11.2026
Если вы нашли ошибку, пожалуйста, выделите текст и нажмите Ctrl+Enter.