Python SQLAlchemy ORM 从0到1精通实战手册(基础到复杂高阶)

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%业务场景:

  1. 查询指定租户下所有启用状态的员工列表

  2. 查询某个角色绑定的所有权限名称、权限编码

  3. 查询所有已过期的员工角色分配记录

  4. 四表联查:员工姓名、角色名称、权限编码、租户名称

  5. 分组统计:每个角色对应的员工数量,按数量倒序排序

  6. 筛选:查询拥有「user:list」权限的所有员工

十二、终极通用解题模板(所有复杂ORM通用)

遇到任何复杂查询,按以下6步执行,零出错:

  1. 定字段:明确最终需要查询的字段/模型

  2. 定源头 :确定业务起始表,用 select_from() 指定起点

  3. 理链路:逐行写出所有表关联关系(A.外键=B.主键)

  4. 分条件:将所有筛选条件归类到对应的数据表

  5. 选连接:根据业务选择 INNER JOIN / LEFT JOIN,按需去重、排序、分页

  6. 调试验证:打印原生SQL,在数据库执行核对结果

(Note: May contain AI-generated content.)

相关推荐
灯澜忆梦1 小时前
【MySQL17】进阶篇 | InnoDB引擎
数据库·mysql
北斗落凡尘1 小时前
LangGraph 入门实战(11)--输出模式
后端·python·langchain
anxiao_m1 小时前
2026制造业云桌面选型攻略,不同生产场景适配方案汇总
大数据·网络·数据库
许彰午2 小时前
# 数据库配拦截器,不用改BPMN
数据库
雪碧聊技术2 小时前
安装Python(保姆级教程)
python·版本更新·python下载
lv__pf2 小时前
redis【msb 2026金三银四redis上】
数据库·redis·缓存
布莱克6053 小时前
理解数据库聚簇索引:原理、优势与适用场景
数据库·mysql
Nturmoils4 小时前
只面对一张表:KingbaseES 超表如何简化海量时序数据管理
数据库