克己笔记 克己笔记
技术笔记

一次 Typecho + PostgreSQL 迁移的踩坑复盘

18

这套博客原先跑在 SQLite 上,图省事。直到某天后台点「保存」转了三秒才回来,我才下决心把它挪到 PostgreSQL 15。挪的过程不算长,一晚上就完了,但踩的坑足够写一篇。

一、迁移的基本盘

环境没什么特别的:一个 Docker 容器跑 Typecho 1.3(PHP 7.4 + Apache),另一个容器跑 PostgreSQL 15,两个容器在同一个自定义网络里,靠容器名互相解析。Typecho 的 config.inc.php 里把 host 写成数据库的容器名就行,端口保持 5432。

真正要注意的是表前缀字符集。前缀我沿用了 typeo_,字符集用 utf8,这两项在迁移脚本里必须和新库一致,否则导入之后中文会变成问号——这类问题当场能发现,不算坑。

真正的坑,都在「Typecho 已经帮你处理好了,但你不知道」的地方。

二、混合大小写的列名

PostgreSQL 有个规矩:不加双引号的标识符会被折叠成小写,而加了双引号的会原样保留大小写。Typecho 建表时生成的列名是 authorIdcommentsNumallowComment 这种驼峰写法,也就是说它们在系统表里是带双引号存下来的

于是你手写一句 SQL:

SELECT commentsNum FROM typeo_contents WHERE cid = 1;

它会报「列 commentsnum 不存在」。注意报错信息里是小写的 commentsnum——因为你没加引号,PG 帮你折叠了,然后就找不到了。

正确的写法是处处加引号:

SELECT "commentsNum" FROM typeo_contents WHERE cid = 1;

这个坑本身不复杂,复杂的是它只在你绕过 Typecho 自己写 SQL 的时候才出现。Typecho 自带的查询构建器会对每个标识符做引号处理,所以用 $db->select(...) 这类方法写代码的人一辈子遇不到它;而写插件时图快直接 $db->query("SELECT ...") 拼裸 SQL 的人,就会在第一次跑的时候撞上。

我的结论是:能用查询构建器就别写裸 SQL。非写不可的时候,把列名当成用户输入一样对待,老老实实加引号。

三、没有 lastInsertId()

迁移里的第二步是把旧数据灌进新库,需要边插边拿到新生成的主键。按 MySQL 的习惯,我写了:

$db = \Typecho\Db::get();
$db->query($insert);
$cid = $db->lastInsertId();

直接抛 \Error,而且是捕不到的那种——\Error 不是 \Exception 的子类,我那个 catch (\Exception $e) 兜了个空。

翻了下源码才明白:Typecho 的 Db 类根本没有这个公共方法,取主键的活儿被下放到了适配器层,而且是靠插入语句自己把主键带回来的。Pgsql 适配器在 prepareQuery() 里看到是 INSERT 动作,就会自动往语句尾巴上加一句 RETURNING "cid"Db::query() 判断动作是 INSERT 时,直接把返回值当主键给你。

所以正确写法反而更短:

$cid = (int)$db->query($db->insert('table.contents')->rows($row));

这件事教我的不是 API 怎么用,而是在别人的框架里别想着绕过去。绕过去省下的三行代码,会在一个你没想到的地方以报错的形式还回来。

四、内容取到的是原始 Markdown

我做的是个「专栏」类插件:把若干篇文章归到一个专栏下,在文章末尾列出同专栏的目录。目录里我想显示每篇的摘要,于是顺手读了 contents.text 字段。

拿到的不是干净文本,是没渲染过的 Markdown 原文——里面有 #、有 **、有图片语法、还有自定义短代码的方括号。直接截前六十个字显示出来,就是一堆符号。

解决办法只有一条:自己清洗。剥掉短代码、去掉语法符号、把图片语法整段丢掉、再按字符截断。这段代码写了三十多行,写完之后我意识到它永远不可能完美——Markdown 的语法比正则表达式复杂,任何基于正则的清洗都是「够用」而不是「正确」。

后来我换了个思路:摘要只从第一个自然段取,且只在这段里做最保守的清洗。宁可偶尔多显示一点符号,也不要为了干净而错误地吃掉正文。

五、反垃圾保护不覆盖自定义接口

站点的评论表单有「反垃圾保护」,我一度以为它是全局的。直到自己写了个图片评论的接口,测的时候用脚本连发二十条,一条都没被拦。

去看源码才发现,那个开关挂在原生评论表单的提交路径上,自定义的 API 端点不在它的管辖范围内。这不是 bug,是设计边界,但文档里不会写这一条。

结果就是:任何绕开原生表单的写入口,都得自己补防护。我现在在接口里加了频率限制和关键字过滤,虽然简陋,至少不会被人一晚上灌一万条。

六、迁移之后

现在后台保存的响应时间稳定在百毫秒级,全站搜索也不再有那种「点下去先愣一下」的迟滞感。

回头看,这次迁移的体力活不多,脑力活全在「理解框架替你做了什么」上。框架替你加引号、替你取主键、替你判断哪些内容需要渲染——这些便利平时是隐形的,只有在你想抄近路的时候才会变成一堵墙。

所以如果你也在做类似的迁移,我的建议是:先花半小时把框架相关的源码翻一遍。这半小时省下的是后面三个小时的困惑。

上一篇 博客正文布局对照:单栏 / 两栏 / 三栏 下一篇 用了三十天的矮轴键盘,我留下了它

暂无评论

Ctrl + Enter 发送

还没有评论,来说点什么吧

克己笔记
Theme Kmmer Robes