示例:储存文章信息

在构建应用程序的时候,我们经常会需要批量地设置和获取多项信息。以博客程序为例子:

  • 当用户想要注册成为博客的作者时,程序就需要把这位作者的名字、账号、密码、注册时间等多项信息储存起来,并在用户登录的时候取出这些信息。

  • 又比如说,当博客的作者想要撰写一篇新文章的时候,程序就需要把文章的标题、内容、作者、发表时间等多项信息储存起来,并在用户阅读文章的时候取出这些信息。

通过使用 MSET 命令、 MSETNX 命令以及 MGET 命令,我们可以实现上面提到的这些批量设置操作和批量获取操作。比如代码清单 2-3 就展示了一个文章储存程序,这个程序使用 MSET 命令和 MSETNX 命令将文章的标题、内容、作者、发表时间等多项信息储存到不同的字符串键里面,并通过 MGET 命令从这些键里面获取文章的各项信息。


代码清单 2-3 文章储存程序:/string/article.py

  1. from time import time # time() 函数用于获取当前 Unix 时间戳
  2.  
  3. class Article:
  4.  
  5. def __init__(self, client, article_id):
  6. self.client = client
  7. self.id = str(article_id)
  8. self.title_key = "article::" + self.id + "::title"
  9. self.content_key = "article::" + self.id + "::content"
  10. self.author_key = "article::" + self.id + "::author"
  11. self.create_at_key = "article::" + self.id + "::create_at"
  12.  
  13. def create(self, title, content, author):
  14. """
  15. 创建一篇新的文章,创建成功时返回 True ,
  16. 因为文章已存在而导致创建失败时返回 False 。
  17. """
  18. article_data = {
  19. self.title_key: title,
  20. self.content_key: content,
  21. self.author_key: author,
  22. self.create_at_key: time()
  23. }
  24. return self.client.msetnx(article_data)
  25.  
  26. def get(self):
  27. """
  28. 返回 ID 对应的文章信息。
  29. """
  30. result = self.client.mget(self.title_key,
  31. self.content_key,
  32. self.author_key,
  33. self.create_at_key)
  34. return {"id": self.id, "title": result[0], "content": result[1],
  35. "author": result[2], "create_at": result[3]}
  36.  
  37. def update(self, title=None, content=None, author=None):
  38. """
  39. 对文章的各项信息进行更新,
  40. 更新成功时返回 True ,失败时返回 False 。
  41. """
  42. article_data = {}
  43. if title is not None:
  44. article_data[self.title_key] = title
  45. if content is not None:
  46. article_data[self.content_key] = content
  47. if author is not None:
  48. article_data[self.author_key] = author
  49. return self.client.mset(article_data)

这个文章储存程序比较长,让我们来逐个分析它的各项功能。首先,Article 类的初始化方法 init() 接受一个 Redis 客户端和一个文章 ID 作为参数,并将文章 ID 从数字转换为字符串:

  1. self.id = str(article_id)

接着程序会使用这个字符串格式的文章 ID ,构建出用于储存文章各项信息的字符串键的键名:

  1. self.title_key = "article::" + self.id + "::title"
  2. self.content_key = "article::" + self.id + "::content"
  3. self.author_key = "article::" + self.id + "::author"
  4. self.create_at_key = "article::" + self.id + "::create_at"

在这些键当中,第一个键将用于储存文章的标题,第二个键将用于储存文章的内容,第三个键将用于储存文章的作者,而第四个键则会用于储存文章的创建时间。

当用户想要根据给定的文章 ID 创建具体的文章时,他就需要调用 create() 方法,并传入文章的标题、内容以及作者作为参数。create() 方法会把以上这些信息以及当前的 UNIX 时间戳放入到一个 Python 字典里面:

  1. article_data = {
  2. self.title_key: title,
  3. self.content_key: content,
  4. self.author_key: author,
  5. self.create_at_key: time()
  6. }

article_data 字典的键储存了代表文章各项信息的字符串键的键名,而与这些键相关联的则是这些字符串键将要被设置的值。接下来,程序会调用 MSETNX 命令,对字典中给定的字符串键进行设置:

  1. self.client.msetnx(article_data)

因为 create() 方法的设置操作是通过 MSETNX 命令来进行的,所以这一操作只会在所有给定字符串键都不存在的情况下进行:

  • 如果给定的字符串键已经有值了,那么说明与给定 ID 相对应的文章已经存在。在这种情况下,MSETNX 命令将放弃执行设置操作,并且 create() 方法也会向调用者返回 False 表示文章创建失败。

  • 与此相反,如果给定的字符串键尚未有值,那么 create() 方法将根据用户给定的信息创建文章,并在成功之后返回 True

在成功创建文章之后,用户就可以使用 get() 方法去获取文章的各项信息了。get() 方法会调用 MGET 命令,从各个字符串键里面取出文章的标题、内容、作者等信息,并把这些信息储存到 result 列表中:

  1. result = self.client.mget(self.title_key,
  2. self.content_key,
  3. self.author_key,
  4. self.create_at_key)

为了让用户可以更方便地访问文章的各项信息,get() 方法会将储存在 result 列表里面的文章信息放入到一个字典里面,然后再返回给用户:

  1. return {"id": self.id, "title": result[0], "content": result[1],
  2. "author": result[2], "create_at": result[3]}

这样做的好处有两点:

  • 它隐藏了 get() 方法由 MGET 命令实现这一底层细节。如果程序直接向用户返回 result 列表,那么用户就必须知道列表中的各个元素代表文章的哪一项信息,然后通过列表索引来访问文章的各项信息。这种做法非常不方便,而且也非常容易出错。

  • 返回一个字典可以让用户以 dict[key] 这样的方式去访问文章的各个属性,比如使用 article["title"] 去访问文章的标题,使用 article["content"] 去访问文章的内容,诸如此类,这使得针对文章数据的各项操作可以更方便地进行。

另外要注意的一点是,虽然用户可以通过访问 Article 类的 id 属性来获得文章的 ID ,但是为了方便起见,get() 方法在返回文章信息的时候也会将文章的 ID 包含在字典里面一并返回。

对文章信息进行更新的 update() 方法是整个程序最复杂的部分。首先,为了让用户可以自由选择需要更新的信息项,这个函数在定义时使用了 Python 的具名参数特性:

  1. 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 字典里面:

  1. article_data = {}
  2. if title is not None:
  3. article_data[self.title_key] = title
  4. if content is not None:
  5. article_data[self.content_key] = content
  6. if author is not None:
  7. article_data[self.author_key] = author

article_data 字典中的键就是需要更新的字符串键的键名,而与之相关联的则是这些字符串键的新值。

在一切准备就绪之后,update() 方法会根据 article_data 字典中设置好的键值对,调用 MSET 命令对文章进行更新:

  1. self.client.mset(article_data)

以下代码展示了这个文章储存程序的使用方法:

  1. >>> from redis import Redis
  2. >>> from article import Article
  3. >>> client = Redis(decode_responses=True)
  4. >>> article = Article(client, 10086) # 指定文章 ID
  5. >>> article.create('message', 'hello world', 'peter') # 创建文章
  6. True
  7. >>> article.get() # 获取文章
  8. {'id': '10086', 'title': 'message', 'content': 'hello world',
  9. 'author': 'peter', 'create_at': '1551199163.4296808'}
  10. >>> article.update(author="john") # 更新文章的作者
  11. True
  12. >>> article.get() # 再次获取文章
  13. {'id': '10086', 'title': 'message', 'content': 'hello world',
  14. '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::titlearticle::10086::author 这些键来说,article 前缀表明这些键都储存着与文章信息相关的数据,而分隔符 :: 则区分开了键名里面的前缀、ID 以及具体的属性。除了 :: 符号之外,常用的键名分隔符还包括 . 符号,比如 article.10086.title ;或者 -> 符号,比如 article->10086->title ;又或者 | 符号,比如 article|10086|title ;诸如此类。

分隔符的选择通常只是一个个人喜好的问题,而键名的具体格式也可以根据需要进行构造:比如说,如果你不喜欢 article::<id>::<attribute> 格式,那么也可以考虑使用 article::<attribute>::<id> 格式,诸如此类。唯一需要注意的是,一个程序应该只使用一种键名分隔符,并且持续地使用同一种键名格式,以免造成混乱。

通过使用相同的格式去命名逻辑上相关联的键,我们可以让程序产生的数据结构变得更容易被理解,并且在有需要的时候,还可以根据特定的键名格式,在数据库里面以模式匹配的方式查找指定的键。