今天,突然受到了一封邮件,内容是LeanCloud宣布将于2027年1月关闭其数据库服务。
我点进去看了一下,发现是真的。
LeanCloud关闭数据库服务通知
毕竟,我博客上评论区、统计阅读量之类的数据都放在了LeanCloud上,所以这件事对我来说还是挺重要的。

然后,我就上了Waline的官网,发现Waline支持TiDB作为后端数据库,而且看起来免费额度还挺大,教程也是第一个推荐这个,于是就选了它

创建TiDB数据库

第一步,当然是跟着官方教程走,创建数据库。不过,因为官方的文档实在是太旧了,UI界面什么的完全不一样,因此我还花了不少时间摸索了一下。总之,和官方的教程差异如下:

  • Chat2Query变成了 SQL Editor
  • 链接数据库的时候选择 General选项就好了,里面的几个信息分别填入 TIDB_HOSTTIDB_USERTIDB_PASSWORDTIDB_DATABASE

导出LeanCloud数据

接下来,就是导出LeanCloud的数据了。
LeanCloud提供了数据导出的功能,不过导出的数据是JSON格式的,而TiDB需要的是 CSV(150MB以下) 格式的,因此还需要进行一些转换。

我最终让Copilot帮我写了一个Python脚本来完成这个任务,代码如下:

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
import json
import csv

input_file = "input.jsonl"
output_file = "output.csv"

rows = []

# 读取 JSONL
with open(input_file, "r", encoding="utf-8") as f:
for line in f:
if line.strip():
rows.append(json.loads(line))

# 自动收集所有字段作为表头
headers = set()
for r in rows:
headers.update(r.keys())
headers = list(headers)

# 写入 CSV
with open(output_file, "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=headers)
writer.writeheader()
writer.writerows(rows)

print("转换完成!")

看起来还是挺不错的
但是,因为原有的Leancloud转换出来的CSV字段和TiDB的表结构并不完全匹配,所以还需要对CSV进行一些手动的修改,主要是删除一些多余的字段,和调整字段顺序。
我于是又让他帮我写了下面这个脚本:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import csv
import json
from datetime import datetime

def parse_parse_date(value):
if not value:
return r"\N"
try:
obj = json.loads(value.replace("'", '"'))
if isinstance(obj, dict) and obj.get("__type") == "Date":
iso = obj.get("iso")
dt = datetime.fromisoformat(iso.replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d %H:%M:%S")
except:
pass
return value or r"\N"

def to_int_or_null(value):
if value is None:
print(0)
return r"\N"
value = str(value).strip()
print(f"Checking value: '{value}'")
if value.isdigit():
print(1)
return value
return r"\N"

def to_str_or_null(value):
if value is None:
return r""
v = str(value).strip()
return v if v else r""

def nullify(value):
"""把空字符串、None、空白都变成 \\N"""
if value is None:
return r"\N"
v = str(value).strip()
return v if v else r"\N"


input_file = "output.csv"
output_file = "db_ready.csv"

target_fields = [
"id", "user_id", "comment", "insertedAt", "ip", "link", "mail", "nick",
"pid", "rid", "sticky", "status", "like", "ua", "url", "createdAt", "updatedAt"
]

# 读取所有行
rows = []
with open(input_file, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
rows.append(row)

# 建立 objectId → new_id 映射
objectId_to_newId = {row["objectId"]: i for i, row in enumerate(rows)}

# 生成新 CSV 数据
output_rows = []
for row in rows:
new_row = {}

# 新 id
new_row["id"] = objectId_to_newId[row["objectId"]]

# user_id
new_row["user_id"] = to_int_or_null(row.get("user_id", ""))

# 普通字段
for field in ["comment", "link", "mail", "nick", "sticky", "like", "ua", "url"]:
new_row[field] = nullify(row.get(field, ""))

# status and ip use empty string as default (not \N)
new_row["status"] = to_str_or_null(row.get("status", ""))
new_row["ip"] = to_str_or_null(row.get("ip", ""))

# 时间字段
new_row["insertedAt"] = parse_parse_date(row.get("insertedAt", ""))
new_row["createdAt"] = parse_parse_date(row.get("createdAt", ""))
new_row["updatedAt"] = parse_parse_date(row.get("updatedAt", ""))

# pid 映射
old_pid = row.get("pid", "")
new_row["pid"] = objectId_to_newId.get(old_pid, r"\N") if old_pid else r"\N"

# rid 映射
old_rid = row.get("rid", "")
new_row["rid"] = objectId_to_newId.get(old_rid, r"\N") if old_rid else r"\N"

output_rows.append(new_row)

# 写入 CSV
with open(output_file, "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=target_fields)
writer.writeheader()
writer.writerows(output_rows)

print("完成:所有空值已转换为 \\N,适用于 TiDB Lightning 导入。")

这段代码可以自动将已有的 ObjectId 转换为 TiDB 需要的递增 id ,自动将pidrid等需要互相对应的值迁移到新的 id 结构里,将空值填上 \N或者空字符串。

不过,因为用户数据并不在这个Comment数据表里面,需要对应回去的话需要手动查查User的数据表迁移后的各个用户对应id再手动替换一下。

在迁移过程中,我踩过最大的坑应该就是空值的问题了
前期,我一直以为直接在 CSV 里留空就行,结果导入 TiDB 的时候各种报错,后来才发现TiDB Lightning 要求空值必须是 \N才行,于是我加上了 nullify函数来处理这些空值。

不过,总算是顺利完成了数据的迁移工作。
最后,用起来感觉的话,似乎TiDB的网络链接会比LeanCloud要慢一点?查表的时候要等待好一会才能出来,加载博客评论的时间似乎也比LeanCloud要长一些。