前言

Memos 是一个非常好用的开源、自托管的笔记服务。可以说从它刚在Github上发布的时候我就一直在用它了。

但是随着它不断的更新,逐步开发了MySQL以及Postgres数据库的支持,但是一直未推出完善可靠的数据库迁移服务。

最近打算将之前的MySQL迁移到Postgres时,使用pgloader等工具,总能出现各种问题,不尽人意,因此只能手动对其数据迁移。

说明

Memos的本质是笔记服务,因此我的数据基本上都是文字形式,即使有图片,大多数都是以外链形式存在,即Memos本身并不存储图片,从而使得迁移工作相对简单。

如果你的数据中有很多图片,以下方法可能不适合你。

准备

Navicat数据库管理软件

步骤

基于我的Memos是Docker compose搭建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
services:
mysql:
image: mysql:latest
restart: unless-stopped
volumes:
- ./data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: memos
MYSQL_USER: memos
MYSQL_PASSWORD: password
memos:
image: neosmemo/memos:stable
restart: always
depends_on:
- mysql
ports:
- "5230:5230"
environment:
- MEMOS_DRIVER=mysql
- MEMOS_DSN=memos:password@(mysql:3306)/memos

第一步: 修改compose文件

  1. 添加Postgres

  2. 开放对应的数据库端口

最终的 compose 文件基本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
services:
mysql:
image: mysql:latest
restart: unless-stopped
ports:
- "10001:3306" # <--- 1.暴露MySQL端口
volumes:
- ./data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: memos
MYSQL_USER: memos
MYSQL_PASSWORD: password

postgres: # <--- 2.添加postgres数据库
image: postgres:15
restart: unless-stopped
ports:
- "10002:5432" # <--- 3.暴露postgres端口
environment:
POSTGRES_DB: memos
POSTGRES_USER: memos
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data

memos:
image: neosmemo/memos:stable
restart: always
depends_on:
- postgres # <--- 4.改成postgres数据库
ports:
- "5230:5230"
environment:
- MEMOS_DRIVER=postgres # <--- 5.对应更改
- MEMOS_DSN=postgresql://memos:password@postgres:5432/memos?sslmode=disable # <--- 6.对应更改

volumes: # <--- 7.添加卷
postgres_data:

我们分析一下上面的代码:

  • MySQL数据库正常启动,添加了对外的端口:10001

  • 添加Postgres数据库,并添加了对外的端口:10002

  • 将Memos数据库变更为postgres

确定这些后,通过 docker compose up -d 启动容器。

第二步: 初始化Memos

说是初始化,其实也就是因为使用了Postgres,导致这个容器是全新的,登陆页面后会要求创建用户,正常创建即可,尝试发布几个笔记。正常的话进入下一步。

第三步:数据库连接

打开Navicat软件,新建数据库连接,将上述的MySQL和Postgres分别添加进去。

连接前检查服务器或者服务器提供商的防火墙是否开放了对外的 1000110002 端口

最终可以看到:

MySQL的数据储存在【memos】-【memos】中:

Postgres的数据储存在【memos】-【public】-【Tables】-【memos】中:

对比2个数据库的差异,可以看得出来结构一致,区别在与 created_ts 以及 updated_ts ,在MySQL中使用的是日期格式(YYYY-MM-DD HH:MM:SS),而Postgres中使用的是Unix 时间戳 (Unix Timestamp),还有个不同在于 pinned (置顶),MySQL的 0 或者 1 ,与Postgres的 f 以及 t,实际上无需转换。

PostgreSQL 中 BOOLEAN 类型的字段可以接受以下所有输入形式,并自动把它们转换为标准的 boolean

输入值 (不区分大小写) Postgres 理解为
1, yes, on, true, t, y True (t)
0, no, off, false, f, n False (f)

通过这一步可以确认,我们只需将MySQL中的笔记按照Posters的格式导入到现在的数据库中应该就能正常识别。

第四步:导出MySQL数据库中的笔记

右键【Memos】-【Export Wizard…】,选择 csv 格式,一路下一步,直到导出得到 memo.csv

第五步:转换memo.csv中的时间戳

memo.csv上传至服务器或者有python环境的系统中,在同一目录创建一个python脚本convert.py,内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import csv
import time
from datetime import datetime, timezone, timedelta

# 定义输入和输出文件名
input_file = 'memo.csv' # 你的原始文件名
output_file = 'memos_converted.csv' # 转换后的文件名

# 定义时间格式:日/月/年 时:分:秒
date_format = "%d/%m/%Y %H:%M:%S"

# 定义时区:北京时间 (UTC+8)
tz_beijing = timezone(timedelta(hours=8))

def to_timestamp(date_str):
try:
# 解析时间字符串
dt = datetime.strptime(date_str, date_format)
# 强制设定为北京时间,然后转为 Unix 时间戳 (秒)
dt = dt.replace(tzinfo=tz_beijing)
return int(dt.timestamp())
except ValueError:
return date_str # 如果解析失败,返回原值

print("正在转换...")

with open(input_file, mode='r', encoding='utf-8', newline='') as infile, \
open(output_file, mode='w', encoding='utf-8', newline='') as outfile:

reader = csv.reader(infile)
writer = csv.writer(outfile, quoting=csv.QUOTE_ALL) # 保持所有字段带引号

headers = next(reader)
writer.writerow(headers) # 写入表头

for row in reader:
# created_ts 在第4列 (索引3)
row[3] = to_timestamp(row[3])
# updated_ts 在第5列 (索引4)
row[4] = to_timestamp(row[4])

writer.writerow(row)

print(f"转换完成!文件已保存为: {output_file}")

在该目录输入 python3 convert.py 运行该转换脚本。

查看 memos_converted.csv 文件,时间戳已经转换成功:

第六步:导入Postgres数据库中

来到Navicat,清空之前测试的几条数据:(或者在表中直接选中那几条数据进行删除也行)

删除后需要点击底部的 Refresh 刷新才能更新状态

然后点击上方的 【Import】 图标-【csv文件】-【Add File…】选择转换后的memos_converted.csv

一路下一步,在下面界面中选择全部:

继续下一步,直至结束:

完成后还是点击查看底部工具栏的刷新图标,确认导入完成即可。

验证

回到网页,进行刷新,以前的数据已经出现,状态也正确。

🛑🛑🛑特别注意:

测试中这一步我可以正常发布,但是在生产过程中,有几百几千条笔记,每个笔记都有一个uid,因此很可能会遇到在发布的时候会提示:该uid已被使用的错误提示

强烈建议在Navicat中,点击左上角的 【新建查询】 (New Query),运行以下这条命令:

1
2
-- 把 memo 表的发号器设置到当前最大的 ID
SELECT setval('memo_id_seq', (SELECT MAX(id) FROM memo));

确定没问题之后,恢复成正常的compose配置文件即可(去掉mysql,2个端口映射):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
services:
postgres:
image: postgres:15
restart: unless-stopped
environment:
POSTGRES_DB: memos
POSTGRES_USER: memos
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data

memos:
image: neosmemo/memos:stable
restart: always
depends_on:
- postgres
ports:
- "5230:5230"
environment:
- MEMOS_DRIVER=postgres
- MEMOS_DSN=postgresql://memos:password@postgres:5432/memos?sslmode=disable

volumes:
postgres_data:

最后

即使这个方法有一丁点的局限性,无法迁移导出对应上传的图片等资源,仅仅针对文字类的。但对我个人而已已经够用了。

我现在仅演示了从MySQL到Postgres的过程,但是如果是SQLite的话也是一样的流程,具体没有尝试,最多由于表的独特性,可能python脚本会相应变更,这部分交给各类AI工具就能搞定。