PostgreSQL/JSONB持久性后端
pjpersist的Python项目详细描述
PostgreSQL/JSONB数据持久性
本文概述了^{tt1}的一般功能$ 包裹。pjpersist是的postgresql/jsonb存储实现 持久的python对象。它不是zodb的存储空间。
pjpersist的目标是提供序列化的数据管理器 对象到事务边界处的jsonb blob。PJ数据管理器是 持久数据管理器,它处理事务边界上的事件(请参见 transaction.interfaces.IDataManager)以及来自 持久性框架(请参见persistent.interfaces.IPersistentDataManager)。
数据管理器的实例应该与 事务,这意味着假设您创建了一个新的数据管理器 创建新事务时:
>>> import transaction
注意:conn对象是一个psycopg.Connection实例。在这种情况下 我们的测试使用pjpersist_test数据库。
现在我们定义一个简单的持久对象:
>>> import datetime >>> import persistent>>> class Person(persistent.Persistent): ... ... def __init__(self, name, phone=None, address=None, friends=None, ... visited=(), birthday=None): ... self.name = name ... self.address = address ... self.friends = friends or {} ... self.visited = visited ... self.phone = phone ... self.birthday = birthday ... self.today = datetime.datetime(2014, 5, 14, 12, 30) ... ... def __str__(self): ... return self.name ... ... def __repr__(self): ... return '<%s %s>' %(self.__class__.__name__, self)
稍后我们将填写其他对象。但现在,让我们创建一个新的 将其放入PJ:
>>> stephan = Person(u'Stephan') >>> stephan <Person Stephan>
datamanager提供了一个root属性,对象树在其中生根 可以储存。它的特殊之处在于它可以立即写入数据 到数据库:
>>> dm.root['stephan'] = stephan >>> dm.root['stephan'] <Person Stephan>
自定义持久性表
默认情况下,持久对象存储在具有转义的 类的python路径:
>>> from pjpersist import serialize >>> person_cn = serialize.get_dotted_name(Person, True) >>> person_cn 'u__main___dot_Person'>>> transaction.commit() >>> dumpTable(person_cn) [{'data': {u'_py_persistent_type': u'__main__.Person', u'address': None, u'birthday': None, u'friends': {}, u'name': u'Stephan', u'phone': None, u'today': {u'_py_type': u'datetime.datetime', u'value': u'2014-05-14T12:30:00.000000'}, u'visited': []}, 'id': u'0001020304050607080a0b0c0'}]
如您所见,为此人存储的文档看起来非常像 自然json文档。但是哦不,我忘了指定 斯蒂芬。让我们这样做:
>>> dm.root['stephan'].name = u'Stephan Richter' >>> dm.root['stephan']._p_changed True
这一次,数据不会自动保存:
>>> fetchone(person_cn)['data']['name'] u'Stephan'
所以我们必须先提交事务:
>>> dm.root['stephan']._p_changed True >>> transaction.commit() >>> dm.root['stephan']._p_changed >>> fetchone(person_cn)['data']['name'] u'Stephan Richter'
现在我们为斯蒂芬添加一个地址。地址也是持久对象:
>>> class Address(persistent.Persistent): ... _p_pj_table = 'address' ... ... def __init__(self, city, zip): ... self.city = city ... self.zip = zip ... ... def __str__(self): ... return '%s (%s)' %(self.city, self.zip) ... ... def __repr__(self): ... return '<%s %s>' %(self.__class__.__name__, self)
pjpersist支持名为_p_pj_table的特殊属性, 它允许您指定要使用的自定义表。
>>> stephan = dm.root['stephan'] >>> stephan.address = Address('Maynard', '01754') >>> stephan.address <Address Maynard (01754)>
请注意,地址不会立即保存在数据库中:
>>> dumpTable('address', isolate=True) relation "address" does not exist ...
但一旦我们提交了事务,一切都可用:
>>> transaction.commit() >>> dumpTable('address') [{'data': {u'_py_persistent_type': u'__main__.Address', u'city': u'Maynard', u'zip': u'01754'}, 'id': u'0001020304050607080a0b0c0'}]>>> dumpTable(person_cn) [{'data': {u'_py_persistent_type': u'__main__.Person', u'address': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'address'}, u'birthday': None, u'friends': {}, u'name': u'Stephan Richter', u'phone': None, u'today': {u'_py_type': u'datetime.datetime', u'value': u'2014-05-14T12:30:00.000000'}, u'visited': []}, 'id': u'0001020304050607080a0b0c0'}]>>> dm.root['stephan'].address <Address Maynard (01754)>
非持久性对象
如您所见,即使引用看起来很好,而且所有组件都很容易 可见的。但如果任意的,非持久的,可挑剔的, 物体?好吧,让我们为此创建一个phone number对象:
>>> class Phone(object): ... ... def __init__(self, country, area, number): ... self.country = country ... self.area = area ... self.number = number ... ... def __str__(self): ... return '%s-%s-%s' %(self.country, self.area, self.number) ... ... def __repr__(self): ... return '<%s %s>' %(self.__class__.__name__, self)>>> dm.root['stephan'].phone = Phone('+1', '978', '394-5124') >>> dm.root['stephan'].phone <Phone +1-978-394-5124>
现在我们提交事务并再次查看jsonb文档:
>>> transaction.commit() >>> dm.root['stephan'].phone <Phone +1-978-394-5124>>>> dumpTable(person_cn) [{'data': {u'_py_persistent_type': u'__main__.Person', u'address': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'address'}, u'birthday': None, u'friends': {}, u'name': u'Stephan Richter', u'phone': {u'_py_type': u'__main__.Phone', u'area': u'978', u'country': u'+1', u'number': u'394-5124'}, u'today': {u'_py_type': u'datetime.datetime', u'value': u'2014-05-14T12:30:00.000000'}, u'visited': []}, 'id': u'0001020304050607080a0b0c0'}]
如您所见,对于任意的非持久性对象,我们需要在 子文档,但非常少。如果__reduce__方法返回 一个更复杂的结构,写更多的元数据。下一步我们会看到的 存储日期和其他任意数据时:
>>> dm.root['stephan'].friends = {'roy': Person(u'Roy Mathew')} >>> dm.root['stephan'].visited = (u'Germany', u'USA') >>> dm.root['stephan'].birthday = datetime.date(1980, 1, 25)>>> transaction.commit() >>> dm.root['stephan'].friends {u'roy': <Person Roy Mathew>} >>> dm.root['stephan'].visited [u'Germany', u'USA'] >>> dm.root['stephan'].birthday datetime.date(1980, 1, 25)
如您所见,字典键总是转换为Unicode,元组是 始终作为列表维护,因为json没有两种序列类型。
>>> import pprint >>> pprint.pprint(dict( ... fetchone(person_cn, """data @> '{"name": "Stephan Richter"}'"""))) {'data': {u'_py_persistent_type': u'__main__.Person', u'address': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'address'}, u'birthday': {u'_py_type': u'datetime.date', u'value': u'1980-01-25'}, u'friends': {u'roy': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'u__main___dot_Person'}}, u'name': u'Stephan Richter', u'phone': {u'_py_type': u'__main__.Phone', u'area': u'978', u'country': u'+1', u'number': u'394-5124'}, u'today': {u'_py_type': u'datetime.datetime', u'value': u'2014-05-14T12:30:00.000000'}, u'visited': [u'Germany', u'USA']}, 'id': u'0001020304050607080a0b0c0'}
自定义序列化程序
(要演示的补丁)
>>> dm.root['stephan'].birthday = datetime.date(1981, 1, 25) >>> transaction.commit()>>> pprint.pprint( ... fetchone(person_cn, ... """data @> '{"name": "Stephan Richter"}'""")['data']['birthday']) {u'_py_type': u'datetime.date', u'value': u'1981-01-25'}
如您所见,生日的序列化是一个iso字符串。我们可以, 但是,请提供使用序号存储数据的自定义序列化程序。
>>> class DateSerializer(serialize.ObjectSerializer): ... ... def can_read(self, state): ... return isinstance(state, dict) and \ ... state.get('_py_type') == 'custom_date' ... ... def read(self, state): ... return datetime.date.fromordinal(state['ordinal']) ... ... def can_write(self, obj): ... return isinstance(obj, datetime.date) ... ... def write(self, obj): ... return {'_py_type': 'custom_date', ... 'ordinal': obj.toordinal()}>>> serialize.SERIALIZERS.append(DateSerializer()) >>> dm.root['stephan']._p_changed = True >>> transaction.commit()
让我们再来看看:
>>> dm.root['stephan'].birthday datetime.date(1981, 1, 25)>>> pprint.pprint(dict( ... fetchone(person_cn, """data @> '{"name": "Stephan Richter"}'"""))) {'data': {u'_py_persistent_type': u'__main__.Person', u'address': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'address'}, u'birthday': {u'_py_type': u'custom_date', u'ordinal': 723205}, u'friends': {u'roy': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'u__main___dot_Person'}}, u'name': u'Stephan Richter', u'phone': {u'_py_type': u'__main__.Phone', u'area': u'978', u'country': u'+1', u'number': u'394-5124'}, u'today': {u'_py_type': u'custom_date', u'ordinal': 735367}, u'visited': [u'Germany', u'USA']}, 'id': u'0001020304050607080a0b0c0'}
好多了!
>>> del serialize.SERIALIZERS[:]
作为子文档的持久对象
以便更好地控制哪些对象接收自己的表 如果没有,开发人员可以提供一个特殊的标志来标记 持久类,使其成为其父对象文档的一部分:
>>> class Car(persistent.Persistent): ... _p_pj_sub_object = True ... ... def __init__(self, year, make, model): ... self.year = year ... self.make = make ... self.model = model ... ... def __str__(self): ... return '%s %s %s' %(self.year, self.make, self.model) ... ... def __repr__(self): ... return '<%s %s>' %(self.__class__.__name__, self)
_p_pj_sub_object用于将对象的类型标记为只是部分 另一份文件:
>>> dm.root['stephan'].car = car = Car('2005', 'Ford', 'Explorer') >>> transaction.commit()>>> dm.root['stephan'].car <Car 2005 Ford Explorer>>>> pprint.pprint(dict( ... fetchone(person_cn, """data @> '{"name": "Stephan Richter"}'"""))) {'data': {u'_py_persistent_type': u'__main__.Person', u'address': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'address'}, u'birthday': {u'_py_type': u'datetime.date', u'value': u'1981-01-25'}, u'car': {u'_py_persistent_type': u'__main__.Car', u'make': u'Ford', u'model': u'Explorer', u'year': u'2005'}, u'friends': {u'roy': {u'_py_type': u'DBREF', u'database': u'pjpersist_test', u'id': u'0001020304050607080a0b0c0', u'table': u'u__main___dot_Person'}}, u'name': u'Stephan Richter', u'phone': {u'_py_type': u'__main__.Phone', u'area': u'978', u'country': u'+1', u'number': u'394-5124'}, u'today': {u'_py_type': u'datetime.date', u'value': u'2014-05-14'}, u'visited': [u'Germany', u'USA']}, 'id': u'0001020304050607080a0b0c0'}
我们希望对象持久化的原因是这样它们可以接受更改 自动:
>>> dm.root['stephan'].car.year = '2004' >>> transaction.commit() >>> dm.root['stephan'].car <Car 2004 Ford Explorer>
表格共享
由于PostgreSQL/JSONB非常灵活,所以有时存储是有意义的。 同一表中有多种(类似)对象。在那些情况下你 指示对象键入以将其python路径存储为文档的一部分。
警告:请注意,尽管此方法效率较低,因为 必须加载文档才能创建导致更多数据库的重影 访问。
>>> class ExtendedAddress(Address): ... ... def __init__(self, city, zip, country): ... super(ExtendedAddress, self).__init__(city, zip) ... self.country = country ... ... def __str__(self): ... return '%s (%s) in %s' %(self.city, self.zip, self.country)
为了完成表共享,只需创建另一个类 与另一个字符串具有相同的_p_pj_table字符串(子分类将 确保)。
现在让我们给斯蒂芬两个扩展地址。
>>> dm.root['stephan'].address2 = ExtendedAddress( ... 'Tettau', '01945', 'Germany') >>> dm.root['stephan'].address2 <ExtendedAddress Tettau (01945) in Germany>>>> dm.root['stephan'].address3 = ExtendedAddress( ... 'Arnsdorf', '01945', 'Germany') >>> dm.root['stephan'].address3 <ExtendedAddress Arnsdorf (01945) in Germany>>>> transaction.commit()
加载地址时,地址类型应正确:
>>> dm.root['stephan'].address <Address Maynard (01754)> >>> dm.root['stephan'].address2 <ExtendedAddress Tettau (01945) in Germany> >>> dm.root['stephan'].address3 <ExtendedAddress Arnsdorf (01945) in Germany>
持久序列化挂钩
当持久性组件实现IPersistentSerializationHooks时,它 对象可以执行一些自定义存储功能。
>>> from pjpersist.persistent import PersistentSerializationHooks >>> class Usernames(PersistentSerializationHooks): ... _p_pj_table = 'usernames' ... format = 'email' ... ... def _pj_after_store_hook(self, conn): ... print('After Store Hook') ... ... def _pj_after_load_hook(self, conn): ... print('After Load Hook')
当我们存储对象时,钩子被称为: (实际上是两次,因为这是一个新对象)
>>> dm.root['stephan'].usernames = Usernames() >>> transaction.commit() After Store Hook After Store Hook
加载时也会发生同样的情况:
>>> dm.root['stephan'].usernames.format After Load Hook 'email'
如果对象不是新对象,存储挂钩只会触发一次:
>>> dm.root['stephan'].usernames.format = 'snailmail' >>> transaction.commit() After Store Hook
列序列化
pjpersist还允许对象指定值,通常是属性或 属性,作为列存储在对象的存储表中。
注意,我们只支持单向转换,因为对象状态 将始终从datajsonb字段反序列化。
>>> import zope.schema >>> class IPerson(zope.interface.Interface): ... ... name = zope.schema.TextLine(title=u'Name') ... address = zope.schema.TextLine(title=u'Address') ... visited = zope.schema.Datetime(title=u'Visited') ... phone = zope.schema.TextLine(title=u'Phone')
最初,我们只在列中存储名称:
>>> from pjpersist.persistent import SimpleColumnSerialization, select_fields >>> @zope.interface.implementer(IPerson) ... class ColumnPerson(SimpleColumnSerialization, Person): ... _p_pj_table = 'cperson' ... _pj_column_fields = select_fields(IPerson, 'name')
因此,一旦我创建了这样一个person并提交了事务,person表就是 扩展以存储属性,并将此人添加到表中:
>>> dm.root['anton'] = anton = ColumnPerson(u'Anton') >>> transaction.commit()>>> dumpTable('cperson') [{'data': {u'_py_persistent_type': u'__main__.ColumnPerson', u'address': None, u'birthday': None, u'friends': {}, u'name': u'Anton', u'phone': None, u'today': {u'_py_type': u'datetime.datetime', u'value': u'2014-05-14T12:30:00.000000'}, u'visited': []}, 'id': u'0001020304050607080a0b0c0', 'name': u'Anton'}]
棘手案例
基本可变类型的更改
棘手,棘手。我们如何使框架检测可变的变化 对象,如列表和字典?答:我们会记录下 它们所属的持久对象并提供持久实现。
>>> type(dm.root['stephan'].friends) <class 'pjpersist.serialize.PersistentDict'>>>> dm.root['stephan'].friends[u'roger'] = Person(u'Roger') >>> transaction.commit() >>> sorted(dm.root['stephan'].friends.keys()) [u'roger', u'roy']
列表也是如此:
>>> type(dm.root['stephan'].visited) <class 'pjpersist.serialize.PersistentList'>>>> dm.root['stephan'].visited.append('France') >>> transaction.commit() >>> dm.root['stephan'].visited [u'Germany', u'USA', u'France']
循环非持久引用
存储在子文档中的任何可变对象都不能有多个 对象树中的引用,因为没有全局引用。这些 检测并报告循环引用:
>>> class Top(persistent.Persistent): ... foo = None>>> class Foo(object): ... bar = None>>> class Bar(object): ... foo = None>>> top = Top() >>> foo = Foo() >>> bar = Bar() >>> top.foo = foo >>> foo.bar = bar >>> bar.foo = foo>>> dm.root['top'] = top Traceback (most recent call last): ... CircularReferenceError: <...>
循环持久引用
一般来说,持久对象之间的循环引用不是问题, 因为我们总是只存储到对象的链接。然而,有一种情况是 循环依赖成为一个问题。
如果使用循环引用设置对象树,然后将该树添加到 存储必须在序列化期间立即插入对象,以便 可以创建引用。但是,需要注意的是,只需创建 最小的引用对象,以便系统不尝试递归地 减少状态。
>>> class PFoo(persistent.Persistent): ... bar = None>>> class PBar(persistent.Persistent): ... foo = None>>> top = Top() >>> foo = PFoo() >>> bar = PBar() >>> top.foo = foo >>> foo.bar = bar >>> bar.foo = foo>>> dm.root['ptop'] = top
容器和桌子
既然我们已经谈了很多关于存储一个对象的血腥细节, 那么反映整个表的映射呢,例如 一桌人。
可以采取许多方法。以下实施 将文档中的属性定义为映射键,并将 表格:
>>> from pjpersist import mapping >>> class People(mapping.PJTableMapping): ... __pj_table__ = person_cn ... __pj_mapping_key__ = 'short_name'
映射将数据管理器作为参数。一个人可以很容易地创造一个 自动分配数据管理器的子类。让我们看看:
>>> People(dm).keys() []
没有人在列表中的原因是没有文档具有密钥 或者密钥为空。让我们改变一下:
>>> People(dm)['stephan'] = dm.root['stephan'] >>> transaction.commit()>>> People(dm).keys() [u'stephan'] >>> People(dm)['stephan'] <Person Stephan Richter>
还要注意,对任何其他人设置“short name”属性将添加 它到映射:
>>> dm.root['stephan'].friends['roy'].short_name = 'roy' >>> transaction.commit() >>> sorted(People(dm).keys()) [u'roy', u'stephan']
更改
1.7.1(2019-06-19)
- 修复了序列化程序使用键dict_data获取映射时的边情况。 读取此类对象失败。
- 修复了序列化程序的边缘情况,当对象的状态处于活动状态时 在持久化对象中变为“空”。基本上是我只是 {“持久”类型:“someClass”} 未调用某个类的setstate,因此该对象可能会丢失 属性。就像userdict的子类一样,它将丢失数据属性。
- 删除了对dict键中0x00字符的检查。原来PostgreSQL只是 无法存储0x00。
1.7.0(2019-05-29)
- 支持序列化期间的次秒日期时间和时间解析。
- 添加使用缓存参数到pjContainer。\u load_one()以支持忽略 缓存。(如果一个容器跟踪多个 项目的版本,然后尝试加载所有旧版本。)
1.6.0(2019-05-29)
- 使id和数据列名可通过\u pj\u id\u列配置 pjContainer中的pj_data_列属性。
- 使用pjcontainer时自动为对象分配名称,而不仅仅是 idnamespjcontainer。
1.5.0(2018-10-10)
- 支持Python3.7。将Python3.5测试从Tox中删除。
1.4.1(2018-09-13)
- 无需登录tpc_完成。
1.4.0(2018-09-13)
- 实现了在dm没有写入时跳过tpc_prepare。
我们发现aws aurora目前在tpc_prepare上速度很慢。
当数据管理器没有写操作时,不需要调用tpc_prepare。
参阅
CaluaTPCApReaRayOngNoxRead Effice < /CIT>,默认为TRUE TRUE 兼容性。 - 添加了记录事务是否有写操作的功能。 请参见记录读写事务,默认为false
1.3.2(2018-04-19)
- 更精确地刷新数据管理器以避免不必要的数据库 写。
1.3.1(2018-04-11)
- 通过消除查询,启用并发添加到idnamepjcontainer 这导致了事务冲突。
1.3.0(2018-03-22)
- python 3兼容性修复程序
- 更高效的pjContainer.values()实现
1.2.2(2017-12-12)
- 需要保护所有数据库调用不受数据库断开连接的影响
1.2.1(2017-12-12)
- psycopg2.operational error和psycopg2.interfaceerror将被捕获 在SQL命令执行时,并将其重新设置为数据库已断开连接
1.2.0(2017-10-24)
- 添加了一个新的助手函数来将子对象链接到主文档对象。这是 当实现自定义的\u getstate\uu()和\u setstate\uu()时需要。一个 提供了详细的例子。
- 为允许 查询期间仅刷新某些对象。刷新提示是一个列表表名 需要刷新以使查询返回正确结果的。
- zope特定的容器使用flush_提示只刷新对象 在容器上运行查询时进行管理。
- 刷新对象时,现在只刷新每个主文档对象 一次。在修复之前,任何子对象都会导致其doc对象被转储 再一次。
注意:这些优化在现实世界中提供了15%的性能改进 应用。
1.1.2(2017-09-14)
- 确保提交后更改的对象不再被更改。
1.1.1(2017-07-03)
- 什么都没变。
1.0.0(2017-03-18)
- 首次公开发行
- 从Mongopersist到W的分叉项目使用postgresql和jsonb数据 键入。主要动力是利用PostgreSQL的 事务性支持。