从零开始写博客系统——数据持久化(文件)
背景
前面的文章,我们写了我们的博客系统,也对我们的博客系统进行了测试,基本上已经有了一个博客系统的雏形,但是整个系统有一个问题 ,那就是我们的数据是在内存中保存的,如果我们把服务停掉了,那么我们新增修改的数据就全部没有了,这样的系统无疑是没用的。
为了解决这个问题,我们要对数据进行持久化,也就是我们的数据不仅仅在内存中存在,也需要在磁盘中存储这部分数据。
数据持久化的方案一般就是两种,文件存储和数据库的方式存储数据。本文我们介绍文件的方式存储。
设计思路
文件存储的方式其实就是把内存中的文件在磁盘中存储一份,在服务启动的时候读取这个文件的数据到内存里面,后续的流程就跟我们之前是一模一样的。
其中有几个注意点。
- 启动服务的时候需要检查文件是否存在,不存在则读取默认数据
- 读取文件数据的时候需要检查文件是否完整
- 新增和修改的时候需要把内容重新备份一份到文件内
上代码
[在article.py](http://在article.py)
中新增一个FileCache
类
# article.py
class FileCache(object):
def __init__(self) -> None:
self._md5 = ""
self._data = []
self._filename = "article_cache"
self._load_cache()
def _file_exist(self):
return os.path.exists(self._filename)
def _load_cache(self):
if self._file_exist():
# 从文件中读取数据
with open(self._filename, "r") as f:
info = f.read()
_md5 = info.split("\n")[0]
_data = json.loads(info.split("\n")[1])
if self._check_md5(_md5, _data):
self._data = _data
self._md5 = _md5
else:
# 从data中读取数据
for x in data.article_list:
at = Article(
x.get("id"),
x.get("title"),
x.get("content"),
x.get("author"),
x.get("created_time"),
x.get("modified_time"),
x.get("category"),
x.get("tag")
)
self._data.append(at.to_dict())
self._dump_cache()
def _dump_cache(self):
self._calc_md5()
with open(self._filename, "w") as f:
f.write(self._md5 + "\n" + json.dumps(self._data))
def _calc_md5(self):
md5hash = hashlib.md5(json.dumps(self._data).encode("utf-8"))
self._md5 = md5hash.hexdigest()
def _check_md5(self, md5info, data):
md5hash = hashlib.md5(json.dumps(data).encode("utf-8"))
return md5hash.hexdigest() == md5info
def to_json(self):
return {
"total": self.total,
"articles": self.articles
}
def append(self, article: Article):
self._data.append(article.to_dict())
self._dump_cache()
def get_by_id(self, id: int) -> Article:
for x in self._data:
if x.get("id") == id:
return Article(**x)
def update_by_id(self, id: int, article: Article) -> bool:
for i, x in enumerate(self._data):
if x.get("id") == id:
self._data[i] = article.to_dict()
self._dump_cache()
return True
return False
def get_categorys(self) -> list:
categorys = []
for x in self._data:
if x.get("category") not in categorys:
categorys.append(x.get("category"))
return categorys
def get_tags(self) -> list:
tags = []
for x in self._data:
for tag in x.get("tag"):
if tag not in tags:
tags.append(tag)
return tags
@property
def total(self):
return len(self._data)
@property
def articles(self):
return self._data
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
修改app.py
中的articles
的对应的类为新类。
# app.py
articles = article.FileCache()
2
3
切换后的结果
按照上面的修改之后,我们运行我们的服务,这个时候就需要用到我们之前写的接口测试用例了。
现在的场景是我们修改了底层获取数据的方式,但是上层对外的接口是不变的,因此我们修改了之后,所有的接口表现应该与原来一致,因此我们可以直接运行我们的测试用例。
python -m unittest test.test_api
.....
----------------------------------------------------------------------
Ran 5 tests in 0.040s
OK
2
3
4
5
6
可以看到,我们所有的接口测试用例都通过了。
同时我们的代码跟目录下会生成一个article_cache
文件,这个文件就是我们的博客内容了。
代码解读
我们创建了一个类,这个类的对外公开的方法,与原来的Articles
类是完全一致的,这种方式的好处就是我们在app.py
切换的时候,直需要替换类名即可,无需修改原来的类。这种方法不会破坏原有代码,一旦编码有问题,可以马上回滚到以前的逻辑,对业务影响的比较小,因此通常在老代码修改的时候会比较常用。
有几个比较特殊的地方特殊说明一下。
- 引入了校验的机制
我这里使用的是把博客内容转字符之后进行一次md5
计算,也就是说如果备份的文件被人为的修改了,那么这份文件就是有问题的,不能使用的。当然我这里加md5
只是一个方式,读者可以使用其他方式,或者对于md5
进行加盐,确保文件一定是程序写的。
- 对于内部的方法和变量,使用下划线开头
这个其实是一个编码规范,类内部的成员,外部不需要感知,因此加下划线对这类数据或者函数进行标识。当然,Python
是一个弱类型语言,就算你加了下划线,外部也依然是可以使用的,但是我们都遵守这样的约定。
- 对于必须要暴露的数据,使用
property
方法暴露出去
这也是一个可选的方案,可能由于类内部的运行机制或者其他原因,在某些命名上会做一些妥协,property
可以很好的进行重命名。当然,更重要的一点是property
可以实现懒加载,在数据量大的时候,会有比较好的效果。
总结
本文我们实现了数据的一个持久化的功能, 保证了我们的数据不会因为程序停止或者挂掉导致数据丢失了,同时我们应用了之前写的接口测试用例对你我们的修改进行测试。
同样,读者实在写不出代码的时候,可以参考:代码浏览 - blog - 从零开始写一个博客系统 - svenweng (coding.net)
思考
我们新写了一个类,那么在写好这个类的时候,最好把测试代码也一起补上。这部分内容与之前雷同,我不单独写,读者可以自行补上。