From abb1c01753979fe7eb088d205c1b046ac1f742f1 Mon Sep 17 00:00:00 2001 From: shahzeydyfah Date: Tue, 17 Feb 2026 22:37:43 +0330 Subject: [PATCH] feat(root):add mysql --- core/service/mysql_db.py | 67 ++++++++++++++++++++++++++++++++ core/service/mysql_orm.py | 81 +++++++++++++++++++++++++++++++++++++++ core/utils/mysql.py | 25 ++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 core/service/mysql_db.py create mode 100644 core/service/mysql_orm.py create mode 100644 core/utils/mysql.py diff --git a/core/service/mysql_db.py b/core/service/mysql_db.py new file mode 100644 index 0000000..d27a793 --- /dev/null +++ b/core/service/mysql_db.py @@ -0,0 +1,67 @@ +""" +اتصال MySQL فقط-خواندنی (read-only). +از پول کانکشن استفاده می‌کند و در هر کانکشن SESSION را به حالت فقط-خواندنی تنظیم می‌کند. +""" +from contextlib import asynccontextmanager +from typing import Optional + +import aiomysql + +from core.utils.mysql import get_mysql_config + +_pool: Optional[aiomysql.Pool] = None +_pool_connection_name: Optional[str] = None + + +async def get_mysql_pool(connection_name: str = None) -> aiomysql.Pool: + """پول کانکشن MySQL را برمی‌گرداند؛ در اولین فراخوانی ساخته می‌شود.""" + global _pool, _pool_connection_name + name = connection_name or "mysql" + if _pool is None or _pool_connection_name != name: + if _pool is not None: + _pool.close() + await _pool.wait_closed() + cfg = get_mysql_config(name) + _pool = await aiomysql.create_pool( + host=cfg["host"], + port=cfg["port"], + user=cfg["user"], + password=cfg["password"], + db=cfg["db"], + charset="utf8mb4", + autocommit=True, + minsize=1, + maxsize=10, + ) + _pool_connection_name = name + return _pool + + +@asynccontextmanager +async def get_mysql_connection(connection_name: str = None): + """ + کانکشن فقط-خواندنی از پول می‌گیرد. + در ابتدا SESSION را به حالت فقط-خواندنی تنظیم می‌کند (برای اطمینان بیشتر). + استفاده: + async with get_mysql_connection() as conn: + async with conn.cursor(aiomysql.DictCursor) as cur: + await cur.execute("SELECT * FROM t LIMIT 10") + rows = await cur.fetchall() + """ + pool = await get_mysql_pool(connection_name) + conn = await pool.acquire() + try: + async with conn.cursor() as cur: + await cur.execute("SET SESSION TRANSACTION READ ONLY") + yield conn + finally: + pool.release(conn) + + +async def close_mysql_pool(): + """بستن پول (مثلاً در shutdown اپ).""" + global _pool + if _pool is not None: + _pool.close() + await _pool.wait_closed() + _pool = None diff --git a/core/service/mysql_orm.py b/core/service/mysql_orm.py new file mode 100644 index 0000000..24a10a1 --- /dev/null +++ b/core/service/mysql_orm.py @@ -0,0 +1,81 @@ +""" +SQLAlchemy async setup برای MySQL (فقط خواندنی). +از asyncmy به عنوان async driver استفاده می‌کند. +""" +from contextlib import asynccontextmanager +from typing import Optional + +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import declarative_base, sessionmaker + +from core.utils.mysql import get_mysql_config + +# Base class برای تمام مدل‌های ORM +Base = declarative_base() + +_engine = None +_async_session_maker = None + + +def get_mysql_url(connection_name: str = None) -> str: + """ساخت URL برای SQLAlchemy از تنظیمات MySQL""" + cfg = get_mysql_config(connection_name) + return f"mysql+asyncmy://{cfg['user']}:{cfg['password']}@{cfg['host']}:{cfg['port']}/{cfg['db']}?charset=utf8mb4" + + +def get_async_engine(connection_name: str = None): + """برگرداندن async engine (در اولین فراخوانی ساخته می‌شود)""" + global _engine + if _engine is None: + url = get_mysql_url(connection_name) + _engine = create_async_engine( + url, + echo=False, # برای debug می‌تونی True کنی + pool_pre_ping=True, # چک کردن کانکشن قبل از استفاده + pool_size=5, + max_overflow=10, + ) + return _engine + + +def get_async_session_maker(connection_name: str = None): + """برگرداندن async sessionmaker""" + global _async_session_maker + if _async_session_maker is None: + engine = get_async_engine(connection_name) + _async_session_maker = sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + autocommit=False, + autoflush=False, + ) + return _async_session_maker + + +@asynccontextmanager +async def get_mysql_session(connection_name: str = None): + """ + Context manager برای گرفتن async session (فقط خواندنی). + استفاده: + async with get_mysql_session() as session: + result = await session.execute(select(User).where(User.id == 1)) + user = result.scalar_one_or_none() + """ + session_maker = get_async_session_maker(connection_name) + async with session_maker() as session: + # تنظیم session به حالت فقط-خواندنی + await session.execute("SET SESSION TRANSACTION READ ONLY") + try: + yield session + finally: + await session.rollback() # برای read-only همیشه rollback می‌کنیم + + +async def close_mysql_engine(): + """بستن engine (مثلاً در shutdown اپ).""" + global _engine, _async_session_maker + if _engine is not None: + await _engine.dispose() + _engine = None + _async_session_maker = None diff --git a/core/utils/mysql.py b/core/utils/mysql.py new file mode 100644 index 0000000..21fd157 --- /dev/null +++ b/core/utils/mysql.py @@ -0,0 +1,25 @@ +""" +تنظیمات اتصال MySQL (فقط خواندنی). +از بخش جداگانه mysql در config.yml (هم‌سطح با database) استفاده می‌کند. +""" +from config import conf + + +def get_mysql_config(connection_name: str = None): + """ + بر اساس محیط (dev/production) تنظیمات MySQL را برمی‌گرداند. + Returns: dict با کلیدهای host, port, user, password, db + """ + if not conf.mysql: + raise ValueError("تنظیمات mysql در config.yml یافت نشد.") + env = "production" if conf.server.is_prod else "dev" + mode = getattr(conf.mysql, env, None) or getattr(conf.mysql, "dev", None) + if not mode: + raise ValueError(f"تنظیمات mysql.{env} در config.yml یافت نشد.") + return { + "host": getattr(mode, "host", "127.0.0.1"), + "port": int(getattr(mode, "port", 3306)), + "user": getattr(mode, "user", ""), + "password": getattr(mode, "password", ""), + "db": getattr(mode, "db", ""), + }