201 lines
7.9 KiB
Python
201 lines
7.9 KiB
Python
"""
|
||
Модуль для подключения к базе данных PostgreSQL через SQLAlchemy
|
||
"""
|
||
|
||
from sqlalchemy import create_engine, MetaData
|
||
from sqlalchemy.ext.declarative import declarative_base
|
||
from sqlalchemy.orm import sessionmaker
|
||
from sqlalchemy.pool import StaticPool
|
||
from contextlib import contextmanager
|
||
import os
|
||
from typing import Generator
|
||
import logging
|
||
|
||
# Настройка логирования
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Базовый класс для всех моделей
|
||
Base = declarative_base()
|
||
|
||
class DatabaseConfig:
|
||
"""Конфигурация базы данных"""
|
||
|
||
def __init__(self):
|
||
# Параметры подключения к PostgreSQL
|
||
self.DB_USER = os.getenv('DB_USER', 'postgres')
|
||
self.DB_PASSWORD = os.getenv('DB_PASSWORD', 'password')
|
||
self.DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||
self.DB_PORT = os.getenv('DB_PORT', '5432')
|
||
self.DB_NAME = os.getenv('DB_NAME', 'telegram_bot')
|
||
|
||
# Строка подключения
|
||
self.DATABASE_URL = f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
||
|
||
# Настройки SQLAlchemy
|
||
self.SQLALCHEMY_ENGINE_OPTIONS = {
|
||
'pool_pre_ping': True, # Проверка соединения перед использованием
|
||
'pool_recycle': 300, # Переподключение каждые 5 минут
|
||
'pool_size': 10, # Размер пула соединений
|
||
'max_overflow': 20, # Максимальное количество дополнительных соединений
|
||
'echo': False, # Логирование SQL запросов (для отладки)
|
||
}
|
||
|
||
class Database:
|
||
"""Класс для управления подключением к базе данных"""
|
||
|
||
def __init__(self, config: DatabaseConfig = None):
|
||
self.config = config or DatabaseConfig()
|
||
self.engine = None
|
||
self.SessionLocal = None
|
||
self._initialize()
|
||
|
||
def _initialize(self):
|
||
"""Инициализация подключения к базе данных"""
|
||
try:
|
||
# Создание движка SQLAlchemy
|
||
self.engine = create_engine(
|
||
self.config.DATABASE_URL,
|
||
**self.config.SQLALCHEMY_ENGINE_OPTIONS
|
||
)
|
||
|
||
# Создание фабрики сессий
|
||
self.SessionLocal = sessionmaker(
|
||
autocommit=False,
|
||
autoflush=False,
|
||
bind=self.engine
|
||
)
|
||
|
||
logger.info("Подключение к базе данных успешно инициализировано")
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при инициализации базы данных: {e}")
|
||
raise
|
||
|
||
def create_tables(self):
|
||
"""Создание всех таблиц в базе данных"""
|
||
try:
|
||
Base.metadata.create_all(bind=self.engine)
|
||
logger.info("Таблицы успешно созданы")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при создании таблиц: {e}")
|
||
raise
|
||
|
||
def drop_tables(self):
|
||
"""Удаление всех таблиц из базы данных"""
|
||
try:
|
||
Base.metadata.drop_all(bind=self.engine)
|
||
logger.info("Таблицы успешно удалены")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при удалении таблиц: {e}")
|
||
raise
|
||
|
||
@contextmanager
|
||
def get_session(self) -> Generator:
|
||
"""Контекстный менеджер для получения сессии базы данных"""
|
||
session = self.SessionLocal()
|
||
try:
|
||
yield session
|
||
session.commit()
|
||
except Exception as e:
|
||
session.rollback()
|
||
logger.error(f"Ошибка в сессии базы данных: {e}")
|
||
raise
|
||
finally:
|
||
session.close()
|
||
|
||
def get_session_sync(self):
|
||
"""Получение синхронной сессии (не забудьте закрыть!)"""
|
||
return self.SessionLocal()
|
||
|
||
def test_connection(self) -> bool:
|
||
"""Проверка подключения к базе данных"""
|
||
from sqlalchemy import text
|
||
try:
|
||
with self.get_session() as session:
|
||
session.execute(text("SELECT 1"))
|
||
logger.info("Подключение к базе данных работает корректно")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка подключения к базе данных: {e}")
|
||
return False
|
||
|
||
def close(self):
|
||
"""Закрытие подключения к базе данных"""
|
||
if self.engine:
|
||
self.engine.dispose()
|
||
logger.info("Подключение к базе данных закрыто")
|
||
|
||
# Глобальный экземпляр базы данных
|
||
db = Database()
|
||
|
||
# Удобные функции для использования в приложении
|
||
def get_db_session():
|
||
"""Получение сессии базы данных для использования в приложении"""
|
||
return db.get_session()
|
||
|
||
def init_database():
|
||
"""Инициализация базы данных (создание таблиц)"""
|
||
db.create_tables()
|
||
|
||
def test_db_connection(max_retries: int = 5, delay: int = 2):
|
||
"""
|
||
Тестирование подключения к базе данных с повторными попытками
|
||
|
||
Args:
|
||
max_retries: Максимальное количество попыток
|
||
delay: Задержка между попытками в секундах
|
||
"""
|
||
import time
|
||
|
||
for attempt in range(max_retries):
|
||
try:
|
||
if db.test_connection():
|
||
return True
|
||
except Exception as e:
|
||
logger.warning(f"Попытка {attempt + 1}/{max_retries} подключения не удалась: {e}")
|
||
|
||
if attempt < max_retries - 1:
|
||
logger.info(f"Ожидание {delay} секунд перед следующей попыткой...")
|
||
time.sleep(delay)
|
||
|
||
logger.error(f"Не удалось подключиться к базе данных после {max_retries} попыток")
|
||
return False
|
||
|
||
def load_image_from_file(file_path: str, alt_text: str = None):
|
||
"""
|
||
Быстрая загрузка изображения из файла в базу данных
|
||
|
||
Args:
|
||
file_path: Путь к файлу изображения
|
||
alt_text: Альтернативный текст
|
||
|
||
Returns:
|
||
Image: Объект изображения или None при ошибке
|
||
"""
|
||
import os
|
||
import mimetypes
|
||
from database.repositories import ImageRepository
|
||
|
||
try:
|
||
if not os.path.isfile(file_path):
|
||
return None
|
||
|
||
filename = os.path.basename(file_path)
|
||
mime_type, _ = mimetypes.guess_type(filename)
|
||
if not mime_type:
|
||
mime_type = 'image/jpeg'
|
||
|
||
with open(file_path, 'rb') as f:
|
||
image_data = f.read()
|
||
|
||
with get_db_session() as session:
|
||
image = ImageRepository.create_image(
|
||
session, filename, mime_type, image_data, alt_text
|
||
)
|
||
session.commit()
|
||
return image
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка загрузки изображения {file_path}: {e}")
|
||
return None |