当前位置:首页>python>Python教程 - 数据库进阶:SQLAlchemy ORM、Alembic 迁移与 PostgreSQL 实战

Python教程 - 数据库进阶:SQLAlchemy ORM、Alembic 迁移与 PostgreSQL 实战

  • 2026-08-18 23:10:36
Python教程 - 数据库进阶:SQLAlchemy ORM、Alembic 迁移与 PostgreSQL 实战

欢迎来到第十二期!

在第一期的基础教程中,我们用一个简单的字典来存学生成绩;在第七期,我们用 SQLite 给 FastAPI 项目搭了个持久化后端;在第十期,FinanceFlow 项目的数据库虽然能跑,但你仔细一看——全是手动拼 SQL,数据库切换还得改一堆代码……

**这就像盖房子只打了地基,还没砌墙呢。**

今天这一期,我们要完成 Python 后端开发中最核心的基础设施之一——**数据库层的工程化**

学完这一期,你将能够:

- 用 **SQLAlchemy ORM** 替代手写 SQL,让 Python 对象直接映射到数据库表

- 用 **Alembic** 管理数据库版本迁移,告别"删库重建"的野蛮操作

- 用 **PostgreSQL** 替代 SQLite,让你的应用能扛住真实流量

- 掌握 **多表关联、外键约束、索引优化** 等数据库进阶技能

- 把 FinanceFlow 的数据层从玩具升级为生产级架构

>**前置知识**:需要熟悉本系列前 11 期的内容,特别是 Episode 07(FastAPI)和 Episode 10(FinanceFlow 项目)。

---

## 12.1 为什么需要 ORM?

### 12.1.1 手写 SQL 的痛苦

先看一段典型的「不写 ORM」的代码。假设我们要做一个用户注册功能:

```python

# ❌ 糟糕的做法:到处散落着字符串拼接的 SQL

import sqlite3

defcreate_user(usernameemailpassword_hash):

    conn = sqlite3.connect("app.db")

    cursor = conn.cursor()

    cursor.execute(

f"INSERT INTO users (username, email, password_hash) "

f"VALUES ('{username}', '{email}', '{password_hash}')"

    )

    conn.commit()

    conn.close()

defget_user_by_username(username):

    conn = sqlite3.connect("app.db")

    cursor = conn.cursor()

# ⚠️ SQL 注入攻击风险!用户名里带单引号就挂了

    cursor.execute(f"SELECT * FROM users WHERE username = '{username}'")

    user = cursor.fetchone()

    conn.close()

return user

```

这种写法有几个致命问题:

1.**SQL 注入**:上面的 `f-string` 拼接是经典漏洞。`username = "admin' OR '1'='1"` 就能直接绕过登录。

2.**代码混乱**:SQL 散落在各个函数里,改一个表结构要找到所有引用它的手动查询。

3.**无法复用连接**:每个函数都新建连接,没有连接池概念。

4.**数据库锁定**:SQLite 是文件锁机制,并发写入会直接报错 "database is locked"。

### 12.1.2 ORM 的思路

ORM(Object-Relational Mapping,对象关系映射)的核心思想很简单:**用操作 Python 对象的方式来操作数据库,而不是写 SQL 语句。**

```python

# ✅ 使用 SQLAlchemy ORM

from app import db, User

# 创建用户——注意,这不是 INSERT 语句,而是创建了一个 Python 对象

new_user = User(username="alice"email="alice@example.com"password_hash="hashed_secret")

db.session.add(new_user)      # 标记"准备插入"

db.session.commit()            # 提交事务,实际执行 INSERT

# 查询用户——不是 SELECT,而是 filter_by

user = db.session.query(User).filter_by(username="alice").first()

print(user.email)   # alice@example.com

```

你看不到任何 SQL 字符串,但底层 SQLAlchemy 会自动帮你翻译成 `INSERT` / `SELECT` / `UPDATE` / `DELETE`

ORM 的三大核心价值:

| 价值 | 说明 |

|------|------|

**安全性** | 参数化查询自动防止 SQL 注入 |

**可维护性** | 表结构定义集中管理,改一处全项目生效 |

**可移植性** | 从 SQLite 切到 PostgreSQL 只需改一行配置 |

---

## 12.2 SQLAlchemy 基础

### 12.2.1 安装与环境准备

```bash

pipinstallsqlalchemy"psycopg2-binary"alembic

```

| 包名 | 用途 |

|------|------|

`sqlalchemy` | ORM 核心框架 |

`psycopg2-binary` | PostgreSQL 驱动(开发环境用 binary 版) |

`alembic` | 数据库迁移工具 |

### 12.2.2 初始化和配置

先创建数据库连接配置模块:

```python

# app/database.py

from sqlalchemy import create_engine

from sqlalchemy.orm import sessionmaker, DeclarativeBase

from sqlalchemy.pool import StaticPool

import os

# 数据库 URL,优先读环境变量,默认用 SQLite

DATABASE_URL = os.getenv(

"DATABASE_URL",

"sqlite:///./app.db"# 开发环境默认 SQLite

)

# 创建引擎

# echo=True 会在控制台打印执行的 SQL(开发时很有用)

engine = create_engine(

DATABASE_URL,

echo=False,           # 生产环境关闭 SQL 日志

connect_args={"check_same_thread"Falseif"sqlite"inDATABASE_URLelse {},

)

# 会话工厂——每次请求创建一个新会话

SessionLocal = sessionmaker(bind=engine, autoflush=Falseautocommit=False)

# Base 类——所有模型都要继承它

classBase(DeclarativeBase):

pass

# 依赖注入:给 FastAPI 用的获取会话函数

defget_db():

    db = SessionLocal()

try:

yield db

finally:

        db.close()

```

关键点:

-**`create_engine`**:创建数据库引擎,它是 ORM 与数据库之间的桥梁。

-**`sessionmaker`**:会话工厂,类比于"数据库连接的管理器"。

-**`DeclarativeBase`**:声明式基类,所有模型通过继承它来声明自己的表结构。

-**`connect_args`**:SQLite 的特有参数,解决多线程下 "database is locked" 的问题。

### 12.2.3 定义第一个模型

```python

# app/models/user.py

from datetime import datetime

from sqlalchemy import String, Integer, DateTime, Boolean

from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.database import Base

classUser(Base):

    __tablename__ = "users"# 数据库中的表名

# ===== 字段定义 =====

id: Mapped[int] = mapped_column(Integer, primary_key=Trueautoincrement=True)

    username: Mapped[str] = mapped_column(String(50), unique=Truenullable=Falseindex=True)

    email: Mapped[str] = mapped_column(String(120), unique=Truenullable=False)

    password_hash: Mapped[str] = mapped_column(String(256), nullable=False)

    is_active: Mapped[bool] = mapped_column(Boolean, default=True)

    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

# ===== 关系定义 =====

# 一个用户有多个交易记录(一对多)

    transactions = relationship("Transaction"back_populates="owner"cascade="all, delete-orphan")

def__repr__(self):

returnf"<User {self.username}>"

```

逐行拆解:

-**`Mapped[T]`**:类型提示语法,告诉 SQLAlchemy 这个字段是什么类型。

-**`mapped_column`**:定义列的属性(主键、唯一、非空、索引等)。

-**`index=True`**:在 username 字段上创建索引,加速 `WHERE username = ?` 查询。

-**`relationship`**:定义模型之间的关系。`cascade="all, delete-orphan"` 表示删除用户时自动删除其所有交易记录。

-**`onupdate=datetime.utcnow`**:字段值更新时自动修改时间戳。

### 12.2.4 多表关系建模

以 FinanceFlow 为例,我们有用户、交易、分类三个实体:

```python

# app/models/transaction.py

from sqlalchemy import String, Float, DateTime, ForeignKey, Enum as SAEnum

from sqlalchemy.orm import Mapped, mapped_column, relationship

import enum

import uuid

from app.database import Base

classTransactionType(strenum.Enum):

INCOME = "income"

EXPENSE = "expense"

classTransaction(Base):

    __tablename__ = "transactions"

id: Mapped[str] = mapped_column(String(36), primary_key=Truedefault=lambdastr(uuid.uuid4()))

    amount: Mapped[float] = mapped_column(Float, nullable=False)

type: Mapped[TransactionType] = mapped_column(SAEnum(TransactionType), nullable=False)

    description: Mapped[str] = mapped_column(String(256), nullable=True)

    date: Mapped[str] = mapped_column(String(10), nullable=Falsedefault=lambda: datetime.utcnow().strftime("%Y-%m-%d"))

# 外键——关联到 users 表

    owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), nullable=Falseindex=True)

    category_id: Mapped[int] = mapped_column(Integer, ForeignKey("categories.id"), nullable=True)

# 关系

    owner = relationship("User"back_populates="transactions")

    category = relationship("Category"back_populates="transactions")

def__repr__(self):

returnf"<Transaction {self.type.value} ${self.amount}>"

# app/models/category.py

from sqlalchemy import String

from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.database import Base

classCategory(Base):

    __tablename__ = "categories"

id: Mapped[int] = mapped_column(Integer, primary_key=Trueautoincrement=True)

    name: Mapped[str] = mapped_column(String(50), nullable=False)

type: Mapped[TransactionType] = mapped_column(SAEnum(TransactionType), nullable=False)

    icon: Mapped[str] = mapped_column(String(10), default="📁")

# 关系

    transactions = relationship("Transaction"back_populates="category")

def__repr__(self):

returnf"<Category {self.icon}{self.name}>"

```

关系总览:

```

User (1) ───< Transaction (>1) ──> Category (1)

一个用户可以有多条交易

一条交易属于一个用户和一个分类

一个分类可以包含多条交易

```

这就是典型的 **1:N(一对多)** 关系。

### 12.2.5 创建表与基础 CRUD

```python

# app/crud/users.py

from sqlalchemy import select

from app.database import SessionLocal, Base, engine

from app.models.user import User

# 第一步:创建所有表(相当于 MySQL 的 CREATE TABLE ...)

Base.metadata.create_all(bind=engine)

```

```python

# app/crud/users.py —— CRUD 操作

from sqlalchemy import select, func

from datetime import datetime

from app.database import SessionLocal

from app.models.user import User

defcreate_user(usernamestremailstrpassword_hashstr) -> User:

"""创建新用户"""

    db = SessionLocal()

try:

        user = User(

username=username,

email=email,

password_hash=password_hash

        )

        db.add(user)

        db.commit()

        db.refresh(user)      # 刷新:获取数据库中生成的 id、created_at 等字段

return user

exceptExceptionas e:

        db.rollback()          # 出错回滚

raise e

finally:

        db.close()

defget_user_by_id(user_idint) -> User | None:

"""通过 ID 查询用户"""

    db = SessionLocal()

try:

return db.query(User).filter(User.id == user_id).first()

finally:

        db.close()

defget_user_by_username(usernamestr) -> User | None:

"""通过用户名查询"""

    db = SessionLocal()

try:

return db.query(User).filter(User.username == username).first()

finally:

        db.close()

deflist_users(skipint = 0limitint = 100) -> list[User]:

"""分页列出用户"""

    db = SessionLocal()

try:

return db.query(User).offset(skip).limit(limit).all()

finally:

        db.close()

defupdate_user_email(user_idintnew_emailstr) -> User | None:

"""更新用户邮箱"""

    db = SessionLocal()

try:

        user = db.query(User).filter(User.id == user_id).first()

ifnot user:

returnNone

        user.email = new_email

        user.updated_at = datetime.utcnow()

        db.commit()

        db.refresh(user)

return user

finally:

        db.close()

defdelete_user(user_idint) -> bool:

"""删除用户"""

    db = SessionLocal()

try:

        user = db.query(User).filter(User.id == user_id).first()

ifnot user:

returnFalse

        db.delete(user)

        db.commit()

returnTrue

finally:

        db.close()

```

```python

# 测试一下

from app.crud.users import create_user, get_user_by_username

# 创建

alice = create_user("alice""alice@example.com""hashed_pwd_123")

print(alice)           # <User alice>

print(alice.id)        # 1

print(alice.created_at)  # 2026-07-09 06:00:00

# 查询

user = get_user_by_username("alice")

print(user.email)      # alice@example.com

```

对应的 SQL:

| ORM 操作 | 生成的 SQL |

|----------|-----------|

`create_user()` | `INSERT INTO users (username, email, password_hash, ...) VALUES (?, ?, ?, ...)` |

`get_user_by_id(1)` | `SELECT * FROM users WHERE id = ? LIMIT ?` |

`list_users(skip=0, limit=100)` | `SELECT * FROM users OFFSET 0 LIMIT 100` |

`update_user_email(1, "new@email")` | `UPDATE users SET email=?, updated_at=? WHERE id=?` |

`delete_user(1)` | `DELETE FROM users WHERE id=?` |

你不需要写任何 SQL,SQLAlchemy 自动生成安全参数化的查询。

---

## 12.3 SQLAlchemy 高级用法

### 12.3.1 复杂查询

```python

from sqlalchemy import select, func, and_, or_

from app.models.user import User

from app.models.transaction import Transaction, TransactionType

from app.models.category import Category

db = SessionLocal()

# 查询 1:找出所有消费超过 1000 元的用户

stmt = (

    select(User)

    .join(Transaction, User.id == Transaction.owner_id)  # JOIN

    .where(Transaction.type == TransactionType.EXPENSE)   # WHERE

    .group_by(User.id)                                   # GROUP BY

    .having(func.sum(Transaction.amount) > 1000)         # HAVING

)

high_spenders = db.execute(stmt).scalars().all()

# 查询 2:用户列表 + 交易统计(子查询)

subquery = (

    select(

        Transaction.owner_id.label("user_id"),

        func.count(Transaction.id).label("tx_count"),

        func.sum(Transaction.amount).label("total_amount")

    )

    .group_by(Transaction.owner_id)

    .subquery()

)

stmt = (

    select(User.username, User.email, subquery.c.tx_count, subquery.c.total_amount)

    .outerjoin(subquery, User.id == subquery.c.user_id)  # LEFT OUTER JOIN

)

results = db.execute(stmt).all()

for row in results:

print(f"{row.username}{row.tx_count} 笔交易,合计 ¥{row.total_amount or0:.2f}")

# 查询 3:模糊搜索 + 排序 + 分页

keyword = "%花呗%"

stmt = (

    select(Transaction)

    .where(Transaction.description.like(keyword))

    .order_by(Transaction.date.desc())   # 按日期倒序

    .limit(20)

    .offset(0)

)

search_results = db.execute(stmt).scalars().all()

```

### 12.3.2 事务处理

```python

from app.models.user import User

from app.models.transaction import Transaction

db = SessionLocal()

try:

# 转账操作:扣 A 的钱,加 B 的钱,要么全部成功,要么全部失败

    sender = db.query(User).filter(User.username == "alice").first()

    receiver = db.query(User).filter(User.username == "bob").first()

# 创建转账记录

    transfer = Transaction(

amount=100.0,

type=TransactionType.EXPENSE,

description="转账给 bob"

    )

    sender.transactions.append(transfer)

    income = Transaction(

amount=100.0,

type=TransactionType.INCOME,

description="收到 alice 的转账"

    )

    receiver.transactions.append(income)

# 提交事务——如果中间任何一步出错,全部回滚

    db.commit()

print("转账成功!")

exceptExceptionas e:

    db.rollback()   # 回滚到转账前的状态

print(f"转账失败:{e}")

finally:

    db.close()

```

### 12.3.3 连接 PostgreSQL

```python

# 将 DATABASE_URL 改为 PostgreSQL:

import os

os.environ["DATABASE_URL"] = "postgresql+psycopg2://user:password@localhost:5432/financeflow"

# 或者在 docker-compose.yml 中传递环境变量:

# environment:

#   - DATABASE_URL=postgresql+psycopg2://postgres:mysecretpassword@db:5432/financeflow

```

只需要改一行配置,从 SQLite 切换到 PostgreSQL,**所有 ORM 代码无需改动**。这就是 ORM 的可移植性优势。

---

## 12.4 Alembic:数据库版本管理

### 12.4.1 为什么需要迁移?

想象这个场景:

```

你上线了 v1.0 版本,数据库表如下:

CREATE TABLE users (

    id INTEGER PRIMARY KEY,

    username VARCHAR(50),

    email VARCHAR(120)

);

后来,你想给用户加上"头像 URL"字段:

ALTER TABLE users ADD COLUMN avatar_url TEXT;

```

手动执行 `ALTER TABLE` 没问题,但当你换了台新服务器、或者有同事拉了最新代码时——**他们本地并没有这张 `avatar_url` 列**

如果你下次又加了"最后登录时间",再手动执行 `ALTER`,第三次手动执行 `ALTER`……最终你的团队会有不同的人在不同的数据库上执行不同的 `ALTER` 语句——混乱不可避免。

**这就是 Alembic 解决的问题:像 Git 管代码一样,用版本控制的方式管数据库表结构。**

### 12.4.2 初始化 Alembic

```bash

# 在项目根目录执行

alembicinitalembic

# 项目结构变成这样:

financeflow/

├──app/

├──database.py# SQLAlchemy 配置

└──models/# 所有模型

├──alembic/

├──env.py# Alembic 运行配置

└──versions/# 迁移脚本存放处

└──.gitkeep

├──alembic.ini# Alembic 全局配置

└──requirements.txt

```

编辑 `alembic.ini`

```ini

# 找到 sqlalchemy.url 这行,改为你的数据库 URL

sqlalchemy.url = postgresql+psycopg2://postgres:mysecretpassword@localhost:5432/financeflow

```

编辑 `alembic/env.py`

```python

# alembic/env.py

import sys

from pathlib import Path

from logging.config import fileConfig

from sqlalchemy import engine_from_config, pool

from alembic import context

# 确保能导入 app 模块

sys.path.insert(0str(Path(__file__).parent.parent))

config = context.config

if config.config_file_name isnotNone:

    fileConfig(config.config_file_name)

# 关键:导入所有模型,让 Alembic 能检测到表结构

from app.database import Base

from app.models.user import User

from app.models.transaction import Transaction

from app.models.category import Category

target_metadata = Base.metadata

defrun_migrations_offline():

    url = config.get_main_option("sqlalchemy.url")

    context.configure(url=url, target_metadata=target_metadata, literal_binds=True)

with context.begin_transaction():

        context.run_migrations()

defrun_migrations_online():

    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()

```

### 12.4.3 自动生成迁移脚本

```bash

# 第一步:检查当前模型有哪些变更

alembiccheck

# 第二步:自动生成迁移脚本

alembicrevision--autogenerate-m"初始迁移,创建所有表"

# 你会看到 alembic/versions/ 目录下多了一个文件:

# 0001_initial_migration.py

# 内容示例:

"""初始迁移,创建所有表

Revision ID: abc123

Revises: 

Create Date: 2026-07-09 10:00:00.000000

"""

fromalembicimportop

importsqlalchemyassa

defupgrade():

op.create_table('users',

sa.Column('id',sa.Integer(),primary_key=True,autoincrement=True),

sa.Column('username',sa.String(length=50),nullable=False),

sa.Column('email',sa.String(length=120),nullable=False),

sa.Column('password_hash',sa.String(length=256),nullable=False),

sa.Column('is_active',sa.Boolean(),server_default=sa.text('true')),

sa.Column('created_at',sa.DateTime(),server_default=sa.func.now()),

sa.UniqueConstraint('username'),

sa.UniqueConstraint('email'),

    )

# ... 其他表的创建语句

defdowngrade():

op.drop_table('users')

# ... 其他表的删除语句(反向操作)

```

### 12.4.4 执行迁移

```bash

# 执行最新的迁移

alembicupgradehead

# 查看当前版本

alembiccurrent

# 查看迁移历史

alembichistory--verbose

# 回滚到上一个版本

alembicdowngrade-1

# 回滚到指定版本

alembicdowngradeabc123

# 回滚到初始状态(删库级别,慎用!)

alembicdowngradebase

```

### 12.4.5 实际工作流

```

日常工作流示意:

第 1 天:创建 models → 生成迁移 → 执行迁移 → 上线 v1.0 ✅

第 5 天:给用户表加 "phone" 字段 → 重新生成迁移 → 执行迁移 → 上线 v1.1 ✅

第 10 天:新同事拉代码 → 运行 "alembic upgrade head" → 自动拿到最新版表结构 ✅

```

---

## 12.5 PostgreSQL 实战

### 12.5.1 为什么选 PostgreSQL?

| 特性 | SQLite | PostgreSQL |

|------|--------|------------|

| 并发写入 | 差(文件锁) | 优秀(MVCC) |

| 数据量 | 适合小项目 |  TB 级没压力 |

| 高级类型 | JSON、数组 | ✅ JSONB、数组、几何、GIS |

| 全文检索 | 无 | ✅ tsvector |

| 权限控制 | 粗粒度 | ✅ 行级安全(RLS) |

| 适用场景 | 开发/小型个人项目 | 生产环境 |

对于个人项目和原型验证,SQLite 完全够用。但一旦你要面对**多用户同时写入****日活上千****数据量过万**,PostgreSQL 是最佳选择。

### 12.5.2 本地启动 PostgreSQL

```bash

# 方式 1:用 Docker 启动(推荐)

dockerrun-d\

  --name pg-financeflow \

-ePOSTGRES_PASSWORD=mysecretpassword\

  -e POSTGRES_DB=financeflow \

-p5432:5432\

  postgres:16-alpine

# 方式 2:macOS Homebrew

brewinstallpostgresql

brewservicesstartpostgresql

# 方式 3:Windows - 下载官方安装包

# https://www.postgresql.org/download/windows/

```

连接测试:

```bash

# 命令行连接

psql-Upostgres-dfinanceflow

# 输出:

# psql (16.0)

# Type "help" for help.

#

# financeflow=# SELECT version();

#  PostgreSQL 16.x on ...

# financeflow=# \q

```

### 12.5.3 集成到 FastAPI

```python

# app/main.py

from fastapi import FastAPI, Depends, HTTPException

from sqlalchemy.orm import Session

from pydantic import BaseModel, EmailStr

from app.database import engine, Base, get_db

from app.models.user import User

from app.models.transaction import Transaction, TransactionType

from app.models.category import Category

from app.crud.users import create_user, get_user_by_username

# 创建表(开发环境)

Base.metadata.create_all(bind=engine)

app = FastAPI(title="FinanceFlow API"version="2.0.0")

# ===== Pydantic 模型(用于 API 请求/响应验证)=====

classUserCreate(BaseModel):

    username: str

    email: EmailStr

    password: str# 实际应在服务端哈希后存入 password_hash

classTransactionCreate(BaseModel):

    amount: float

type: TransactionType

    description: str | None = None

    category_id: int | None = None

@app.post("/api/users"response_model=UserCreate)

defregister_user(user_data: UserCreate, db: Session = Depends(get_db)):

"""注册新用户"""

    existing = get_user_by_username(user_data.username)

if existing:

raise HTTPException(status_code=409detail="用户名已存在")

# 密码哈希(生产环境用 passlib + bcrypt)

from passlib.context import CryptContext

    pwd_ctx = CryptContext(schemes=["bcrypt"], deprecated="auto")

    hashed = pwd_ctx.hash(user_data.password)

    user = create_user(user_data.username, user_data.email, hashed)

return user

@app.get("/api/users/{user_id}/stats")

defget_user_stats(user_idintdb: Session = Depends(get_db)):

"""获取用户的财务统计"""

from sqlalchemy import func

    user = db.query(User).filter(User.id == user_id).first()

ifnot user:

raise HTTPException(status_code=404detail="用户不存在")

    stats = db.query(

        func.sum(Transaction.amount).label("total_expense"),

        func.count(Transaction.id).label("tx_count")

    ).filter(

        Transaction.owner_id == user_id,

        Transaction.type == TransactionType.EXPENSE

    ).first()

return {

"user_id": user_id,

"username": user.username,

"total_expenses"float(stats.total_expense or0),

"transaction_count": stats.tx_count or0,

    }

```

```bash

# 启动服务

uvicornapp.main:app--reload--host0.0.0.0--port8000

# 测试注册

curl-XPOSThttp://localhost:8000/api/users\

  -H "Content-Type: application/json" \

-d'{"username": "alice", "email": "alice@example.com", "password": "secure123"}'

# 返回:

# {"username": "alice", "email": "alice@example.com"}

```

---

## 12.6 实操练习

### 练习 1:从零搭建一个简单的图书管理系统

**题目**:设计 `Book` 和 `Author` 两个模型,实现多对多关系(一个作者可以写多本书,一本书也可以有多个合著者)。

**任务**

1. 定义 `Book``Author` 表和中间关联表(`book_authors`

2. 编写 `add_author_to_book(book_id, author_id)` 和 `get_books_by_author(author_id)` 函数

3. 编写 Alembic 迁移脚本并执行

<details>

<summary>参考答案</summary>

```python

from sqlalchemy import Table, Column, Integer, String, ForeignKey

from sqlalchemy.orm import Mapped, mapped_column, relationship

# 多对多中间表(不需要继承 Base,只是纯关联)

book_authors = Table(

"book_authors",

    Base.metadata,

    Column("book_id", Integer, ForeignKey("books.id"), primary_key=True),

    Column("author_id", Integer, ForeignKey("authors.id"), primary_key=True),

)

classAuthor(Base):

    __tablename__ = "authors"

id: Mapped[int] = mapped_column(Integer, primary_key=True)

    name: Mapped[str] = mapped_column(String(100), nullable=False)

    books: Mapped[list["Book"]] = relationship("Book"secondary=book_authors, back_populates="authors")

classBook(Base):

    __tablename__ = "books"

id: Mapped[int] = mapped_column(Integer, primary_key=True)

    title: Mapped[str] = mapped_column(String(200), nullable=False)

    isbn: Mapped[str] = mapped_column(String(13), unique=True)

    authors: Mapped[list["Author"]] = relationship("Author"secondary=book_authors, back_populates="books")

```

```python

# CRUD 操作

from sqlalchemy import select

defadd_author_to_book(db: Session, book_idintauthor_idint):

    book = db.query(Book).filter(Book.id == book_id).first()

    author = db.query(Author).filter(Author.id == author_id).first()

if book and author:

        book.authors.append(author)

        db.commit()

defget_books_by_author(db: Session, author_idint) -> list[Book]:

    author = db.query(Author).filter(Author.id == author_id).first()

ifnot author:

return []

return author.books

```

```bash

# 生成并执行迁移

alembicrevision--autogenerate-m"创建书籍和作者表"

alembicupgradehead

```

</details>

---

### 练习 2:性能优化——给慢查询加索引

**题目**:FinanceFlow 的 `/api/transactions` 接口在数据量大时变得很慢。分析原因并优化。

```python

# 原始查询(慢)

@app.get("/api/transactions")

deflist_transactions(

user_idint,

start_datestr,

end_datestr,

db: Session = Depends(get_db)

):

return db.query(Transaction).filter(

        Transaction.owner_id == user_id,

        Transaction.date >= start_date,

        Transaction.date <= end_date

    ).order_by(Transaction.date.desc()).all()

```

**任务**

1. 指出为什么这个查询在万级数据时会慢

2. 在模型中添加复合索引

3. 用 `EXPLAIN ANALYZE` 验证优化效果

<details>

<summary>参考答案</summary>

```python

# 解决方案 1:在模型中给常用查询字段加索引

classTransaction(Base):

    __tablename__ = "transactions"

# 已有 owner_id 索引

    owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True)

# 新增:给 date 也加索引(用于范围查询)

    date: Mapped[str] = mapped_column(String(10), index=True)

# 更优方案:加复合索引(需要原生 SQL)

# 可以用 event listener 或手动写 migration:

```

```python

# 在 Alembic 迁移脚本中手动添加复合索引:

defupgrade():

    op.create_index(

"ix_transaction_owner_date",

"transactions",

        ["owner_id""date"],

unique=False,

    )

defdowngrade():

    op.drop_index("ix_transaction_owner_date"table_name="transactions")

```

```bash

# 验证:在 psql 中执行 EXPLAIN ANALYZE

EXPLAINANALYZESELECT*FROMtransactions

WHEREowner_id=1ANDdate >='2026-01-01'ANDdate <='2026-12-31';

--优化前:SeqScan(全表扫描),耗时~50ms(1万条数据)

--优化后:IndexScan(索引扫描),耗时~1ms

```

</details>

---

### 练习 3:写一个复杂的聚合查询

**题目**:为每个用户生成分月的支出报告。

**任务**

1. 用 SQLAlchemy 写出按月分组 + 按类别分组的支出汇总

2. 返回格式如:

```json

{

"month""2026-06",

"categories": [

    {"name""餐饮""amount"1500.00},

    {"name""交通""amount"300.00},

    {"name""购物""amount"800.00}

  ],

"total"2600.00

}

```

<details>

<summary>参考答案</summary>

```python

from sqlalchemy import extract, func

from datetime import datetime

defget_monthly_report(db: Session, user_idintyearintmonthint):

# 筛选当月数据

    monthly_txns = (

        db.query(Transaction, Category.name.label("cat_name"))

        .outerjoin(Category, Transaction.category_id == Category.id)

        .join(User, Transaction.owner_id == User.id)

        .filter(

            User.id == user_id,

            extract("year", Transaction.date) == year,

            extract("month", Transaction.date) == month,

            Transaction.type == TransactionType.EXPENSE,

        )

        .all()

    )

# 按类别聚合

    cat_totals = {}

    total = 0.0

for txn, cat_name in monthly_txns:

        key = cat_name or"未分类"

        cat_totals[key] = cat_totals.get(key, 0.0) + txn.amount

        total += txn.amount

return {

"month"f"{year}-{month:02d}",

"categories": [

            {"name": k, "amount"round(v, 2)}

for k, v insorted(cat_totals.items(), key=lambdax: -x[1])

        ],

"total"round(total, 2),

    }

```

</details>

---

### 练习 4:从 SQLite 迁移到 PostgreSQL

**题目**:你有一个用 SQLite 开发的 FinanceFlow 项目,现在要迁移到 PostgreSQL。

**任务**

1. 列出完整迁移步骤

2. 如果迁移过程中发现 Alembic 迁移丢失了,该怎么办?

<details>

<summary>参考答案</summary>

**完整迁移步骤**

```bash

# 1. 启动 PostgreSQL 容器

dockerrun-d--namepg-test-ePOSTGRES_PASSWORD=test-ePOSTGRES_DB=ff-p5433:5432postgres:16-alpine

# 2. 导出 SQLite 数据

sqlite3app.db".dump" > backup.sql

# 3. 导入到 PostgreSQL(或用 python 脚本转换)

psql-Upostgres-dff-fbackup.sql

# 注意:SQLite dump 可能需要简单修改语法(如 AUTOINCREMENT → SERIAL)

# 4. 改 DATABASE_URL

exportDATABASE_URL="postgresql+psycopg2://postgres:test@localhost:5433/ff"

# 5. 测试所有 API 端点

# 6. 如果没有 Alembic 迁移记录:

alembicstamphead

# 用 "stamp" 告诉 Alembic "当前数据库已经是最新的了",不用重新生成迁移

```

**如果迁移记录丢失的应急方案**

```bash

# 方案 A:用 stamp 对齐版本(推荐)

alembicstamphead

# 方案 B:从头生成迁移(会丢失后续迁移)

alembicrevision--autogenerate-m"重新初始化"

alembicupgradehead

```

</details>

---

### 练习 5:设计一个博客系统的数据库模型

**题目**:设计一个简易博客系统的数据模型,包括:

-`User`(用户):id, username, email, bio, created_at

-`Post`(文章):id, title, content, slug(URL 友好标识), author_id, created_at, updated_at

-`Comment`(评论):id, content, post_id, author_id, created_at

-`Tag`(标签):id, name

-`post_tags`(多对多关联表)

**任务**

1. 用 SQLAlchemy 声明所有模型

2. 设置适当的 `back_populates` / `backref`

3. 给常用查询字段加索引

4. 在 Article.slug 上实现唯一约束,并创建一个自动生成 slug 的方法

<details>

<summary>参考答案</summary>

```python

import re

from sqlalchemy import String, Text, DateTime, ForeignKey, Integer

from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.database import Base

from datetime import datetime

post_tags = Table(

"post_tags",

    Base.metadata,

    Column("post_id", Integer, ForeignKey("posts.id"), primary_key=True),

    Column("tag_id", Integer, ForeignKey("tags.id"), primary_key=True),

)

classUser(Base):

    __tablename__ = "users"

id: Mapped[int] = mapped_column(Integer, primary_key=True)

    username: Mapped[str] = mapped_column(String(50), unique=Trueindex=True)

    email: Mapped[str] = mapped_column(String(120), unique=True)

    bio: Mapped[str | None] = mapped_column(Text, nullable=True)

    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    posts = relationship("Post"back_populates="author"cascade="all, delete-orphan")

    comments = relationship("Comment"back_populates="author")

classPost(Base):

    __tablename__ = "posts"

id: Mapped[int] = mapped_column(Integer, primary_key=True)

    title: Mapped[str] = mapped_column(String(200), nullable=False)

    slug: Mapped[str] = mapped_column(String(200), unique=Trueindex=True)

    content: Mapped[str] = mapped_column(Text, nullable=False)

    author_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))

    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    author = relationship("User"back_populates="posts")

    comments = relationship("Comment"back_populates="post"cascade="all, delete-orphan")

    tags = relationship("Tag"secondary=post_tags, back_populates="posts")

@staticmethod

defgenerate_slug(titlestr) -> str:

"""从标题生成 URL 友好的 slug"""

        slug = re.sub(r'[^\w\s-]''', title.lower().strip())

        slug = re.sub(r'[\s]+''-', slug)

return slug

classComment(Base):

    __tablename__ = "comments"

id: Mapped[int] = mapped_column(Integer, primary_key=True)

    content: Mapped[str] = mapped_column(Text, nullable=False)

    post_id: Mapped[int] = mapped_column(Integer, ForeignKey("posts.id"), index=True)

    author_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))

    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    post = relationship("Post"back_populates="comments")

    author = relationship("User"back_populates="comments")

classTag(Base):

    __tablename__ = "tags"

id: Mapped[int] = mapped_column(Integer, primary_key=True)

    name: Mapped[str] = mapped_column(String(50), unique=Trueindex=True)

    posts = relationship("Post"secondary=post_tags, back_populates="tags")

```

</details>

---

## 12.7 知识点小结

| 知识点 | 在实际项目中的价值 |

|--------|-------------------|

| SQLAlchemy ORM | 用 Python 对象操作数据库,告别手写 SQL 的隐患 |

| 多表关系(1:N, N:M) | 构建真实业务数据模型的能力 |

| 参数化查询 | 自动防止 SQL 注入 |

| Alembic 迁移 | 像 Git 一样管理数据库版本,团队协作标配 |

| PostgreSQL | 从 SQLite 过渡到生产级数据库的第一选择 |

| 索引优化 | 让慢查询变快的最简手段 |

---

## 12.8 小结与展望

恭喜!你已经掌握了 **Python 数据库开发的完整工程链路**——从 ORM 建模到版本迁移,从本地 SQLite 到生产 PostgreSQL。

这一期补上了你作为一个 Python 开发者最重要的拼图之一。现在你的能力栈是这样的:

```

学习阶段          你得到了什么能力

─────────────────────────────────────────────────

Episode 01-04     写好 Python 代码的基础

Episode 05        抓取外部数据的能力

Episode 06        分析和理解数据的能力

Episode 07        构建后端服务的能力

Episode 08        接入 AI 智能的能力

Episode 09        打造用户界面的能力

Episode 10        整合所有能力的综合能力

Episode 11        将项目部署到世界任何地方的能力

Episode 12        ⭐ 构建健壮数据层的能力——这是从"玩具项目"到"正经产品"的分水岭

```

有了这一章,你的项目不再是"跑在我机器上是好的",而是**在任何环境下都有统一、可追踪的数据结构**

---

## 12.9 下期预告

接下来的方向将继续深入后端工程化:

-**Episode 13:Celery 异步任务** —— 让耗时操作(邮件发送、文件处理、AI 分析)后台跑,不阻塞用户请求。

-**Episode 14:认证与安全** —— JWT 鉴权、OAuth2、RBAC 权限模型、中间件防护。

-**Episode 15:API 测试与文档** —— pytest + httpx,让每个端点都有自动化测试。

-**Episode 16:缓存策略** —— Redis 缓存、缓存失效模式、分布式缓存入门。

-**Episode 17:GraphQL vs REST** —— 下一代 API 查询语言的实践对比。

无论哪条路,扎实的数据库功底都会让你在后续学习中如鱼得水。下期满载而归!🗄️⚡✨

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-08-22 02:51:53 HTTP/2.0 GET : https://f.mffb.com.cn/a/505010.html
  2. 运行时间 : 0.237002s [ 吞吐率:4.22req/s ] 内存消耗:4,964.55kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=b3e1f5a8070e524f632962be1f1aa479
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.001221s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001928s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000756s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000692s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001506s ]
  6. SELECT * FROM `set` [ RunTime:0.000578s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001873s ]
  8. SELECT * FROM `article` WHERE `id` = 505010 LIMIT 1 [ RunTime:0.003318s ]
  9. UPDATE `article` SET `lasttime` = 1787338313 WHERE `id` = 505010 [ RunTime:0.015236s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 66 LIMIT 1 [ RunTime:0.000682s ]
  11. SELECT * FROM `article` WHERE `id` < 505010 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001356s ]
  12. SELECT * FROM `article` WHERE `id` > 505010 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.006892s ]
  13. SELECT * FROM `article` WHERE `id` < 505010 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.003079s ]
  14. SELECT * FROM `article` WHERE `id` < 505010 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.007491s ]
  15. SELECT * FROM `article` WHERE `id` < 505010 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.003934s ]
0.243740s