Python--orm框架:sqlalchemy2.0【异步】

一、SQLAlchemy 是什么

总体:可对照Python--orm框架:sqlalchemy - Edmond辉仔 - 博客园 一起看

SQLAlchemy 是 Python 中最流行的数据库工具库,它让开发者可以用 Python 代码代替 SQL 语句来操作数据库。
它分为两层:底层叫 Core,负责连接池管理和 SQL 语句构建,适合需要精细控制 SQL 的场景;上层叫 ORM,能将数据库表映射成 Python 类,让你像操作普通对象一样增删改查,无需手写 SQL。简单说,它把数据库的行变成了对象,把列变成了属性。

二、安装与数据库连接

安装只需一行 pip install sqlalchemy(基础安装),但连接不同数据库还需要对应的驱动。
连接的核心是创建引擎,它通过一个 URL 来指定数据库地址、账号密码和目标库。引擎内部维护着一个连接池,复用连接能显著提升性能。除了引擎,你还需要一个会话工厂,每次数据库操作都应该创建一个新的会话,用完即关,避免数据混乱。

# 连接的核心是创建引擎,它通过一个 URL 来指定数据库地址、账号密码和目标库。
# 引擎内部维护着一个连接池,复用连接能显著提升性能。
# 除了引擎,你还需要一个会话工厂,每次数据库操作都应该创建一个新的会话,用完即关,避免数据混乱。


pip install aiomysql  SQLAlchemy[asyncio] (包含异步扩展安装)
# mysql8 还需要安装 依赖cryptography库来密码的解密和验证
pip install cryptography  


### 同步
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "mysql+pymysql://root:1234@localhost:3306/snail-cloud?charset=utf8mb4"

# 创建引擎
engine = create_engine(
    DATABASE_URL,
    echo=True,  # 打印 SQL,调试用
    pool_size=10,  # 连接池大小
    max_overflow=5,  # 最大溢出连接数
    pool_recycle=3600,  # 连接回收时间
    pool_pre_ping=True  # 检查连接是否可用
)

# 创建会话工厂
SessionLocal  = sessionmaker(
    bind=engine,
    autocommit=False,
    autoflush=False,
    expire_on_commit=False
)


# 获取数据库池中的 连接  可用 with 语句
conn = engine.raw_connection()  # 建立链接

异步操作

###  异步

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import sessionmaker

ASYNC_DATABASE_URL = "mysql+aiomysql://root:123456@localhost:3306/fastapi-demo?charset=utf8mb4"

# 创建异步引擎
async_engine = create_async_engine(
    ASYNC_DATABASE_URL,
    echo=True,  # 输出 SQL日志,调试用
    max_overflow=5,  # 最大溢出连接数
    pool_size=10,  # 连接池大小
    pool_recycle=3600,  # 连接回收时间
    pool_pre_ping=True  # 检查连接是否可用
)

# 创建异步会话工厂  AsyncSession
AsyncSessionLocal = async_sessionmaker(
    bind=async_engine,
    class_=AsyncSession, # 指定会话类
    autocommit=False,
    autoflush=False,
    expire_on_commit=False # 会话对象不过期,不重新查询数据库
)

# 获取数据库池中的 连接  可用 with 语句
conn = async_engine.begin()  # 建立链接

三、定义 ORM 模型

模型就是一个 Python 类,用来描述一张表的结构:类名对应表名,属性对应列。
首先创建一个基类,然后让所有模型继承它。每个属性用类型注解标明它存什么数据,比如 int 还是 str,同时可以加上约束:主键、唯一、索引、默认值等。定义好模型后,数据库表的结构就被清晰地表达在代码中了,这是后续一切操作的基础。

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy import String, Integer, DateTime, Text, ForeignKey, func
from datetime import datetime

# ============================================================
# 第一步:创建基类
# 所有模型类都继承自它,SQLAlchemy 通过它识别哪些是 ORM 模型、哪些是普通类
# ============================================================
class Base(DeclarativeBase):
    pass

# ============================================================
# 第二步:定义 User 模型——映射到 users 表
# ============================================================

class User(Base):
    # 表名
    __tablename__ = "users"
    
    # 创建索引:提升索引速度
    __table_args__ = (
    	 # Index('索引名称', 字段名)    索引名称  建议显式指定
           # 外键索引:通常以 fk_ 开头
		  # 普通查询索引:通常以 idx_ 开头
		Index('fk_news_category_idx', 'category_id'),  
         Index('idx_publish_time', 'publish_time')
    )
    
    # 主键,自增,唯一标识每一行
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
        
    # 用户名,最长50字符,不可重复,加索引加速查询
    username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
        
    # 邮箱,最长100字符,不可重复
    email: Mapped[str] = mapped_column(String(100), unique=True)
        
    # 年龄,整数,默认值为 0
    age: Mapped[int] = mapped_column(Integer, default=0)
        
    # 个人简介,长文本,可选(允许为空)
    bio: Mapped[str | None] = mapped_column(Text, nullable=True)
        
    # 创建时间,插入行时自动填入当前时间
    created_at: Mapped[datetime] = mapped_column(
        DateTime, 
        insert_default=func.now(),  # 首次插入时的默认值  func.now()是SQL层面的函数【MySQL的NOW()】
        default=func.now(),  # 首次插入时的默认值  Python层面的默认值  只保留insert_default【通常】
        comment="创建时间"
    )
        
    # 更新时间,每次修改行时自动更新为当前时间
    updated_at: Mapped[datetime] = mapped_column(
        DateTime, 
        insert_default=func.now(),   # 首次插入时,由数据库生成时间 
        server_onupdate=func.now(),   # 更新时由数据库自动生成时间
        onupdate=func.now(),  # 仅在Python层面执行update时触发  只保留server_onupdate【通常】
        comment="更新时间"
    )
        
    # ----------------------------------------------------------
    # 关联关系:User 与 Post 是一对多(一个用户有多篇文章)
    # 这不是数据库中的真实列,而是 ORM 层面的对象引用
    # 双向显式绑定:必须在两个模型中都显式定义 relationship,并通过 back_populates 参数互相指向对方的属性名
    # back_populates 指向 Post 模型中对应关系的属性名
    # ----------------------------------------------------------
    posts: Mapped[list["Post"]] = relationship(back_populates="author")


# ============================================================
# 第三步:定义 Post 模型——映射到 posts 表
# ============================================================
class Post(Base):
    # 表名
    __tablename__ = "posts"

    # 主键,自增
    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

    # 文章标题,最长200字符
    title: Mapped[str] = mapped_column(String(200))

    # 文章内容,长文本
    content: Mapped[str] = mapped_column(Text)

    # 外键,指向 users 表的 id 列,表名该文章属于哪个用户
    # 加索引,方便根据用户查文章时加速
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)

    # 创建时间
    created_at: Mapped[datetime] = mapped_column(default=func.now())

    # ----------------------------------------------------------
    # 反向关联关系:通过 post.author 可直接拿到对应的 User 对象
    # ----------------------------------------------------------
    author: Mapped["User"] = relationship(back_populates="posts")

四、自动创建数据表

模型定义好之后,调用基类的 create_all 方法,SQLAlchemy 会自动检查数据库中哪些表还不存在,并帮你创建。非常适合开发阶段快速建表。但生产环境通常会改用专业的迁移工具(如 Alembic)来精细控制表结构的变更历史。

# ============================================================
# 自动建表——开发阶段用
# 检查数据库中哪些表不存在,只创建缺失的表,已存在的不会动
# 注意:如果表结构变了(比如加了列),它不会自动更新,需要删表重建
# ============================================================

# 同步  写在 定义引擎后,直接调用就行,不用写在数据库连接中
Base.metadata.create_all(bind=engine)  # 创建基于Base的所有表
Base.metadata.drop_all(engine)     # 删除基于Base的所有表


# 异步:创建数据库表
# 1.从连接池获取异步连接,开启事务,执行ORM操作
async def create_tables():
    # 同步连接
    # conn = engin.raw_connection()
    
    async with async_engine.begin() as conn:   
        await conn.run_sync(Base.metadata.create_all)  # 连接操作,异步执行-创建所有表 
        
        
# 2.FastAPI应用启动时,就创建数据库表
### 旧版:FastAPI 官方现已明确弃用(Deprecated)这种写法
@app.on_event("startup")   
async def startup_event():
    print("应用正在启动,正在初始化数据库...")
    await create_tables()
    
    
@app.on_event("shutdown")
async def shutdown_event():
    print("应用正在关闭,正在清理资源...")
    await async_engine.dispose()
    
    
### 新版:FastAPI 引入了基于Python异步上下文管理器(async with)的 lifespan 参数
from contextlib import asynccontextmanager
from fastapi import FastAPI

# 定义 Lifespan 上下文管理器
@asynccontextmanager
async def lifespan(_: FastAPI):  # 写 app: FastAPI 编辑器会报格式错误提醒,换成 _  
    # 应用启动阶段:yield 之前的代码
    print("应用正在启动,正在初始化数据库...")
    
    # 可以在这里执行建表等操作: 
    # async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all)
    await create_tables()
    yield  # 应用在此处开始接收请求,并运行
    
    # 应用关闭阶段:yield 之后的代码
    print("应用正在关闭,正在清理资源...")
    await async_engine.dispose()  # 确保优雅关闭,防止连接泄漏

# 将 lifespan 传递给 FastAPI 实例
app = FastAPI(lifespan=lifespan)

五、数据库会话依赖注入

在 Web 应用中,每个请求都应该拥有独立的数据库会话,请求结束时就关闭它,避免连接泄漏和数据串扰。
借助 FastAPI 的依赖注入机制,可以把“获取会话”写成一个可复用的函数。在路由函数中声明依赖后,框架会自动在请求到来时创建会话,在请求处理完毕后关闭它。这样路由函数里只需关注业务逻辑,不用关心会话的创建和销毁。

from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# ============================================================
# 全局:引擎和会话工厂(只创建一次)
# ============================================================
DATABASE_URL = "mysql+pymysql://root:password@localhost:3306/mydb?charset=utf8mb4"
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False, expire_on_commit=False)

# ============================================================
# 依赖函数:每个请求获取一个独立会话
# 用 yield 实现:请求进来时创建会话,请求结束时自动关闭
# ============================================================
def get_db():
    """FastAPI 依赖注入,为每个请求提供独立的数据库会话"""
    session = SessionLocal()     # 请求进来,创建新会话
    try:
        yield session            # 把会话交给路由函数使用
    finally:
        session.close()          # 请求结束,关闭会话,归还连接

# ============================================================
# 创建 FastAPI 应用
# ============================================================
app = FastAPI()

# ============================================================
# 路由函数:只需声明 db 依赖,框架自动注入
# 业务代码里直接用 db,不用关心会话生命周期
# ============================================================
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
    """查询单个用户"""
    user = db.query(User).filter(User.id == user_id).first()
    return user

@app.post("/users")
def create_user(name: str, email: str, db: Session = Depends(get_db)):
    """新增用户"""
    user = User(name=name, email=email)
    db.add(user)
    
    db.commit()
    db.refresh(user)
    return user

异步操作

from typing import Annotated, AsyncGenerator
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy import select
from uvicorn import lifespan


# 全局:引擎和会话工厂(只创建一次)
见:二、安装与数据库连接 和 四、自动创建数据库表

# 创建 FastAPI 应用,挂载 lifespan
app = FastAPI(lifespan=lifespan)


# 依赖函数:每个请求获取一个独立会话
# 用 yield 实现:请求进来时创建会话,请求结束时自动关闭

async def get_db() -> AsyncGenerator[AsyncSession, None]:
    """FastAPI 依赖注入,为每个请求提供独立的数据库会话"""
    async with AsyncSessionLocal() as session:  # 请求进来,创建新会话
        try:
            yield session  # 把会话交给路由函数使用
            # 注意:不要全局 commit
            # 过度提交:查询接口不需要commit
            # 增删改接口请在业务逻辑中自行 await session.commit()
            # await session.commit()  # 无异常,提交事务
        except Exception:
            await session.rollback()  # 有异常,则回滚
            raise 
            
        # 注意:不需要 finally: await session.close()
        # async with AsyncSessionLocal() 是一个异步上下文管理器,会在退出时,自动处理连接的释放和关闭    
        # finally:
        #    await session.close()  # 请求结束,关闭会话,归还连接

        
# 类型别名: 将依赖和类型绑定在一起  告别冗长的 Depends
DBSession = Annotated[AsyncSession, Depends(get_db)]
        

# 路由函数:只需声明 db 依赖,框架自动注入
# 业务代码里直接用 db,不用关心会话生命周期
from config.db_config import get_db, DBSession

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    """查询单个用户"""
    # 异步模式: 必须使用 execute + select
    result = await db.execute(select(User).filter(User.id == user_id))
    
    # 手动处理:查询不到 返回None, 而不是抛出异常
    user = result.scalar_one_or_none()  # scalar n.数量
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user


@app.post("/users")
async def create_user(name: str, email: str, db: DBSession):
    """新增用户"""
    user = User(name=name, email=email)
    db.add(user)
    
    # 注意:因为 get_db 中已经配置了自动 commit,这里不需要手动 await db.commit()
    #      使用 flush 代替手动提交, 将数据推送到数据库以获取自增ID,但不提交事务
    await db.flush()  
    
    await db.refresh(user)  # 刷新对象状态,获取自增ID和数据库默认值
    return user  # 安全地返回对象(FastAPI 序列化时不会为空)

六、基础增删改查操作

这是最常用的四项操作,都通过会话对象来执行。
增:先创建模型实例,然后用 add 加入会话,最后 commit 提交。提交后可用 refresh 获取数据库生成的默认值(如自增 ID)。
查:用 query 链式调用 filter 指定条件,再调用 first() 取第一条或 all() 取所有。
改:先查到要修改的对象,直接修改它的属性,再 commit 即可。ORM 会自动追踪属性的变化。
删:查到对象后,用 delete 标记删除,然后 commit 提交。10
核心思路是:一切操作先发生在 Python 对象上,commit 时才同步到数据库。

# ============================================================
# 增删改查 —— 全部通过会话对象 db 执行
# ============================================================


# ============================================================
# 一、增(Create)
# 1. 创建模型实例(此时只在 Python 内存中,未进数据库)
# 2. add 加入会话的待提交队列
# 3. commit 真正写入数据库
# 4. refresh 从数据库回读,拿到自增 ID 等数据库生成的值
# ============================================================
new_user = User(username="张三", email="zhangsan@test.com", age=25)
db.add(new_user)        # 加入待提交队列,还没写入数据库
db.commit()             # 真正执行 INSERT 语句,写入数据库
db.refresh(new_user)    # 回读数据库生成的值,比如 new_user.id 现在是自增后的 ID


# ============================================================
# 二、查(Read)
# query(模型类) 指定查哪张表
# filter() 加条件过滤
# first() 取第一条,查不到返回 None
# all() 取所有,返回列表
# ============================================================
# 查单条
user = db.query(User).filter(User.email == "zhangsan@test.com").first()

# 查所有
users = db.query(User).filter(User.age > 18).all()

# 按主键查(更快的简写)
user = db.query(User).get(1)


# ============================================================
# 三、改(Update)
# 1. 先查到要改的对象
# 2. 直接修改属性值(ORM 会自动追踪变化)
# 3. commit 生成 UPDATE 语句同步到数据库
# 无需手动调用 update 方法
# ============================================================
user = db.query(User).filter(User.id == 1).first()
if user:
    user.age = 30          # 直接改属性
    user.email = "new@test.com"
    db.commit()            # ORM 自动对比变化,生成 UPDATE 语句


# ============================================================
# 四、删(Delete)
# 1. 先查到要删的对象
# 2. delete 标记为待删除
# 3. commit 执行 DELETE 语句
# ============================================================
user = db.query(User).filter(User.id == 1).first()
if user:
    db.delete(user)        # 标记删除
    db.commit()            # 真正从数据库删除


# ============================================================
# 核心流程总结:
# Python 对象(内存) → add / 改属性 / delete → commit → 数据库
# commit 之前,一切变化只在内存中,不影响数据库
# 如果中途异常,没 commit 的改动会自动丢弃,天然安全
# ============================================================

提交与刷新

await db.flush()            # 刷新数据到数据库,但不提交事务  
                            # 把内存中的修改推送到数据库(为了拿到 生成自增ID 和触发数据库默认值)
    
await db.refresh(new_user)  # 按需执行:显式地从数据库拉取最新状态到内存(为了保证Pydantic序列化时,不报错)
                            # 比如 new_user.id 现在是自增后的 ID
    
await db.commit()           # 提交事务,真正执行 INSERT 语句,写入数据库  【数据库连接中,已设置提交】
    
# 注:这三个 也是执行流程顺序
1.flush() 和 refresh():放在路由函数中,仅在新增或更新,且【需要返回最新完整对象】时,调用

这两步,在异步中很重要,当执行完 await db.commit() 后,SQLAlchemy 为了保证数据的一致性
会默认将当前 Session中所有已加载的ORM对象标记为 Expired(过期)
如果没有 refresh() 显式地从数据库重新拉取最新状态,Pydantic 在读取属性时会触发隐式的懒加载
在异步环境下,会直接抛出 MissingGreenlet 异常,或 导致返回给前端的数据是空的 {}
    
    # 若不想写 await db.refresh(db_user),需要在 get_db 依赖中配置
    async_session = async_sessionmaker(
        engine, 
        class_=AsyncSession, 
        expire_on_commit=False  # 关键:关闭自动过期
    )
    
2.commit() 和 close():放在 get_db 依赖的 finally 块中,作为全局的事务兜底

异步操作

# 增删改查 —— 全部通过会话对象 db 执行


# ============================================================
# 一、增(Create)
# 1. 创建模型实例(此时只在 Python 内存中,未进数据库)
# 2. add 加入会话的待提交队列
# 3. commit 真正写入数据库
# 4. refresh 从数据库回读,拿到自增 ID 等数据库生成的值

from pydantic import BaseModel, EmailStr, ConfigDict

# 请求模型:用于接收前端传来的数据,自动进行类型校验
class UserCreate(BaseModel):
    name: str
    email: EmailStr  # 自动校验是否为合法的邮箱格式  但需要安装邮箱扩展  pip install 'pydantic[email]'
    age: int | None = None  # 可选字段

# 响应模型:用于格式化返回给前端的数据,隐藏敏感信息
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: int | None = None

    class Config:
        # 关键:允许从 SQLAlchemy ORM 对象中读取数据
        from_attributes = True 
        # 它告诉Pydantic:传入的不再是字典,而是一个带有属性的ORM对象(如user.name),请通过属性名去提取数据
        
    # 新版本写法 - 设置:允许从ORM对象读取属性
    model_config = ConfigDict(from_attributes=True)
    

@app.post("/users/", response_model=UserResponse, status_code=201)
async def create_user(
    user_data: UserCreate,  # FastAPI 会自动将 JSON Body 解析为 UserCreate对象
    db: AsyncSession = Depends(get_db)
):
    new_user = User(**user_data.model_dump())  # 使用 ** 解包传入数据,非常优雅
    
    db.add(new_user)        # 加入待提交队列,还没写入数据库
    
    # 刷新对象状态,确保返回给前端的是数据库中的最新数据
  	# await db.flush()
    # await db.refresh(new_user)
    
    # 注:因为在async_sessionmaker中,配置了关闭自动过期,直接提交 不刷新也可以
    # await db.commit()  # 有get_db 兜底 提交事务,不用再手动提交事务
    
    return new_user



# ============================================================
# 二、查(Read)
# 核心语句:
# 1. await db.execute(select(模型类).filter(条件))  # 返回一个ORM对象
     await db.execute(select(模型类).where(条件))    # 完全等效  官方更推荐

# 从ORM对象中,提取数据:   
# 注意:提取数据 不能直接挂在 .execute() 后面,必须先拿到result之后【因为这是异步操作,不是马上返回结果】,再提取 !!!
  .all()  返回原始的 Row 对象列表  # Row对象类似于Python的元组,但支持通过列名进行索引。
    # 查询模型类:select(User)  返回: [Row(User(id=1)), Row(User(id=2))]   包裹着ORM对象的Row对象列表
    # 查询多字段:select(User.id, User.name)  返回:[Row(id=1, name='Alice'), Row(id=2, name='Bob')]  元组列表 

  .scalars().all()  提取纯净的实体或单列值  # 取所有【只提取出每行的第一个元素(标量值)】,返回列表
    # 查询模型类:select(User)  返回: [User(id=1), User(id=2)]   纯净的ORM对象列表 
    # 查询多字段:select(User.id, User.name)  返回:['Alice', 'Bob']   会丢弃name字段,只保留id

  .mappings().all()  返回字典列表,更适合转JSON
    # 查询多字段:select(User.id, User.name)  返回:[{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}]  字典列表
    
    
  .scalars().first()  取第一条,查不到返回 None  # 查询非唯一字段
  .scalar_one_or_none()  查询一条,查不到返回 None  # 查询唯一字段
    
  # 本质区别:
  .scalar_one_or_none() 是根据唯一字段,最多一个结果,多个会抛异常
  .first() 是多个结果取一个, 底层sql: 通常会在SQL末尾自动追加 LIMIT 1
    
  .scalar()  # 用于提取单个标量值  常用于 聚合值 或 字段的单列值
     # COUNT(*) 或 SELECT name WHERE id=1
    

# 2. await db.get(模型类, 主键值)  # 根据主键查询,直接返回数据


# 查单条
result = await db.execute(select(User).where(User.email == "zhangsan@test.com"))
user = result.scalars().first()

# 查所有
result = await db.execute(select(User))
users = result.scalars().all()

# 按主键查(更快的简写)
user = await db.get(User, 1)



# ============================================================
# 三、改(Update)
# 方式一:查询对象,修改对象 【可全覆盖或 局部修改】
1. 先查到要改的对象
2. 直接修改属性值(ORM 会自动追踪变化)
3. commit 生成 UPDATE 语句同步到数据库
# 无需手动调用 update 方法

from fastapi import FastAPI, Depends, HTTPException

# 更新专用模型:所有字段都设为 Optional,方便前端按需传值
class UserUpdate(BaseModel):
    name: str | None = None
    email: EmailStr | None = None
    age: int | None = None

@app.put("/users/{user_id}", response_model=UserResponse)
async def update_user(
    user_id: int,
    user_data: UserUpdate,  # 接收前端传来的部分更新数据
    db: AsyncSession = Depends(get_db)
):
    # 1. 查询目标用户是否存在
    user = await db.get(User, user_id)
    
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    # 2. 核心: 提取前端实际传递的字段(忽略未传递的字段,局部更新!!!)
    # exclude_unset=True 排除未显式设置的字段  但不能排除 name=None
    # exclude_none=True 排除值为None的字段  不管是默认值,还是前端显示赋值为None的
    update_data = user_data.model_dump(exclude_unset=True, exclude_none=True)
    
    # 3. 动态赋值:修改数据库对象的属性
    for field, value in update_data.items():
        setattr(user, field, value)
        
    # 4.因为在async_sessionmaker中,配置了关闭自动过期,直接提交 不刷新也可以
    # await db.commit()  # 有get_db 兜底 提交事务,不用再手动提交事务
    
    return user


# 方式二:使用 Core 层的 字段局部更新
核心语句: await db.execute(update(模型类).where(条件).values(修改字段值))

async def update_user_by_map(db: AsyncSession, user_id: int, update_data: dict):
    # 直接根据 key-value 形式更新对应字段
    sql = update(User).where(User.id == user_id).values(**update_data)
    
    cursor_result = await db.execute(sql)
    await db.commit()
    
    # 返回受影响的行数
    return cursor_result.rowcount



# ============================================================
# 四、删(Delete)
# 1. 先查到要删的对象
# 2. delete 标记为待删除
# 3. commit 执行 DELETE 语句

user = await db.get(User, user_id)
if user:
    await db.delete(user)  # 标记删除  注意 需要await
    # db.commit()    # 有get_db 兜底 提交事务,不用再手动提交事务


# ============================================================
# 核心流程总结:
# Python 对象(内存) → add / 改属性 / delete → commit → 数据库
# commit 之前,一切变化只在内存中,不影响数据库
# 如果中途异常,没 commit 的改动会自动丢弃,天然安全
# ============================================================

七、复杂条件查询

除了简单的等值过滤,SQLAlchemy 还支持丰富的查询表达。
你可以组合多个条件,比如 “年龄大于 18 且城市是北京”;也可以做模糊匹配,比如 “名字中含‘张’”;还能进行聚合统计,比如 “每个城市各有多少用户”。对于多表查询,如果模型之间定义了关联关系,可以直接用 join 跨表过滤,就像操作单个对象一样自然。

from sqlalchemy import or_, and_, func, between

# ============================================================
# 一、多条件组合
# ============================================================

# AND 连接:年龄大于18 且 城市是北京
users = db.query(User).filter(
    and_(User.age > 18, User.city == "北京")
).all()

# OR 连接:城市是北京 或 上海
users = db.query(User).filter(
    or_(User.city == "北京", User.city == "上海")
).all()

                   
# 链式 filter 也等同于 AND
users = db.query(User).filter(User.age > 18).filter(User.city == "北京").all()

# 区间查询:年龄在 20 到 30 之间
users = db.query(User).filter(between(User.age, 20, 30)).all()

# IN 查询:城市在给定列表中
users = db.query(User).filter(User.city.in_(["北京", "上海", "广州"])).all()

                         
# ============================================================
# 二、模糊匹配
# ============================================================

# 名字中含"张"
users = db.query(User).filter(User.name.like("%张%")).all()

# 名字以"张"开头
users = db.query(User).filter(User.name.like("张%")).all()

# 邮箱以 @gmail.com 结尾
users = db.query(User).filter(User.email.like("%@gmail.com")).all()


# ============================================================
# 三、聚合统计
# ============================================================

# 统计用户总数
count = db.query(func.count(User.id)).scalar()

# 按城市分组,统计每个城市的用户数
result = db.query(User.city, func.count(User.id)).group_by(User.city).all()
# 返回:[("北京", 15), ("上海", 12), ("广州", 8)]

# 平均年龄
avg_age = db.query(func.avg(User.age)).scalar()


# ============================================================
# 四、多表关联查询
# 前提:User 和 Post 已通过 relationship 和 ForeignKey 建立关联
# ============================================================

# 方式一:join + filter,跨表过滤
# 查所有"张三"写的文章
posts = db.query(Post).join(User).filter(User.name == "张三").all()

# 方式二:直接用 relationship 属性,更自然
# 先拿到用户,再通过 .posts 拿到他的所有文章
user = db.query(User).filter(User.name == "张三").first()
posts = user.posts   # 这就是他写的所有文章

# 反向查:这篇文章的作者是谁
post = db.query(Post).filter(Post.title == "某文章标题").first()
author = post.author  # 直接拿到 User 对象

异步操作

from sqlalchemy import or_, and_, func, between

# ============================================================
# 一、多条件组合

# AND 连接:年龄大于18 且 城市是北京
方式1: and_() 函数  # 官方推荐,条件不需要加括号
方式2: await db.execute(select(User).where( (条件1) & (条件2))
  # 注意:每个条件必须用括号()
     
result = await db.execute(select(User).where(
    User.age > 18 & User.city == "北京"
)
                         
result = await db.execute(select(User).where(
	and_(User.age > 18, User.city == "北京")
	)
)
users = result.scalars().all()


# OR 连接:城市是北京 或 上海
方式1: or_() 函数  # 官方推荐,条件不需要加括号
方式2:await db.execute(select(User).where(条件1 | 条件2)
                     
result = await db.execute(select(User).where(
    (User.city == "北京") | (User.city == "上海")
)

result = await db.execute(select(User).where(
	or_(User.city == "北京", User.city == "上海")
    )
)
                         
users = result.scalars().all()
                         
# 链式 where 也等同于 AND
result = await db.execute(select(User).where(User.age > 18).where(User.city == "北京"))

# 区间查询:年龄在 20 到 30 之间
result = await db.execute(select(User).where(between(User.age, 20, 30)))

# IN 查询:城市在给定列表中
result = await db.execute(select(User).where(User.city.in_(["北京", "上海", "广州"])))
                          
users = result.scalars().all()

# ============================================================
# 二、模糊匹配
# 名字中含"张"
result = await db.execute(select(User).where(User.name.like("%张%"))).scalars().all()
# 名字以"张"开头
result = await db.execute(select(User).where(User.name.like("张%"))).scalars().all() 
# 邮箱以 @gmail.com 结尾
result = await db.execute(select(User).where(User.name.like("%@gmail.co"))).scalars().all() 
users = result.scalars().all()

                          
# ============================================================
# 三、聚合统计
func.聚合方法(模型类.属性)
                            
# 统计用户总数
result =  await db.execute(select(func.count(User.id)))
count = result.scalar()
                          
# 按城市分组,统计每个城市的用户数
result = await db.execute(
    select(User.city, func.count(User.id).label("user_count")
	).group_by(User.city)
rows = result.all()
# 返回:[("北京", 15), ("上海", 12), ("广州", 8)]

                          
# ============================================================
# 四、分页查询                       
# 核心公式是 skip = (page 当前页码 - 1) * page_size 每页数量
select(模型类).offset(skip 跳过的记录数).limit(page_size 返回的记录数)
                 
result = await db.execute(
        select(User).offset(skip).limit(page_size)
)

users = result.scalars().all()                     
          
                          
# ============================================================
# 五、多表关联查询
# 前提:User 和 Post 已通过 relationship 和 ForeignKey 建立关联

# 方式一:join + where,跨表查询  【只需要部分字段:推荐】
# 查所有"张三"写的文章
# 构建一个 SQL Statement 对象(SQL 语句)  stmt: statement的缩写
stmt = (
	select(Post)
	.join(User, Post.author_id == User.id)  # 显式关联 User 表
	.where(User.username == "张三")       # 跨表条件过滤
)

stmt = (
	select(Post)
    .join(User)  # SQLAlchemy 会自动根据外键找到关联条件
    .where(User.username == "张三")            
) 
                          
result = await db.execute(stmt)
return result.scalars().all()


# 方式二:利用 relationship 属性,预加载关联数据(子查询)  【完整的ORM对象:推荐】
在异步环境中,绝对不能直接访问 .posts 触发懒加载(会报 MissingGreenlet 错误)
必须使用 selectinload 显式预加载
                          
# 正向关联:先拿到用户,再通过 .posts 拿到他的所有文章
stmt = (
    select(User)
    .options(selectinload(User.posts))   # 核心:显式预加载 posts 集合
    .where(User.username == "张三") 
)
result = await db.execute(stmt)
user = result.scalar_one_or_none()

if user:
    # 此时可以安全、无感地直接访问 user.posts,不会触发额外查询
    return user.posts  
return []

                          
# 反向关联:获取某篇文章及其作者信息
post = db.query(Post).filter(Post.title == "某文章标题").first()
stmt = (
    select(Post)
    .options(selectinload(Post.author))  # 预加载作者对象
    .where(Post.title == "某文章标题")
)
result = await db.execute(stmt)
post = result.scalar_one_or_none()

if post:
    # 安全访问 post.author.name
    return post 
return None                    

八、原生 SQL 执行

当 ORM 的表达能力不够用时,可以直接写 SQL。SQLAlchemy 提供了 text() 函数来包裹原生 SQL 字符串,并通过字典方式传入参数,能有效防止 SQL 注入。这种方法保留了 ORM 的连接管理和事务控制,同时让你在复杂场景下直接掌控 SQL 逻辑,是 ORM 能力的有力补充。

from sqlalchemy import text

# ============================================================
# 原生 SQL 执行 —— text() 包裹 SQL,字典传参防注入
# ============================================================
# ============================================================
# 一、查询操作
# ============================================================

# 简单查询
sql = "SELECT * FROM users WHERE age > :age"
rows = db.execute(text(sql), {"age": 18}).fetchall()
# rows 是列表,每行是一个 Row 对象,可按列名取值

# 遍历结果
for row in rows:
    print(row.id, row.name, row.email)


# 取单条
sql = "SELECT * FROM users WHERE id = :user_id"
row = db.execute(text(sql), {"user_id": 1}).fetchone()


# 多条件参数化
sql = """
    SELECT * FROM users 
    WHERE age > :min_age 
    AND city = :city
"""
rows = db.execute(text(sql), {"min_age": 20, "city": "北京"}).fetchall()


# 聚合查询
sql = "SELECT city, COUNT(*) as cnt FROM users GROUP BY city"
rows = db.execute(text(sql)).fetchall()
for row in rows:
    print(row.city, row.cnt)


# ============================================================
# 二、写操作(增删改)
# 注意:execute 后必须手动 commit,和 ORM 方式一样
# ============================================================

# 插入
sql = "INSERT INTO users (name, email, age) VALUES (:name, :email, :age)"
db.execute(text(sql), {"name": "王五", "email": "wangwu@test.com", "age": 28})
db.commit()   # 必须提交

# 更新
sql = "UPDATE users SET age = :age WHERE id = :user_id"
db.execute(text(sql), {"age": 30, "user_id": 1})
db.commit()

# 删除
sql = "DELETE FROM users WHERE id = :user_id"
db.execute(text(sql), {"user_id": 5})
db.commit()

# ============================================================
# 三、复杂查询示例(ORM 难表达的 SQL)
# ============================================================

# 子查询
sql = """
    SELECT * FROM users 
    WHERE id IN (
        SELECT user_id FROM posts 
        WHERE created_at > :since
    )
"""
rows = db.execute(text(sql), {"since": "2025-01-01"}).fetchall()

# 联表查询
sql = """
    SELECT u.name, p.title, p.created_at
    FROM users u
    JOIN posts p ON u.id = p.user_id
    WHERE u.city = :city
    ORDER BY p.created_at DESC
"""
rows = db.execute(text(sql), {"city": "北京"}).fetchall()

九、事务管理

事务能保证一组操作要么全部成功,要么全部撤销,这是确保数据一致性的关键。
SQLAlchemy 默认在 commit 时提交事务。如果中间发生异常,需要手动调用 rollback 回滚。更推荐的方式是使用 with 语句开启事务,代码块结束时自动提交,有异常则自动回滚,代码更简洁也更安全。

# ============================================================
# 事务管理 —— 保证一组操作同生共死
# ============================================================

# ============================================================
# 场景示例:用户下单
# 需要同时做两件事:1. 扣减库存  2. 创建订单
# 任何一步失败,另一条也必须撤销,不能出现库存扣了但订单没生成
# ============================================================

# ============================================================
# 方式一:手动提交 / 回滚(原始写法)
# ============================================================
def create_order_manual(user_id, product_id, quantity):
    db = SessionLocal()
    try:
        # 步骤1:扣减库存
        product = db.query(Product).filter(Product.id == product_id).first()
        product.stock -= quantity

        # 步骤2:创建订单
        order = Order(user_id=user_id, product_id=product_id, quantity=quantity)
        db.add(order)

        # 都成功,提交
        db.commit()
    except Exception:
        # 任何一步出错,回滚所有改动
        db.rollback()
        raise   # 把异常继续往上抛
    finally:
        db.close()


# ============================================================
# 方式二:with 语句自动管理(推荐)
# session.begin() 开启事务
# 代码块正常结束 → 自动 commit
# 代码块抛出异常 → 自动 rollback
# 不用手写 try/except/finally
# ============================================================
def create_order_with(user_id, product_id, quantity):
    with SessionLocal() as db:               # 自动关闭会话
        with db.begin():                     # 开启事务,自动提交或回滚
            # 步骤1:扣减库存
            product = db.query(Product).filter(Product.id == product_id).first()
            product.stock -= quantity

            # 步骤2:创建订单
            order = Order(user_id=user_id, product_id=product_id, quantity=quantity)
            db.add(order)

        # 到这里事务已自动提交,无需手动 commit


# ============================================================
# 方式三:更简洁的嵌套 with(一个 with 同时管会话和事务)
# ============================================================
def create_order_compact(user_id, product_id, quantity):
    with SessionLocal() as db, db.begin():   # 逗号并列,一个缩进层级
        # 扣库存
        product = db.query(Product).filter(Product.id == product_id).first()
        product.stock -= quantity

        # 建订单
        order = Order(user_id=user_id, product_id=product_id, quantity=quantity)
        db.add(order)
    # 离开 with 块时,自动提交并关闭会话


# ============================================================
# 对比说明
# ============================================================
写法 优点 缺点
手动 try/except 流程清晰,可自定义异常处理 代码啰嗦,容易忘写 rollback 或 close
with db.begin() 异常自动回滚,不会忘 需要额外嵌一层 with
with SessionLocal(), db.begin() 一行搞定,最简洁 无法在中间插入自定义异常处理
posted @ 2026-07-02 00:33  Edmond辉仔  阅读(13)  评论(0)    收藏  举报