示例:存储文章信息

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

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

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

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

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

        fromtimeimporttime  # 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"

           defcreate(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()
                }
                returnself.client.msetnx(article_data)

           defget(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]}

           defupdate(self, title=None, content=None, author=None):
                """
                对文章的各项信息进行更新更新成功时返回True,失败时返回False
                """
                article_data = {}
                iftitle isnotNone:
                    article_data[self.title_key] = title
                ifcontent isnotNone:
                    article_data[self.content_key] = content
                ifauthor isnotNone:
                    article_data[self.author_key] = author
                returnself.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的具名参数特性:

        defupdate(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 = {}
        iftitle isnotNone:
            article_data[self.title_key] = title
        ifcontent isnotNone:
            article_data[self.content_key] = content
        ifauthor isnotNone:
            article_data[self.author_key] = author

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

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

        self.client.mset(article_data)

以下代码展示了这个文章存储程序的使用方法:
        >>> fromredisimportRedis
        >>> fromarticleimportArticle
        >>> 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'}

表2-1展示了上面这段代码创建出的键以及这些键的值。

表2-1 文章数据存储示例

键的命名格式

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>格式,诸如此类。唯一需要注意的是,一个程序应该只使用一种键名分隔符,并且持续地使用同一种键名格式,以免造成混乱。

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