基于 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__])
注意:
create_all只会创建不存在的表,不会修改已有表的结构。如果需要加列、改类型,必须用 Alembic 迁移- 执行前确保 MySQL 中对应的数据库已创建:
CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;- 生产环境不要用
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
-
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,只想要一个简单数值。
-
.scalars()复数(把所有行的第一个标量变成迭代器)调用后返回
ScalarResult迭代对象,不是直接返回列表,后面再接.all()/.first()/.one()/.one_or_none()。pythonstmt = 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() # ["张三","李四","王五"].scalars().all()获取全部 ORM 对象.scalars().first()获取第一条 ORM 对象.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 中,filter 和 filter_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 仍需扫描前面的行,性能很差。优化方案:
- 使用「上一页最后一条 ID」作为游标:
WHERE id > last_id LIMIT 10- 使用延迟关联:先子查询查出 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),下次访问属性时会重新查询 MySQLrollback后 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 生成)