SQLAlchemy

1. 简介

SQLAlchemy 是一个功能强大的 Python SQL 工具包和对象关系映射(ORM)框架,旨在提供高效、灵活且便于扩展的数据库交互解决方案。它支持多种数据库,并通过其核心(Core)和 ORM 两个层次为开发者提供不同的抽象级别。

为什么选择 SQLAlchemy?

  • 灵活性:允许你选择使用核心的 SQL 构建器,或完全依赖 ORM 来处理数据库操作。
  • 性能:优化的查询生成和连接池管理,适用于高并发和大规模应用。
  • 丰富的功能:支持复杂的查询、关系管理、迁移工具(Alembic)等。
  • 社区支持:活跃的社区和详尽的文档,便于学习和问题解决。

SQLAlchemy 的组成

  1. Core:低级别的数据库接口,提供对数据库的直接操作。
  2. ORM:高级别的对象关系映射,允许你以面向对象的方式操作数据库。

本指南将主要聚焦于 ORM 层,同时涵盖 Core 的一些关键概念。


2. 核心概念

在深入 ORM 之前,理解 SQLAlchemy 的核心组件是至关重要的。这些组件构成了与数据库交互的基础。

2.1 Engine

Engine 是 SQLAlchemy 与数据库之间的接口,负责管理数据库连接和执行 SQL 语句。

from sqlalchemy import create_engine

DATABASE_URL = "sqlite:///./example.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False}  # SQLite特有参数
)
作用
  • 连接管理:管理与数据库的连接,包括连接池的管理。
  • 执行 SQL:将生成的 SQL 语句发送到数据库,并获取结果。
  • 事务管理:处理事务的开始、提交和回滚。
参数详解
  • DATABASE_URL:数据库的连接字符串,格式根据不同的数据库而异。
    • SQLitesqlite:///./example.db
    • PostgreSQLpostgresql://user:password@localhost:5432/mydatabase
    • MySQLmysql+pymysql://user:password@localhost:3306/mydatabase
  • connect_args:传递给数据库驱动的额外连接参数。
    • SQLite 的 check_same_thread=False:允许在多个线程中使用同一个 SQLite 连接。默认情况下,SQLite 不允许跨线程使用连接,这是为了防止潜在的线程安全问题。
示例解释
engine = create_engine(
    "sqlite:///./example.db",
    connect_args={"check_same_thread": False}
)

这行代码创建了一个与 example.db SQLite 数据库的连接,并允许跨线程使用该连接。这在多线程应用中非常有用,例如 Web 服务器处理多个并发请求时。

2.2 MetaData

MetaData 是 SQLAlchemy 用于存储关于数据库结构信息的对象。它包含了所有表、列、约束等的定义。

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
作用
  • 集中管理:所有 ORM 模型共享同一个 MetaData 对象。
  • 生成 SQL:基于 MetaData 自动生成创建表、添加列等 SQL 语句。
  • 反射:可以从已有的数据库结构反射出 SQLAlchemy 的模型定义。

2.3 Table 和 Column

TableColumn 是定义数据库表和其列的基础组件。

from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime

Base = declarative_base()

class ToDo(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True, nullable=False)
    description = Column(String, nullable=True)
    completed = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
Column 属性详解
  • 类型:如 Integer, String, Boolean, DateTime 等,定义列的数据类型。
  • 主键(primary_key=True):指定该列为主键。
  • 索引(index=True):为该列创建索引,提高查询性能。
  • 可空(nullable=False/True):指定该列是否允许为空。
  • 默认值(default):为该列设置默认值。
  • 自动更新时间(onupdate):在每次更新时自动设置列的值。
表属性
  • tablename:指定对应的数据库表名。
  • table_args(可选):用于定义表的额外参数,如约束条件。

3. 对象关系映射(ORM)基础

SQLAlchemy 的 ORM 层允许你将数据库表映射为 Python 类,并以面向对象的方式进行数据库操作。

3.1 声明式基础类(Declarative Base)

声明式基础类是定义 ORM 模型的推荐方式。通过继承 Base,你可以定义与数据库表对应的 Python 类。

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()
优点
  • 简洁:通过继承 Base,模型类的定义更加直观和简洁。
  • 集成性:所有模型类共享同一个 MetaData 对象,便于统一管理。
  • 灵活性:支持多种高级映射功能,如混合属性、联合表等。

3.2 定义模型(Models)

模型类代表数据库中的表,每个实例代表表中的一行。

from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime

class ToDo(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True, nullable=False)
    description = Column(String, nullable=True)
    completed = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    # 如果有外键关系,可以定义 relationship
    # owner_id = Column(Integer, ForeignKey('users.id'))
    # owner = relationship("User", back_populates="todos")
关键点
  • 继承 Base:模型类必须继承自声明式基础类 Base
  • __tablename__:指定对应的数据库表名。
  • 列定义:通过 Column 类定义表中的列及其属性。
  • 关系定义(可选):使用 relationship 定义表之间的关联关系。

3.3 关系(Relationships)

关系(Relationships)是 SQLAlchemy ORM 的核心功能,允许你在不同的数据库表之间建立关联,从而以面向对象的方式进行复杂的数据操作。主要关系类型包括一对多(One-to-Many)和多对多(Many-to-Many)。

1. 一对多关系(One-to-Many)

场景:一个用户可以拥有多个待办事项。

实现步骤

  1. 定义外键
    • 在子表(ToDo)中定义指向父表(User)的外键。
  2. 定义关系
    • 在父表中使用 relationship 定义子对象的集合。
    • 在子表中使用 relationship 定义父对象的单一引用。

示例代码

from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True, nullable=False)

    # 一对多关系:一个用户有多个待办事项
    todos = relationship("ToDo", back_populates="owner", cascade="all, delete-orphan")

class ToDo(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True, nullable=False)
    owner_id = Column(Integer, ForeignKey('users.id'))

    # 反向关系:一个待办事项属于一个用户
    owner = relationship("User", back_populates="todos")

关键参数

  • relationship("ToDo", back_populates="owner")
    • "ToDo":关联的模型类名。
    • back_populates="owner":在 ToDo 模型中对应的关系属性,确保双向同步。
  • ForeignKey('users.id')
    • 定义外键,指向 User 模型的 id 字段。

使用示例

# 创建用户和待办事项
user = User(username="john_doe")
todo1 = ToDo(title="Buy groceries", owner=user)
todo2 = ToDo(title="Write report", owner=user)

session.add(user)
session.commit()

# 查询用户的待办事项
retrieved_user = session.query(User).filter_by(username="john_doe").first()
print(retrieved_user.todos)  # 输出: [, ]

2. 多对多关系(Many-to-Many)

场景:多个学生可以选修多门课程,每门课程也可以被多个学生选修。

实现步骤

  1. 创建关联表(Association Table)
    • 一个独立的表包含两个外键,分别指向两个主表。
  2. 定义关系
    • 在两个主表中使用 relationship 并通过 secondary 参数指定关联表。

示例代码

from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

# 关联表
student_course = Table('student_course', Base.metadata,
    Column('student_id', Integer, ForeignKey('students.id'), primary_key=True),
    Column('course_id', Integer, ForeignKey('courses.id'), primary_key=True)
)

class Student(Base):
    __tablename__ = 'students'

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, nullable=False)

    # 多对多关系
    courses = relationship("Course", secondary=student_course, back_populates="students")

class Course(Base):
    __tablename__ = 'courses'

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, nullable=False)

    # 多对多关系
    students = relationship("Student", secondary=student_course, back_populates="courses")

关键参数

  • secondary=student_course
    • 指定关联表,用于多对多关系的中介。
  • back_populates="students"back_populates="courses"
    • 确保双向关系同步。

使用示例

# 创建学生和课程
student1 = Student(name="Alice")
student2 = Student(name="Bob")
course1 = Course(title="Math")
course2 = Course(title="Science")

# 建立关联
student1.courses.append(course1)
student1.courses.append(course2)
student2.courses.append(course1)

session.add_all([student1, student2, course1, course2])
session.commit()

# 查询课程的学生
retrieved_course = session.query(Course).filter_by(title="Math").first()
print(retrieved_course.students)  # 输出: [, ]

3. 关键参数详解

  • relationship
    • back_populates:定义双向关系,确保两个模型之间的关联同步更新。
    • secondary:在多对多关系中指定关联表。
    • cascade:控制父对象操作对子对象的影响(如 all, delete-orphan 确保删除父对象时自动删除关联的子对象)。
  • ForeignKey
    • 定义外键约束,确保数据的参照完整性。

4. 常见用法与最佳实践

  • 双向关系:始终使用 back_populates 确保关系的双向同步,避免数据不一致。
  • 级联操作:合理配置 cascade 参数,自动管理关联对象的生命周期,减少手动操作的繁琐。
  • 避免 N+1 查询问题:在需要访问关联对象时,使用预加载(如 joinedload)优化查询性能。

预加载示例

from sqlalchemy.orm import joinedload

# 一对多关系预加载
users = session.query(User).options(joinedload(User.todos)).all()

# 多对多关系预加载
students = session.query(Student).options(joinedload(Student.courses)).all()

4. 会话与事务管理

Session 是 SQLAlchemy ORM 与数据库交互的主要接口,负责管理对象的持久化和事务。

4.1 会话(Session)

创建一个会话工厂,用于生成会话对象。

from sqlalchemy.orm import sessionmaker

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
参数详解
  • autocommit=False
    • 默认值False
    • 作用:设置是否自动提交事务。
    • 推荐:保持 False,需要手动调用 session.commit() 来提交事务。这样可以更好地控制事务边界,确保数据一致性。
  • autoflush=False
    • 默认值True
    • 作用:设置是否自动刷新,自动将挂起的更改同步到数据库。
    • 推荐:根据需要设置,通常与事务管理配合使用,避免不必要的刷新操作,提高性能。
  • bind=engine
    • 作用:指定 Session 将使用哪个 Engine 对象进行数据库连接。
    • 效果:所有通过这个 Session 创建的数据库操作都将通过绑定的 Engine 执行。

4.2 事务管理

通过会话对象进行数据库操作,并管理事务的提交与回滚。

def get_db():
    """
    依赖函数,用于获取数据库会话。
    """
    db = SessionLocal()
    try:
        yield db # 将会话对象db提供给依赖他的路由处理函数。当路由处理完成后,生成器会回复执行finally块。
    finally:
        db.close()# 确保即使在请求处理过程中发生异常,也能执行finally块,关闭数据库会话。

get_db函数是一个生成器函数,它通过依赖注入(Dependency Injection)的方式,为FastAPI的路由处理函数提供一个数据库会话(Session)。这个函数确保每个HTTP请求都有一个独立的数据库会话,并在请求完成后自动关闭会话,避免资源泄露和潜在的连接问题。

依赖注入是一种设计模式,允许你再函数或类中生命所需的依赖性,而不需要在函数内部创建这些依赖项。FastAPI强大地支持这种模式,通过Depends函数来实现。

yield

  • 生成器函数:包含yield语句的函数成为生成器函数。调用这样的函数会返回一个生成器对象,而不是立即执行函数体。
  • 暂停于恢复:yield允许函数在返回值的同时暂停其执行状态。下次迭代时,函数会从上次暂停的位置继续执行,而不是从头开始。
  • 多次返回:生成器可以通过多次yield语句返回多个值,每次迭代时提供一个新的值。

示例

def simple_generator():
    yield "Hello"
    yield "World"

gen = simple_generator()
print(next(gen))  # 输出: Hello
print(next(gen))  # 输出: World
使用依赖注入

在 FastAPI 中,通过 Depends 注入会话对象,确保每个请求使用独立的会话。

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from pydantic import BaseModel

app = FastAPI()

# Pydantic模型
class ToDoCreate(BaseModel):
    title: str
    description: Optional[str] = None

class ToDoOut(BaseModel):
    id: int
    title: str
    description: Optional[str] = None
    completed: bool
    created_at: datetime
    updated_at: datetime

    class Config:
        orm_mode = True

# API端点
@app.post('/todo', response_model=ToDoOut)
def create_todo(todo: ToDoCreate, db: Session = Depends(get_db)):
    new_todo = ToDo(title=todo.title, description=todo.description)
    db.add(new_todo)
    db.commit()
    db.refresh(new_todo)
    return new_todo

@app.get('/todo', response_model=List[ToDoOut])
def get_todos(db: Session = Depends(get_db)):
    todos = db.query(ToDo).all()
    return todos
关键步骤
  1. 添加对象
    • db.add(new_todo):将新对象添加到会话。
  2. 提交事务
    • db.commit():提交事务,将更改保存到数据库。
  3. 刷新对象
    • db.refresh(new_todo):刷新对象状态,从数据库获取最新数据。
  4. 关闭会话
    • 会话在 finally 块中自动关闭,确保数据库连接被释放。

事务边界

  • 开始事务:当会话首次执行写操作时自动开始事务。
  • 提交事务:通过 session.commit() 提交事务。
  • 回滚事务:通过 session.rollback() 回滚事务,以恢复到事务开始前的状态。

5. 查询(Querying)

使用 SQLAlchemy ORM 进行数据库查询是其核心功能之一。理解如何构建和优化查询是高效使用 SQLAlchemy 的关键。

5.1 基本查询

查询所有记录
todos = db.query(ToDo).all()
  • 作用:获取 ToDo 表中的所有记录。
  • 返回值:一个包含所有 ToDo 实例的列表。
查询特定记录
todo = db.query(ToDo).filter(ToDo.id == todo_id).first()
  • 作用:根据 id 获取特定的待办事项。
  • 返回值:匹配的 ToDo 实例,如果不存在则返回 None

5.2 过滤和条件

使用过滤条件限定查询结果
completed_todos = db.query(ToDo).filter(ToDo.completed == True).all()
  • 作用:获取所有已完成的待办事项。
  • 返回值:一个包含所有 completed=TrueToDo 实例的列表。
结合多个条件
from sqlalchemy import and_

filtered_todos = db.query(ToDo).filter(
    and_(
        ToDo.completed == False,
        ToDo.title.contains("urgent")
    )
).all()
  • 作用:获取所有未完成且标题包含 “urgent” 的待办事项。
  • 返回值:一个包含满足条件的 ToDo 实例的列表。

5.3 联接(Joins)

在涉及多个表的查询中,联接(Join)是必不可少的。SQLAlchemy 支持多种联接方式,如内联接、左外联接等。

内联接(Inner Join)
todos_with_users = db.query(ToDo, User).join(User).filter(User.username == 'john').all()
  • 作用:获取所有属于用户名为 “john” 的用户的待办事项。
  • 返回值:一个包含 ToDoUser 实例的元组列表。
左外联接(Left Outer Join)
from sqlalchemy.orm import aliased

user_alias = aliased(User)
todos_with_optional_users = db.query(ToDo, user_alias).outerjoin(user_alias).all()
  • 作用:获取所有待办事项及其对应的用户信息,即使某些待办事项没有关联的用户。
  • 返回值:一个包含 ToDoUser(可能为 None)实例的元组列表。

5.4 聚合函数

聚合函数用于对查询结果进行统计,如计数、求和、平均值等。

计数(Count)
from sqlalchemy import func

todo_count = db.query(func.count(ToDo.id)).scalar()
  • 作用:计算 ToDo 表中的记录数量。
  • 返回值:整数,表示记录总数。
求和(Sum)

假设 ToDo 表中有一个 priority 列:

total_priority = db.query(func.sum(ToDo.priority)).scalar()
  • 作用:计算所有待办事项的 priority 总和。
  • 返回值:整数或 None,如果没有记录则返回 None
分组(Group By)
from sqlalchemy import func

todos_grouped_by_status = db.query(ToDo.completed, func.count(ToDo.id)).group_by(ToDo.completed).all()
  • 作用:按完成状态分组,统计每组的待办事项数量。
  • 返回值:一个包含 completed 状态和对应计数的元组列表。

6. 高级功能

6.1 预加载与懒加载(Eager and Lazy Loading)

懒加载(Lazy Loading)

懒加载是 ORM 的默认行为,关联对象在访问时才加载。

todos = db.query(ToDo).all()
for todo in todos:
    print(todo.owner.username)  # 触发懒加载
  • 优点:减少初始查询的复杂性和数据量。
  • 缺点:当遍历大量关联对象时,可能导致多次数据库查询(N+1 查询问题),影响性能。
预加载(Eager Loading)

预加载在主查询时加载关联对象,减少查询次数。

from sqlalchemy.orm import joinedload

todos = db.query(ToDo).options(joinedload(ToDo.owner)).all()
for todo in todos:
    print(todo.owner.username)  # 已经加载,不会触发额外查询
  • 优点:减少数据库查询次数,避免 N+1 查询问题。
  • 缺点:可能会加载不必要的数据,增加内存使用。
使用 subqueryload

另一种预加载方式,适用于复杂的嵌套关联。

from sqlalchemy.orm import subqueryload

todos = db.query(ToDo).options(subqueryload(ToDo.owner)).all()
for todo in todos:
    print(todo.owner.username)

6.2 混合属性(Hybrid Properties)

混合属性允许定义既可以作为对象属性访问,又可以用于查询表达式的属性。

from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy import func

class ToDo(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, index=True, nullable=False)
    completed = Column(Boolean, default=False)

    @hybrid_property
    def title_length(self):
        return len(self.title)

    @title_length.expression
    def title_length(cls):
        return func.length(cls.title)
使用混合属性
# 访问属性
todo = db.query(ToDo).first()
print(todo.title_length)

# 用于查询
long_titles = db.query(ToDo).filter(ToDo.title_length > 10).all()

6.3 映射属性(Mapped Attributes)

通过使用不同的映射选项,定制对象属性与数据库列之间的映射关系。

列别名

为列设置别名,便于使用不同的名称访问数据库列。

class ToDo(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True)
    title = Column("task_title", String)
联合属性

组合多个列为一个属性。

from sqlalchemy.ext.hybrid import hybrid_property

class ToDo(Base):
    __tablename__ = 'todo'

    id = Column(Integer, primary_key=True)
    first_name = Column(String)
    last_name = Column(String)

    @hybrid_property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

7. 数据库迁移与版本控制

在开发过程中,数据库模式常常需要变更。使用迁移工具可以有效地管理这些变更,确保数据库结构与代码模型同步。

7.1 Alembic 简介

Alembic 是 SQLAlchemy 的官方数据库迁移工具,允许你轻松地管理数据库模式的变更。它支持生成和应用迁移脚本,确保数据库结构的演进与代码同步。

主要功能
  • 生成迁移脚本:自动检测模型变更并生成对应的迁移脚本。
  • 应用迁移:将迁移脚本应用到数据库,更新数据库结构。
  • 版本控制:跟踪数据库迁移的历史,支持回滚操作。

7.2 创建迁移脚本

初始化 Alembic

在项目根目录下初始化 Alembic 配置:

alembic init alembic

这将创建一个 alembic 目录,包含迁移脚本和配置文件。

配置 alembic.ini

编辑 alembic.ini 文件,设置数据库连接 URL:

# alembic.ini

[alembic]
script_location = alembic
sqlalchemy.url = sqlite:///./example.db  # 根据实际数据库URL修改
编辑 env.py

确保 Alembic 能找到你的模型定义。编辑 alembic/env.py,导入你的 Base

# alembic/env.py

from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context

import sys
import os
sys.path.append(os.path.abspath('.'))

from your_module import Base  # 替换为实际的模块路径

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
fileConfig(config.config_file_name)

# add your model's MetaData object here
target_metadata = Base.metadata

def run_migrations_offline():
    """Run migrations in 'offline' mode."""
    url = config.get_main_option("sqlalchemy.url")
    context.configure(
        url=url,
        target_metadata=target_metadata,
        literal_binds=True,
        dialect_opts={"paramstyle": "named"},
    )

    with context.begin_transaction():
        context.run_migrations()

def run_migrations_online():
    """Run migrations in 'online' mode."""
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix='sqlalchemy.',
        poolclass=pool.NullPool
    )

    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata
        )

        with context.begin_transaction():
            context.run_migrations()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()
生成迁移脚本

在每次模型变更后,生成迁移脚本:

alembic revision --autogenerate -m "描述迁移内容"
  • 参数:
    • --autogenerate:自动检测模型变更并生成迁移脚本。
    • -m "描述":为迁移脚本添加描述信息。
示例

假设你新增了 User 模型:

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True, nullable=False)
    email = Column(String, unique=True, index=True, nullable=False)

生成迁移脚本:

alembic revision --autogenerate -m "新增用户模型"

生成的迁移脚本将包含创建 users 表的 SQL 语句。

7.3 应用迁移

将迁移脚本应用到数据库:

alembic upgrade head
  • 作用:将所有未应用的迁移脚本执行到数据库,更新数据库结构至最新版本。
回滚迁移

如果需要回滚到上一个迁移版本:

alembic downgrade -1
  • 作用:撤销最近的一次迁移,恢复数据库结构到上一个版本。

你可能感兴趣的:(git,学习,elasticsearch)