Python 现代开发学习笔记

(八)Engine、Session 与数据库文件

1. 先把三个位置分开

数据库文件保存长期记录;Engine 管理数据库连接及连接池;Session 跟踪本次工作中的 ORM 对象和事务。一个应用可以复用 Engine,每条请求取得自己的 Session。

配置先把数据库位置单独保存:

# 项目文件:api_project/src/bookapi/config.py
import os
from dataclasses import dataclass, field
from pathlib import Path


@dataclass(frozen=True)
class Settings:
    # default_factory 在创建配置对象时读取环境变量。
    database_path: Path = field(
        default_factory=lambda: Path(os.getenv("BOOKS_DB", "data/api.sqlite3"))
    )
    token_seconds: int = 3600

frozen=True 的数据类让配置对象创建后保持固定。os.getenv 读取环境变量,未设置 BOOKS_DB 时使用项目下的 data/api.sqlite3。default_factory 在创建 Settings() 时执行,因此测试可以直接传临时路径,启动服务也可以读取这次终端设置的路径。

# 项目文件:api_project/src/bookapi/database.py
from sqlalchemy import URL, create_engine, event
from sqlalchemy.orm import Session, sessionmaker

from .config import Settings
from .models import Base


def open_database(settings: Settings):
    path = settings.database_path.resolve()
    path.parent.mkdir(parents=True, exist_ok=True)
    # URL.create 正确保存 Windows 路径;Engine 管理连接池。
    engine = create_engine(
        URL.create("sqlite", database=str(path)),
        connect_args={"check_same_thread": False, "timeout": 10},
    )

    @event.listens_for(engine, "connect")
    def enable_foreign_keys(connection, _record):
        # SQLite 的外键开关按连接设置,交回连接池后仍保留。
        cursor = connection.cursor()
        cursor.execute("PRAGMA foreign_keys=ON")
        cursor.close()

    Base.metadata.create_all(engine)
    factory = sessionmaker(engine, class_=Session, expire_on_commit=False)
    return engine, factory

URL.create 组织 SQLite 地址,保留 Windows 盘符和路径。create_engine 建立引擎;真正需要数据库通信时再取得连接。sessionmaker 是一个工厂,调用 factory() 会新建 Session,调用 factory.begin() 会同时管理会话和事务。

这里设置 check_same_thread=False,因为同步请求的准备与执行可能由不同工作线程完成。每条请求仍有自己的 Session,查询依次执行;这个开关不会把 Session 变成可供多个线程同时操作的对象。

event.listens_for(engine, "connect") 在建立每条 DBAPI 连接时调用函数,打开 SQLite 外键检查。Base.metadata.create_all(engine) 读取已登记的表结构,创建缺少的表。下一次启动继续使用这些表;增加字段时,需要第五章介绍的迁移步骤,create_all 不会自动重写现有表。

expire_on_commit=False 使已经加载的普通字段在提交后仍可读取;这不会关闭事务,也不会自动加载所有关系。engine.dispose() 用来释放连接池,稍后放进应用结束时的收尾代码。

应用启动创建一份引擎和会话工厂,两个请求分别创建 Session,按需借用连接,结束后归还,文件继续保存记录

笔记正文与全部题解 · Ctrl / ⌘ K