11.4 手动完成从Python对象到数据库中行的映射

    我们可以将SQL的行映射为类定义,这样就可以基于数据库的数据创建适当的Python对象实例。如果谨慎处理数据库和类定义,该过程不会非常复杂。然而,如果不够谨慎,可能构造出的Python对象的SQL表示逻辑就会非常复杂。复杂度的其中一个因素是在对象和数据库行之间的映射包含了大量的查询。在面向对象涉及与SQL数据库中所规定的约束之间找到平衡。

    我们需要修改类定义来更好地与SQL实现相结合。在第10章“用Shelve保存和获取对象”中还会对BlogPost类定义做一些修改。

    以下是一个Blog类的定义。

    from collections import defaultdict
    class Blog:
      def init( self, *kw ):
        """Requires title"""
        self.id= kw.pop('id', None)
        self.title= kw.pop('title', None)
        if kw: raise TooManyValues( kw )
        
    self.entries= list() # ???
      def append( self, post ):
        self.entries.append(post)
      def by_tag(self):
        tag_index= defaultdict(list)
        
    for post in self.entries: # ???
          for tag in post.tags:
            tag_index[tag].append( post )
        return tag_index
      def as_dict( self ):
        return dict(
          title= self.title,
          underline= "="
    len(self.title),
          entries= [p.as_dict() for p in self.entries],
        )

    将一个数据库ID作为对象的第1级是允许的。进一步说,我们修改了初始化过程,完全基于关键字。每个关键字的值来自kw参数。再有额外的值则会触发TooManyValues异常。

    还有两个问题的答案仍没有得到,如何处理一个博客所对应的文章列表?我们会修改如下的类来添加这个功能,以下是一个Post类的定义。

    import datetime
    class Post:
      def init( self, *kw ):
        """Requires date, title, rst_text."""
        self.id= kw.pop('id', None)
        self.date= kw.pop('date', None)
        self.title= kw.pop('title', None)
        self.rst_text= kw.pop('rst_text', None)
        self.tags= list()
        if kw: raise TooManyValues( kw )
      def append( self, tag ):
        self.tags.append( tag )
      def as_dict( self ):
        return dict(
          date= str(self.date),
          title= self.title,
          underline= "-"
    len(self.title),
          rst_text= self.rst_text,
          tag_text= " ".join(self.tags),
        )

    因为对于Blog而言,我们会将数据库ID作为对象第1级的一部分。进而,我们修改了初始化过程,完全基于关键字。如下是异常类的定义。

    class TooManyValues( Exception ):
      pass

    一旦完成了类定义,需要写一个访问层用于完成在数据库与类对象之间的数据转换。这个访问层将提供一个更复杂的实现版本,用于完成 Python 类到数据表中行的转换和适配。

    11.4.1 为SQLite设计一个访问层

    由于本例的对象模型很小,因此可以在一个简单的类中完成整个访问层的实现。这个类中将包含一些方法,用于每个持久化类的CRUD操作。在更大的应用中,我们需要对访问层进行解耦,并针对每个持久化类将它分为各自的策略类。然后会统一放在一个访问层中,这一层可以是外观模式或是封装。

    以下例子并不会包括访问层中所有方法完整的实现,只会介绍一些重要的信息。我们会将其分为几个不同的节来介绍BlogsPosts和迭代器,以下是访问层的第1部分。

    class Access:
      get_last_id= """
      SELECT last_insert_rowid()
      """
      def open( self, filename ):
        self.database= sqlite3.connect( filename )
        self.database.row_factory = sqlite3.Row
      def get_blog( self, id ):
        query_blog= """
        SELECT * FROM BLOG WHERE ID=?
        """
        row= self.database.execute( query_blog, (id,) ).fetchone()
        blog= Blog( id= row['ID'], title= row['TITLE'] )
        return blog
      def add_blog( self, blog ):
        insert_blog= """
        INSERT INTO BLOG(TITLE) VALUES(:title)
        """
        self.database.execute( insert_blog, dict(title=blog.title) )
        row = self.database.execute( get_last_id ).fetchone()
        blog.id= row[0]
        return blog

    在这个类中,设置了Connection.row_factorysqlite3.Row类,而不是一个简单的元组,Row类允许通过数字索引和列名来访问。

    get_blog()方法从所获取的数据库行中构造了一个Blog对象。因为我们使用的是sqlite3.Row对象,因此可以通过名字来引用列。这次可以看出SQL和Python类之间的映射关系。

    add_blog()方法基于blog对象,向BLOG表中插入了一行记录。这个操作分为两步:首先,创建新行;然后执行一个SQL查询来获取新行的ID。

    注意,表定义中使用了 INTEGER PRIMARY KEY AUTOINCREMENT。因此,表的主键会和新行的ID相匹配并且新行的ID可以通过last_insert_rowid()函数来获取。这样我们就能拿到所分配的新行ID,进而可以存在Python对象中并为之后的使用作准备。以下代码中演示了我们如何从数据库中获取一个单独的Post对象。

    def get_post( self, id ):
      query_post= """
      SELECT FROM POST WHERE ID=?
      """
      row= self.database.execute( query_post, (id,) ).fetchone()
      post= Post( id= row['ID'], title= row['TITLE'],
        date= row['DATE'], rst_text= row['RST_TEXT'] )

      query_tags= """
      SELECT TAG.

      FROM TAG JOIN ASSOC_POST_TAG ON TAG.ID = ASSOC_POST_TAG.TAG_ID
      WHERE ASSOC_POST_TAG.POST_ID=?
      """
      results= self.database.execute( query_tags, (id,) )
      for id, tag in results:
        post.append( tag )
      return post

    为了创建Post,需要做两个查询。首先,从POST表中获取一行记录来创建Post对象的一部分。然后,从TAG表中查询所引用的记录。这一步用于创建与Post对象相关的标签列表。

    当保存一个Post对象时,通常包括几个部分。先要向POST表中添加一条记录。并且,需要向ASSOC_POST_TAG表中添加所连接的行。如果一个标签是新的,那么就需要向TAG表中添加一行。如果标签已经存在,那么只需更新文章所引用的标签ID。以下是add_post()方法的实现。

    def add_post( self, blog, post ):
      insert_post="""
      INSERT INTO POST(TITLE, DATE, RST_TEXT, BLOG_ID)
        VALUES(:title, :date, :rst_text, :blog_id)
      """
      query_tag="""
      SELECT * FROM TAG WHERE PHRASE=?
      """
      insert_tag= """
      INSERT INTO TAG(PHRASE) VALUES(?)
      """
      insert_association= """
      INSERT INTO ASSOC_POST_TAG(POST_ID, TAG_ID) VALUES(:post_id,:tag_id)
      """
      with self.database:
        self.database.execute(
    insert_post,
          dict(title=post.title, date=post.date,
            rst_text=post.rst_text, blog_id=blog.id) )
        row = self.database.execute(
    get_last_id).fetchone()
        post.id= row[0]
        for tag in post.tags:
          tag_row= self.database.execute(
    query_tag, (tag,)
    ).fetchone()
          if tag_row is not None:
            tag_id= tag_row['ID']
          else:
            self.database.execute(
    insert_tag, (tag,))
            row = self.database.execute(
    get_last_id
    ).fetchone()
            tag_id= row[0]
          self.database.execute(
    insert_association,
            dict(tag_id=tag_id,post_id=post.id))
      return post

    对于发布一篇文章的操作,数据库中分为这么几个 SQL 步骤。可以使用 insert_post语句在POST表中创建新行,也可以使用通用的get_last_id查询来返回新插入的POST行所对应的主键。

    query_tag语句用于查询标签在数据库中是否已存在。如果查询结果不为None,说明找到了一个TAG行,并且这行的ID也是存在的。否则,必须先使用insert_tag语句创建新行,然后使用get_last_id查询来获取所分配的ID。

    ASSOC_POST_TAG表中的记录关联了POST和相关的标签。insert_association语句创建了所需的行。以下是两种不同风格的查询,用于查找blogsposts

    def blog_iter( self ):
      query= """
      SELECT * FROM BLOG
      """
      results= self.database.execute( query )
      for row in results:
        blog= Blog( id= row['ID'], title= row['TITLE'] )
        yield blog
    def post_iter( self, blog ):
      query= """
      SELECT ID FROM POST WHERE BLOG_ID=?
      """
      results= self.database.execute( query, (blog.id,) )
      for row in results:
        yield self.get_post( row['ID'] )

    blog_iter()方法查找了所有的BLOG行并且基于这些行创建blog实例。

    post_iter()用于查找出与一个BLOG ID相关的所有POST ID。POST ID将被get_post()方法用于创建Post实例。由于get_post()POST表执行另外一个查询,因此这两个方法还是可以被进一步优化的。

    11.4.2 实现容器的关系

    我们所定义的blog类包含了两个功能,它们需要获取博客内的所有文章。Blog.entries属性和Blog.by_tag()方法中都做了假设:在一个博客中包含了完整的文章实例的集合。

    为了实现这个功能,Blog类必须考虑到Access对象,这样它就可以使用Access. post_iter()方法来实现Blog.entries,对此我们有两种设计方式。

    • 使用一个全局的Access对象可以简单有效地工作。我们必须要确保全局的数据库连接被正确地打开了,而有时使用全局的Access对象也会带来一些挑战。
    • 将Access对象注入需要保存的Blog对象中。这种方式会麻烦些,因为需要对每一个数据库中相关联的对象进行操作。

    因为每个数据库相关的对象都由Access类来创建,Access类的设计需要使用工厂模式,对于这个工厂可以考虑3点需要改动的地方。以下几点可以确保一个博客或文章是由激活的Access对象创建的。

    • 每个return blog语句需要扩展为blog._access = self; return blog ,需要在get_blog()、add_blog()和blog_iter()中做这个改动。
    • 每个return post语句需要扩展为post._access = self; return post,需要在get_post()、add_post()和post_iter()中做这个改动。
    • 修改add_blog()方法,可以接收参数来创建Blog对象,而不是直接接收一个在Access工厂外部创建好的Blog或Post对象。定义可能会是def add_blog (self , title):。
    • 修改add_post()方法,接收一个博客对象和一些参数来创建一个Post对象。定义可能看起来是def add_post(self, blog, title,date,rst_text,tags):。

    一旦将_access属性注入到每个Blog实例中,代码如下所示。

    @property
    def entries( self ):
     return self._access.post_iter( self )

    这将返回一个博客所对应的文章列表。这样就可以在类定义中创建一些方法,用于处理父子关系的对象,就好像它们包含在对象的内部一样。