就像他们的朋友使用静态类型的语言进行编程一样,Python和其他动态语言的程序员经常使用关系数据库作为后端数据存储。 无论他们是使用SQL直接定义数据库表还是使用其框架或ORM提供的模式语言作为替代,所有这些解决方案都提供了类似的工作流程:应用程序设计者必须指定每个属性的名称,类型和约束,而不是实例。每个类的实例都将支持,作为回报,实例化数据库关系可以持久化那些对象的实例。
十多年前,Zope公司首先提供了一种替代方案,他们旨在更好地适应动态语言的数据模型:Zope对象数据库(ZODB),该数据库专门研究可扩展对象而不是经典关系的存储。 如今,数据库引擎通常是Zope支持的Web应用程序的基础。
在关系数据库之上使用ZODB还是ORM的决定涉及重要的权衡,我们将在本文中进行探讨,以及维护生产ZODB实例的基本指南。
Jim Fulton的ZODB提示
ZODB包含一个BTree实现,该实现允许定义和使用类似关系的结构。 Zope“目录”可以像关系表一样使用。 用于根对象的对象不是非常可伸缩的。 大型集合应使用放置在根对象(或下面)中的BTree。 而且,根对象的键不限于字符串。
吉姆·富尔顿是谁?
Jim Fulton是Zope对象数据库的创建者和维护者之一。 Jim还是Zope对象发布环境和Zope公司CTO的创建者之一。
Jim Fulton对ZODB的评价
ZODB适用于关系数据库,Python适用于诸如Java™之类的静态类型语言。 对于许多应用程序,尤其是涉及复杂对象的应用程序,像ZODB这样的数据库要容易得多。 这是ZODB成为Zope应用程序流行的后端的关键原因。
对象数据库和关系数据库之间的另一个重要区别是,对象数据库存储已组装的对象。 在关系数据库中,不能表示为基本数据值记录的对象必须通过数据库联接进行组装。 从概念上和计算上,这可能是非常昂贵和麻烦的。
ZODB对提供的服务采取了低限度的方法。 它提供了基本的持久性,别无其他。 必须在应用程序级别提供其他服务,例如安全性和索引编制。 这种方法具有缺点。 它使ZODB开发人员可以专注于核心服务并使其更易于创新,但是却使某些人想要更多。 缺乏数据库级别的安全模型使得难以充分利用ZEO存储服务器,因为客户端或多或少地不受限制地访问数据库,因此必须受到信任。
为ZODB建立索引工具非常简单,但是除了Python之外没有其他查询语言。 例如,要从人员集合中选择名为Smith的人员,我们可以使用Python列表理解:
[p for p in people if p.last_name == 'Smith']
但这是线性搜索。 有一种可以对结构进行内部检查和索引编制的查询语言会很好 。 另一方面,如果查询是临时的,则索引将不可用。 在OODB中,大多数非临时查询都是不必要的。
ZODB的功能
接触关系数据库经验丰富的人可能会对ZODB的两个功能印象深刻:首先,在ZODB中,您将对象存储在层次结构中; 其次,ZODB对象没有架构,并且可以即时获取或丢失实例属性。 让我们依次看一下这些功能。
ZODB数据库中的所有对象都是以“根对象”开头的层次结构的一部分,“根对象”是由数据库引擎自动创建的,包含数据库中所有其他对象的相当通用的容器对象。 它就像Python字典一样,允许将对象放置在由字符串类型的键标识的名称上(然后从中删除)。 因此,如果程序员的野心不超过存储一些由简单或定界文本字符串索引的对象,则可以直接使用根对象,类似于老式的伯克利数据库可用于创建廉价且持久的关联的方式数组。
但是,当将容器对象放入根对象时,ZODB真正发挥了自己的作用,而根对象又可以容纳其他对象。 像根对象一样,容器对象被设计为维护一个键列表并将一个对象与每个键相关联。 容器可以像根对象本身一样支持字符串作为键,也可以使用整数作为备用键系统。
通过将容器放置在容器内部,可以形成对象的层次结构。 这种功能使ZODB在实施内容管理系统的程序员中非常受欢迎! 在显示它们以供公众使用时,只需将它们的ZODB对象容器称为“文件夹”,用户就可以愉快地浏览您的对象层次结构,就像熟悉的文件系统一样。 Plone内容管理系统特别使用了此概念。
现在,对于ZODB和关系数据库之间的另一个巨大区别:ZODB对象完全缺少任何架构规范。 ZODB数据库中的对象被允许具有实例属性的任何集合。 可以将它们放在对象上,然后随意删除。 此外,除了基本限制(ZODB仅支持由基本可腌制的Python类型(例如数字,字符串,元组,列表和字典)的组合组成的属性值)外,没有施加任何类型限制。
这种行为非常适合Python本身,尽管与静态类型语言的做法形成鲜明对比,这种做法是在一个人首先设计一个对象并编写其定义时指定该类的对象可以拥有的属性列表。 Python对象的属性的行为就像一个关联数组,在运行时可以向其中添加新键,而删除旧键。
当程序员编写如下内容: myhost.hostname = 'aix.example.com'
,程序员不会将字符串存储在某种名为“ hostname”的预定义且受约束的插槽中,该插槽是“ myhost”的固有属性目的。 相反,它们将创建或覆盖与“主机名”属性关联的值,该属性仅在分配给该主机名时才存在,并且如果执行以下简单操作,该值就会从对象中消失: 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”包装以提供其实际的数据库行为,一个“连接”对象以与之开始特定的对话数据库,后是一个“ 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()
请注意,上面的代码片段未使用“事务”模块。 我们导入了它,因为下面的示例将使用它。
存储简单的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'
- dbroot._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”继承的要求也绝不会阻止您自己的数据库就绪类也从其他基类继承。)
一个模型
- # 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数据库实例易于维护。 由于它不包含需要设计或修改的架构,因此需要执行的常规维护是定期打包以防止其消耗整个磁盘。
数据库管理员已经习惯了现代关系数据库的行为,除非定期执行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企业对象”(ZEO),它与ZODB代码本身打包在一起。 由于一次只能安全地打开一个程序“ Data.fs”文件,因此ZEO服务器是支持来自多个客户端的连接的方法。 当数据库支持以负载平衡配置排列的多个前端服务器时,这尤其重要。
甚至许多只有一个数据库客户端的安装都选择使用ZEO服务器,因为-如上一节所述-这允许打包数据库而无需客户端断开连接(或自行执行打包)。
您应该阅读ZEO文档(可在doc / ZEO / howto.txt中的ZODB源代码中找到该文档),以获取更多详细信息,但通常会为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客户
- 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公司提供的“ Zope复制服务”套件。 他们的集群系统支持读写主服务器,然后将更改转发到任意数量的只读辅助服务器,其中任何标准虚拟服务器前端都可以分配读取器负载。 当主服务器发生故障时,系统将退回到只读模式,直到其中一台辅助服务器被提升为主服务器为止。
近出现了一个名为“ ZEORaid”的开源解决方案,更多冒险家开发人员可能想尝试一下。 它使用与RAID文件服务器相同的技术,以允许整个ZEO数据库集群进行冗余操作,所有服务器都参与读取和写入操作。 有关更多信息,请参见http://pypi.python.org/pypi/gocept.zeoraid/ 。
结论
尽管对象数据库看起来有些奇怪,但对于动态,面向对象的语言的用户而言,它们是有用的工具。 它们在对象结构方面的灵活姿态非常适合动态对象的相应属性,并且它们的层次结构是内容管理系统层次结构性质的自然补充。 使用ZODB,所有这些功能都在客户端/服务器配置中提供,并且通过添加一种可用的群集解决方案,可以将其带到企业级的健壮性。
终,让我们进入重点。 ZODB是前沿的东西,它被用于多个重要项目中,包括Plone(基于Python的企业质量的内容管理系统)。 以及下一代Python Web应用程序框架Grok。 如果您真的想动手使用对象数据库,请转至“ 资源”部分,并阅读《 Grok教程》。 您将能够构建一个在短时间内具有持久对象的Web应用程序。 这应该使您了解持久性对象为Web应用程序开发带来了多少功能,您可能会问自己,我为什么以前没有想到这一点?
翻译自: https://www.ibm.com/developerworks/aix/library/au-zodb/index.html