Python SQLAlchemy ORM 从0到1精通实战手册(基础到复杂高阶)
前言
很多开发者写ORM最大的痛点:单表查询没问题,多表JOIN、子查询、复杂筛选、嵌套逻辑直接混乱,分不清表关联方向、看不懂生成的SQL、不知道什么时候用JOIN/子查询/EXISTS。
本文档为从零到精通 体系化教程,基于 SQLAlchemy 2.0 新版语法,摒弃老旧 query 松散写法,统一标准化编码风格,包含:环境搭建、模型设计、事务封装、基础CRUD、复杂筛选、多表联查、子查询、聚合分组、RBAC权限实战、多租户场景、高阶复杂查询、全网最全避坑指南,所有案例均可直接复制运行。
适合零基础入门、进阶突破、解决复杂业务SQL转ORM难题的开发者。
一、环境搭建与核心概念认知
1.1 安装依赖
bash
pip install sqlalchemy pymysql python-dotenv
1.2 核心核心名词(必记)
吃透这5个概念,ORM就懂了一半:
-
DeclarativeBase:所有数据库模型的基类,统一数据表映射规范
-
Model模型:Python类 = 数据库表,类属性 = 数据表字段
-
Session会话:数据库连接上下文,所有增删改查的执行载体,自带事务特性
-
relationship:ORM虚拟关联(纯Python层级),不生成数据库物理外键,用于快速关联查询对象
-
ForeignKey:数据库物理外键,约束数据表关联关系
1.3 核心编码准则(终身受用)
-
复杂查询优先:确定查询源头 → 梳理关联链条 → 分配筛选条件 → 执行查询
-
多表联查晕方向时,永远用
select_from(源头表)正向关联 -
条件混乱时,按数据表拆分条件,不堆在一起
-
多角色、多关联场景,必须加
distinct()去重
二、项目基础架构封装(企业级标准)
搭建可复用的基础架构,包含数据库连接、事务上下文、全局基类,所有后续案例均基于此架构。
2.1 数据库基础配置(db_base.py)
python
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker, scoped_session
from datetime import datetime
# 数据库连接地址(自行修改账号密码库名)
DB_URL = "mysql+pymysql://root:123456@127.0.0.1:3306/orm_demo?charset=utf8mb4"
# 创建引擎,开启连接池,调试可开启echo=True打印SQL
engine = create_engine(
DB_URL,
echo=False,
pool_recycle=3600,
pool_pre_ping=True
)
# 全局模型基类
class Base(DeclarativeBase):
"""所有数据表模型统一继承的基类"""
pass
# 会话工厂、全局会话对象
SessionFactory = sessionmaker(bind=engine, autocommit=False, autoflush=False)
db_session = scoped_session(SessionFactory)
2.2 事务上下文工具(db_utils.py)
企业级必备,自动提交、异常回滚、关闭会话,杜绝事务混乱
python
from contextlib import contextmanager
from db_base import db_session
@contextmanager
def session_scope():
"""数据库事务上下文管理器"""
session = db_session()
try:
yield session
session.commit()
except Exception as e:
session.rollback()
raise e
finally:
session.close()
三、全套业务模型定义(RBAC+多租户)
定义通用业务模型(租户、员工、角色、权限、关联中间表),后续所有基础、复杂案例均复用这套模型,贴合真实业务场景。
python
# models.py
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from db_base import Base
from datetime import datetime
# 租户表(多租户隔离核心)
class Tenant(Base):
__tablename__ = "sys_tenant"
id = Column(Integer, primary_key=True, autoincrement=True, comment="租户ID")
name = Column(String(100), nullable=False, comment="租户名称")
create_time = Column(DateTime, default=datetime.now, comment="创建时间")
# 员工表
class Employee(Base):
__tablename__ = "sys_employee"
id = Column(Integer, primary_key=True, autoincrement=True, comment="员工ID")
tenant_id = Column(Integer, ForeignKey("sys_tenant.id"), comment="租户ID")
username = Column(String(50), nullable=False, comment="员工账号")
real_name = Column(String(50), comment="真实姓名")
is_active = Column(Boolean, default=True, comment="是否启用")
create_time = Column(DateTime, default=datetime.now, comment="创建时间")
# 角色表
class Role(Base):
__tablename__ = "sys_role"
id = Column(Integer, primary_key=True, autoincrement=True, comment="角色ID")
tenant_id = Column(Integer, ForeignKey("sys_tenant.id"), comment="租户ID")
role_name = Column(String(50), nullable=False, comment="角色名称")
remark = Column(String(200), comment="角色备注")
# 权限表
class Permission(Base):
__tablename__ = "sys_permission"
id = Column(Integer, primary_key=True, autoincrement=True, comment="权限ID")
code = Column(String(100), nullable=False, comment="权限编码(核心)例:user:list")
name = Column(String(100), comment="权限名称")
enabled = Column(Boolean, default=True, comment="是否启用")
# 员工-角色中间表(多对多)
class EmployeeRole(Base):
__tablename__ = "sys_employee_role"
id = Column(Integer, primary_key=True, autoincrement=True)
tenant_id = Column(Integer, nullable=False, comment="租户ID")
employee_id = Column(Integer, ForeignKey("sys_employee.id"), comment="员工ID")
role_id = Column(Integer, ForeignKey("sys_role.id"), comment="角色ID")
expires_at = Column(DateTime, nullable=True, comment="角色过期时间,null=永不过期")
# 角色-权限中间表(多对多)
class RolePermission(Base):
__tablename__ = "sys_role_permission"
id = Column(Integer, primary_key=True, autoincrement=True)
role_id = Column(Integer, ForeignKey("sys_role.id"), comment="角色ID")
permission_id = Column(Integer, ForeignKey("sys_permission.id"), comment="权限ID")
# 批量创建数据表(首次执行)
if __name__ == "__main__":
from db_base import engine
Base.metadata.create_all(bind=engine)
print("所有数据表创建成功!")
四、单表全套CRUD基础案例(入门必练)
覆盖新增、查询、更新、删除、单表复杂筛选,所有写法为企业标准化2.0语法。
4.1 新增数据
python
from models import Employee
from db_utils import session_scope
# 新增单条员工
def create_employee():
with session_scope() as db:
emp = Employee(
tenant_id=1,
username="emp001",
real_name="张三",
is_active=True
)
db.add(emp)
# 批量新增
def batch_create_employee():
with session_scope() as db:
emp_list = [
Employee(tenant_id=1, username="emp002", real_name="李四"),
Employee(tenant_id=1, username="emp003", real_name="王五")
]
db.bulk_save_objects(emp_list)
4.2 查询数据(基础+复杂筛选)
python
from sqlalchemy import select, or_, and_
from models import Employee
from db_utils import session_scope
from datetime import datetime
# 1. 根据ID查单条
def get_emp_by_id(emp_id: int):
with session_scope() as db:
return db.get(Employee, emp_id)
# 2. 基础条件查询、模糊查询、范围查询、OR/AND、NULL判断、排序分页
def single_table_complex_query():
with session_scope() as db:
# 等值查询
stmt1 = select(Employee).where(Employee.tenant_id == 1)
# 模糊查询
stmt2 = select(Employee).where(Employee.real_name.like("%张%"))
# 范围查询
stmt3 = select(Employee).where(Employee.id > 1)
# OR 多条件或
stmt4 = select(Employee).where(or_(Employee.id == 1, Employee.id == 2))
# AND 多条件且(逗号默认AND)
stmt5 = select(Employee).where(Employee.tenant_id == 1, Employee.is_active == True)
# NULL / 非NULL判断(重点:禁止用==None)
from models import EmployeeRole
stmt6 = select(EmployeeRole).where(EmployeeRole.expires_at.is_(None))
stmt7 = select(EmployeeRole).where(EmployeeRole.expires_at.is_not(None))
# IN 查询
stmt8 = select(Employee).where(Employee.id.in_([1,2,3]))
# 排序+分页
stmt9 = select(Employee).order_by(Employee.id.desc()).offset(0).limit(10)
# 执行查询
res = db.execute(stmt9).scalars().all()
print(res)
4.3 更新、删除数据
python
from models import Employee
from db_utils import session_scope
# 更新单条
def update_employee(emp_id: int):
with session_scope() as db:
emp = db.get(Employee, emp_id)
if emp:
emp.real_name = "张三-已修改"
emp.is_active = True
# 删除单条
def delete_employee(emp_id: int):
with session_scope() as db:
emp = db.get(Employee, emp_id)
if emp:
db.delete(emp)
五、多表联查核心突破(解决90%晕代码问题)
核心口诀 :联表晕方向,就用 select_from(业务源头表),正向顺着链条JOIN,逻辑100%清晰。
两种联表类型:join() = 内连接INNER JOIN(必须匹配) 、outerjoin() = 左连接LEFT JOIN(左表数据全保留)
5.1 两表联查(员工+员工角色)
python
from sqlalchemy import select
from models import Employee, EmployeeRole
from db_utils import session_scope
# 正向联表:从员工角色表出发,关联员工表
def join_two_table():
with session_scope() as db:
stmt = select(Employee.username, EmployeeRole.role_id)\
.select_from(EmployeeRole)\
.join(Employee, EmployeeRole.employee_id == Employee.id)\
.where(EmployeeRole.tenant_id == 1)
rows = db.execute(stmt).all()
for username, role_id in rows:
print(f"员工:{username},角色ID:{role_id}")
5.2 三表联查(员工-角色-权限)
python
from sqlalchemy import select
from models import Employee, EmployeeRole, Role, RolePermission, Permission
from db_utils import session_scope
# 完整RBAC三表链式联查
def join_rbac_three_table():
with session_scope() as db:
stmt = select(
Employee.real_name,
Role.role_name,
Permission.code
).select_from(EmployeeRole)\
.join(Employee, EmployeeRole.employee_id == Employee.id)\
.join(Role, EmployeeRole.role_id == Role.id)\
.join(RolePermission, Role.id == RolePermission.role_id)\
.join(Permission, RolePermission.permission_id == Permission.id)\
.where(EmployeeRole.tenant_id == 1)
rows = db.execute(stmt).all()
for name, role, perm in rows:
print(f"员工{name} | 角色{role} | 权限{perm}")
5.3 左连接LEFT JOIN案例
场景:查询所有员工,包含未分配任何角色的员工(内连接会丢失无角色员工)
python
from sqlalchemy import select
from models import Employee, EmployeeRole
from db_utils import session_scope
def left_join_demo():
with session_scope() as db:
stmt = select(Employee.real_name, EmployeeRole.role_id)\
.outerjoin(EmployeeRole, Employee.id == EmployeeRole.employee_id)\
.where(Employee.tenant_id == 1)
rows = db.execute(stmt).all()
print(rows)
六、子查询高阶实战(复杂逻辑拆分神器)
适用场景:联表链条过长、条件复杂、需要分层筛选的业务,新手首选子查询,逻辑零混乱。
6.1 基础子查询(IN子查询)
业务:先查员工有效角色ID,再通过角色ID查询对应权限(复刻你之前的核心权限逻辑)
python
from sqlalchemy import select, subquery, or_
from models import EmployeeRole, RolePermission, Permission
from db_utils import session_scope
from datetime import datetime
def get_employee_perm_by_subquery(emp_id: int, tenant_id: int):
with session_scope() as db:
# 子查询1:筛选员工所有未过期、有效的角色ID
valid_role_sub = select(EmployeeRole.role_id).where(
EmployeeRole.employee_id == emp_id,
EmployeeRole.tenant_id == tenant_id,
or_(
EmployeeRole.expires_at.is_(None),
EmployeeRole.expires_at > datetime.now()
)
).subquery()
# 主查询:通过角色ID查询权限编码
stmt = select(Permission.code).distinct()\
.join(RolePermission, RolePermission.permission_id == Permission.id)\
.where(RolePermission.role_id.in_(valid_role_sub), Permission.enabled == True)
rows = db.execute(stmt).all()
return {row[0] for row in rows}
6.2 EXISTS存在性子查询
场景:查询所有已经分配过角色的员工
python
from sqlalchemy import select, exists
from models import Employee, EmployeeRole
from db_utils import session_scope
def exists_query_demo():
with session_scope() as db:
stmt = select(Employee.real_name).where(
exists().where(EmployeeRole.employee_id == Employee.id)
)
return db.execute(stmt).scalars().all()
七、分组聚合复杂查询(GROUP BY / HAVING)
适用于统计类业务:数量统计、分组求和、筛选分组结果
python
from sqlalchemy import select, func
from models import Employee
from db_utils import session_scope
# 统计每个租户的员工数量,只展示员工数≥1的租户
def group_aggregate_demo():
with session_scope() as db:
stmt = select(
Employee.tenant_id,
func.count(Employee.id).label("emp_count")
).group_by(Employee.tenant_id)\
.having(func.count(Employee.id) >= 1)
rows = db.execute(stmt).all()
for item in rows:
print(f"租户ID:{item.tenant_id},员工数量:{item.emp_count}")
八、核心实战:员工权限查询三种写法对比(从易到难)
针对你之前困惑的权限查询逻辑,整理三种写法,新手优先子查询,进阶用正向JOIN
写法1:子查询拆分(最易懂、零出错)
即本文6.1案例,分层执行,逻辑清晰,适合复杂业务
写法2:正向JOIN(推荐生产使用,性能最优)
python
from sqlalchemy import or_
from models import Permission, RolePermission, EmployeeRole
from db_utils import session_scope
from datetime import datetime
def load_perms_forward(emp_id: int, tenant_id: int):
with session_scope() as db:
rows = db.query(Permission.code)\
.select_from(EmployeeRole)\
.join(RolePermission, EmployeeRole.role_id == RolePermission.role_id)\
.join(Permission, RolePermission.permission_id == Permission.id)\
.filter(
EmployeeRole.employee_id == emp_id,
EmployeeRole.tenant_id == tenant_id,
Permission.enabled == True,
or_(EmployeeRole.expires_at.is_(None), EmployeeRole.expires_at > datetime.now())
).distinct().all()
return {r[0] for r in rows}
写法3:反向JOIN(原生写法,最难懂,不推荐)
python
from sqlalchemy import or_
from models import Permission, RolePermission, EmployeeRole
from db_utils import session_scope
from datetime import datetime
def load_perms_backward(emp_id: int, tenant_id: int):
with session_scope() as db:
rows = db.query(Permission.code)\
.join(RolePermission, RolePermission.permission_id == Permission.id)\
.join(EmployeeRole, EmployeeRole.role_id == RolePermission.role_id)\
.filter(
EmployeeRole.employee_id == emp_id,
EmployeeRole.tenant_id == tenant_id,
Permission.enabled == True,
or_(EmployeeRole.expires_at.is_(None), EmployeeRole.expires_at > datetime.now())
).distinct().all()
return {r[0] for r in rows}
九、ORM调试神器:打印真实执行SQL
看不懂查询结果、排查错误时,直接打印ORM生成的原生SQL,可复制到数据库直接执行
python
from sqlalchemy.dialects import mysql
def print_raw_sql(stmt, engine):
"""打印ORM生成的完整可执行SQL"""
sql = stmt.compile(
engine,
dialect=mysql.dialect(),
compile_kwargs={"literal_binds": True}
)
print("原生SQL:", sql)
# 使用示例
if __name__ == "__main__":
from sqlalchemy import select
from models import Employee
from db_base import engine
stmt = select(Employee).where(Employee.tenant_id == 1)
print_raw_sql(stmt, engine)
十、ORM高频避坑指南(新手必看)
-
NULL判断坑 :禁止使用
== None,必须使用字段.is_(None)/字段.is_not(None) -
联表方向坑 :长链路联表优先
select_from(源头表)正向关联,杜绝反向混乱 -
数据重复坑 :多角色、多关联查询权限、列表数据,必须加
distinct()去重 -
多租户坑 :所有业务查询必须携带
tenant_id,防止跨租户数据泄露 -
时间坑 :默认时间必须写
default=datetime.now,禁止写datetime.now()(全局固定时间) -
内连接坑:INNER JOIN 任意关联无数据,整体结果为空,无兜底数据
-
事务坑 :所有数据库操作必须包裹
session_scope,防止事务未提交、连接泄露
十一、进阶练习(做完彻底精通ORM)
基于本文RBAC模型,独立编写ORM代码,完成即可熟练应对99%业务场景:
-
查询指定租户下所有启用状态的员工列表
-
查询某个角色绑定的所有权限名称、权限编码
-
查询所有已过期的员工角色分配记录
-
四表联查:员工姓名、角色名称、权限编码、租户名称
-
分组统计:每个角色对应的员工数量,按数量倒序排序
-
筛选:查询拥有「user:list」权限的所有员工
十二、终极通用解题模板(所有复杂ORM通用)
遇到任何复杂查询,按以下6步执行,零出错:
-
定字段:明确最终需要查询的字段/模型
-
定源头 :确定业务起始表,用
select_from()指定起点 -
理链路:逐行写出所有表关联关系(A.外键=B.主键)
-
分条件:将所有筛选条件归类到对应的数据表
-
选连接:根据业务选择 INNER JOIN / LEFT JOIN,按需去重、排序、分页
-
调试验证:打印原生SQL,在数据库执行核对结果
(Note: May contain AI-generated content.)