跳转到主要内容

事务性对象数据库,纯Python实现。

项目描述

概述

Dobbin是一种快速方便的方式,将Python对象图持久化到磁盘。

对象图由持久节点组成,这些节点是基于持久基类之一的对象

from dobbin.persistent import Persistent

foo = Persistent()
foo.bar = 'baz'

每个节点都可以连接任意对象;唯一的要求是Python的pickle模块可以序列化这些对象。

持久对象是全面向象的

class Frobnitz(Persistent):
    ...

对象图是通过对象引用构建的

foo.frob = Frobnitz()

要提交更改到磁盘,我们使用transaction模块中的commit()方法。请注意,我们必须首先选择一个根对象,从而将对象图连接到数据库句柄

from dobbin.database import Database

jar = Database('data.fs')
jar.elect(foo)

transaction.commit()

因此,如果我们想更改图中的一个或多个对象,我们首先必须对相关的对象进行检出

from dobbin.persistent import checkout

checkout(foo)
foo.bar = 'boz'

transaction.commit()

checkout(obj)函数将对象置于共享状态。它只适用于持久节点。

Dobbin适用于Python 2.6及更高版本,包括Python 3.x。

主要功能

  • 100% Python,完全符合PEP8

  • 线程尽可能共享数据

  • 多线程、多进程MVCC并发模型

  • 高效的二进制数据块存储和流传输

  • 可插拔架构

获取代码

您可以从Python包索引下载此软件包,或使用setuptools或更新的distribute(Python 3.x所需)安装最新版本。

$ easy_install dobbin

注意,这将安装transaction模块作为依赖项。

该项目托管在GitHub仓库中。欢迎代码贡献。最简单的方法是使用pull request接口。

作者和许可证

由Malthe Borch编写 <mborch@gmail.com>。

此软件基于BSD许可证提供。

注释

常见问题

本节列出了常见问题。

  1. Dobbin与ZODB有何不同?

    Python中还有其他对象数据库可供使用,最著名的是Zope公司提供的ZODB

    主要区别

    • Dobbin 100%用Python编写。ZODB中的持久化层是用C编写的。ZODB还支持B-Trees,这也是用C编写的。

    • Dobbin可在Python 3上使用(但需要POSIX系统)。

    • ZODB支持B-Trees,允许进程按需加载对象(因为隐式弱引用)。Dobbin目前一次性加载所有数据并保持其在内存中。

    • Dobbin使用一种持久化模型,尝试在线程之间共享活动对象中的数据,但依赖于显式操作将对象置于允许对其进行更改的模式。ZODB仅共享不活动对象数据。

    • ZODB包含ZEO,这是一个企业级网络数据库层,允许不同机器上的进程连接到同一数据库。

    • ZODB包含一个内存管理系统,根据使用情况将对象数据从内存中驱逐。Dobbin不试图管理内存消耗,而是调用虚拟内存管理器将不活动对象数据交换到磁盘。

  2. 数据库文件格式是什么?

    默认存储选项将事务顺序写入单个文件。

    每个事务由多个记录组成,这些记录包含一个Python pickle和有时附加的数据负载(在这种情况下,pickle包含控制信息)。最后,事务以一个事务记录对象结束,也是一个Python pickle。

  3. 我能否用多个进程连接到单个数据库?

    是的。

    默认存储选项将事务写入单个文件,这本身构成了存储记录。多个进程可以连接到同一文件并共享相同的数据库,并发连接。无需进一步配置;数据库使用POSIX文件锁定以确保独占写入访问,并且进程会自动保持同步。

  4. 我如何限制内存消耗?

    为了避免内存颠簸,限制Python进程的物理内存配额,并确保有足够的虚拟内存可用(至少与数据库大小相同)[1]

    您可能想使用--without-pymalloc标志编译Python以使用原生内存分配。这可能在连接到大型数据库的应用程序中提高性能,因为更好的分页。

用户指南

这是数据库的主要文档。它使用交互式叙事,同时也作为doctest。发行版中包含了一系列回归测试。

您可以在命令行提示符下运行以下命令来执行测试

$ python setup.py test

设置

默认存储选项将事务顺序写入单个文件。它针对长时间运行的过程进行了优化,例如应用服务器。

第一步是初始化一个数据库对象。为了配置它,我们在文件系统上提供一个路径。该路径无需事先存在。

>>> from dobbin.database import Database
>>> db = Database(database_path)

此特定路径尚不存在。这是一个新数据库。我们可以通过使用 len 方法来确定存储的对象数量来验证它。

>>> len(db)
0

数据库使用对象图持久性模型。对象必须通过Python引用与数据库的根节点进行传递性连接。

由于这是一个空数据库,还没有根对象。

>>> db.root is None
True

持久对象

任何持久对象都可以被选为数据库根对象。持久对象必须继承自 Persistent 类。这些对象构成了并发模型的基础;重叠的事务可能会写入一组不重叠的对象(提供冲突解决机制以简化此要求)。

>>> from dobbin.persistent import Persistent
>>> obj = Persistent()

持久对象从 本地 状态开始。在此状态下,我们可以读取和写入属性。然而,当我们想要写入以前已持久化到数据库中的对象时,我们必须显式使用 checkout 方法来检查它。我们将很快看到这是如何工作的。

>>> obj.name = 'John'
>>> obj.name
'John'

选择数据库根

我们可以选择此对象作为数据库的根。

>>> db.elect(obj)
>>> obj._p_jar is db
True

该对象现在是对象图的根。要持久化磁盘上的更改,我们需要提交事务。

>>> transaction.commit()

不出所料,数据库包含一个对象。

>>> len(db)
1

tx_count 属性返回已写入数据库的事务数量(成功和失败的)。

>>> db.tx_count
1

检查对象

对象现在已持久化到数据库中。这意味着我们现在必须检查它,才能允许我们写入它。

>>> obj.name = "John"
Traceback (most recent call last):
 ...
TypeError: Can't set attribute on shared object.

我们使用对象的 checkout 方法来改变其状态为本地。

>>> from dobbin.persistent import checkout
>>> checkout(obj)

checkout 方法没有返回值;这是因为对象的标识符实际上从未真正改变。相反,使用自定义属性访问器和修改器方法来提供线程局部对象状态。这对用户来说是透明的。

在检查出对象后,我们可以读取和写入属性。

>>> obj.name = 'James'

当一个对象首次被某个线程检查出来时,会设置一个计数器来跟踪有多少线程检查出了该对象。当它降到零时(总是在事务边界上),它会被撤回到之前的共享状态。

>>> transaction.commit()

这会增加事务计数器一位。

>>> db.tx_count
2

并发

对象管理器(实现低级功能)天生是线程安全的;它使用MMVC并发模型。

数据库必须支持在共享同一数据库的外部进程之间的并发(包含的数据库实现使用文件锁定方案将MVCC并发模型扩展到外部进程;无需配置)。

我们可以通过在同一个线程中运行第二个数据库实例来演示两个单独进程之间的并发。

>>> new_db = Database(database_path)
>>> new_obj = new_db.root

此数据库的对象与第一个数据库的对象不重叠。

>>> new_obj is obj
False

新数据库实例已经读取了先前提交的事务并将它们应用到其对象图上。

>>> new_obj.name
'James'

让我们进一步探讨这个问题。如果我们从第一个数据库实例中检查出一个持久对象并提交更改,那么第二个数据库中的相同对象将在我们开始一个新事务时立即被更新。

>>> checkout(obj)
>>> obj.name = 'Jane'
>>> transaction.commit()

数据库已注册事务;新的实例尚未注册。

>>> db.tx_count - new_db.tx_count
1

对象图没有同步。

>>> new_obj.name
'James'

应用程序必须开始一个新事务以保持同步。

>>> tx = transaction.begin()
>>> new_obj.name
'Jane'

冲突

当并发事务尝试修改相同的对象时,除了第一个获得提交锁的事务外,其他所有事务都会出现写冲突。

对象可以提供冲突解决能力,使得两个并发事务可以更新相同的对象。

例如,让我们创建一个计数器对象;它可以代表一个跟踪网站访问者的计数器。为了为这个类的实例提供冲突解决,我们实现一个 _p_resolve_conflict 方法。

>>> class Counter(Persistent):
...     def __init__(self):
...         self.count = 0
...
...     def hit(self):
...         self.count += 1
...
...     @staticmethod
...     def _p_resolve_conflict(old_state, saved_state, new_state):
...         saved_diff = saved_state['count'] - old_state['count']
...         new_diff = new_state['count']- old_state['count']
...         return {'count': old_state['count'] + saved_diff + new_diff}

作为一个doctest技术细节,我们设置类在builtins模块上(Python 2.x系列和3.x系列之间存在差异,这解释了回退导入位置)。

>>> try:
...     import __builtin__ as builtins
... except ImportError:
...     import builtins
>>> builtins.Counter = Counter

接下来,我们实例化一个计数器实例,然后将其添加到对象图中。

>>> counter = Counter()
>>> checkout(obj)
>>> obj.counter = counter
>>> transaction.commit()

为了演示这个类的冲突解决功能,我们在两个并发事务中更新计数器。我们将尝试其中一个事务在单独的线程中。

>>> from threading import Semaphore
>>> flag = Semaphore()
>>> flag.acquire()
True
>>> def run():
...     counter = db.root.counter
...     assert counter is not None
...     checkout(counter)
...     counter.hit()
...     flag.acquire()
...     try: transaction.commit()
...     finally: flag.release()
>>> from threading import Thread
>>> thread = Thread(target=run)
>>> thread.start()

在主线程中,我们检查相同的对象并赋予不同的属性值。

>>> checkout(counter)
>>> counter.count
0
>>> counter.hit()

释放信号量后,线程将提交事务。

>>> flag.release()
>>> thread.join()

当我们提交主线程中的事务时,我们期望计数器增加了两次。

>>> transaction.commit()
>>> counter.count
2

更多对象

在将持久对象保存在数据库中之前,它们必须连接到对象图。如果我们检查一个持久对象并提交事务,但没有将其添加到对象图中,将抛出一个异常。

>>> another = Persistent()
>>> from dobbin.exc import ObjectGraphError
>>> try:
...     transaction.commit()
... except ObjectGraphError as exc:
...     print(str(exc))
<dobbin.persistent.LocalPersistent object at ...> not connected to graph.

我们取消事务并再次尝试,这次通过属性引用连接对象。

>>> transaction.abort()
>>> checkout(another)
>>> another.name = 'Karla'
>>> checkout(obj)
>>> obj.another = another

我们提交事务并观察到对象计数已增长。新对象还被分配了一个oid(这些通常不可预测;它们是在提交时由数据库分配的)。

>>> transaction.commit()
>>> len(db)
3
>>> another._p_oid is not None
True

如果我们开始一个新事务,新对象将传播到第二个数据库实例。

>>> tx = transaction.begin()
>>> new_obj.another.name
'Karla'

当我们检查带有引用的对象并访问任何属性时,后台会制作共享状态的深拷贝。然而,持久对象永远不会被复制,简单的身份检查可以确认这一点。

>>> checkout(obj)
>>> obj.another is another
True

允许循环引用。

>>> checkout(another)
>>> another.another = obj
>>> transaction.commit()

再次,我们可以验证身份。

>>> another.another is obj
True

存储文件

我们可以通过将其封装在 持久文件 包装器中来持久化打开的文件(或任何流对象)。包装器是不可变的;它仅限单次使用。

>>> from tempfile import TemporaryFile
>>> file = TemporaryFile()
>>> length = file.write(b'abc')
>>> pos = file.seek(0)

请注意,文件是从当前位置读取到文件末尾。

>>> from dobbin.persistent import PersistentFile
>>> pfile = PersistentFile(file)

让我们将这个持久文件作为我们对象的属性来存储。

>>> checkout(obj)
>>> obj.file = pfile
>>> transaction.commit()

请注意,持久文件已被赋予了一个新的类。它是对同一对象(在对象身份方面)的引用,但由于它现在存储在数据库中,并且仅作为文件流可用,所以我们称之为 持久流

>>> obj.file
<dobbin.database.PersistentStream object at ...>

我们必须手动关闭提供给持久包装器的文件(或者让它超出范围)。

>>> file.close()
>>> pfile.closed
True

使用持久流

有两种使用持久流的方法;要么通过迭代它,在这种情况下它将自动获得文件句柄(在垃圾收集器回收迭代器时隐式关闭),要么通过文件API。

我们使用 open 方法打开流;当使用流作为文件时,始终需要这样做。

>>> obj.file.open()
>>> print(obj.file.read().decode('ascii'))
abc

seek 和 tell 方法按预期工作。

>>> int(obj.file.tell())
3

我们可以回到文件开头并重复练习。

>>> obj.file.seek(0)
>>> print(obj.file.read().decode('ascii'))
abc

像任何文件一样,使用后必须关闭。

>>> obj.file.close()

此外,我们可以使用迭代来读取文件;在这种情况下,我们不需要担心打开或关闭文件。这会自动为我们完成。注意,这使得持久流适合作为 WSGI 应用程序的返回值。

>>> print("".join(thunk.decode('ascii') for thunk in obj.file))
abc

迭代严格独立于其他方法。我们可以观察到文件保持关闭状态。

>>> obj.file.closed
True

启动新事务(以触发数据库同步)并确认文件来自第二个数据库。

>>> tx = transaction.begin()
>>> print("".join(thunk.decode('ascii') for thunk in new_obj.file))
abc

持久字典

通常不建议在数据库中存储记录时使用内置的 dict 类型,特别是如果您预计会有频繁的微小变化。相反,应使用 PersistentDict 类(直接使用或子类化)。

它作为一个正常的 Python 字典运行,并提供相同的方法。

>>> from dobbin.persistent import PersistentDict
>>> pdict = PersistentDict()

检查对象并连接到对象图。

>>> checkout(obj)
>>> obj.pdict = pdict

您可以存储任何与标准字典兼容的键/值组合。

>>> pdict['obj'] = obj
>>> pdict['obj'] is obj
True

PersistentDict 还存储属性。请注意,属性和字典条目是相互独立的。

>>> pdict.name = 'Bob'
>>> pdict.name
'Bob'

提交更改。

>>> transaction.commit()
>>> pdict['obj'] is obj
True
>>> pdict.name
'Bob'

快照

我们可以使用 snapshot 方法合并所有数据库事务,直到给定的时间戳,并将快照作为单个事务写入新数据库。

>>> tmp_path = "%s.tmp" % database_path
>>> tmp_db = Database(tmp_path)

要包括所有事务(即当前状态),只需传递目标数据库。

>>> db.snapshot(tmp_db)

快照包含三个对象。

>>> len(tmp_db)
4

它们在单个事务中持久化。

>>> tmp_db.tx_count
1

我们可以确认状态确实与当前数据库的状态相匹配。

>>> tmp_obj = tmp_db.root

对象图与原始数据库相等。

>>> tmp_obj.name
'Jane'
>>> tmp_obj.another.name
'Karla'
>>> tmp_obj.pdict['obj'] is tmp_obj
True
>>> tmp_obj.pdict.name
'Bob'

快照中也包含二进制流。

>>> print("".join(thunk.decode('ascii') for thunk in tmp_obj.file))
abc

清理

>>> transaction.commit()
>>> db.close()
>>> new_db.close()
>>> tmp_db.close()

这个故事到此结束。

更改

0.3 (2012-02-02)

  • 添加对 Python 3 的支持。

  • 当可用时,使用 C 优化的 pickle 模块。

0.2 (2009-10-22)

  • 子类现在可以覆盖现有方法(例如 __setattr__)并使用 super 获取覆盖的方法。

  • 事务现在可以隔离地看到数据。

  • 当持久对象首次创建时,其状态立即是局部的。这允许 __init__ 方法初始化对象。

  • 添加了创建现有数据库时间点快照的方法。

  • 添加了 PersistentDict 类。

  • Persistent 类现在以更改集的形式持久化,而不是完整的对象状态。

  • 设置使用 nose 测试运行器(或使用 setuptools)运行的测试。

0.1 (2009-09-26)

  • 首次公开发布。

项目详情


下载文件

下载适合您平台的自定义文件。如果您不确定选择哪一个,请了解更多关于 安装软件包 的信息。

源代码分发

dobbin-0.3.tar.gz (27.8 kB 查看哈希)

上传时间 源代码

支持