PostgreSQL/JSONB持久化后端
项目描述
PostGreSQL/JSONB 数据持久化
本文档概述了 pjpersist 包的一般功能。pjpersist 是一个用于持久化 Python 对象的 PostGreSQL/JSONB 存储实现。它 不是 ZODB 的存储。
pjpersist 的目标是提供一个数据管理器,该管理器在事务边界处将对象序列化为 JSONB 块。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('Stephan') >>> stephan <Person Stephan>
数据管理器提供了一个 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': {'_py_persistent_type': '__main__.Person', 'address': None, 'birthday': None, 'friends': {}, 'name': 'Stephan', 'phone': None, 'today': {'_py_type': 'datetime.datetime', 'value': '2014-05-14T12:30:00.000000'}, 'visited': []}, 'id': '0001020304050607080a0b0c0'}]
如您所见,存储的个人文档看起来非常类似于自然 JSON 文档。但是哦,我忘记为 Stephan 指定全名了。让我们来指定它。
>>> dm.root['stephan'].name = 'Stephan Richter' >>> dm.root['stephan']._p_changed True
这次,数据不是自动保存的
>>> fetchone(person_cn)['data']['name'] 'Stephan'
因此,我们必须首先提交事务
>>> dm.root['stephan']._p_changed True >>> transaction.commit() >>> dm.root['stephan']._p_changed >>> fetchone(person_cn)['data']['name'] 'Stephan Richter'
现在让我们为 Stephan 添加一个地址。地址也是持久化对象
>>> 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': {'_py_persistent_type': '__main__.Address', 'city': 'Maynard', 'zip': '01754'}, 'id': '0001020304050607080a0b0c0'}]>>> dumpTable(person_cn) [{'data': {'_py_persistent_type': '__main__.Person', 'address': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'address'}, 'birthday': None, 'friends': {}, 'name': 'Stephan Richter', 'phone': None, 'today': {'_py_type': 'datetime.datetime', 'value': '2014-05-14T12:30:00.000000'}, 'visited': []}, 'id': '0001020304050607080a0b0c0'}]>>> dm.root['stephan'].address <Address Maynard (01754)>
非持久化对象
如您所见,引用看起来很漂亮,所有组件都很容易看到。但关于任意非持久化但可 picklable 对象怎么办?好吧,让我们为这个创建一个电话号码对象
>>> 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': {'_py_persistent_type': '__main__.Person', 'address': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'address'}, 'birthday': None, 'friends': {}, 'name': 'Stephan Richter', 'phone': {'_py_type': '__main__.Phone', 'area': '978', 'country': '+1', 'number': '394-5124'}, 'today': {'_py_type': 'datetime.datetime', 'value': '2014-05-14T12:30:00.000000'}, 'visited': []}, 'id': '0001020304050607080a0b0c0'}]
如您所见,对于任意非持久化对象,我们需要在子文档中提供一个小提示,但它非常少。如果 __reduce__ 方法返回一个更复杂的结构,则会写入更多的元数据。我们将在存储日期和其他任意数据时看到这一点。
>>> dm.root['stephan'].friends = {'roy': Person('Roy Mathew')} >>> dm.root['stephan'].visited = ('Germany', 'USA') >>> dm.root['stephan'].birthday = datetime.date(1980, 1, 25)>>> transaction.commit() >>> dm.root['stephan'].friends {'roy': <Person Roy Mathew>} >>> dm.root['stephan'].visited ['Germany', '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': {'_py_persistent_type': '__main__.Person', 'address': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'address'}, 'birthday': {'_py_type': 'datetime.date', 'value': '1980-01-25'}, 'friends': {'roy': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'u__main___dot_Person'}}, 'name': 'Stephan Richter', 'phone': {'_py_type': '__main__.Phone', 'area': '978', 'country': '+1', 'number': '394-5124'}, 'today': {'_py_type': 'datetime.datetime', 'value': '2014-05-14T12:30:00.000000'}, 'visited': ['Germany', 'USA']}, 'id': '0001020304050607080a0b0c0'}
自定义序列化器
(一个用于演示的补丁)
>>> dm.root['stephan'].birthday = datetime.date(1981, 1, 25) >>> transaction.commit()>>> pprint.pprint( ... fetchone(person_cn, ... """data @> '{"name": "Stephan Richter"}'""")['data']['birthday']) {'_py_type': 'datetime.date', 'value': '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': {'_py_persistent_type': '__main__.Person', 'address': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'address'}, 'birthday': {'_py_type': 'custom_date', 'ordinal': 723205}, 'friends': {'roy': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'u__main___dot_Person'}}, 'name': 'Stephan Richter', 'phone': {'_py_type': '__main__.Phone', 'area': '978', 'country': '+1', 'number': '394-5124'}, 'today': {'_py_type': 'custom_date', 'ordinal': 735367}, 'visited': ['Germany', 'USA']}, 'id': '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': {'_py_persistent_type': '__main__.Person', 'address': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'address'}, 'birthday': {'_py_type': 'datetime.date', 'value': '1981-01-25'}, 'car': {'_py_persistent_type': '__main__.Car', 'make': 'Ford', 'model': 'Explorer', 'year': '2005'}, 'friends': {'roy': {'_py_type': 'DBREF', 'database': 'pjpersist_test', 'id': '0001020304050607080a0b0c0', 'table': 'u__main___dot_Person'}}, 'name': 'Stephan Richter', 'phone': {'_py_type': '__main__.Phone', 'area': '978', 'country': '+1', 'number': '394-5124'}, 'today': {'_py_type': 'datetime.date', 'value': '2014-05-14'}, 'visited': ['Germany', 'USA']}, 'id': '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 字符串的类。
所以现在让我们给Stephan两个扩展地址。
>>> 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 还允许对象指定要存储在对象存储表上的值,通常是属性或属性。
请注意,我们只支持单向转换,因为对象状态将始终从 data jsonb 字段反序列化。
>>> import zope.schema >>> class IPerson(zope.interface.Interface): ... ... name = zope.schema.TextLine(title='Name') ... address = zope.schema.TextLine(title='Address') ... visited = zope.schema.Datetime(title='Visited') ... phone = zope.schema.TextLine(title='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')
因此,一旦我创建这样一个人物并提交事务,人物表就扩展为存储属性,并且人物被添加到表中
>>> dm.root['anton'] = anton = ColumnPerson('Anton') >>> transaction.commit()>>> dumpTable('cperson') [{'data': {'_py_persistent_type': '__main__.ColumnPerson', 'address': None, 'birthday': None, 'friends': {}, 'name': 'Anton', 'phone': None, 'today': {'_py_type': 'datetime.datetime', 'value': '2014-05-14T12:30:00.000000'}, 'visited': []}, 'id': '0001020304050607080a0b0c0', 'name': 'Anton'}]
棘手的情况
基本可变类型更改
棘手,棘手。我们如何使框架检测可变对象(如列表和字典)中的更改?答案:我们跟踪它们所属的持久对象,并提供持久实现。
>>> type(dm.root['stephan'].friends) <class 'pjpersist.serialize.PersistentDict'>>>> dm.root['stephan'].friends['roger'] = Person('Roger') >>> transaction.commit() >>> sorted(dm.root['stephan'].friends.keys()) ['roger', 'roy']
对于列表也是一样
>>> type(dm.root['stephan'].visited) <class 'pjpersist.serialize.PersistentList'>>>> dm.root['stephan'].visited.append('France') >>> transaction.commit() >>> dm.root['stephan'].visited ['Germany', 'USA', '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() []
列表中还没有人,因为没有文档有这个键,或者键是null。让我们改变这个
>>> People(dm)['stephan'] = dm.root['stephan'] >>> transaction.commit()>>> People(dm).keys() ['stephan'] >>> People(dm)['stephan'] <Person Stephan Richter>
还请注意,在任何人身上设置“短名”属性都会将其添加到映射中
>>> dm.root['stephan'].friends['roy'].short_name = 'roy' >>> transaction.commit() >>> sorted(People(dm).keys()) ['roy', 'stephan']
更改
3.1.4 (2024-03-27)
将CI移动到github actions。
声明Python 3.11兼容性
修复日期序列化,其中年份少于4位长
3.1.3 (2022-11-23)
当PJDataManager包含大量对象时,flush()更快。
3.1.2 (2022-07-15)
将集合替换为collections.abc以实现python 3.10兼容性
3.1.1 (2022-06-06)
修复MappingView问题,日志消息中不再发出hint,提示对象是被加载的对象,这只会引起痛苦。
3.1.0 (2022-06-03)
修复collections.abc.MappingView子类的持久性。它非常糟糕,根本没有存储底层映射,导致数据丢失。建议紧急更新!同时,在加载此类状态时也失败了。
3.0.2 (2022-05-03)
修复DBRef比较,使其在比较到None和非DBRef实例时返回有效结果,而不是硬失败。(__neq__未使用,因为__ne__是正确的方法,而__ne__无论如何都委托给__eq__)
3.0.1 (2022-02-03)
修复testing.py中的导入
声明Python 3.9兼容性
pjpersist.zope.container.PJContainer._load_one的微小改进:仅获取本地缓存一次,因为_cache属性变得有些昂贵。
添加sqlbuilder.ILIKE – 不区分大小写的LIKE对
3.0.0 (2021-02-22)
向后不兼容的更改:PJDataManager现在接受一个池而不是连接对象。当加入事务时,PJDataManager将从池中获取连接,并在事务完成时(中止或提交)将其返回。这允许更灵活的连接管理。连接池必须实现IPJConnectionPool接口(它与psycopg2.pool兼容)。
IPJDataManager.begin()已重命名为setTransactionOptions()
执行SQL语句时发生的错误现在会使整个事务失败,在尝试提交时引发transaction.interfaces.DoomedTransaction异常。失败的交易必须被中止。
2.0.1 (2020-10-13)
修复了持久化键为元组的字典。持久化此类对象是可行的,但读取失败了。
2.0.0 (2020-06-02)
放弃对Python 2.7和3.6的支持,添加3.8。
移除buildout支持。
支持嵌套刷新。在复杂的使用场景中,在序列化对象期间可能会执行查询以查找另一个对象。这反过来又导致刷新,结果是在刷新内部进行刷新。flush()方法没有预料到这种行为,如果内部刷新会刷新外部刷新已经处理过的对象,就会失败。
1.7.2 (2020-02-10)
优化:当我们有一个本地字段为_pj_mapping_key时,不要在数据中挖掘,应该允许创建查找索引
1.7.1 (2019-06-19)
修复了序列化器获取键为dict_data的映射的边缘情况。读取此类对象失败了。
修复了序列化器的一个边缘情况,当持久化对象中存在的对象状态变为“空”时。基本上,状态只是{‘_py_persistent_type’: ‘SomeClass’},SomeClass.__setstate__没有被调用,因此对象可能会丢失属性。就像UserDict的子类可能会丢失数据属性。
移除了对字典键中0x00字符的检查。结果表明PostGreSQL根本无法存储0x00。
1.7.0 (2019-05-29)
在序列化期间支持子秒日期时间和时间精度。
向PJContainer._load_one()添加了use_cache参数,以支持忽略缓存。(如果容器跟踪多个版本的条目并尝试加载所有旧版本,这将很有用。)
1.6.0 (2019-05-29)
通过在PJContainer中分别通过_pj_id_column和_pj_data_column属性配置id和data列名。
使用PJContainer时自动为对象分配名称,而不仅仅是IdNamesPJContainer。
1.5.0 (2018-10-10)
支持Python 3.7。从tox中删除了Python 3.5测试。
1.4.1 (2018-09-13)
在tpc_finish中不需要登录。
1.4.0 (2018-09-13)
当DM没有写入时,实现了跳过tpc_prepare的功能。我们发现AWS Aurora在tpc_prepare时的速度非常慢。当DataManager没有写入时,不需要调用tpc_prepare。参见CALL_TPC_PREPARE_ON_NO_WRITE_TRANSACTION,默认为True以保持向后兼容。
添加了记录事务是否具有写入的能力。参见LOG_READ_WRITE_TRANSACTION,默认为False。
1.3.2 (2018-04-19)
更精确地刷新datamanager,以避免不必要的数据库写入。
1.3.1 (2018-04-11)
通过消除导致事务冲突的查询,启用了IdNamesPJContainer的并发添加。
1.3.0 (2018-03-22)
Python 3兼容性修复
更高效的PJContainer.values()实现
1.2.2 (2017-12-12)
需要保护所有数据库调用以防止DatabaseDisconnected
1.2.1 (2017-12-12)
psycopg2.OperationalError和psycopg2.InterfaceError将在SQL命令执行时捕获,并重新抛出为DatabaseDisconnected
1.2.0 (2017-10-24)
添加了一个新的辅助函数来将子对象链接到主文档对象。当实现自定义__getstate__()和__setstate__()时需要这个功能。提供了一个详细的示例。
为IDataManager.execute()实现了flush_hint参数,允许在查询期间只刷新一些对象。需要刷新以使查询返回正确结果的表名列表是flush_hints。
特定于Zope的容器使用flush_hint来仅在查询容器时刷新它们管理的对象。
在刷新对象时,每个主文档对象现在只刷新一次。在之前的修复中,任何子对象都会导致其文档对象再次转储。
注意:这些优化在现实世界的应用中提供了15%的性能提升。
1.1.2 (2017-09-14)
确保提交后更改的对象不再是_p_changed。
1.1.1 (2017-07-03)
尚未更改。
1.0.0 (2017-03-18)
初始公共版本
从mongopersist分叉项目以与PostgreSQL和JSONB数据类型一起使用。主要动机是能够利用PostgreSQL的优秀事务支持。
pjpersist-3.1.4.tar.gz的散列值
算法 | 散列摘要 | |
---|---|---|
SHA256 | 0cda09009de89d90bf437d7a5df710f9b8d71869b26fa0e6f339b2db9ac067d8 |
|
MD5 | b00b75e97de64a359a58d7e4fe3d9124 |
|
BLAKE2b-256 | f0f06c93fa9d415bdb296903e40cdedf6ff20e3986dbbfd2f567ab74a7037edb |