10.5 为shelve设计数据访问层

    下面讲解应用程序中可能的shelve用法,会介绍应用程序中修改和保存博客文章的部分。我们将这个应用分成两层:应用层和数据层。其中,又将应用层分成了两层。

    • 应用处理(Application processing):这些对象不是持久的。这些类会包括程序中所有的行为。这些类会响应用户选择的命令、菜单项、按钮和其他处理元素。
    • 问题域数据模型(Problem domain data model):这些对象会被保存到shelf上,这些对象包含了程序的所有状态。

    前面介绍的博客和文章的例子中,博客和它的文章集合没有显式的联系。它们是互相独立的,所以我们可以在shelf上分别处理它们。我们不想通过把Blog变成一个集合类型来创建一个独立的巨大集合对象。

    在数据层中,依据数据存储的复杂度,可能会有若干特性。我们只关注其中的两个。

    • 访问(Access):这些组件为问题域中的对象提供了统一的访问方法。我们会定义一个提供了访问Blog和Post实例的方法的Access类,它也会管理用于定位shelf上的Blog和Post对象的键。
    • 持久化(Persistence):这些组件将问题域对象序列化之后保存到持久化存储模块中。这是shelve模块。

    我们会将Access类分成3个部分,下面是处理打开和关闭文件的第1个部分。

    import shelve
    class Access:
      def new( self, filename ):
        self.database= shelve.open(filename,'n')
        self.max= { 'Post': 0, 'Blog': 0 }
        self.sync()
      def open( self, filename ):
        self.database= shelve.open(filename,'w')
        self.max= self.database['_DB:max']
      def close( self ):
        if self.database:
          self.database['_DB:max']= self.max
          self.database.close()
        self.database= None
      def sync( self ):
        self.database['_DB:max']= self.max
        self.database.sync()
      def quit( self ):
        self.close()

    我们用Access.new()创建一个新的空shelf。用Access.open()打开一个已经存在的shelf。用Access.close()Access.sync()将当前最大的键保存在shelf上的一个小字典中。

    我们还有一些没有实现的功能,例如,实现用于复制文件的Save As…方法。我们也还没有实现能够恢复到前一个版本的数据库文件的不保存关闭(quit-without-saving)选项。这些额外的功能会使用os模块管理文件。我们已经提供了close()quit()方法。这让设计一个GUI应用程序简单了一些。下面是用来更新shelf上的BlogPost对象的一些不同方法。

    def add_blog( self, blog ):
      self.max['Blog'] += 1
      key= "Blog:{id}".format(id=self.max['Blog'])
      blog._id= key
      
    self.database[blog._id]= blog
      return blog
    def get_blog( self, id ):
      return self.database[id]
    def add_post( self, blog, post ):
      self.max['Post'] += 1
      try:
        key= "{blog}:Post:{id}".format(blog=blog._id,id=self.
    max['Post'])
      except AttributeError:
        raise OperationError( "Blog not added" )
      post._id= key
      post._blog= blog._id
      
    self.database[post._id]= post
      return post
    def get_post( self, id ):
      return self.database[id]
    def replace_post( self, post ):
      
    self.database[post._id]= post
      return post
    def delete_post( self, post ):
      del self.database[post._id]

    我们提供了将Blog和与它相关的Post实例保存到shelf上所需要的最基本的方法。当我们添加Blog时,add_blog()方法首先算出一个新的键,然后用这个键更新Blog对象,最后,它将Blog对象保存到shelf上。我们突出显示了用于修改shelf的代码。简单地在shelf上设置一个元素和在字典中设置一个元素的操作类似,这个操作会把对象保存起来。

    当添加一个Post时,必须提供父对象Blog,这样shelf上的这两个对象才能正确地关联起来。在这种情况下,我们首先获取Blog的键,然后为Post创建一个新键,最后用这个新键更新Post对象。更新后的Post对象可以被保存到shelf上,add_post()中突出显示的行是用于将对象保存到shelf上的。

    试图添加一个没有父对象BlogPost的情形属于异常,在这种情况下,我们会得到属性错误,因为Blog._id属性不存在。

    我们提供了典型的替换Post和删除Post的方法。还有一些可能需要的操作,比如,我们还没有定义替换Blog和删除Blog的方法。当我们实现删除Blog的方法时,必须要决定当还有Posts存在时,选择禁止删除Blog还是级联删除所有相关的Posts。最后,还有一些作为迭代器使用的搜索方法,它们可以用来查询BlogPost实例。

    def iter( self ):
      for k in self.database:
        if k[0] == "_": continue
        yield self.database[k]
    def blog_iter( self ):
      for k in self.database:
        if not k.startswith("Blog:"): continue
        if ":Post:" in k: continue # Skip children
        yield self.database[k]
    def post_iter( self, blog ):
      key= "{blog}:Post:".format(blog=blog._id)
      for k in self.database:
        if not k.startswith(key): continue
        yield self.database[k]
    def title_iter( self, blog, title ):
      return ( p for p in self.post_iter(blog) if p.title == title )

    我们定义了默认的迭代器——iter(),它会返回所有以"_"作为键的开头的内部对象。目前,我们只定义了一个这样的键——_DB:max,但是这样的设计为我们创建其他的键预留了空间。

    blog_iter()方法会遍历所有Blog对象。由于BlogPost对象都是以"Blog:"作为键的开头,因此我们必须显式地丢弃Blog的所有子Post对象。一个更好的方法通常是创建一个定制的索引对象,我们会在下面的章节中介绍和索引有关的主题。

    post_iter()方法遍历某个Blog的所有Post对象。title_iter()方法扫描所有的Post对象并返回所有与给定标题匹配的Post对象,这个操作会扫描shelf上的所有键,所以它有潜在的性能问题。

    我们也必须定义一个用于在某个给定Blog中查找包含特定标题的Post对象的迭代器。这是一个简单的生成器函数,它会重用post_iter()方法并且只返回标题匹配的Post对象。

    编写演示脚本

    我们会用演示脚本技术展示一个应用程序会如何使用这个Access类来处理博客中的对象。演示脚本会保存一些BlogPost对象到数据库中,然后会基于这些数据展示一系列应用程序中可能用到的操作。这些演示脚本可以被扩展为单元测试用例,更完整的单元测试会确保所有的功能都存在并且正常工作。以下演示脚本向我们展示Access是如何工作的。

    from contextlib import closing
    with closing( Access() ) as access:
      access.new( 'blog' )
      access.add_blog( b1 )
      # b1._id is set.
      for post in p2, p3:
        access.add_post( b1, post )
        # post._id is set
      b = access.get_blog( b1._id )
      print( b._id, b )
      for p in access.post_iter( b ):
        print( p._id, p )
      access.quit()

    我们在访问层上创建了Access类,这样它就可以被包含在一个上下文管理器中。这样做的目的是为了确保不管有没有异常发生,都会正确地关闭访问层。

    我们用Access.new()创建了一个新的名为'blog'的shelf。在GUI程序中,这个操作可通过单击File | New完成。然后我们添加了一个新的博客b1到shelf上。Access. add_blog()方法会用它的shelf键来更新Blog对象。在GUI程序中,这个操作有可能是某些人在页面上写了一些内容然后单击了New Blog。

    一旦我们添加了Blog,我们就可以添加两个属于这个BlogPost。父Blog对象中的键会被用来为它的每个孩子Post对象创建键。同样地,在GUI程序中,这个情况下是一个用户在页面填了一些内容,然后单击了New Post。

    最后,还有一些查询会使用shelf上的键和对象。这些查询会向我们展示这个脚本的最终运行结果。我们可以运行Access.get_blog()获取某个已经创建的Blog对象,用Access.post_iter()遍历某个Blog对象的所有子Post对象。最后的Access.quit()确保会保存用来生成唯一键的最大值并且正确地关闭shelf。