SQLAlchemy 从零到精通,这一篇就够了
前言
如果你正在学 Python Web 开发,迟早会碰到一个问题:怎么优雅地操作数据库?
裸写 SQL 字符串拼接?辣眼睛,还容易 SQL 注入。手动管理连接开关?忘了 close 就内存泄漏。
SQLAlchemy 就是来解决这些问题的。它是 Python 生态中最成熟的 ORM 框架,Flask、FastAPI、Django 的数据库层都能看到它的影子。这篇文章带你从零开始,一步步搞懂 SQLAlchemy 的核心用法------不堆概念,直接上代码。
适合人群:有 Python 基础、了解 MySQL 基本语法、没用过或刚接触 SQLAlchemy 的同学。
一、SQLAlchemy 是什么
先说人话:SQLAlchemy 是一座桥,一头连着你的 Python 对象,一头连着数据库表。
你操作 Python 对象,它在后台帮你翻译成 SQL 发给数据库;数据库返回结果,它再帮你拼装成 Python 对象。你全程不碰一行 SQL。
| 传统方式 | SQLAlchemy ORM |
|---|---|
| 手写 SQL 字符串 | 操作 Python 对象 |
| 手动管理连接 | 自动连接池管理 |
| 手动映射字段 | 自动对象-表映射 |
| 换数据库要改 SQL | 换数据库只改连接串 |
| SQL 注入风险 | 参数化查询,天然防注入 |
核心优势:
- 跨数据库兼容(MySQL、PostgreSQL、SQLite、Oracle 等)
- 自动防 SQL 注入
- 事务管理完善
- 查询 API 丰富,复杂条件也能链式拼出来
- 社区大,文档全,遇到问题搜得到答案
二、核心概念速览
在看代码之前,先把几个关键名词搞清楚,后面不会晕。
| 概念 | 一句话解释 | 类比 |
|---|---|---|
| Engine | 数据库连接的引擎入口 | 钥匙,开门用的 |
| Connection URL | 告诉引擎连哪个库、怎么连 | 门牌号 |
| Base | 所有模型的父类,继承它才叫"模型" | 户口本 |
| Model | 一个类 = 一张表,属性 = 字段 | 房产证 |
| Column | 定义字段类型和约束 | 房间里的家具 |
| Session | 与数据库对话的会话,所有操作都走它 | 物业窗口 |
踩坑提醒 :很多人把 Session 和 Connection 搞混。Connection 是底层的,Session 是上层封装,日常开发只用 Session,不碰 Connection。
三、环境准备
3.1 安装依赖
bash
# 安装 SQLAlchemy 核心
pip install sqlalchemy
# 安装 MySQL 驱动(二选一)
pip install pymysql # 纯 Python 实现,轻量好用
# pip install mysql-connector-python # MySQL 官方驱动,兼容性也好
验证安装是否成功:
python
import sqlalchemy
import pymysql
print(sqlalchemy.__version__) # 2.0.x
print(pymysql.__version__) # 1.1.x
3.2 准备数据库
在 MySQL 里建一个数据库,后面所有操作都在这个库里进行:
sql
-- 登录 MySQL
mysql -u root -p
-- 创建数据库
CREATE DATABASE IF NOT EXISTS sa_demo
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
-- 确认
SHOW DATABASES;
踩坑提醒 :字符集一定要选
utf8mb4,不要用utf8。MySQL 的utf8是阉割版,存不了 emoji 和部分生僻字。
四、从零搭建:Engine + Base + Model
4.1 统一配置文件
实际项目中,数据库配置应该集中管理,而不是每个文件都写一遍连接串。我们抽一个 db_config.py:
python
# db_config.py
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
# 连接字符串:驱动://用户名:密码@地址:端口/库名?参数
DB_URL = "mysql+pymysql://root:123456@localhost:3306/sa_demo?charset=utf8mb4"
# 创建引擎(echo=True 会打印底层 SQL,调试时开着,上线关掉)
engine = create_engine(DB_URL, echo=True)
# 会话工厂,绑定引擎
SessionLocal = sessionmaker(bind=engine)
# 模型基类,所有 Model 继承它
Base = declarative_base()
提示 :
declarative_base()在 SQLAlchemy 2.0 中推荐从sqlalchemy.orm导入,旧版从sqlalchemy.ext.declarative导入,效果一样,新写法更规范。
4.2 定义模型
我们不用老套的 user 表,换一个**商品表(product)**做示例,更有实际意义:
python
# models.py
from datetime import datetime
from sqlalchemy import Column, Integer, String, Float, DateTime, Text
from db_config import Base
class Product(Base):
"""商品模型,对应数据库中的 product 表"""
__tablename__ = "product"
id = Column(Integer, primary_key=True, autoincrement=True, comment="商品ID")
name = Column(String(100), nullable=False, comment="商品名称")
price = Column(Float, nullable=False, default=0.0, comment="价格")
stock = Column(Integer, nullable=False, default=0, comment="库存")
description = Column(Text, comment="商品描述")
created_at = Column(DateTime, default=datetime.now, comment="创建时间")
def __repr__(self):
return f"<Product id={self.id} name={self.name} price={self.price}>"
class Category(Base):
"""分类模型,对应数据库中的 category 表"""
__tablename__ = "category"
id = Column(Integer, primary_key=True, autoincrement=True, comment="分类ID")
name = Column(String(50), nullable=False, unique=True, comment="分类名称")
def __repr__(self):
return f"<Category id={self.id} name={self.name}>"
常用字段类型速查
| SQLAlchemy 类型 | MySQL 对应 | 用途 |
|---|---|---|
| Integer | INT | 整数 |
| String(n) | VARCHAR(n) | 定长字符串 |
| Text | TEXT | 长文本 |
| Float | FLOAT/DOUBLE | 浮点数 |
| Boolean | TINYINT(1) | 布尔 |
| DateTime | DATETIME | 时间 |
| DECIMAL | DECIMAL | 精确小数(金额) |
常用约束速查
| 参数 | 类型 | 作用 |
|---|---|---|
| primary_key | bool | 主键 |
| autoincrement | bool | 自增 |
| nullable | bool | 是否允许 NULL |
| default | Any | Python 层默认值 |
| unique | bool | 唯一约束 |
| comment | str | 字段注释 |
| index | bool | 索引 |
4.3 创建表
python
# init_db.py
from db_config import engine, Base
from models import Product, Category # 必须导入,否则 Base 不知道有这些表
# 根据所有继承 Base 的模型,在数据库中创建对应表
# 如果表已存在则跳过,不会报错
Base.metadata.create_all(engine)
print("表创建完成!")
执行后,可以在 MySQL 中确认:
sql
SHOW TABLES;
-- 应该能看到 product 和 category 两张表
DESC product;
-- 查看表结构,确认字段类型和约束
踩坑提醒 :
create_all只创建不更新。如果你后来给模型加了字段,create_all不会自动在表里加列。需要用 Alembic(SQLAlchemy 的迁移工具)或手动 ALTER TABLE。
五、Session:操作数据库的唯一入口
所有增删改查都通过 Session 完成。三种使用方式对比:
python
from db_config import SessionLocal
from models import Product
# ===== 方式一:手动开关(不推荐,容易忘关) =====
session = SessionLocal()
try:
product = Product(name="键盘", price=199.0, stock=100)
session.add(product)
session.commit()
finally:
session.close()
# ===== 方式二:with 上下文(推荐日常用) =====
with SessionLocal() as session:
product = Product(name="鼠标", price=59.0, stock=200)
session.add(product)
session.commit()
# 出 with 块自动 close,commit 需要手动调用
# ===== 方式三:函数封装(推荐工程用) =====
def get_session():
"""统一会话获取入口"""
return SessionLocal()
with get_session() as session:
# 业务逻辑写这里
pass
踩坑提醒 :
with SessionLocal() as session在退出时只关闭连接,不会自动 commit 。你在 with 块里改了数据,必须显式调session.commit(),否则改动不生效。这是新手最常踩的坑。
六、CRUD 核心操作
6.1 Create ------ 新增数据
python
from db_config import SessionLocal
from models import Product
with SessionLocal() as session:
# --- 单条新增 ---
p1 = Product(name="机械键盘", price=399.0, stock=50, description="青轴,段落感强")
session.add(p1)
session.commit() # 提交后才真正写入数据库
print(f"新增成功,ID={p1.id}") # commit 后 id 自动回填
# --- 多条新增 ---
p2 = Product(name="显示器", price=1599.0, stock=20)
p3 = Product(name="鼠标垫", price=19.9, stock=500)
session.add_all([p2, p3])
session.commit()
# --- 批量字典新增(高效,适合大量数据) ---
products = [
{"name": "U盘", "price": 39.0, "stock": 300},
{"name": "移动硬盘", "price": 299.0, "stock": 150},
{"name": "读卡器", "price": 15.0, "stock": 800},
]
session.bulk_insert_mappings(Product, products)
session.commit()
| 方法 | 适用场景 | 特点 |
|---|---|---|
add() |
单条 | 自动回填 id |
add_all() |
少量多条 | 对象已创建,逐个 add |
bulk_insert_mappings |
大批量 | 不实例化对象,效率高,但不回填 id |
6.2 Read ------ 查询数据
基础查询
python
from db_config import SessionLocal
from models import Product
with SessionLocal() as session:
# 1. 查全部(SELECT * FROM product)
all_products = session.query(Product).all()
for p in all_products:
print(p)
# 2. 按主键查一条(SELECT * FROM product WHERE id=1)
product = session.get(Product, 1)
print(product)
# 3. 查第一条(SELECT * FROM product LIMIT 1)
first = session.query(Product).first()
print(first)
# 4. 只查指定字段(SELECT name, price FROM product)
rows = session.query(Product.name, Product.price).all()
for row in rows:
print(f"名称:{row.name},价格:{row.price}")
条件查询
python
from sqlalchemy import and_, or_
with SessionLocal() as session:
# 等于:WHERE price = 199.0
r = session.query(Product).filter(Product.price == 199.0).all()
# 不等于:WHERE price != 199.0
r = session.query(Product).filter(Product.price != 199.0).all()
# 大于/小于:WHERE price > 100
r = session.query(Product).filter(Product.price > 100).all()
# 模糊查询:WHERE name LIKE '%盘%'
r = session.query(Product).filter(Product.name.like("%盘%")).all()
# IN 查询:WHERE id IN (1, 2, 3)
r = session.query(Product).filter(Product.id.in_([1, 2, 3])).all()
# 多条件 AND:WHERE price > 100 AND stock > 50
r = session.query(Product).filter(
and_(Product.price > 100, Product.stock > 50)
).all()
# 多条件 OR:WHERE price < 50 OR stock > 500
r = session.query(Product).filter(
or_(Product.price < 50, Product.stock > 500)
).all()
排序、分页、聚合
python
from sqlalchemy import func
with SessionLocal() as session:
# 排序:ORDER BY price DESC
r = session.query(Product).order_by(Product.price.desc()).all()
# 分页:每页10条,取第2页
page, size = 2, 10
r = session.query(Product).order_by(Product.id) \
.limit(size).offset((page - 1) * size).all()
# 统计总数:SELECT COUNT(*) FROM product
total = session.query(Product).count()
# 聚合查询:按价格区间统计商品数量
# SELECT price, COUNT(*) FROM product GROUP BY price
r = session.query(
Product.price,
func.count(Product.id).label("cnt")
).group_by(Product.price).all()
for row in r:
print(f"价格 {row.price}:{row.cnt} 件商品")
# HAVING 筛选分组
r = session.query(
Product.price,
func.count(Product.id).label("cnt")
).group_by(Product.price).having(func.count(Product.id) > 1).all()
分页公式 :
offset = (页码 - 1) × 每页条数。第 1 页 offset=0,第 2 页 offset=10。
6.3 Update ------ 更新数据
python
from db_config import SessionLocal
from models import Product
with SessionLocal() as session:
# --- 单条更新:先查后改 ---
product = session.query(Product).filter(Product.name == "机械键盘").first()
if product:
product.price = 499.0 # 改价格
product.stock = 30 # 改库存
session.commit()
print(f"更新后:{product}")
# --- 批量更新:WHERE name LIKE '%盘%' ---
session.query(Product).filter(
Product.name.like("%盘%")
).update({"price": 29.9}, synchronize_session=False)
session.commit()
踩坑提醒 :批量
update()的synchronize_session=False参数很重要。默认情况下 Session 会在内存中同步对象状态,批量操作时这个行为可能出错或报错,显式关掉更安全。
6.4 Delete ------ 删除数据
python
from db_config import SessionLocal
from models import Product
with SessionLocal() as session:
# --- 单条删除:先查后删 ---
product = session.query(Product).filter(Product.name == "读卡器").first()
if product:
session.delete(product)
session.commit()
# --- 批量删除:WHERE stock = 0 ---
session.query(Product).filter(Product.stock == 0).delete()
session.commit()
踩坑提醒 :删除不可逆,生产环境建议用软删除(加
is_deleted字段标记),而不是真 DELETE。数据恢复的成本远大于多加一个字段的成本。
七、完整项目结构参考
sa_demo/
├── db_config.py # 数据库配置(Engine、Session、Base)
├── models.py # 模型定义
├── init_db.py # 创建表
├── crud/
│ ├── create.py # 新增操作
│ ├── read.py # 查询操作
│ ├── update.py # 更新操作
│ └── delete.py # 删除操作
└── main.py # 入口
把配置和操作分离,比把所有代码塞在一个文件里清晰得多。项目大了以后这个结构会帮你省很多事。
八、总结
回顾一下全文的核心脉络:
| 步骤 | 关键动作 | 核心代码 |
|---|---|---|
| 1. 连数据库 | 创建 Engine | create_engine(url) |
| 2. 定义模型 | 继承 Base,写字段 | class Product(Base) |
| 3. 建表 | 根据模型生成表 | Base.metadata.create_all(engine) |
| 4. 开会话 | 创建 Session | SessionLocal() |
| 5. 增 | add + commit | session.add(obj) |
| 6. 查 | query + filter | session.query(Model).filter(...) |
| 7. 改 | 查出来改属性 | obj.field = new_value |
| 8. 删 | 查出来 delete | session.delete(obj) |
| 9. 提交 | commit 生效 | session.commit() |
| 10. 关会话 | with 自动关 / 手动 close | with SessionLocal() as session |
几个新手必知的原则:
- 改了数据一定要
commit(),否则白改 - 用
with管理 Session 比手动开关节省心智 - 生产环境关掉
echo=True,日志会刷爆 - 不要在 Session 没关闭时做耗时操作,连接池有限
- 复杂查询写不出 ORM 链时,可以混用原生 SQL,不丢人