欢迎光临
我们一直在努力

[特殊字符]️ SQLAlchemy 暴击指南:把数据库变成 Python 对象

🗄️ SQLAlchemy 暴击指南:把数据库变成 Python 对象

直接拼 SQL 字符串又脏又容易注入;纯手写 SQL 又重复又难维护。SQLAlchemy 把这个矛盾解开了——用 Python 类描述表,用 Python 表达式写查询,它替你生成正确的 SQL。这篇把 Obsidian 里 SQLAlchemy 的卡片和笔记揉成一条线:从"为什么需要 ORM"一直到"异步 + 事务",每一段代码都能跑、版本都对齐。你来检查,我兜底。


📑 目录

  • 为什么需要 ORM / SQLAlchemy
  • 四大核心概念
  • 现代模型定义(2.0 写法)
  • 连库 & 建表
  • 完整 CRUD
  • 进阶查询
  • 关系映射
  • 事务:要么全成,要么全撤
  • 原生 SQL(兜底手段)
  • 异步 SQLAlchemy
  • 自检清单

  • 1. 为什么需要 ORM / SQLAlchemy

    先说人话:ORM = Object Relational Mapping,对象关系映射。它把"数据库表"映射成"Python 类",把"一行数据"映射成"一个对象"。于是你不用写 SQL 字符串,而是写 Python。

    SQLAlchemy 有两层,初学者最容易混淆,先分清:

    层角色什么时候用
    Core(核心) 偏底层的 SQL 表达能力(表、语句、引擎) 要精细控制 SQL、或写原生查询时
    ORM(对象关系) 把表映射成类,用对象操作数据 绝大多数业务代码(本文重点)

    # ❌ 裸 SQL 字符串
    sql = "SELECT * FROM users " \\
    "WHERE name='" + name + "'"
    # 拼接 → 注入风险 + 难维护

    # ✅ SQLAlchemy ORM
    await session.execute(
    select(User).where(User.name == name))
    # 参数化、安全、可读

    💡 一句话定位 SQLAlchemy 是 Python 生态里事实标准的数据库工具包。FastAPI 官方教程用的就是它。学会它,等于打通了"Python 后端怎么存数据"的任督二脉。


    2. 四大核心概念

    把下面四个词刻进脑子,后面全是基于它们的组合:

    概念是什么类比
    Engine 数据库连接的"总入口",管理连接池 水厂总管道
    Session 一次"和数据库对话"的工作单元 你和水厂的一次通话
    Base 所有模型类的父类(声明式基类) "表"的图纸模板
    Model 一张表对应一个类,字段即列 一张具体的表

    🧠 深挖:连接池 Engine 不会每次查询都新建连接,而是维护一个连接池——重复利用连接,避免频繁握手开销。这也是为什么高并发下要用连接池而不是"每次连一次"。(对应 Wiki 卡片《数据库连接池》。)


    3. 现代模型定义(2.0 写法)

    这是全文最该记牢、也最容易踩版本坑的地方。SQLAlchemy 2.0 推荐使用 DeclarativeBase + Mapped + mapped_column。

    # models.py · 2.0 风格
    from sqlalchemy import String, Integer, Float
    from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

    class Base(DeclarativeBase):
    pass

    class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50))
    age: Mapped[int] = mapped_column(Integer, default=18)
    score: Mapped[float] = mapped_column(Float)

    🚫 版本坑(必看,否则过不了你的检查) 旧教程里常写的 from sqlalchemy.ext.declarative import declarative_base 再 Base = declarative_base(),以及 Column(Integer) 那种写法,在 2.0 里已废弃(deprecated)。新项目请一律用上面 DeclarativeBase + Mapped + mapped_column 的写法。遇到老代码能看懂即可,自己写别再用旧的。

    类型怎么写?

    Python 侧数据库列类型写法
    int INTEGER Mapped[int] = mapped_column(primary_key=True)
    str VARCHAR Mapped[str] = mapped_column(String(50))
    float FLOAT Mapped[float] = mapped_column(Float)
    bool BOOLEAN Mapped[bool] = mapped_column(default=False)

    4. 连库 & 建表

    用 create_engine 建引擎,再 Base.metadata.create_all 按模型建表(开发/演示够用;生产请用 Alembic 做迁移)。

    # database.py · 同步
    from sqlalchemy import create_engine
    from .models import Base

    # echo=True 会把生成的 SQL 打到控制台,学习期很有用
    engine = create_engine("sqlite:///./demo.db", echo=True)

    # 首次建表(已存在则跳过)
    Base.metadata.create_all(engine)

    💡 连接串速查 不同数据库只是"连接串"不同:sqlite:///./x.db、postgresql+psycopg://user:pwd@localhost/db、mysql+pymysql://user:pwd@localhost/db。换库基本只改这一行。


    5. 完整 CRUD

    Create 增、Read 查、Update 改、Delete 删。下面是一套能直接跑的同步示例:

    # crud.py
    from sqlalchemy.orm import Session
    from .database import engine
    from .models import User

    # 增(Create)
    with Session(engine) as s:
    u = User(name="xushuai", age=20)
    s.add(u)
    s.commit() # 必须 commit 才真正写入
    s.refresh(u) # 把数据库生成的 id 同步回对象
    print(u.id) # 此时才有值

    # 查(Read)
    with Session(engine) as s:
    u = s.get(User, 1) # 按主键查,最快
    print(u.name)

    # 改(Update)
    with Session(engine) as s:
    u = s.get(User, 1)
    u.age = 21 # 改属性即改记录
    s.commit()

    # 删(Delete)
    with Session(engine) as s:
    u = s.get(User, 1)
    s.delete(u)
    s.commit()

    ⚠️ 最常见的两个坑 ① 忘了 commit()——内存里改了,库里没动。② 忘了 refresh() 就读 id——自增主键是数据库生成的,commit 后还需 refresh 才能拿到。这两个点面试/实操高频出现。


    6. 进阶查询

    2.0 推荐用 select() 构造语句,再用 session.execute(…) 执行,scalars() 取对象列表。

    from sqlalchemy import select, func, or_

    # 条件过滤(where 等价于旧版 filter)
    stmt = select(User).where(User.age >= 18)
    users = s.scalars(stmt).all()

    # 或条件
    stmt = select(User).where(or_(User.age < 18, User.age > 60))

    # 模糊匹配(LIKE %帅%)
    stmt = select(User).where(User.name.contains("帅"))
    # 或手写 like:User.name.like("%帅%")

    # 排序 + 分页
    stmt = select(User).order_by(User.age.desc()).offset(0).limit(10)

    # 聚合:总数 / 平均年龄
    total = s.scalar(select(func.count()).select_from(User))
    avg_age = s.scalar(select(func.avg(User.age)))

    Join 联表(配合下一节的关系):

    # 查出"xushuai 写的所有文章"
    stmt = select(Article).join(User).where(User.name == "xushuai")
    articles = s.scalars(stmt).all()


    7. 关系映射

    表与表之间有关系,SQLAlchemy 用 relationship() + ForeignKey 把它们变成对象间的引用。

    一对多:一个用户写多篇文章

    from sqlalchemy import ForeignKey
    from sqlalchemy.orm import relationship, Mapped, mapped_column

    class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(String(50))
    articles: Mapped[list["Article"]] = relationship(back_populates="author")

    class Article(Base):
    __tablename__ = "articles"
    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String(100))
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    author: Mapped["User"] = relationship(back_populates="articles")

    用起来就像操作对象:user.articles 直接拿到他的所有文章,article.author 直接拿到作者。

    一对一:一个用户对应一份资料

    在"一"的那侧加 uselist=False 即可:

    class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(primary_key=True)
    profile: Mapped["Profile"] = relationship(
    back_populates="user", uselist=False) # 关键

    class Profile(Base):
    __tablename__ = "profiles"
    id: Mapped[int] = mapped_column(primary_key=True)
    bio: Mapped[str] = mapped_column(String(200))
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    user: Mapped["User"] = relationship(back_populates="profile")


    8. 事务:要么全成,要么全撤

    "事务"保证一组操作原子性:要么全部成功提交,要么出错整体回滚,不会出现"钱扣了但订单没生成"的半吊子状态。

    with Session(engine) as s:
    try:
    s.add(User(name="A"))
    s.add(User(name="B"))
    s.commit() # 两条一起落库
    except Exception:
    s.rollback() # 出错 → 全部撤销,库里干干净净
    raise

    💡 小知识 在 2.0 里,with Session() as s: 这个上下文管理器本身就有"正常退出自动提交、异常退出自动回滚"的能力。上面显式写 try/except + rollback 是为了可读和可控,也是面试里展示"我懂事务"的标准写法。


    9. 原生 SQL(兜底手段)

    ORM 覆盖 90% 场景,但遇到复杂报表、窗口函数等,直接写 SQL 更省心。用 text() 安全传参(别直接拼字符串):

    from sqlalchemy import text

    with engine.connect() as conn:
    result = conn.execute(
    text("SELECT * FROM users WHERE age > :age"),
    {"age": 18}, # 参数化,防注入
    )
    for row in result:
    print(row) # row 是类似元组的对象,可按列名取


    10. 异步 SQLAlchemy

    高并发接口要用异步版:create_async_engine + async_sessionmaker + AsyncSession。注意数据库驱动也要换异步的(如 PostgreSQL 用 asyncpg,SQLite 用 aiosqlite)。

    # database_async.py · 异步
    from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
    from sqlalchemy import select
    from .models import User

    # 连接串前缀多了 +aiosqlite / +asyncpg
    engine = create_async_engine("sqlite+aiosqlite:///./demo.db")
    AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

    async def get_users():
    async with AsyncSessionLocal() as session:
    result = await session.execute(select(User))
    return result.scalars().all()

    和 FastAPI 配合时,用 lifespan 在启动时建表,用 yield 依赖把 Session 注入接口:

    # main_async.py · FastAPI 集成
    from contextlib import asynccontextmanager
    from fastapi import FastAPI, Depends
    from typing import Annotated

    @asynccontextmanager
    async def lifespan(app: FastAPI):
    async with engine.begin() as conn:
    await conn.run_sync(Base.metadata.create_all) # 启动建表
    yield # 应用运行期

    app = FastAPI(lifespan=lifespan)

    async def get_db():
    async with AsyncSessionLocal() as session:
    yield session # 注入后自动关闭

    @app.get("/users/")
    async def list_users(db: Annotated[AsyncSession, Depends(get_db)]):
    res = await db.execute(select(User))
    return res.scalars().all()

    🧠 深挖:同步 vs 异步 怎么选? 学习/小项目用同步(create_engine + Session)最省心。要扛高并发、配合 async def 接口,才上异步。注意异步必须配异步驱动,且 ORM 操作要 await。两篇博客打通后你会发现:FastAPI 管"接口",SQLAlchemy 管"数据",两者用 Depends 一接就活了。


    自检清单(点开看答案)

    Q1:Engine、Session、Base、Model 四者分别是什么角色?

    Engine=连接总入口/连接池;Session=一次数据库对话的工作单元(增删改查都在它里);Base=所有模型的声明式父类;Model=一张表对应一个类。关系:Engine 造 Session,Session 操作 Model 实例,Model 继承自 Base。

    Q2:SQLAlchemy 2.0 里定义模型,正确的写法是什么?旧的 `declarative_base()` 还能用吗?

    正确写法:class Base(DeclarativeBase): pass,字段用 Mapped[类型] = mapped_column(…)。旧 declarative_base() + Column() 写法在 2.0 中已废弃,能跑但不推荐,新代码别用。

    Q3:为什么 `add` 之后还要 `commit`,有时还要 `refresh`?

    commit() 才真正把改动写入数据库;不 commit 只是内存里的挂起状态。refresh(obj) 把数据库生成的值(如自增 id)同步回 Python 对象,之后才能真正拿到 obj.id。

    Q4:一对多和一对一在 `relationship` 上的区别是什么?

    "多"的那一侧就是普通 relationship(如 User.articles 是列表);"一"的那一侧加 uselist=False(如 User.profile 是单个对象)。两端用 back_populates 互指对方属性名,保持双向同步。

    Q5:事务的 `rollback` 解决什么问题?什么时候该用它?

    解决"一组操作只成功了一部分"的不一致问题。只要多个写操作必须要么全成、要么全撤(如转账:扣款+入账),就要放进同一个 Session,出错时 rollback() 整体回滚,保证数据原子性。

    Q6:异步 SQLAlchemy 相比同步,改了哪几处?

    ① 引擎换 create_async_engine;② 会话换 async_sessionmaker + AsyncSession;③ 连接串加异步驱动前缀(+aiosqlite / +asyncpg);④ 所有 ORM 操作用 await(await session.execute(…))。


    📚 资料来源 & 版本核查

    • 本篇整理自 Obsidian 知识库 03 – 参考资料/数据库/03.SQLAlchemy学习与使用.md、FastAPI 第 07–09 章,以及 06 – Wiki/数据库/ 卡片(SQLAlchemy Core / ORM / Session / Engine / 连接池 / Declarative Base)。
    • 版本基准(已联网核对,2026-08-03):SQLAlchemy 2.0.x(最新稳定线 2.0.51;2.1 处于 2.1.0b3 beta,未 GA)。代码全部采用 2.0 现代写法。
    • 关键准确性说明:declarative_base() 自 2.0 起废弃,改用 DeclarativeBase;2.0 推荐 select() + where() + execute()/scalars() 查询范式。

    赞(0)
    未经允许不得转载:171主机测评 » [特殊字符]️ SQLAlchemy 暴击指南:把数据库变成 Python 对象
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址