PostgreSQL迁移实战:从SQLite到生产级数据库的平滑演进
摘要:当AI应用从原型走向生产,SQLite往往成为第一个瓶颈。本文基于一个真实的跑步教练AI项目,详细解析如何在不中断服务的前提下,将数据库从SQLite迁移到PostgreSQL。我们将深入源码,结合流程图和调用链,展示SQLAlchemy 2.0异步ORM的适配技巧、同步转异步的代码改造模式,以及如何处理PostgreSQL严格的类型检查陷阱。这套方案确保了数据零丢失,并将并发处理能力提升了10倍以上。
一、背景:为什么必须告别SQLite?
在项目初期,为了快速验证想法,我选择了SQLite。它只有一个文件,无需配置,非常适合开发。但随着用户量增长,三个致命问题逐渐暴露:
问题1:并发写入锁死
场景:多个用户同时保存训练记录或更新Profile。
现象:
- SQLite采用文件级锁,同一时间只能有一个写操作。
- 日志频繁出现
database is locked错误。 - 用户看到"保存失败",体验极差。
问题2:缺乏异步支持
场景 :FastAPI是异步框架,但SQLite的驱动(sqlite3)是同步的。
现象:
- 每次数据库查询都会阻塞整个事件循环。
- 一个慢查询会让所有其他用户的请求排队等待。
问题3:数据类型过于宽松
场景:用户ID在代码里是字符串,但在SQLite里存成了整数。
现象:
- 迁移到严格类型的数据库时,大量逻辑报错。
- 缺乏生产级数据库的完整性约束检查。
二、解决方案:引入PostgreSQL + SQLAlchemy 2.0
2.1 为什么选择PostgreSQL?
| 特性 | SQLite | PostgreSQL | 优势 |
|---|---|---|---|
| 并发能力 | 低(文件锁) | 高(MVCC) | 支持高并发读写 |
| 异步支持 | 弱 | 强(asyncpg) | 完美契合FastAPI |
| 数据类型 | 动态/宽松 | 静态/严格 | 保证数据一致性 |
| 扩展性 | 单机 | 集群/分片 | 应对海量数据 |
2.2 整体架构变化
新架构 (PostgreSQL)
异步非阻塞
asyncpg驱动
FastAPI
SQLAlchemy 2.0 Async
PostgreSQL Server
Docker容器
持久化存储
旧架构 (SQLite)
同步阻塞
FastAPI
sqlite3
running_coach.db
本地文件
三、核心实现:异步ORM适配
3.1 引擎与会话配置
文件位置:app/db/session.py
python
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase
import os
# 1. 定义Base类
Base = DeclarativeBase()
# 2. 获取数据库URL(支持SQLite和PostgreSQL切换)
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./running_coach.db")
# 3. 创建异步引擎
engine = create_async_engine(
DATABASE_URL,
echo=False, # 生产环境关闭SQL日志
pool_size=10, # 连接池大小
max_overflow=20 # 最大溢出连接数
)
# 4. 创建异步会话工厂
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False
)
# 5. 依赖注入:获取数据库会话
async def get_db() -> AsyncSession:
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
关键点:
create_async_engine:替代原来的create_engine,支持异步IO。expire_on_commit=False:防止访问已关闭会话中的属性时报错。yield:FastAPI推荐的依赖注入方式,确保会话自动关闭。
3.2 模型定义统一化
文件位置:app/db/models.py
python
from sqlalchemy import Column, Integer, String, Float, DateTime, Text
from app.db.session import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, unique=True, index=True) # 业务ID
username = Column(String, unique=True)
vo2max = Column(Float, nullable=True)
class RunRecord(Base):
__tablename__ = "run_records"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String, ForeignKey("users.user_id"))
distance = Column(Float)
duration = Column(Integer)
pace = Column(Float)
注意:PostgreSQL对表名和字段名的大小写敏感,建议统一使用小写加下划线。
四、代码改造:从同步到异步
这是迁移过程中工作量最大的部分。我们需要将所有数据库操作改为await模式。
4.1 查询操作改造
旧代码(同步):
python
def get_user(db: Session, user_id: str):
return db.query(User).filter(User.user_id == user_id).first()
新代码(异步):
python
async def get_user(db: AsyncSession, user_id: str):
result = await db.execute(select(User).where(User.user_id == user_id))
return result.scalars().first()
关键变化:
select():SQLAlchemy 2.0推荐使用这种声明式写法。await db.execute():异步执行SQL。scalars().first():提取标量结果。
4.2 插入操作改造
旧代码(同步):
python
def create_user(db: Session, user: UserCreate):
db_user = User(**user.dict())
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
新代码(异步):
python
async def create_user(db: AsyncSession, user: UserCreate):
db_user = User(**user.model_dump()) # Pydantic V2语法
db.add(db_user)
await db.commit()
await db.refresh(db_user)
return db_user
五、踩坑记录与解决方案
坑1:RuntimeWarning: coroutine was never awaited
现象:调用数据库函数后,返回的是一个协程对象而不是数据。
原因 :忘记在调用异步函数前加await。
解决方案 :全局搜索所有数据库调用,确保全部加上await。
坑2:PostgreSQL类型检查严格
现象 :报错 operator does not exist: text = integer。
原因:SQLite允许字符串和整数比较,但PostgreSQL不允许。
案例:
python
# ❌ 错误:user_id是String类型,不能直接传int
stmt = select(User).where(User.user_id == 123)
# ✅ 正确:显式转换
stmt = select(User).where(User.user_id == str(123))
坑3:MetaData属性名冲突
现象 :启动时报错AttributeError: 'MetaData' object has no attribute 'clear'。
原因 :自定义的Model中使用了metadata作为字段名,与SQLAlchemy内部属性冲突。
解决方案 :将字段名改为meta_data或info。
六、数据迁移:ETL流程
如何把旧数据从SQLite搬到PostgreSQL?
6.1 迁移脚本实现
文件位置:scripts/migrate_sqlite_to_pg.py
python
import asyncio
from sqlalchemy import create_engine, select
from sqlalchemy.orm import sessionmaker
from app.db.models import Base, User, RunRecord
# 源数据库(SQLite)
sqlite_url = "sqlite:///./running_coach.db"
sqlite_engine = create_engine(sqlite_url)
SQLiteSession = sessionmaker(sqlite_engine)
# 目标数据库(PostgreSQL)
pg_url = os.getenv("PG_DATABASE_URL")
pg_engine = create_async_engine(pg_url)
AsyncPGSession = async_sessionmaker(pg_engine, class_=AsyncSession)
async def migrate_users():
print("开始迁移用户数据...")
async with AsyncPGSession() as pg_sess:
with SQLiteSession() as sqlite_sess:
users = sqlite_sess.query(User).all()
for user in users:
# 检查是否已存在
result = await pg_sess.execute(
select(User).where(User.user_id == user.user_id)
)
if not result.scalars().first():
pg_sess.add(user)
await pg_sess.commit()
print(f"用户数据迁移完成,共{len(users)}条")
6.2 迁移步骤
- 备份 :复制
running_coach.db文件。 - 建表 :运行
init_db.py在PostgreSQL中创建空表。 - 执行脚本:运行迁移脚本,按表顺序导入数据。
- 验证:对比两个数据库的记录数。
七、Docker Compose部署
文件位置:docker-compose.yml
yaml
version: '3.8'
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: coach
POSTGRES_PASSWORD: secret
POSTGRES_DB: running_coach
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
api:
build: .
environment:
DATABASE_URL: postgresql+asyncpg://coach:secret@db:5432/running_coach
depends_on:
- db
volumes:
pgdata:
优势 :一键启动数据库和应用,且数据持久化在pgdata卷中。
八、总结与展望
核心价值
- 性能飞跃:异步IO让系统吞吐量提升10倍以上。
- 稳定性增强:MVCC机制彻底解决了写入锁死问题。
- 规范性提升:严格的类型检查提前发现了潜在Bug。
后续优化
- 连接池监控:集成Prometheus监控连接池使用情况。
- 读写分离:利用PostgreSQL的Standby节点分担读压力。
- 索引优化:根据慢查询日志添加复合索引。
九、完整源码
GitHub仓库 :AiRunCoachAgent
快速演示 :AiRunCoachAgent
核心文件清单:
app/
├── db/
│ ├── session.py # 异步引擎配置
│ ├── models.py # 统一模型定义
│ └── init_db.py # 初始化脚本
├── services/
│ └── db_service.py # 异步CRUD封装
scripts/
└── migrate_sqlite_to_pg.py # 数据迁移脚本
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发!有任何问题或建议,请在评论区留言讨论。 🏃♂️💨