基于 SQLAlchemy 2.x,面向 MySQL 数据库的从入门到精通实战教程(2)

基于 SQLAlchemy 2.x,面向 MySQL 数据库的从入门到精通实战教程(1)

基于 SQLAlchemy 2.x,面向 MySQL 数据库的从入门到精通实战教程(3)

文章目录

  • [第6章 基本操作(CRUD)](#第6章 基本操作(CRUD))
    • [6.1 创建与删除表](#6.1 创建与删除表)
    • [6.2 插入数据(Create)](#6.2 插入数据(Create))
    • [6.3 查询数据(Read)](#6.3 查询数据(Read))
    • [6.4 更新数据(Update)](#6.4 更新数据(Update))
    • [6.5 删除数据(Delete)](#6.5 删除数据(Delete))
    • [6.6 操作数据总结🚩](#6.6 操作数据总结🚩)
      • [6.6.1 one_or_none() 和 one()的区别](#6.6.1 one_or_none() 和 one()的区别)
      • [6.6.2 scalar() 和 scalars() 详解](#6.6.2 scalar() 和 scalars() 详解)
      • [6.6.3 事务](#6.6.3 事务)
  • [第7章 查询进阶](#第7章 查询进阶)
    • [7.1 filter 与 filter_by](#7.1 filter 与 filter_by)
    • [7.2 常用查询条件](#7.2 常用查询条件)
    • [7.3 排序、分页与聚合](#7.3 排序、分页与聚合)
    • [7.4 关联查询与 join](#7.4 关联查询与 join)
  • [第8章 关系映射](#第8章 关系映射)
    • [8.1 一对多关系](#8.1 一对多关系)
    • [8.2 多对一关系](#8.2 多对一关系)
    • [8.3 一对一关系](#8.3 一对一关系)
    • [8.4 多对多关系](#8.4 多对多关系)
    • [8.5 级联操作(cascade)](#8.5 级联操作(cascade))
    • [8.6 懒加载策略(lazy)](#8.6 懒加载策略(lazy))
  • [第9章 事务与会话管理](#第9章 事务与会话管理)
    • [9.1 事务的概念](#9.1 事务的概念)
    • [9.2 commit 与 rollback](#9.2 commit 与 rollback)
    • [9.3 上下文管理器 with 语句](#9.3 上下文管理器 with 语句)
    • [9.4 Session 生命周期最佳实践](#9.4 Session 生命周期最佳实践)

第6章 基本操作(CRUD)

6.1 创建与删除表

python 复制代码
# 创建所有表(基于已定义的模型类)
Base.metadata.create_all(engine)

# 删除所有表
Base.metadata.drop_all(engine)

# 只创建/删除特定表
Base.metadata.create_all(engine, tables=[User.__table__])

注意

  1. create_all 只会创建不存在的表,不会修改已有表的结构。如果需要加列、改类型,必须用 Alembic 迁移
  2. 执行前确保 MySQL 中对应的数据库已创建:CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  3. 生产环境不要用 create_all,应使用 Alembic 管理表结构

创建 MySQL 数据库的推荐语句:

sql 复制代码
CREATE DATABASE IF NOT EXISTS mydb
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

6.2 插入数据(Create)

python 复制代码
from sqlalchemy.orm import Session

# 单条插入
with Session(engine) as session:
    user = User(name="张三", email="zhangsan@example.com", age=25)
    session.add(user)
    session.commit()
    # commit 后自增主键会自动回填
    print(user.id)

# 批量插入
with Session(engine) as session:
    users = [
        User(name="李四", email="lisi@example.com", age=30),
        User(name="王五", email="wangwu@example.com", age=28),
        User(name="赵六", email="zhaoliu@example.com", age=35),
    ]
    session.add_all(users)
    session.commit()

MySQL 高性能批量插入(Core 方式,绕过 ORM 状态跟踪):

python 复制代码
from sqlalchemy import insert

with Session(engine) as session:
    session.execute(
        insert(User),
        [
            {"name": "用户1", "email": "user1@example.com", "age": 20},
            {"name": "用户2", "email": "user2@example.com", "age": 21},
            {"name": "用户3", "email": "user3@example.com", "age": 22},
        ],
    )
    session.commit()

批量插入超过 1000 条时,建议分批执行(每批 500~1000 条),避免 MySQL 的 max_allowed_packet 限制。

6.3 查询数据(Read)

SQLAlchemy 2.0 使用 select() 构造查询,通过 session.execute() 执行:

python 复制代码
from sqlalchemy import select

# 查询所有用户
with Session(engine) as session:
    stmt = select(User)  # 构造查询语句,等价于 SELECT * FROM user;
    result = session.execute(stmt)  # 把语句提交给数据库执行,返回一行行元组
    users = result.scalars().all()
    for user in users:
        print(user.id, user.name, user.email)
    # 等价SQL:SELECT user.id, user.name, user.email FROM user;

# 按主键查询
with Session(engine) as session:
    user = session.get(User, 1)  # session.get(模型类, 主键值),返回 User 对象或 None
    if user:
        print(user.name)
    # 等价SQL:SELECT * FROM user WHERE id = 1;

# 条件查询
with Session(engine) as session:
    stmt = select(User).where(User.name == "张三")
    user = session.execute(stmt).scalar_one_or_none()
    """
    scalar_one_or_none() 说明:
        查到 1 条:返回 User 对象
        查不到:返回 None
        查到多于 1 条,直接抛异常!
    """
    # 等价SQL:SELECT * FROM user WHERE name = '张三'

# 查询第一条
with Session(engine) as session:
    stmt = select(User).where(User.age > 25)
    user = session.execute(stmt).scalars().first()
    # scalars() 取出 ORM 对象,.first():拿结果集第一条;没有匹配返回None
    # 等价SQL:SELECT * FROM user WHERE age > 25 LIMIT 1;

6.4 更新数据(Update)

python 复制代码
# 方式一:查询后修改属性(ORM 方式,推荐单条更新)
with Session(engine) as session:
    user = session.get(User, 1)  # 根据主键 id=1 查询用户
    if user:
        user.age = 26
        user.email = "newemail@example.com"  # 直接修改数据
        session.commit()  # 提交事务,自动检测变化并执行 UPDATE
    # 等价SQL:UPDATE user SET age=26, email='newemail@example.com' WHERE user.id = 1;

# 方式二:批量更新(Core 方式,效率更高)
from sqlalchemy import update

with Session(engine) as session:
    stmt = (
        update(User)  # 指定更新的表
        .where(User.age > 30)  # WHERE 条件判断
        .values(age=User.age + 1)  # 更新数据
    )
    result = session.execute(stmt)  # 将 SQL 提交给数据库执行
    session.commit()  # 提交事务
    print(f"更新了 {result.rowcount} 条记录")
    # 等价SQL:UPDATE user SET age = age + 1 WHERE age > 30;

ORM 方式下,Session 会自动跟踪对象属性的变化(dirty checking),commit 时只更新发生变化的字段。

6.5 删除数据(Delete)

python 复制代码
# 方式一:查询后删除(ORM 方式)
with Session(engine) as session:
    user = session.get(User, 1)  # 按主键 id=1 查询用户
    if user:
        session.delete(user)  # 把这个对象标记为待删除
        session.commit()  # 提交事务,执行 SQL
    # 等价SQL:DELETE FROM user WHERE id = 1;

# 方式二:批量删除(Core 方式)
from sqlalchemy import delete

with Session(engine) as session:
    stmt = delete(User).where(User.age < 20)  # WHERE 条件判断
    result = session.execute(stmt)  # 执行 SQL
    session.commit()  # 提交事务
    print(f"删除了 {result.rowcount} 条记录")
    # 等价SQL:DELETE FROM user WHERE age < 20;

MySQL 注意 :删除大量数据时(如超过 1 万行),建议分批删除并配合 LIMIT,避免长事务锁表影响线上业务。

6.6 操作数据总结🚩

常用结果获取方法:

方法 返回值 说明
.all() 列表 返回所有结果
.first() 对象 / None 返回第一条,无结果返回 None
.one() 对象 返回唯一一条,无结果或多条均抛异常
scalar_one_or_none() 对象 / None 多条直接抛异常,适合唯一键查询
.one_or_none() 对象 / None 返回一条或 None,多条抛异常
.scalar() 标量值 返回第一行第一列的值
.scalars() 标量迭代器 将每行的第一列作为标量返回
.get(User, pk) 对象 / None 优先读会话缓存,只能查询主键

6.6.1 one_or_none() 和 one()的区别

.scalar_one_or_none() .one()
0 条数据 返回 None 抛异常 NoResultFound
1 条数据 返回 ORM 对象 返回 ORM 对象
≥2 条数据 抛异常 MultipleResultsFound 抛异常 MultipleResultsFound
返回值 对象 / None 对象(永远不会返回 None)

6.6.2 scalar() 和 scalars() 详解

标量 = 一行 Row 里的第一个元素,把外层 Row 元组外壳剥掉,直接拿里面第一个位置的值。

举例:

python 复制代码
stmt = select(User)
result = session.execute(stmt)
# result.all() → [ Row( User对象 ), Row( User对象 ), ... ]
# 每一个Row本质是元组:(User(id=1), )
# Row[0] 就是这个 User实例,这个Row[0]的值,就叫「标量」

标量不一定是数字!

  • 查询整张 ORM 模型:标量 = ORM 对象实例(User 对象)
  • 查询单列select(User.name):标量 = 字符串"张三"
  • 查询聚合select(func.count(User.id)):标量 = 数字10
  1. scalar()单数(拿第一条行的第一个标量)

    python 复制代码
    # 示例1:count统计
    stmt = select(func.count(User.id))
    res = session.execute(stmt)
    total = res.scalar()
    # SQL返回一行:(15,) → scalar()直接取出15
    
    # 示例2:查单个字段
    stmt = select(User.name).where(User.id == 1)
    name = session.execute(stmt).scalar()
    # 返回 "张三" 或者 None

    适合:聚合函数 count / sum / max,只想要一个简单数值。

  2. .scalars() 复数(把所有行的第一个标量变成迭代器)

    调用后返回ScalarResult迭代对象,不是直接返回列表,后面再接 .all() / .first() / .one() / .one_or_none()

    python 复制代码
    stmt = select(User)
    res = session.execute(stmt)
    users = res.scalars().all()
    # res.all()        → [ Row(User1), Row(User2) ]
    # res.scalars()    → 迭代器,逐个取出Row[0]
    # res.scalars().all() → [User1对象, User2对象]
    
    # 查询单列
    stmt = select(User.name).where(User.age>20)
    names = session.execute(stmt).scalars().all()
    # ["张三","李四","王五"]
    1. .scalars().all() 获取全部 ORM 对象
    2. .scalars().first() 获取第一条 ORM 对象
    3. .scalars().one() / .one_or_none() 获取唯一 ORM 对象

6.6.3 事务

事务提醒:

  • commit():提交,修改落库;如果没有写 <font style="background-color:#FBDE28;">commit()</font>,delete 仅仅是内存标记,数据库不会修改任何数据。
  • session.rollback():回滚,放弃本次会话所有修改。

第7章 查询进阶

7.1 filter 与 filter_by

在 1.x 的 Query API 中,filterfilter_by 是常用的过滤方法。2.0 推荐使用 select().where()

python 复制代码
# filter_by:关键字参数,只能做等于比较,简洁
session.query(User).filter_by(name="张三", age=25).all()

# filter:列表达式,支持所有比较运算符,更灵活
session.query(User).filter(User.name == "张三", User.age > 20).all()

# 2.0 风格(推荐)
stmt = select(User).where(User.name == "张三", User.age > 20)
session.execute(stmt).scalars().all()

7.2 常用查询条件

python 复制代码
from sqlalchemy import and_, or_, not_, between, in_, like, func

# 等于 / 不等于
select(User).where(User.name == "张三")
select(User).where(User.name != "张三")

# 模糊查询(MySQL 中 LIKE 不区分大小写取决于排序规则)
select(User).where(User.name.like("%张%"))
select(User).where(User.name.ilike("%zhang%"))  # 显式不区分大小写

# 范围查询
select(User).where(User.age.between(20, 30))
select(User).where(User.age.in_([20, 25, 30]))

# 空值判断
select(User).where(User.age.is_(None))
select(User).where(User.age.is_not(None))

# 逻辑组合
select(User).where(and_(User.age > 20, User.age < 30))
select(User).where(or_(User.name == "张三", User.name == "李四"))
select(User).where(not_(User.name.like("%张%")))

# 多个 where 默认是 AND 关系
select(User).where(User.age > 20).where(User.age < 30)

7.3 排序、分页与聚合

python 复制代码
from sqlalchemy import func, desc, asc

# 排序
select(User).order_by(User.age.asc())        # 升序
select(User).order_by(User.age.desc())       # 降序
select(User).order_by(desc(User.age))        # 降序(函数方式)
select(User).order_by(User.age.desc(), User.name.asc())  # 多字段排序

# 分页(MySQL 中生成 LIMIT ... OFFSET ...)
select(User).order_by(User.id).limit(10).offset(20)
# 等价于 SQL: LIMIT 10 OFFSET 20(第3页,每页10条)

# 统计总数
count = session.execute(select(func.count(User.id))).scalar()

# 按分组统计
stmt = (
    select(User.age, func.count(User.id))
    .group_by(User.age)
    .having(func.count(User.id) > 1)
)
results = session.execute(stmt).all()

# 常用聚合函数
func.sum(User.age)       # 求和
func.avg(User.age)       # 平均值
func.max(User.age)       # 最大值
func.min(User.age)       # 最小值
func.count(User.id)      # 计数

MySQL 深分页优化 :当 OFFSET 很大时(如几十万),MySQL 仍需扫描前面的行,性能很差。优化方案:

  1. 使用「上一页最后一条 ID」作为游标:WHERE id > last_id LIMIT 10
  2. 使用延迟关联:先子查询查出 ID,再 JOIN 回原表

7.4 关联查询与 join

python 复制代码
# 内连接
stmt = select(User, Address).join(Address, User.id == Address.user_id)

# 使用 relationship 定义的关系进行 join(更简洁)
stmt = select(User).join(User.addresses)

# 左外连接
stmt = select(User).outerjoin(User.addresses)

# 带条件的 join
stmt = (
    select(User, Address)
    .join(Address, and_(User.id == Address.user_id, Address.is_primary == True))
)

# 查询特定列
stmt = select(User.name, Address.city).join(Address)

第8章 关系映射

8.1 一对多关系

一个用户可以有多篇文章:

python 复制代码
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy import ForeignKey
from typing import List

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

    # 一对多:一个用户有多篇文章
    posts: Mapped[List["Post"]] = relationship(back_populates="author")

class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String(200))
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)

    # 多对一:多篇文章属于一个用户
    author: Mapped["User"] = relationship(back_populates="posts")

使用方式:

python 复制代码
with Session(engine) as session:
    user = session.get(User, 1)
    for post in user.posts:
        print(post.title)

    post = session.get(Post, 1)
    print(post.author.name)

关键点:

  • ForeignKey 定义在多的一方(Post 表中存储 user_id)
  • 外键列建议加 index=True,提升 JOIN 查询性能
  • relationship() 是纯 Python 层面的关联,不创建数据库列
  • back_populates 定义双向关系,两边属性名要对应

8.2 多对一关系

多对一与一对多是同一关系的两个视角。上面的例子中,Post 到 User 就是多对一关系。在多对一方使用标量类型注解(Mapped["User"]),在一方使用列表类型(Mapped[List["Post"]])。

8.3 一对一关系

通过 uselist=False 限制:

python 复制代码
class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(50))

    profile: Mapped["UserProfile"] = relationship(back_populates="user", uselist=False)

class UserProfile(Base):
    __tablename__ = "user_profiles"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    bio: Mapped[str] = mapped_column(String(500))
    user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)

    user: Mapped["User"] = relationship(back_populates="profile")

外键列加 unique=True 约束,确保 MySQL 层面的一对一完整性。

8.4 多对多关系

需要一个中间关联表:

python 复制代码
from sqlalchemy import Table, Column, ForeignKey

# 关联表(纯 Core 风格,不需要模型类)
post_tags = Table(
    "post_tags",
    Base.metadata,
    Column("post_id", ForeignKey("posts.id"), primary_key=True),
    Column("tag_id", ForeignKey("tags.id"), primary_key=True),
)

class Post(Base):
    __tablename__ = "posts"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    title: Mapped[str] = mapped_column(String(200))

    tags: Mapped[List["Tag"]] = relationship(
        secondary=post_tags, back_populates="posts"
    )

class Tag(Base):
    __tablename__ = "tags"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(50))

    posts: Mapped[List["Post"]] = relationship(
        secondary=post_tags, back_populates="tags"
    )

使用方式:

python 复制代码
with Session(engine) as session:
    post = session.get(Post, 1)
    tag = session.get(Tag, 1)

    post.tags.append(tag)
    session.commit()

    post.tags.remove(tag)
    session.commit()

如果关联表需要存储额外字段(如关联时间、排序权重),应使用 Association Object(关联对象)模式。

8.5 级联操作(cascade)

python 复制代码
class User(Base):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(50))

    posts: Mapped[List["Post"]] = relationship(
        back_populates="author",
        cascade="all, delete-orphan",
    )
级联选项 说明
save-update 父对象 add 到 session 时,关联对象也自动 add
merge session.merge() 父对象时级联合并关联对象
expunge 父对象从 session 移除时,关联对象也移除
delete 删除父对象时删除关联对象
delete-orphan 删除与父对象解除关联的对象(孤儿删除)
refresh-expire 父对象刷新/过期时级联到关联对象
all 等同于 save-update, merge, expunge, delete, refresh-expire

常用组合:

  • cascade="all, delete-orphan":完全拥有关系
  • cascade="save-update, merge":默认值

MySQL 外键级联 vs SQLAlchemy 级联

MySQL 本身支持 ON DELETE CASCADE 外键约束,但 InnoDB 的外键级联在某些场景下有性能问题和触发不触发的差异。通常推荐用 SQLAlchemy 层面的 cascade 来管理,更可控。如果使用 MySQL 外键级联,需要在 ForeignKey 中指定 ondelete="CASCADE"

8.6 懒加载策略(lazy)

lazy 选项 说明 适用场景
select(默认) 首次访问关联属性时执行额外查询(懒加载) 通用场景
joined 使用 JOIN 在同一查询中加载关联对象 确定需要关联数据时
subquery 使用子查询加载关联对象 一对多关系中避免 JOIN 膨胀
selectin 使用 IN 查询批量加载关联对象 一对多关系,推荐替代 subquery
raise 访问未加载的关联属性时抛出异常 防止意外的 N+1 查询
noload 不加载关联对象,始终返回空 不需要关联数据时
immediate 父对象加载后立即用额外查询加载关联 需要立即获取但不适合 JOIN 时
python 复制代码
from sqlalchemy.orm import selectinload, joinedload

# 查询时动态指定加载策略
stmt = select(User).options(selectinload(User.posts))
users = session.execute(stmt).scalars().all()
# 此时每个 user.posts 已经加载好,访问不会产生额外查询

第9章 事务与会话管理

9.1 事务的概念

事务(Transaction)是数据库操作的逻辑单元,具有 ACID 特性:

  • Atomicity(原子性):事务中的操作要么全部成功,要么全部失败回滚
  • Consistency(一致性):事务执行前后数据库从一个一致状态变为另一个一致状态
  • Isolation(隔离性):并发事务之间互不干扰
  • Durability(持久性):事务提交后,修改永久保存

MySQL 事务注意 :MySQL 的 InnoDB 引擎支持事务,MyISAM 不支持。确保表使用 InnoDB 引擎(MySQL 5.5+ 默认就是 InnoDB)。可以通过 ENGINE=InnoDB__table_args__ 中指定。

SQLAlchemy 的 Session 默认工作在事务模式下:第一次执行数据库操作时自动开启事务,直到显式调用 commit()rollback()

9.2 commit 与 rollback

python 复制代码
with Session(engine) as session:
    try:
        user = User(name="张三", email="zhangsan@example.com")
        session.add(user)
        session.commit()  # 提交事务
    except Exception as e:
        session.rollback()  # 回滚事务
        print(f"操作失败: {e}")
        raise

注意事项:

  • commit 后 Session 中的对象会过期(expire),下次访问属性时会重新查询 MySQL
  • rollback 后 Session 中所有未提交的对象都会被回滚
  • 发生异常后必须 rollback,否则 Session 会处于异常状态无法继续使用

9.3 上下文管理器 with 语句

python 复制代码
# 基础用法
with Session(engine) as session:
    user = session.get(User, 1)
    user.name = "新名字"
    session.commit()

# 2.0 推荐的事务管理写法
with Session(engine) as session:
    with session.begin():
        # 块内操作退出时自动 commit,异常自动 rollback
        session.add(User(name="张三", email="zhangsan@example.com"))

# 简写
with Session(engine) as session, session.begin():
    session.add(User(name="张三", email="zhangsan@example.com"))

9.4 Session 生命周期最佳实践

场景 推荐方式 说明
脚本 / 一次性任务 with Session(engine) as session 简单直接,自动关闭
Web 应用(FastAPI/Flask) 请求级别 Session,使用 sessionmaker 每个请求一个 Session
桌面应用 用户会话级别 一个用户会话一个 Session
多线程 每个线程独立 Session Session 非线程安全

Web 应用中常用的 sessionmaker 模式

python 复制代码
from sqlalchemy.orm import sessionmaker

# 应用启动时创建一次
SessionLocal = sessionmaker(
    bind=engine,
    autoflush=False,
    autocommit=False,
    expire_on_commit=False,
)

# FastAPI 依赖注入
from fastapi import Depends

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
    return db.get(User, user_id)

(注:文档部分内容可能由 AI 生成)

相关推荐
凤山老林1 小时前
数据库读写分离与动态路由实战:Spring Boot + ShardingSphere-JDBC 生产配置
数据库·spring boot·后端·分库分表·sharding-jdbc
DS随心转插件1 小时前
Grok生成的html怎么导出——AI导出鸭:大模型结构化输出的“最后一公里”工程化解构
前端·人工智能·ai·html·豆包·deepseek·ai导出鸭
GHL2842710901 小时前
Skill学习
学习·ai
TechEdu2026062 小时前
[人工智能]Claude、GPT、Gemini、Copilot与Llama:概念、架构、应用与评估
人工智能·ai·llm
长谷深风1112 小时前
为什么你的 Tool 总被模型选错?
java·大数据·ai·llm·ai agent·工具设计·agent设计
l1258652 小时前
# RAG噪声知识库治理:一致性与可信度的四层防线设计
数据库·人工智能·python·自然语言处理·langchain
clorinda2 小时前
MySQL 入门笔记:DQL 基础查询与常用函数
adb
Patrick在香港2 小时前
Claude Agent 进阶:4 种工具调用编排模式,让 0820 那 13 个 endpoint 并行起来
python·ai·ai编程
MicrosoftReactor3 小时前
技术速递|GitHub Copilot App 入门指南:管理你的工作
ai·github·copilot·agent
码士集团小青3 小时前
细拆DeepSeek Harness设计思路以及未来定位
ai