SQLAIchemy 异步DBManager封装-02熟悉掌握

一、引言

在上一篇文章中 SQLAIchemy 异步DBManager封装-01入门理解 我们深入讨论了SQLAlchemy异步DBManager整体的封装结构与思路。详细地介绍了如何封装添加和批量添加的操作方法,并通过实际示例进行了演示。SQL 全称是结构化查询语言,无疑查询是最复杂的部分。因此,在这篇文章中,我将详细介绍如何封装通用的数据库查询方法,并通过具体的示例来讲解这一过程,使得这一复杂的任务变得更为简单。

二、通用查询封装

指定主键id查询

python 复制代码
class DBManager(metaclass=SingletonMetaCls):
    DB_CLIENT: SQLAlchemyManager = None
    orm_table: Type[BaseOrmTable] = None
    
    @with_session
    async def query_by_id(
            self,
            pk_id: int,
            *,
            orm_table: Type[BaseOrmTable] = None,
            session: AsyncSession = None,
    ) -> Union[T_BaseOrmTable, None]:
        """
        根据主键id查询
        Args:
            pk_id: 主键id
            orm_table: orm表映射类
            session: 数据库会话对象,如果为 None,则通过装饰器在方法内部开启新的事务

        Returns:
            orm映射类的实例对象
        """
        orm_table = orm_table or self.orm_table
        ret = await session.get(orm_table, pk_id)
        return ret

这个封装很简单,直接看demo吧

python 复制代码
class UserTable(BaseOrmTableWithTS):
    """用户表"""

    __tablename__ = "user"
    username: Mapped[str] = mapped_column(String(30), default="", comment="用户昵称")
    age: Mapped[int] = mapped_column(default=0, comment="年龄")
    password: Mapped[str] = mapped_column(String(30), default="", comment="用户密码")
    phone: Mapped[str] = mapped_column(String(11), default="", comment="手机号")
    email: Mapped[str] = mapped_column(String(30), default="", comment="邮箱")
    avatar: Mapped[str] = mapped_column(String(100), default="", comment="头像")



class UserManager(DBManager):
    orm_table = UserTable

    async def get_name_by_email(self, email):
        username = await self.query_one(cols=["username"], conds=[self.orm_table.email == email], flat=True)
        return username
        
async def query_demo():
    user = await UserManager().query_by_id(pk_id=1)
    print("user", user)

>>> out
user {'id': 1, 'username': 'hui', 'age': 18, 'password': '', 'phone': '', 'email': 'huidbk.163.com', 'avatar': '', 'created_at': datetime.datetime(2024, 4, 15, 1, 0, 43), 'updated_at': datetime.datetime(2024, 4, 15, 1, 0, 43)}

查询单条

python 复制代码
@with_session
async def _query(
        self,
        *,
        cols: list = None,
        orm_table: BaseOrmTable = None,
        conds: list = None,
        orders: list = None,
        limit: int = None,
        offset: int = 0,
        session: AsyncSession = None,
) -> Result[Any]:
    """
    通用查询
    Args:
        cols: 查询的列表字段
        orm_table: orm表映射类
        conds: 查询的条件列表
        orders: 排序列表, 默认id升序
        limit: 限制数量大小
        offset: 偏移量
        session: 数据库会话对象,如果为 None,则通过装饰器在方法内部开启新的事务

    Returns: 查询结果集
        cursor_result
    """
    cols = cols or []
    cols = [column(col_obj) if isinstance(col_obj, str) else col_obj for col_obj in cols]  # 兼容字符串列表

    conditions = conds or []
    orders = orders or [column("id")]
    orm_table = orm_table or self.orm_table

    # 构造查询
    if cols:
        # 查询指定列
        query_sql = select(*cols).select_from(orm_table).where(*conditions).order_by(*orders)
    else:
        # 查询全部字段
        query_sql = select(orm_table).where(*conditions).order_by(*orders)

    if limit:
        query_sql = query_sql.limit(limit).offset(offset)

    # 执行查询
    cursor_result = await session.execute(query_sql)
    return cursor_result

@with_session
async def query_one(
        self,
        *,
        cols: list = None,
        orm_table: Type[BaseOrmTable] = None,
        conds: list = None,
        orders: list = None,
        flat: bool = False,
        session: AsyncSession = None,
) -> Union[dict, T_BaseOrmTable, Any]:
    """
    查询单行
    Args:
        cols: 查询的列表字段
        orm_table: orm表映射类
        conds: 查询的条件列表
        orders: 排序列表
        flat: 单字段时扁平化处理
        session: 数据库会话对象,如果为 None,则通过装饰器在方法内部开启新的事务

    Examples:
        # 指定列名
        ret = await UserManager().query_one(cols=["username", "age"], conds=[UserTable.id == 1])
        sql => select username, age from user where id=1
        ret => {"username": "hui", "age": 18}

        # 指定列名,单字段扁平化处理
        ret = await UserManager().query_one(cols=["username"], conds=[UserTable.id == 1])
        sql => select username from user where id=1
        ret => {"username": "hui"} => "hui"

        # 计算总数
        ret = await UserManager().query_one(cols=[func.count()], flat=True)
        sql => select count(*) as count from user
        ret => {"count": 10} => 10

        # 不指定列名,查询全部字段, 返回表实例对象
        ret = await UserManager().query_one(conds=[UserTable.id == 1])
        sql => select id, username, age from user where id=1
        ret => UserTable(id=1, username="hui", age=18)

    Returns:
        Union[dict, BaseOrmTable(), Any(flat=True)]
    """
    cursor_result = await self._query(cols=cols, orm_table=orm_table, conds=conds, orders=orders, session=session)
    if cols:
        if flat and len(cols) == 1:
            # 单行单字段查询: 直接返回字段结果
            # eg: select count(*) as count from user 从 {"count": 100} => 100
            # eg: select username from user where id=1 从 {"username": "hui"} => "hui"
            return cursor_result.scalar_one()

        # eg: select username, age from user where id=1 => {"username": "hui", "age": 18}
        return cursor_result.mappings().one() or {}
    else:
        # 未指定列名查询默认全部字段,返回的是表实例对象 BaseOrmTable()
        # eg: select id, username, age from user where id=1 => UserTable(id=1, username="hui", age=18)
        return cursor_result.scalar_one()

查询无疑就只有两种结果单条、多条结果数据。这里统一封装一个 _query 通用查询方法,以供内部使用。

  • 支持指定查询的列(cols)
  • 条件查询(conds)
  • 排序(orders)
  • 分页(limit、offset)

主要封装就是利用 sqlaichemy 提供的 select 语法进行组织sql,通过 column 兼容列名字段字符串列表。query_one 方法,如果指定了 cols 返回字典格式,不指定则是库表映射类实例对象,一开始封装的时候我想统一出参都是返回 库表映射类实例对象 。

python 复制代码
query_ret = cursor_result.mappings().one() or {}
return orm_table(**query_ret)

如果是 id as user_id 取别名查询会导致映射不上,但可以查询时不指定别名,orm_table_obj.to_dict(alias_dict={"id": "user_id"}) 时进行别名转换,还有一些flat 扁平化、统计数量的时候都不能使用 orm_table(**query_ret) 故而不好统一,再实际web场景中,出参还是要转成dict、json格式化进行响应,故而进行保留。看看具体使用效果

python 复制代码
from sqlalchemy import String, func, label

async def query_demo():
    ret = await UserManager().query_one(cols=["username", "age"], conds=[UserTable.id == 1])
    print("指定列名 ret", ret)
    
    ret = await UserManager().query_one(
        cols=[UserTable.username, label("user_age", UserTable.age)], conds=[UserTable.id == 1]
    )
    print("取别名 ret", ret)

    ret = await UserManager().query_one(cols=["username"], conds=[UserTable.id == 1], flat=True)
    print("指定列名,单字段扁平化处理", ret)

    ret = await UserManager().query_one(cols=[func.count()], flat=True)
    print("计算总数", ret)

    ret = await UserManager().query_one(conds=[UserTable.id == 1])
    print("不指定列名,查询全部字段, 返回表实例对象", ret)

查询结果

python 复制代码
指定列名 ret {'username': 'hui', 'age': 18}

取别名 ret {'username': 'hui', 'user_age': 18}

指定列名,单字段扁平化处理 hui

计算总数 6

不指定列名,查询全部字段, 返回表实例对象 {'username': 'hui', 'age': 18, 'password': '', 'phone': '', 'email': 'huidbk.163.com', 'avatar': '', 'id': 1, 'created_at': datetime.datetime(2024, 4, 15, 1, 0, 43), 'updated_at': datetime.datetime(2024, 4, 15, 1, 0, 43)}

查询多条

python 复制代码
@with_session
async def query_all(
        self,
        *,
        cols: list = None,
        orm_table: BaseOrmTable = None,
        conds: list = None,
        orders: list = None,
        flat: bool = False,
        limit: int = None,
        offset: int = None,
        session: AsyncSession = None,
) -> Union[List[dict], List[T_BaseOrmTable], Any]:
    """
    查询多行
    Args:
        cols: 查询的列表字段
        orm_table: orm表映射类
        conds: 查询的条件列表
        orders: 排序列表
        flat: 单字段时扁平化处理
        limit: 限制数量大小
        offset: 偏移量
        session: 数据库会话对象,如果为 None,则通过装饰器在方法内部开启新的事务
    """
    cursor_result = await self._query(
        cols=cols, orm_table=orm_table, conds=conds, orders=orders, limit=limit, offset=offset, session=session
    )
    if cols:
        if flat and len(cols) == 1:
            # 扁平化处理
            # eg: select id from user 从 [{"id": 1}, {"id": 2}, {"id": 3}] => [1, 2, 3]
            return cursor_result.scalars().all()

        # eg: select username, age from user => [{"username": "hui", "age": 18}, [{"username": "dbk", "age": 18}]]
        return cursor_result.mappings().all() or []
    else:
        # 未指定列名查询默认全部字段,返回的是表实例对象 [BaseOrmTable()]
        # eg: select id, username, age from user
        # [User(id=1, username="hui", age=18), User(id=2, username="dbk", age=18)
        return cursor_result.scalars().all()

查询多条与query_one一致内部调用 _query() 获取查询结果集,最后通过 cursor_result.mappings().all()cursor_result.scalars().all() 获取列表数据,同样支持单字段扁平化处理,还支持分页处理。具体看如下例子

python 复制代码
from sqlalchemy import String, func, label, or_

ret = await UserManager().query_all()
user_ids = [user.id for user in ret]
print("查询全部", user_ids)

user_ids = await UserManager().query_all(cols=[UserTable.id], flat=True)
print("查询全部的用户id(扁平化处理)", user_ids)

ret = await UserManager().query_all(
    cols=[UserTable.username],
    conds=[
        UserTable.id > 1,
        or_(UserTable.age < 20, UserTable.email == "huidbk.163.com")
    ],
    orders=[UserTable.id],
    flat=True
)
# sql => select username from user where user.id > 1 and (age < 20 or email='huidbk.163.com') order by id
print("条件查询", ret)

查询结果

python 复制代码
查询全部 [1, 2, 3, 4, 5, 6]
查询全部的用户id(扁平化处理) [1, 2, 3, 4, 5, 6]
条件查询 ['zack']

分页查询

python 复制代码
async def list_page(
        self,
        *,
        cols: list = None,
        orm_table: BaseOrmTable = None,
        conds: list = None,
        orders: list = None,
        curr_page: int = 1,
        page_size: int = 20,
        session: AsyncSession = None,
):
    """
    单表通用分页查询
    Args:
        cols: 查询的列表字段
        orm_table: orm表映射类
        conds: 查询的条件列表
        orders: 排序列表
        curr_page: 页码
        page_size: 每页数量
        session: 数据库会话对象,如果为 None,则通过装饰器在方法内部开启新的事务

    Returns: 
        total_count, data_list
    """
    conds = conds or []
    orders = orders or [column("id")]
    orm_table = orm_table or self.orm_table

    limit = page_size
    offset = (curr_page - 1) * page_size
    total_count, data_list = await asyncio.gather(
        self.query_one(
            cols=[func.count()], orm_table=orm_table, conds=conds, orders=orders, flat=True, session=session
        ),
        self.query_all(
            cols=cols, orm_table=orm_table, conds=conds, orders=orders, limit=limit, offset=offset, session=session
        ),
    )

    return total_count, data_list

这里分页查询就用 query_one 查询总数,query_all 分页查询,然后通过 asyncio.gather 并发执行获取结果。

python 复制代码
total_count, data_list = await UserManager().list_page(
    cols=[UserTable.id, UserTable.username, UserTable.age],
    conds=[UserTable.id > 1],
    curr_page=2,
    page_size=3,
    orders=[desc(UserTable.age)]
)
print("分页查询 total_count", total_count)
print("分页查询 data_list", data_list)

分页查询结果

python 复制代码
分页查询 total_count 5
分页查询 data_list [{'id': 3, 'username': 'wang', 'age': 20}, {'id': 2, 'username': 'zack', 'age': 19}]

三、封装说明

SQL 的话还是查询用的多,查询也复杂,这里的话只封装了一些通用的查询操作,有一些分组查询、连表查询等我都没有封装,我认为这些操作还是写原生sql更直观一些,用ORM进行组装这些操作会感觉语法很别扭不简洁。如何执行原始sql,请看下一篇。SQLAIchemy 异步DBManager封装-03得心应手

四、Github源代码

源代码已上传到了Github,里面也有具体的使用Demo,欢迎大家一起体验、贡献。

HuiDBK/py-tools: 打造 Python 开发常用的工具,让Coding变得更简单 (github.com)

相关推荐
AlienZHOU1 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
科技苑1 小时前
Python简单网络爬虫教程
爬虫·python
狗头大军之江苏分军4 小时前
《潮水漫过十七岁》开学了
后端
Patrick在香港4 小时前
Claude Prompt 香港场景:公文里「今日」落在 6 个日历日上,「翌日」锚错了 5 天
python·自然语言处理·正则表达式·claude·数据清洗
苏三说技术4 小时前
如何看待GPT-6在UP主众测中碾压夺冠?它是现在最强大模型吗?
后端
mldong4 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
wno7044 小时前
Spring Boot WebFlux增删改查
java·spring boot·后端
Captaincc4 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
YsyaaabB5 小时前
Python 数值分析
python
阿洛学长5 小时前
计算机二级 Python 基本操作题(15 分)真题笔记(0101 ~ 1903 全套)
python·pycharm