欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

ZODB 入门

shiping 的头像

ZODB 入门

如何通过面向对象的动态语言 Python 使用对象数据库

关系数据库并不是企业中的 Python 程序员唯一可用的解决方案。通常,对象数据库可能更适合解决某些问题。本文将讨论 ZODB,ZODB 是一个可扩展和冗余的对象数据库,其专注于存储可扩展的对象,而没有天生的“对象-关系”不匹配情况;在尝试将面向对象的语言与关系查询系统映射对象建立关系时,可能会出现这种不匹配情况。

Noah Gift, 软件工程师, Giftcs

Brandon Craig Rhodes(brandon@rhodesmill.org), 软件工程师

2008 年 6 月 19 日

引言

正如采用静态类型化的语言进行编程的朋友们一样,Python 和其他动态语言的程序员通常使用关系数据库作为后端数据存储区。无论他们是使用 SQL 来直接定义数据库表,还是使用他们的框架或 ORM 作为备选方案提供的模式语言,所有此类解决方案都提供了相似的工作流:应用程序设计人员必须指定每个类的实例将支持的每个属性的名称、类型和约束,相应地,实例化的数据库关系要能够持久化那些对象的实例。

十多年以前,Zope Corporation 最先提供了一种备选方案,该备选方案旨在更好地适应动态语言的数据模型:Zope 对象数据库(Zope Object Database,ZODB),该数据库专注于存储可扩展的对象而不是传统的关系。今天,ZODB 已成为由 Zope 提供支持的 Web 应用程序的最常用数据库引擎。

应该使用 ZODB 还是使用关系数据库之上的 ORM,这个决定涉及到重要的权衡,本文将对此问题进行探索,并提供了 ZODB 实例的基本维护指南。

来自 Jim Fulton 的 ZODB 提示

ZODB 包括一个 BTree 实现,允许定义和使用关系样式的结构。Zope“目录”的用法非常类似于关系表。用于根对象的对象不是非常可伸缩。大型集合应该使用放在根对象(当然包括根对象之下)中的 Btree。此外,根对象的键并不仅限于字符串。

Jim Fulton 是谁?

Jim Fulton 是 Zope 对象数据库的创建者,并且是其维护人员之一。Jim 还是 Zope 对象发布环境 (Zope Object Publishing Environment) 的创建者之一,并且是 Zope Corporation 的 CTO。

Jim Fulton 要表达他对 ZODB 的什么看法

ZODB 与关系数据库的关系就如同 Python 与诸如 Java™ 等静态类型化语言的关系。对于许多应用程序,尤其是涉及到复杂对象的应用程序,诸如 ZODB 等数据库要容易处理得多。这是 ZODB 成为 Zope 应用程序的最常用后端的关键原因。

对象数据库和关系数据库之间的另一个重要区别在于,对象数据库存储已经组装好的对象。在关系数据库中,无法表示为基本数据值的记录的对象必须通过数据库联结进行组装。无论是在概念上还是在计算上,这可能代价非常高并且非常麻烦。

ZODB 对其提供的服务采用了一种最低要求的方法。它提供基本的持久性,但也仅此而已。诸如安全性和索引等其他服务必须在应用程序级别提供。此方法具有优点和缺点。它允许 ZODB 开发人员集中于核心服务,并使得创新更加容易,但是也让有些人感觉意犹未尽。缺乏数据库级别的安全模型使得充分利用 ZEO 存储服务器变得非常困难,因为客户端或多或少地拥有对数据库的自由访问,因此客户端必须受信任。

为 ZODB 构建索引功能是相当简单,但是除了 Python 以外,不存在任何其他查询语言。例如,要从人员集合中选择名为 Smith 的人员,我们可以使用 Python 列表理解 (list comprehension):

        [p for p in people if p.last_name == 'Smith']

但这是一个线性搜索。拥有某种能够自检的查询语言和索引结构将是非常理想 的。另一方面,如果查询是特别的,则不大可能有索引可用。在对象数据库 中,大多数非特别查询都是不必要的。

 

ZODB 的功能

对于拥有关系数据库经验并且刚接触 ZODB 的人来说,也许有两个 ZODB 功能是最引人注目的:首先,在 ZODB 中,您以层次结构存储对象;其次,ZODB 对象绝对没有任何模式,并且可以动态获得或失去实例属性。下面让我们依次研究一下其中每个功能。

ZODB 数据库中的所有对象都是从“根对象”开始的层次结构的一部分——根对象是一个相当通用的容器对象,由数据库引擎自动创建以包含数据库中的所有其他对象。其工作方式类似于 Python 字典,允许将对象放在由字符串类型的键确定的名称处(并在以后从该位置删除)。因此,如果程序员没有更大的野心,而是只想存储一些通过简单或分隔文本字符串建立索引的对象,则可以直接使用根对象,这与可以使用旧式的 Berkeley 数据库来创建低代价和持久的关联数组的方式非常类似。

但是在将容器对象放到根对象中,然后根对象反过来又可以包含其他对象时,ZODB 就真正派上了用场。与根对象一样,容器对象旨在维护键的列表并将某个对象与每个键相关联。容器可以支持将字符串作为其键值,就像根对象本身一样,或者可以使用整数作为备选键值系统。

通过将容器放在容器中,就可以形成对象的层次结构。此能力使得 ZODB 在实现内容管理系统的程序员中非常流行!当显示用于公共使用时,只需将 ZODB 对象容器称为“文件夹”,用户将非常愉快地导航对象层次结构,就像是导航熟悉的文件系统一样。Plone 内容管理系统大量使用了这一概念。

下面讨论一下 ZODB 和关系数据库之间的其他主要区别:ZODB 对象完全缺乏任何模式规范。ZODB 数据库中的对象允许具有任何实例属性的集合。可以随心所欲地将这些属性放在对象上并在以后删除。此外,除了 ZODB 仅支持由诸如数值、字符串、元组、列表和字典等基本的可挑选 Python 类型的组合组成的属性值以外,没有施加任何类型限制。

这种行为非常适合于 Python 本身,不过与静态类型化的语言形成了非常鲜明对比,后者在最初设计某个对象并编写其定义时指定该类的对象所能拥有的属性列表。Python 对象的属性行为就像是一个可在运行时向其添加新键和删除旧键的关联数组。

当程序员编写类似如下的代码时: myhost.hostname = 'aix.example.com' ,该程序员不是在作为“myhost”对象固有属性的名为“hostname”的某种预定义和受约束的存储区中存储该字符串。相反,他们是在创建或覆盖与“hostname”属性相关联的值,该属性仅在最初分配时才存在,如果执行以下简单操作,该属性将从对象中消失: del myhost.hostname .

当然,经验丰富的 Python 程序员是非常明智和有条理的,一般不会使用这种自由来任意创建和删除属性。通常,他们将使用类的 __init__() 初始化方法来为他们曾经打算使用的每个实例属性分配初始或缺省值。这意味着该对象的方法中的后续代码可以假设每个属性都存在,因此只需使用“self.hostname”(举例而言)而不必担心该属性是否已设置。

如果 Python 程序员不进一步在实例化时创建特定的对象属性,他们必须在访问属性之前使用诸如 hasattr() 等函数来检查属性的存在性,或者使用 try...except 子句来捕获在尝试访问不存在的属性时引发的 AttributeError 异常。

但是尽管如此,非正式 Python 规则(在实例化时创建所有实例属性)与静态类型化的语言中可用的实践(创建所谓的类型签名)之间存在重要的区别。因此,希望使用关系数据库的 Python 程序员一般会发现,他们在自己身上强加了额外和不必要的规则,也就是为自己的每个类型提供模式。

Python 程序员并不总是反对模式,模式的设计目的是为了驱动 Web 框架中的表单生成和约束检查,或者为了定义动态组件框架中的接口。但也有些非常特定的情形,其中当类对外面向其他组件或面向用户。当使用仅在内部可见的类时,对许多 Python 程序员来说,只是为了获取关系数据库中的持久性工作而必须提供数据定义可能是相当麻烦的。当然,一旦他们这样做,则会发现向类添加新属性——通常是非常简单的操作——变成了有时很难理解的关系数据库模式修改方面的工作。

由于所有这些原因,在尝试使用关系数据库来持久化诸如 Python 等动态语言中的对象时,关系数据库导致了显著的不匹配。

 

如何使用 ZODB

为了介绍如何使用 ZODB,以及说明其属性,我们将演示三个基本操作:首先,我们将介绍如何建立和断开到 ZODB 的连接。第二,我们将直接在位于数据库根的键下面存储然后删除一些简单的值和数据结构。第三,我们将持久化一些实际的 Python 对象,并说明其属性得到了自动存储。

我们必须留出一些时间来介绍如何创建用以持久化完整对象层次结构的容器对象。

建立和断开与 ZODB 的连接

连接到 ZODB 的标准方法涉及到创建四个对象:存储数据库数据的方法、围绕存储并为存储提供实际数据库行为“db”包装、启动与该数据库的特定会话的“connection”对象,最后是允许我们访问包含在数据库中的对象层次结构的根的“dbroot”对象。以下所有示例都需要将下面这同一个代码片段放在 Python 文件中的示例代码前面,以便正确地打开和关闭 ZODB 实例:

使用 ZODB
        # myzodb.py
        
        from ZODB import FileStorage, DB
        import transaction
        
        class MyZODB(object):
          def __init__(self, path):
            self.storage = FileStorage.FileStorage(path)
            self.db = DB(self.storage)
            self.connection = self.db.open()
            self.dbroot = self.connection.root()
        
        def close(self):
          self.connection.close()
          self.db.close()
          self.storage.close()

请注意,上面的代码片段没有使用“transaction”模块;我们导入它是因为下面的示例将使用到它。

 

存储简单 Python 数据

Zope 数据库可以存储所有类型的 Python 对象。下面的脚本存储几个值:

存储简单 Python 数据
        # store_simple.py - place some simple data in a ZODB
        
        from myzodb import MyZODB, transaction
        db = MyZODB('./Data.fs')
        dbroot = db.dbroot
        dbroot['a_number'] = 3
        dbroot['a_string'] = 'Gift'
        dbroot['a_list'] = [1, 2, 3, 5, 7, 12]
        dbroot['a_dictionary'] = { 1918: 'Red Sox', 1919: 'Reds' }
        dbroot['deeply_nested'] = {
          1918: [ ('Red Sox', 4), ('Cubs', 2) ],
          1919: [ ('Reds', 5), ('White Sox', 3) ],
          }
        transaction.commit()
        db.close()

然后以下脚本将重新打开数据库,并证明所有的值都已完好地存储:

获取简单数据
        # fetch_simple.py - show what's in the database
        
        from myzodb import MyZODB
        db = MyZODB('./Data.fs')
        dbroot = db.dbroot
        for key in dbroot.keys():
          print key + ':', dbroot[key]
        db.close()

注意:我们使用文件名“Data.fs”纯粹是为了遵循传统,因为许多 ZODB 安装在使用特定文件名方面实际上已实现了标准化;但是您可以随心所欲地使用任何名称。当我们在本文的其他部分提到“Data.fs”文件时,实际上是指“在其中放置 Zope 数据库的任何文件”。

当您将某个键设置为新值时,ZODB 始终能够了解这一点。因此,对上面的数据库进行的类似如下的更改将会自动被检测和持久化:

dbroot['a_string'] = 'Something Else' transaction.commit() db.close()

您需要显式地将对列表或字典的更改告诉 ZODB,因为 ZODB 无法了解所做的更改。这是持久性框架中的一个规定的可变性和参与功能。下面的代码不会导致随后的“fetch_simple.py”运行将会看到的更改:

        # broken code!
        a_dictionary = dbroot['a_dictionary']
        a_dictionary[1920] = 'Indians'
        transaction.commit()
        db.close()

如果打算修改——而不是完全替换——类似如此的复杂对象,您需要设置数据库根的属性 _p_changed,以通知它需要重新存储其下的属性:

        a_dictionary = dbroot['a_dictionary']
        a_dictionary[1920] = 'Indians'
        db._p_changed = 1
        transaction.commit()
        db.close()

如果随后重新运行“fetch_simple.py”,您将看到该更改已正确地持久化。

删除对象是非常简单的,如下所示:

        del dbroot['a_number']
        transaction.commit()
        db.close()

请注意,如果没有调用 transaction.commit(),则上面的所有示例都不会对数据库产生任何影响。正如在关系数据库中一样,只有通过提交已执行的操作,这些操作才会出现在数据库中。

 

持久化对象

当然,很少有 Python 程序员希望使用上面的诸如列表、元组和字典等日益复杂的数据结构。相反,他们希望创建全能的 Python 对象,并且其属性将会自动地持久化。下面创建一个很小的 Python 文件,其中定义一个可持久化到数据库中的类型。

为此,该类将必须从“Persistent”继承。(请注意,由于 Python 允许多重继承,该类从“Persistent”继承的要求无论如何也不应该阻止您自己为数据库做好准备的类从其他基类继承。)

A model
        # mymodel.py - a tiny object model
        
        from persistent import Persistent
        
        class Host(Persistent):
          def __init__(self, hostname, ip, interfaces):
            self.hostname = hostname
            self.ip = ip
            self.interfaces = interfaces

现在我们可以创建该类的几个实例,并在 ZODB 中将其持久化,就像我们持久化上面的简单数据结构一样:

存储对象
        # store_hosts.py
        
        from myzodb import MyZODB, transaction
        db = MyZODB('./Data.fs')
        dbroot = db.dbroot
        
        from mymodel import Host
        host1 = Host('www.example.com', '192.168.7.2', ['eth0', 'eth1'])
        dbroot['www.example.com'] = host1
        host2 = Host('dns.example.com', '192.168.7.4', ['eth0', 'gige0'])
        dbroot['dns.example.com'] = host2
        
        transaction.commit()
        db.close()

下面的脚本将重新打开数据库,并证明所有的主机对象都已成功地持久化(通过检查所获取的每个项的类型,并忽略自从上次运行第一个示例以来仍然保留在 ZODB 中的所有对象):

获取对象
        # fetch_hosts.py - show hosts in the database
        
        from myzodb import MyZODB
        db = MyZODB('./Data.fs')
        dbroot = db.dbroot
        
        from mymodel import Host
        for key in dbroot.keys():
          obj = dbroot[key]
          if isinstance(obj, Host):
            print "Host:", obj.name
            print "  IP address:", obj.ip, "  Interfaces:", obj.interfaces
        
        db.close()

正如“dbroot”对象能够自动检测在其键索引处放置的值一样,持久对象将在您设置其属性时自动执行检测,并将属性保存到数据库。因此下面的代码将更改第一个主机的 IP 地址:

        host = dbroot['www.example.com']
        host.ip = '192.168.7.141'
        transaction.commit()
        db.close()

但是,如果您在某个对象下面存储复杂数据类型,则会出现与连接到数据库根的复杂数据类型完全相同的问题。下面的代码不将其更改持久化到数据库中,因为 ZODB 无法看到该更改已发生:

        # broken code!
        host = dbroot['www.example.com']
        host.interfaces.append('eth2')
        transaction.commit()
        db.close()

相反,您必须像前面一样设置 _p_changed 属性;但是,这次您必须在该对象本身而不是 在数据库根上设置该属性,因为对象充当其下属性自己的根:

        host = dbroot['www.example.com']
        host.interfaces.append('eth2')
        host._p_changed = 1
        transaction.commit()
        db.close()

在运行此改进的代码之后,您应该能够重新运行上面的“fetch_hosts.py”脚本,并看到该主机的确已获得了一个接口。

 

例行维护

ZODB 数据库实例很容易维护。由于 ZODB 数据库实例不包含需要设计或修改的模式,要执行的唯一例行维护是定期压缩以防止其耗尽整个磁盘。

数据库管理员已经习惯于现代关系数据库的行为,除非定期执行 SQL“VACUUM”命令,否则关系数据库的表文件通常会在磁盘上无限地增长。差不多是由于相同的原因——即为了支持事务回滚——写到 ZODB 数据库的每个新的更改实际上都追加到“Data.fs”文件,而不是更新该文件中已经存在的信息。要删除随着事务提交而积累的旧信息,ZODB 管理员必须偶尔对其数据进行压缩。

虽然当今的许多关系数据库提供了自动清理功能来对每个表定期运行“VACUUM”——也许是按设定的间隔,或者可能是在已写入一定量的新数据之后——ZODB 当前没有提供这样的功能。相反,ZODB 管理员通常将建立一个 cron 作业来定期执行该操作。每日压缩通常完美地适合于不会每小时通过其 ZODB 发布大量新信息的站点。

存在两种执行压缩的方法。如果您是在运行直接打开“Data.fs”的简单脚本,例如上面演示的脚本,则需要创建一个打开“Data.fs”然后对最终的数据库对象运行“pack”命令的小脚本:

        db = DB(storage)
        db.pack()

由于没有任何两个程序能够同时打开“Data.fs”文件,当任何其他脚本正在运行时,您不能运行压缩脚本。

如果与采用直接打开“Data.fs”的脚本不同,您是在使用 ZEO 服务器(将在下一个部分中介绍),则压缩将会更加简单——并且还不要求客户端断开连接!只需使用 ZODB 附带的“zeopack”命令行工具:

        $ zeopack -h localhost -p 8080

这将连接到 ZEO 服务器,并发出一条导致服务器压缩数据库的特殊指令。通常最好在数据库上的负载非常轻的时候运行该命令;许多站点在每天早晨开始营业之前运行该命令。

 

使用 ZEO 提供远程访问

虽然上述所有示例脚本都只是直接打开本地“Data.fs”文件,但是大多数生产 ZODB 安装将其数据库作为服务器来运行。ZODB 服务器产品名为“Zope Enterprise Objects”(ZEO),并且与 ZODB 代码本身打包在一起。由于一次只有一个程序能够安全地打开“Data.fs”文件,ZEO 服务器是支持来自多个客户端的连接的唯一方法。当数据库在支持多个安排为负载平衡配置的前端服务器时,这一点特别关键。

许多只有一个客户端的数据库选择使用 ZEO 服务器,因为——正如前一部分所描述的——这允许在不必断开客户端的情况下压缩数据库(或自己执行数据库压缩)。

虽然您应该阅读 ZEO 文档(可在 ZODB 源代码中的 doc/ZEO/howto.txt 中找到)以了解更多详细信息,但是一般可以为 ZEO 创建一个类似如下的配置文件:

         >zeo]
           address zeo.example.com:8090
           monitor-address zeo.example.com:8091
           </zeo>
         
         <filestorage 1>
           path /var/tmp/Data.fs
           </filestorage>
         
         <eventlog>
         <logfile>
             path /var/tmp/zeo.log
             format %(asctime)s %(message)s
             </logfile>
             </eventlog>

一旦编写了这个配置文件,运行 zeo 实例就非常简单了,如下所示:

        $ zeoctl ... start

正如标准的 UNIX®“init.d”脚本一样,“zeoctl”命令也接受诸如“status”和“stop”等子命令。

在指定您希望与哪个 ZEO 服务器通信方面,不同的 ZEO 客户端具有不同的方法。例如,Zope 实例具有“zope.conf”文件,其中带有类似如下的节:

        <zodb>
          <filestorage>
            path /srv/myapp/var/Data.fs
          </filestorage>
        </zodb>

如果希望从自己的某个程序连接,可以将上面给出的“MyZODB”示例类替换为某个连接到 ZEO 的类:

Zeo client
      from ZEO.ClientStorage import ClientStorage
      from ZODB import DB
      
      class MyRemoteZODB(object):
        def __init__(self, server, port):
          server_and_port = (server, port)
          self.storage = ClientStorage(server_and_port)
          self.db = DB(self.storage)
          self.connection = self.db.open()
          self.dbroot = self.connection.root()
      
        def close(self):
          self.connection.close()
          self.db.close()
          self.storage.close()
      
      mydb = MyRemoteZODB('localhost', 8080)
      dbroot = mydb.dbroot

最终的“dbroot”对象完全可以像上面演示的“dbroot”对象一样使用,并将对远程 ZODB 实例执行与上面的脚本对本地“Data.fs”文件所执行的完全相同的操作。

 

复制和冗余解决方案

复制是否对数据库可用,这通常决定了是否可以将该数据库部署为企业中的存储基础设施。对于任务关键型应用程序,与数据库能够在不可避免的硬件和服务器故障中存活下来而不遭遇停机的要求相比,开发人员的任何灵活性和方便性都显得不那么重要了。

传统的冗余 ZODB 实现是“Zope Replication Services”套件,由 Zope Corporation 以商业化的方式提供。该公司的集群系统支持读-写主服务器,然后该服务器将修改的内容转发到任意数量的只读辅助服务器,读取器负载可通过任何标准虚拟服务器前端分布在这些辅助服务器之间。当主服务器发生故障时,系统将故障转移到只读模式,直到将其中某个辅助服务器升级为主服务器。

如果想了解更多信息,请参见 Zope Corporation 的网站。 最近出现了名为“ZEORaid”的开放源代码解决方案,更富有冒险精神的开发人员可能希望试用一下。该解决方案使用与 RAID 文件服务器相同的技术,允许整个 ZEO 数据库集群以冗余的方式操作,并且所有服务器都参与读和写。有关更多信息,请参见 http://pypi.python.org/pypi/gocept.zeoraid/

 

结束语

尽管对象数据库似乎有点新奇,但它们对于面向对象的动态语言的用户非常有用。它们在对象结构方面的灵活性非常适合于动态对象的对应属性,并且其层次结构是对内容管理系统层次结构性质的自然补充。使用 ZODB,所有这些功能都在客户机/服务器配置中交付,并且在添加某种可用的集群解决方案的情况下,可以实现企业级的可靠性。

不过,最后还是让我们了解一下关键词。ZODB 是一个前沿产品,并在若干重要项目中使用,包括 Plone(一个基于 Python 的企业品质的内容管理系统) 和 Grok(一个下一代 Python Web 应用程序框架)。如果您真正希望动手使用对象数据库,请转到参考资料部分并仔细阅读 Grok 教程。您将能够在非常短的时间内构建具有持久对象的 Web 应用程序。这应该能够使您大致了解持久对象为 Web 应用程序开发带来了多少强大的功能,并且您可能会自问“为什么我以前没有想到它?”

来自 http://www.ibm.com/developerworks/cn/aix/library/au-zodb/


普通分类: