在Python+FastAPI项目中使用SqlAlchemy操作数据的几种常见方式

在Python+FastAPI的后端项目中,我们往往很多时候需要对数据进行相关的处理,本篇随笔介绍在Python+FastAPI项目中使用SqlAlchemy操作数据的几种常见方式。

使用 FastAPI, SQLAlchemy, Pydantic构建后端项目的时候,其中数据库访问采用SQLAlchemy 的异步方式处理。一般我们在操作数据库操作的时候,采用基类继承的方式减少重复代码,提高代码复用性。不过我们在分析SQLAlchemy的时候,我们可以简单的方式来剖析几种常见的数据库操作方式,来介绍SQLAlchemy的具体使用。

1、SQLAlchemy介绍

SQLAlchemy 是一个功能强大且灵活的 Python SQL 工具包和对象关系映射(ORM)库。它被广泛用于在 Python 项目中处理关系型数据库的场景,既提供了高级的 ORM 功能,又保留了对底层 SQL 语句的强大控制力。/* by 01022.hk - online tools website : 01022.hk/zh/txtreplace.html */ SQLAlchemy 允许开发者通过 Python 代码与数据库进行交互,而无需直接编写 SQL 语句,同时也支持直接使用原生 SQL 进行复杂查询。下面是SQLAlchemy和我们常规数据库对象的对应关系说明。
Engine    连接对象         驱动引擎
Session   连接池           事务  由此开始查询
Model     表                   类定义
Column     列  
Query     若干行         可以链式添加多个条件
 
在使用SQLAlchemy时,通常会将其与数据库对象对应起来。
SQLAlchemy: 使用 /* by 01022.hk - online tools website : 01022.hk/zh/txtreplace.html */ Table 对象或 Declarative Base 中的类来表示。
对应关系: 数据库中的每一个表对应于SQLAlchemy中的一个类,该类继承自 declarative_base()
from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'  # 数据库表名
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String)

数据库列 (Database Column):使用 Column 对象来表示。每个数据库表中的列在SQLAlchemy中表示为 Column 对象,并作为类的属性定义。

id = Column(Integer, primary_key=True)
name = Column(String(50))

数据库行 (Database Row):每个数据库表的一个实例(对象)代表数据库表中的一行。在SQLAlchemy中,通过实例化模型类来表示数据库表中的一行。

new_user = User(id=1, name='John Doe', email='john@example.com')

主键 (Primary Key):使用 primary_key=True 参数定义主键。

id = Column(Integer, primary_key=True)

外键 (Foreign Key): 使用 ForeignKey 对象来表示。

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    user = relationship('User')

关系 (Relationships): 使用 relationship 对象来表示。数据库中表与表之间的关系在SQLAlchemy中通过 relationship 来定义。

addresses = relationship("Address", back_populates="user")

 

2、常规的单表处理

下面我们通过异步处理的方式,介绍如何在单表中操作相关的数据库数据。

async def get(self, db: AsyncSession, id: Any) -> Any:
    """根据主键获取一个对象"""

    if isinstance(id, str):
        query = select(self.model).filter(func.lower(self.model.id) == id.lower())
    else:
        query = select(self.model).filter(self.model.id == id)

    result = await db.execute(query)
    item = result.scalars().first()

    return item

如果我们需要强制对外键的类型进行匹配(如对于Postgresql的严格要求,数据比较的类型必须一致),那么我们需要在基类或者CRUD类初始化的时候,获得对应的主键类型。

class BaseCrud(Generic[ModelType, PrimaryKeyType, PageDtoType, DtoType]):
    """
    基础CRUD操作类,传入参数说明:
    * `ModelType`: SQLAlchemy 模型类
    * `PrimaryKeyType`: 限定主键的类型
    * `PageDtoType`: 分页查询输入类
    * `DtoType`: 数据传输对象类,如新增、更新的单个对象DTO
    """

    def __init__(self, model: Type[ModelType]):
        """
        数据库访问操作的基类对象(CRUD).
        * `model`: A SQLAlchemy model class
        """
        
        self.model = model  # 模型类型

        # 运行期获取主键字段类型
        pk_column = inspect(model).primary_key[0]
        self._pk_type = pk_column.type.python_type  # int / str

因此对于单表的Get方法,我们修改下,让他匹配主键的类型进行比较,这样过对于严格类型判断的Postgresql也正常匹配了。

    async def get(self, db: AsyncSession, id: PrimaryKeyType) -> Optional[ModelType]:
        """根据主键获取一个对象"""
        
        #对id的主键进行类型转换,self._pk_type在构造函数的初始化中获取
        try:
            id = self._pk_type(id)
        except Exception:
            raise ValueError(f"Invalid primary key type: {id}")
        
        if isinstance(id, str):
            query = select(self.model).filter(func.lower(self.model.id) == id.lower())
        else:
            query = select(self.model).filter(self.model.id == id)

        result = await db.execute(query)
        item = result.scalars().first()

        return item

对于删除的数据,我们也可以类似的处理对比进行了。

from sqlalchemy.orm import Session, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import delete as sa_delete, update as sa_update

async def delete_byid(self, db: AsyncSession, id: PrimaryKeyType) -> bool:
    """根据主键删除一个对象
    
    :param id: 主键值
    """
    
    #对id的主键进行类型转换,self._pk_type在构造函数的初始化中获取
    try:
        id = self._pk_type(id)
    except Exception:
        raise ValueError(f"Invalid primary key type: {id}")
    
    del_query: sa_delete
    if isinstance(id, str):
        del_query = sa_delete(self.model).where(
            func.lower(self.model.id) == id.lower()
        )
    else:
        del_query = sa_delete(self.model).where(self.model.id == id)

    result = await db.execute(del_query)

    await db.commit()
    return result.rowcount > 0

对于提供多条件的查询或者过滤,我们可以使用where函数或者filter函数,在 SQLAlchemy 中,select(...).where(...) 和 select(...).filter(...) 都用于构造查询条件,如下所示等效。

query = select(self.model).where(self.model.id == id)

query = select(self.model).filter(self.model.id == id)

我们可以通过sqlAlchemy的and_和or_函数来进行组合多个条件。

from sqlalchemy import ( Table,Column,and_,or_,asc,desc,select,func,distinct,text, Integer)

....
    match expression:
        case "and":
            query = await db.execute(
                select(self.model)
                .filter(and_(*where_list))
                .order_by(*order_by_list)
            )
        case "or":
            query = await db.execute(
                select(self.model).filter(or_(*where_list)).order_by(*order_by_list)
            )

Python的SqlAlchemy提供 InstrumentedAttribute 对象来操作多个条件,如我们对于一些多条件的处理,可以利用它来传递多个参数。

    async def get_all_by_attributes(
        self, db: AsyncSession, *attributes: InstrumentedAttribute, sorting: str = ""
    ) -> List[ModelType] | None:
        """根据列名称和值获取相关的对象列表

        :param sorting: 格式:name asc 或 name asc,age desc
        :param attributes: SQLAlchemy InstrumentedAttribute objects,可以输入多个条件
        例子:User.id != 1 或者 User.username == "JohnDoe"
        """

        order_by_list = parse_sort_string(sorting, self.model)
        query = select(self.model).filter(and_(*attributes)).order_by(*order_by_list)
        
        result = await db.execute(query)
        return result.scalars().all()

例如,对于 模型 Material 对象,我们对它进行多个条件的查询处理,如下所示,红色部分为 *attributes: InstrumentedAttribute 参数。

items = await super().get_all_by_attributes(
    db,
    Material.id == vercol.id,
    Material.vercol == vercol.vercol,
    Material.ischecked == 0,
    Material.status == 0,
)

同样我们可以利用它来获取数量,或者判断多条件的记录是否存在。

image

 在数据插入或者更新的操作中,我们可以接受对象类型或者字典类型的参数对象,因此方法如下所示。

   async def update(self, db: AsyncSession, obj_in: DtoType | dict[str, Any]) -> bool:
        """更新对象
        
        :param obj_in: 对象输入数据,可以是 DTO 对象或字典
        """
        try:
            if isinstance(obj_in, dict):
                obj_id = obj_in.get("id")
                if obj_id is None:
                    raise ValueError("id is required for update")
                update_data = obj_in
            else:
                obj_id = obj_in.id
                # update_data = vars(obj_in)  
                update_data = obj_in.model_dump(exclude_unset=True)

            query = select(self.model).filter(self.model.id == obj_id)
            result = await db.execute(query)
            db_obj = result.scalars().first()

            if db_obj:
                # 更新对象字段
                for field, value in update_data.items():
                    # 跳过以 "_" 开头的私有属性
                    if field.startswith("_"):
                        continue
                    setattr(db_obj, field, value)

                # 处理更新前的回调处理
                self.on_before_update(update_data, db_obj)
                # 提交事务
                await db.commit()
                return True
            else:
                return False
        except SQLAlchemyError as e:
            self.logger.error(f"update 操作出现错误: {e}")
            await db.rollback()  # 确保在出错时回滚事务
            return False

我们在插入或者更新数据的时候,一般会默认更新一些字段,如创建人,创建日期、编辑人,编辑日期等信息,我们可以把它单独作为一个可以给子类重写的函数,基类做一些默认的处理。

    def on_before_update(self, update_data: dict[str, Any], db_obj: ModelType) -> None:
        """更新对象前的回调函数,子类可以重写此方法

        可通过 setattr(db_obj, field, value) 设置字段值
        """
        
        setattr(db_obj, "edittime", datetime.now())
        user :CurrentUserIns  = get_current_user()
        if user:
            setattr(db_obj, "editor", user.fullname)
            setattr(db_obj, "editor_id", user.id)

            setattr(db_obj, "company_id", user.company_id)
            setattr(db_obj, "companyname", user.companyname)

有时候,如果我们需要获取某个字段非重复的列表,用来做为动态下拉列表的数据,那么我们可以通过下面函数封装下。

    async def get_field_list(self, db: AsyncSession, field_name: str) -> Iterable[str]:
        """获取指定字段值的唯一列表

        :param field_name: 字段名称
        """

        field = getattr(self.model, field_name)
        query = select(distinct(field))
        result = await db.execute(query)
        return result.scalars().all()

 

3、多表联合的处理操作 

多表操作,也是我们经常碰到的处理方式,如对于字典类型和字典项目,他们是两个表,需要联合起来获取数据,那么就需要多表的联合操作。

image

 如下是字典CRUD类中,联合字典类型获取数据的记录处理。

    async def get_dict_by_typename(self, db: AsyncSession, dicttype_name: str) -> dict:
        """根据字典类型名称获取所有该类型的字典列表集合"""
        result = await db.execute(
            select(self.model)
            .join(DictType, DictType.id == self.model.dicttype_id)  # 关联字典类型表
            .filter(DictType.name == dicttype_name)  # 过滤字典类型名称
            .order_by(DictData.seq)  # 排序
        )
        items = result.scalars().all()

        dict = {}
        for info in items:
            if info.name not in dict:
                dict[info.name] = info.value

        return dict

如果我们需要对某个表的递归获取树列表,可以如下处理

    async def get_tree(self, db: AsyncSession, pid: str) -> list[DictType]:
        """获取字典类型一级列表及其下面的内容"""

        # 使用三元运算符将 pid 设为 "-1"(如果 pid 是 null 或空白)或保持原值
        pid = "-1" if not pid or pid.strip() == "" else pid
        result = await db.execute(
            select(self.model)
            .filter(self.model.pid == pid)
            .options(selectinload(DictType.children))
        )
        nodes = result.scalars().all()
        return nodes

我们来假设用户和文章的示例表结构(ORM 模型,如下所示。

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String)
    email = Column(String)
    articles = relationship("Article", back_populates="author")


class Article(Base):
    __tablename__ = "articles"
    id = Column(Integer, primary_key=True, index=True)
    title = Column(String)
    content = Column(Text)
    user_id = Column(Integer, ForeignKey("users.id"))
    author = relationship("User", back_populates="articles")

我们可以通过下面函数处理获得相关的记录集合。

async def get_user_articles(db: AsyncSession):
    stmt = (
        select(User, Article)
        .join(Article, Article.user_id == User.id)
    )
    result = await db.execute(stmt)
    return result.all()   # [(User(), Article()), ...]

如果需要可以使用outer_join函数处理

async def get_users_with_articles(db: AsyncSession):
    stmt = (
        select(User, Article)
        .outerjoin(Article, Article.user_id == User.id)
    )
    result = await db.execute(stmt)
    return result.all()  # 用户即便没有文章也会出现

 

如果我们需要获取有文章的所有用户,如下所示。

async def get_users_with_articles(db: AsyncSession):
    stmt = select(User).options(selectinload(User.articles))  # 自动 load 关联
    result = await db.execute(stmt)
    return result.scalars().all()

selectinload 会执行两次 SQL,但效率高,不会产生笛卡尔积,非常适合集合查询。

多表链式 Join的处理,可以获得两个表的不同信息进行组合。

async def get_articles_with_author(db: AsyncSession):
    stmt = (
        select(Article.title, User.name.label("author"))
        .join(User, Article.user_id == User.id)
    )
    rows = await db.execute(stmt)
    return rows.mappings().all()  # 以 dict 形式返回 [{'title':..., 'author':...}]

带筛选条件与分页的处理实现,如下所示

async def search_articles(db: AsyncSession, keyword: str, page: int = 1, size: int = 10):
    stmt = (
        select(Article, User.name.label("author"))
        .join(User)
        .filter(Article.title.contains(keyword))
        .offset((page - 1) * size)
        .limit(size)
    )
    result = await db.execute(stmt)
    return result.mappings().all()

 

对于权限管理系统来说,一般有用户、角色,以及用户角色的中间表,我们来看看这个在SQLAlchemy最佳实践是如何的操作。

from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column
from database import Base

# --- 中间表写法 ---
role_user = Table(
    "role_user",
    Base.metadata,
    Column("user_id", ForeignKey("users.id"), primary_key=True),
    Column("role_id", ForeignKey("roles.id"), primary_key=True)
)

class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    username: Mapped[str] = mapped_column()

    roles: Mapped[list["Role"]] = relationship(
        secondary=role_user,
        back_populates="users",
        lazy="selectin"
    )

class Role(Base):
    __tablename__ = "roles"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column()

    users: Mapped[list[User]] = relationship(
        secondary=role_user,
        back_populates="roles",
        lazy="selectin"
    )

在 SQLAlchemy 声明多对多关系时,secondary 参数既可以填 字符串形式的表名,也可以填 已经定义好的中间表对象(Table 对象)。

① econdary="role_user" —— 使用字符串表名

roles = relationship("Role", secondary="role_user", back_populates="users")

② secondary=role_user —— 传入中间表对象(推荐方式)

role_user = Table(
    "role_user",
    Base.metadata,
    Column("user_id", ForeignKey("users.id"), primary_key=True),
    Column("role_id", ForeignKey("roles.id"), primary_key=True)
)

roles = relationship("Role", secondary=role_user, back_populates="users")

对于如果获取对应角色的用户记录,我们可以通过下面方式获取(通过连接中间表的方式)

async def get_users_by_role(db: AsyncSession, role_id: int) -> list[User]:
    stmt = (
        select(User)
        .join(role_user, role_user.c.user_id == User.id)
        .where(role_user.c.role_id == role_id)
    )
    result = await db.execute(stmt)
    return result.scalars().all()

也可以下面的方式进行处理(使用 relationship any()),效果是一样的。

select(User).filter(User.roles.any(id=role_id))

如果需要写入用户、角色的关联关系,我们可以使用下面方法来通过中间表进行判断并写入记录。

from sqlalchemy import select, insert

async def add_users_to_role(db: AsyncSession, role_id: int, user_ids: list[int]):
    # 1️⃣ 查询已有关联 user_id
    stmt = select(role_user.c.user_id).where(role_user.c.role_id == role_id)
    res = await db.execute(stmt)
    existing_user_ids = {row[0] for row in res.fetchall()}

    # 2️⃣ 过滤出新的 user_id
    new_user_ids = [uid for uid in user_ids if uid not in existing_user_ids]
    if not new_user_ids:
        return 0  # 没有新增

    # 3️⃣ 批量插入
    values = [{"user_id": uid, "role_id": role_id} for uid in new_user_ids]
    stmt = insert(role_user).values(values)
    await db.execute(stmt)
    await db.commit()

    return len(new_user_ids)

如果只是单个记录的插入,可以利用下面的方式处理。

from sqlalchemy import select, insert

async def add_user_to_role(db: AsyncSession, role_id: int, user_id: int) -> bool:
    # 1️⃣ 检查是否已存在关联
    stmt = select(role_user).where(
        role_user.c.role_id == role_id,
        role_user.c.user_id == user_id
    )
    res = await db.execute(stmt)
    exists = res.first()

    if exists:
        return False  # 已存在,不再插入

    # 2️⃣ 插入记录
    stmt = insert(role_user).values(user_id=user_id, role_id=role_id)
    await db.execute(stmt)
    await db.commit()

    return True

以上就是对于在Python+FastAPI的后端项目中使用SqlAlchemy操作数据的几种常见方式,包括单表处理,多表关联、中间表的数据维护和定义等内容,是我们在操作常规数据的时候,经常碰到的几种方式。

希望上文对你有所启发和帮助,感谢阅读。

alt 专注于代码生成工具、.Net/Python 框架架构及软件开发,以及各种Vue.js的前端技术应用。著有Winform开发框架/混合式开发框架、微信开发框架、Bootstrap开发框架、ABP开发框架、SqlSugar开发框架、Python开发框架等框架产品。
  转载请注明出处:撰写人:伍华聪  http://www.iqidi.com 
    
液体化学品实例分割数据集 一、基础信息 数据集名称:液体化学品实例分割数据集 图片数量: - 训练集:2550张图片 - 验证集:233张图片 - 测试集:82张图片 - 总计:2865张实际场景图片 分类类别: - 电池酸:常见的腐蚀性液体,用于电池等设备。 - 漂白剂:强氧化性液体,常用于清洁和消毒。 - 冷却剂:用于发动机或工业设备的散热液体。 - 燃料:如汽油、柴油等易燃液体。 - 液压燃料:用于液压系统的专用液体。 - 机油:润滑油,用于机械维护。 标注格式: YOLO格式,包含实例分割多边形标注,适用于实例分割任务。 数据格式:来源于真实环境图像,格式为JPEG/PNG,覆盖多种场景。 二、适用场景 工业安全与检查: 数据集支持实例分割任务,帮助构建能够自动识别和分割液体区域的AI模型,用于检测泄漏、溢出或不当存储,提升工作场所安全。 环境监测与风险评估: 集成至环境监控系统,实时识别污染物液体,支持环境保护和风险预警。 制造业与自动化: 在制造过程中监控液体使用和状态,实现质量控制和自动化管理。 学术研究与创新: 支持计算机视觉和人工智能在工业应用中的研究,促进算法开发和论文发表。 教育与培训: 可用于工程或安全培训课程,作为学习液体识别和处理的视觉资料。 三、数据集优势 精准标注与高质量: 每个实例分割多边形经过严格验证,确保标注准确性和一致性,覆盖液体区域的细节。 类别丰富多样: 包含六种关键工业液体,涵盖不同性质和风险,增强模型在多样场景下的识别能力。 真实场景数据: 图片来源于实际工业和环境设置,提供真实世界的挑战,提升模型的实用性和鲁棒性。 任务适配性强: 标注兼容YOLO等主流框架,便于快速集成和训练,支持实例分割及相关任务。 应用价值突出: 专注于工业安全和环境健康,为自动检测系统提供可靠数据,助力智能监控解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值