示例:储存文章信息
在构建应用程序的时候,我们经常会需要批量地设置和获取多项信息。以博客程序为例子:
当用户想要注册成为博客的作者时,程序就需要把这位作者的名字、账号、密码、注册时间等多项信息储存起来,并在用户登录的时候取出这些信息。
又比如说,当博客的作者想要撰写一篇新文章的时候,程序就需要把文章的标题、内容、作者、发表时间等多项信息储存起来,并在用户阅读文章的时候取出这些信息。
通过使用 MSET
命令、 MSETNX
命令以及 MGET
命令,我们可以实现上面提到的这些批量设置操作和批量获取操作。比如代码清单 2-3 就展示了一个文章储存程序,这个程序使用 MSET
命令和 MSETNX
命令将文章的标题、内容、作者、发表时间等多项信息储存到不同的字符串键里面,并通过 MGET
命令从这些键里面获取文章的各项信息。
代码清单 2-3 文章储存程序:/string/article.py
- from time import time # time() 函数用于获取当前 Unix 时间戳
- class Article:
- def __init__(self, client, article_id):
- self.client = client
- self.id = str(article_id)
- self.title_key = "article::" + self.id + "::title"
- self.content_key = "article::" + self.id + "::content"
- self.author_key = "article::" + self.id + "::author"
- self.create_at_key = "article::" + self.id + "::create_at"
- def create(self, title, content, author):
- """
- 创建一篇新的文章,创建成功时返回 True ,
- 因为文章已存在而导致创建失败时返回 False 。
- """
- article_data = {
- self.title_key: title,
- self.content_key: content,
- self.author_key: author,
- self.create_at_key: time()
- }
- return self.client.msetnx(article_data)
- def get(self):
- """
- 返回 ID 对应的文章信息。
- """
- result = self.client.mget(self.title_key,
- self.content_key,
- self.author_key,
- self.create_at_key)
- return {"id": self.id, "title": result[0], "content": result[1],
- "author": result[2], "create_at": result[3]}
- def update(self, title=None, content=None, author=None):
- """
- 对文章的各项信息进行更新,
- 更新成功时返回 True ,失败时返回 False 。
- """
- article_data = {}
- if title is not None:
- article_data[self.title_key] = title
- if content is not None:
- article_data[self.content_key] = content
- if author is not None:
- article_data[self.author_key] = author
- return self.client.mset(article_data)
这个文章储存程序比较长,让我们来逐个分析它的各项功能。首先,Article
类的初始化方法 init()
接受一个 Redis 客户端和一个文章 ID 作为参数,并将文章 ID 从数字转换为字符串:
- self.id = str(article_id)
接着程序会使用这个字符串格式的文章 ID ,构建出用于储存文章各项信息的字符串键的键名:
- self.title_key = "article::" + self.id + "::title"
- self.content_key = "article::" + self.id + "::content"
- self.author_key = "article::" + self.id + "::author"
- self.create_at_key = "article::" + self.id + "::create_at"
在这些键当中,第一个键将用于储存文章的标题,第二个键将用于储存文章的内容,第三个键将用于储存文章的作者,而第四个键则会用于储存文章的创建时间。
当用户想要根据给定的文章 ID 创建具体的文章时,他就需要调用 create()
方法,并传入文章的标题、内容以及作者作为参数。create()
方法会把以上这些信息以及当前的 UNIX 时间戳放入到一个 Python 字典里面:
- article_data = {
- self.title_key: title,
- self.content_key: content,
- self.author_key: author,
- self.create_at_key: time()
- }
article_data
字典的键储存了代表文章各项信息的字符串键的键名,而与这些键相关联的则是这些字符串键将要被设置的值。接下来,程序会调用 MSETNX
命令,对字典中给定的字符串键进行设置:
- self.client.msetnx(article_data)
因为 create()
方法的设置操作是通过 MSETNX
命令来进行的,所以这一操作只会在所有给定字符串键都不存在的情况下进行:
如果给定的字符串键已经有值了,那么说明与给定 ID 相对应的文章已经存在。在这种情况下,
MSETNX
命令将放弃执行设置操作,并且create()
方法也会向调用者返回False
表示文章创建失败。与此相反,如果给定的字符串键尚未有值,那么
create()
方法将根据用户给定的信息创建文章,并在成功之后返回True
。
在成功创建文章之后,用户就可以使用 get()
方法去获取文章的各项信息了。get()
方法会调用 MGET
命令,从各个字符串键里面取出文章的标题、内容、作者等信息,并把这些信息储存到 result
列表中:
- result = self.client.mget(self.title_key,
- self.content_key,
- self.author_key,
- self.create_at_key)
为了让用户可以更方便地访问文章的各项信息,get()
方法会将储存在 result
列表里面的文章信息放入到一个字典里面,然后再返回给用户:
- return {"id": self.id, "title": result[0], "content": result[1],
- "author": result[2], "create_at": result[3]}
这样做的好处有两点:
它隐藏了
get()
方法由MGET
命令实现这一底层细节。如果程序直接向用户返回result
列表,那么用户就必须知道列表中的各个元素代表文章的哪一项信息,然后通过列表索引来访问文章的各项信息。这种做法非常不方便,而且也非常容易出错。返回一个字典可以让用户以
dict[key]
这样的方式去访问文章的各个属性,比如使用article["title"]
去访问文章的标题,使用article["content"]
去访问文章的内容,诸如此类,这使得针对文章数据的各项操作可以更方便地进行。
另外要注意的一点是,虽然用户可以通过访问 Article
类的 id
属性来获得文章的 ID ,但是为了方便起见,get()
方法在返回文章信息的时候也会将文章的 ID 包含在字典里面一并返回。
对文章信息进行更新的 update()
方法是整个程序最复杂的部分。首先,为了让用户可以自由选择需要更新的信息项,这个函数在定义时使用了 Python 的具名参数特性:
- def update(self, title=None, content=None, author=None):
通过具名参数,用户可以根据自己想要更新的文章信息项来决定传入哪个参数,而不需要更新的信息项则会被赋予默认值 None
:
比如说,如果用户只想要更新文章的标题,那么只需要调用
update(title=new_title)
即可;又比如说,如果用户想要同时更新文章的内容和作者,那么只需要调用
update(content=new_content, author=new_author)
即可;
诸如此类。
在定义了具名参数之后,update()
方法会检查各个参数的值,并将那些不为 None
的参数以及与之相对应的字符串键键名放入到 article_data
字典里面:
- article_data = {}
- if title is not None:
- article_data[self.title_key] = title
- if content is not None:
- article_data[self.content_key] = content
- if author is not None:
- article_data[self.author_key] = author
article_data
字典中的键就是需要更新的字符串键的键名,而与之相关联的则是这些字符串键的新值。
在一切准备就绪之后,update()
方法会根据 article_data
字典中设置好的键值对,调用 MSET
命令对文章进行更新:
- self.client.mset(article_data)
以下代码展示了这个文章储存程序的使用方法:
- >>> from redis import Redis
- >>> from article import Article
- >>> client = Redis(decode_responses=True)
- >>> article = Article(client, 10086) # 指定文章 ID
- >>> article.create('message', 'hello world', 'peter') # 创建文章
- True
- >>> article.get() # 获取文章
- {'id': '10086', 'title': 'message', 'content': 'hello world',
- 'author': 'peter', 'create_at': '1551199163.4296808'}
- >>> article.update(author="john") # 更新文章的作者
- True
- >>> article.get() # 再次获取文章
- {'id': '10086', 'title': 'message', 'content': 'hello world',
- 'author': 'john', 'create_at': '1551199163.4296808'}
表 1-1 展示了上面这段代码创建出的键,以及这些键的值。
表 1-1 文章数据储存示例
被储存的内容 | 数据库中的键 | 键的值 |
---|---|---|
文章的标题 | article::10086::title | 'message' |
文章的内容 | article::10086::content | 'hello world' |
文章的作者 | article::10086::author | 'john' |
文章的创建时间戳 | article::10086::create_at | '1461145575.631885' |
注解
键的命名格式
Article
程序使用了多个字符串键去储存文章信息,并且每个字符串键的名字都是以 article::<id>::<attribute>
格式命名的,这是一种 Redis 使用惯例:Redis 用户通常会为逻辑上相关联的键设置相同的前缀,并通过分隔符来区分键名的各个部分,以此来构建一种键的命名格式。
比如对于 article::10086::title
、 article::10086::author
这些键来说,article
前缀表明这些键都储存着与文章信息相关的数据,而分隔符 ::
则区分开了键名里面的前缀、ID 以及具体的属性。除了 ::
符号之外,常用的键名分隔符还包括 .
符号,比如 article.10086.title
;或者 ->
符号,比如 article->10086->title
;又或者 |
符号,比如 article|10086|title
;诸如此类。
分隔符的选择通常只是一个个人喜好的问题,而键名的具体格式也可以根据需要进行构造:比如说,如果你不喜欢 article::<id>::<attribute>
格式,那么也可以考虑使用 article::<attribute>::<id>
格式,诸如此类。唯一需要注意的是,一个程序应该只使用一种键名分隔符,并且持续地使用同一种键名格式,以免造成混乱。
通过使用相同的格式去命名逻辑上相关联的键,我们可以让程序产生的数据结构变得更容易被理解,并且在有需要的时候,还可以根据特定的键名格式,在数据库里面以模式匹配的方式查找指定的键。