全栈架构:三套 Schema

全栈架构:三套 Schema

在一个数据驱动的全栈系统中,最核心的工作流莫过于:前端发送请求 -> 后端处理逻辑 -> 读写数据库 -> 数据返回前端

在这个过程中,同一个业务实体(比如一只“动物”或一朵“花”),虽然代表的信息是一致的,但在不同的系统层级中,其表现形式(Schema)承载的职责是截然不同的。

通常,一个规范的全栈项目需要维护“三套 Schema”:

  1. Database Schema:用于数据库存储(ORM 模型)。
  2. API Schema:用于后端接口的数据验证与序列化(Pydantic 模型)。
  3. Frontend Schema:用于前端页面的类型检查与展示(TypeScript 接口)。

以 Python (FastAPI/SQLAlchemy) + Frontend (TypeScript) 为例,梳理这三套 Schema 的定义与协作。

第一套:Database Schema (ORM Layer)

数据库层是数据的源头。在 Python 后端中,我们通常使用 SQLAlchemy 这样的 ORM(对象关系映射)库,将数据库表结构映射为 Python 类。通常在 backend/app/db 目录下维护数据库连接逻辑。

  • Engine: 负责与数据库的实际通信。
  • Session: 数据库会话,相当于一个“连接句柄”,用于执行 CRUD 操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# app/backend/db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from app.core.config import settings

# 1. 创建引擎
# pool_pre_ping=True 可以在数据库连接断开时自动重连
engine = create_engine(settings.DATABASE_URL, pool_pre_ping=True)

# 2. 创建 Session 工厂
# 注意:SessionLocal 本身不是单例,但它是一个生产 Session 的工厂,通常全局只需定义一次
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db() -> Session:
"""
依赖注入工具函数:
每个请求创建一个独立的 Session,请求结束后自动关闭
"""
db = SessionLocal()
try:
yield db
finally:
db.close()

backend/models 中定义表结构。所有模型继承自 SQLAlchemy 的 Base 类。

1
2
3
4
5
6
7
8
9
10
from sqlalchemy import Column, Integer, String, Date, Float
from app.db.base_class import Base

class Animal(Base):
__tablename__ = "animals"

id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
acquire_date = Column(Date, nullable=False)
# ... 其他字段定义

这一层 Schema 的核心职责:精确描述数据库表的结构(字段类型、主键、外键、索引),直接对应 SQL 语句。

第二套:API Schema (Pydantic Layer)

这是后端与外界交互的“关口”,在 FastAPI 中,我们使用 Pydantic 来定义这套 Schema。通常位于 backend/app/schemas

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import date, datetime

# 基础类:包含共享字段
class AnimalBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="动物名称")
quantity: int = Field(..., ge=0, description="数量")
acquire_date: date = Field(..., description="购入/出生日期")
notes: Optional[str] = Field(None, max_length=500, description="备注")

# 创建时使用的 Schema:用户输入的数据
class AnimalCreate(AnimalBase):
pass

# 响应时使用的 Schema:返回给前端的数据
class AnimalResponse(AnimalBase):
id: int
created_at: datetime
updated_at: datetime

# 核心配置:允许 Pydantic 读取 ORM 模型数据
model_config = ConfigDict(from_attributes=True)

默认情况下,Pydantic 只能读取字典(如 data['id'])。开启 from_attributes=True 后 ,Pydantic 可以读取对象属性(如 data.id)。此时我们可以直接把 SQLAlchemy 返回的数据库对象扔给 Pydantic,它能自动提取数据。

在 API 中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@router.post("/", response_model=AnimalResponse, status_code=201)
def create_animal(
animal_data: AnimalCreate, # 1. 接收并校验前端数据
db: Session = Depends(get_db)
):
# 2. 将 Pydantic Schema 转换为字典,解包传给 SQLAlchemy Model
db_animal = Animal(**animal_data.model_dump())

db.add(db_animal)
db.commit()
db.refresh(db_animal)

# 3. 直接返回 ORM 对象
return db_animal

我们在这里直接 return 了一个 SQLAlchemy 的表结构类,它自动转换成了 AnimalResponse,这是 FastAPI 的强大功能之一。虽然函数 return db_animal 返回的是一个 ORM 对象,但装饰器中的 response_model=AnimalResponse 会介入。FastAPI 会利用 Pydantic 的 from_attributes=True 特性,从 db_animal 中提取字段,过滤掉未在 AnimalResponse 中定义的字段,并将数据序列化为 JSON 返回给前端。

第三套:Frontend Schema (TypeScript Layer)

数据流出后端后,前端也需要一套标准来“接住”这些数据。在 TypeScript 项目中,我们在 src/types 中定义 Interface。

这一层定义应与后端的 Pydantic Schema 保持一一对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 对应后端的 AnimalResponse
export interface Animal {
id: number
name: string
quantity: number
acquire_date: string // JSON 中日期通常是字符串
notes: string | null
created_at: string
updated_at: string
}

// 对应后端的 AnimalCreate
export interface AnimalCreate {
name: string
quantity: number
acquire_date: string
notes?: string // 可选字段
}

为什么需要 TypeScript 接口?因为在 JavaScript 中,写 user.nmae (拼写错误) 只有在运行时才会报错。而在 TypeScript 中,因为有了 Interface 充当“模具”,编辑器会在敲代码的那一刻就标红报错,极大地提高了开发效率和安全性。

前端通过 Axios 发送请求时,泛型(Generics)能发挥巨大作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { apiClient } from './client'

export const animalsApi = {
// 显式声明:输入是 AnimalCreate,输出 Promise 包含 Animal
create: async (data: AnimalCreate): Promise<Animal> => {
// 泛型 <Animal> 告诉 axios,返回的 response.data 格式是 Animal
const response = await apiClient.post<Animal>('/animals', data)
return response.data
},

getList: async (): Promise<Animal[]> => {
const response = await apiClient.get<Animal[]>('/animals')
return response.data
}
}

总结:三套 Schema 的协作流

让我们看一个完整的“创建动物”流程,数据是如何变形的:

  1. 前端 (TypeScript): 用户填写表单,数据符合 AnimalCreate 接口。前端发送 JSON。
  2. 后端入口 (Pydantic): FastAPI 接收 JSON,使用 AnimalCreate (Pydantic) 进行校验(比如数量不能小于0)。
  3. 后端处理 (ORM): 校验通过的数据被转换为 Animal (SQLAlchemy) 模型,写入数据库表。
  4. 后端出口 (Pydantic): 数据库返回的 ORM 对象,被 AnimalResponse (Pydantic) 过滤和序列化,变回 JSON。
  5. 前端接收 (TypeScript): 前端收到 JSON,将其识别为 Animal 接口类型,渲染到列表中。

这三套 Schema 分别守护了数据库的完整性API 的安全性前端的类型安全。在大型系统中,这种分层架构是保持代码清晰、可维护的基石。