📖 文章摘要
6 张 ORM 表的设计决策:文章-标签多对多、自引用评论、JSON 扩展字段、SQLite 路径兼容。本文详解每个字段的设计意图。
数据库设计:6 张表支撑一个博客
博客数据库共 6 张表,用 SQLAlchemy ORM 管理。后端使用 SQLite 数据库,文件路径 data/blog.db。
第 1 张表:Article(文章)
核心表,承载博客的全部内容:
class Article(Base):
__tablename__ = "articles"
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
slug = Column(String(200), unique=True, nullable=False)
content_md = Column(Text, nullable=False) # Markdown 原文
content_html = Column(Text, default="") # 渲染后的 HTML 缓存
summary = Column(String(500), default="") # 摘要
cover_image = Column(String(500), default="")
category_id = Column(Integer, ForeignKey("categories.id"))
is_published = Column(Boolean, default=False)
is_top = Column(Boolean, default=False)
allow_comment = Column(Boolean, default=True)
view_count = Column(Integer, default=0)
meta = Column(JSON, default=dict) # 扩展字段
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
published_at = Column(DateTime, nullable=True)
设计要点:
- slug 而非 id 做 URL 标识 — slug 对 SEO 友好(
/p/redis-cache优于/p/42),且文章删除后 slug 不会重用 - content_html 缓存 — Markdown 渲染在写入时完成并缓存,读取时直接返回 HTML,避免每次请求都渲染
- meta JSON — 预留扩展,未来可追加自定义字段而不改表结构
- published_at 与 created_at 分离 — 文章可以创建为草稿,发布时才写入 published_at
- Pydantic v2 注意 — cover_image 和 meta 必须默认空字符串/空对象,不能为 NULL,否则 API 返回 500
第 2-3 张表:Category 和 Tag(分类与标签)
两者结构相似,都包含 type 字段预留扩展:
class Category(Base):
__tablename__ = "categories"
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
slug = Column(String(100), unique=True, nullable=False)
description = Column(String(500), default="")
type = Column(String(50), default="blog")
多对多关系通过 article_tag 中间表实现:
article_tag = Table(
"article_tag", Base.metadata,
Column("article_id", Integer, ForeignKey("articles.id", ondelete="CASCADE"), primary_key=True),
Column("tag_id", Integer, ForeignKey("tags.id", ondelete="CASCADE"), primary_key=True),
)
选择多对多而非一对多是因为一篇文章可以有多个标签,一个标签也可以归属多篇文章。
第 4 张表:Comment(评论)
class Comment(Base):
__tablename__ = "comments"
id = Column(Integer, primary_key=True)
article_id = Column(Integer, ForeignKey("articles.id", ondelete="CASCADE"))
parent_id = Column(Integer, ForeignKey("comments.id", ondelete="SET NULL"))
author = Column(String(100), nullable=False)
content = Column(Text, nullable=False)
is_approved = Column(Boolean, default=False)
is_pinned = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
parent_id 字段预留给楼中楼回复,使用自引用外键。注意:自引用关系不能加 cascade="all, delete-orphan",否则 SQLAlchemy 报错。ondelete="SET NULL" 更安全——父评论删除时子评论保留。
第 5 张表:Admin(管理员)
class Admin(Base):
id = Column(Integer, primary_key=True)
username = Column(String(100), unique=True, nullable=False)
password_hash = Column(String(200), nullable=False)
密码使用 SHA256 哈希存储。为什么不加盐?单用户博客的泄露风险低,且 passlib 在 bcrypt 5.x 下有兼容问题(见踩坑记录)。
第 6 张表:SiteSetting(站点配置)
简单的 key-value 表:
class SiteSetting(Base):
key = Column(String(100), primary_key=True)
value = Column(Text, default="")
常用配置项:site_name、author_name、author_avatar、author_tags、social_links、background_url。全部通过后台管理界面修改,无需改代码。
SQLite 的注意事项
路径兼容
SQLite URL 在 Windows 和 Linux 下的路径格式不同。使用 .as_posix() 统一转正斜杠:
DATABASE_URL = f"sqlite:///{(DATA_DIR / 'blog.db').as_posix()}"
WAL 模式
SQLite 默认的 journal 模式在并发读写时性能较差。虽然本博客并发量不大,但 WAL(Write-Ahead Logging)模式可以显著提升读性能:
PRAGMA journal_mode=WAL;
备份
单文件数据库最大的好处——备份就是复制:
cp data/blog.db backup/blog-$(date +%Y%m%d).db
最后更新:2026年6月29日CC BY-NC-SA 4.0
评论
暂无评论,来写第一条吧
