Пробую ZODB ∞
Как и обещал - пару слов о работе с ZODB и рабочий пример.
Первые шаги
Импортирую необходимые модули:
>>> from ZODB import FileStorage, DB
>>> import transaction
Для того, чтобы использовать ZODB, нужно указать хранилище:
>>> storage = FileStorage.FileStorage('/tmp/zodb.fs')
Открыть соединение:
>>> db = DB(storage)
>>> conn = db.open()
И получить корень ZODB:
>>> dbroot = conn.root()
Корень dbroot есть аналог словаря (dict):
>>> type(dbroot)
<class 'persistent.mapping.PersistentMapping'>
>>> filter(lambda x: not x.startswith('_'), dir(dbroot))
['clear', 'copy', 'data', 'fromkeys', 'get', 'has_key', 'items',
'iteritems', 'iterkeys', 'itervalues', 'keys', 'pop', 'popitem',
'setdefault', 'update', 'values']
Поясню вторую строку:
filter(somefun, somelist)возвращает только те элементы спискаsomelist, для которыхsomefun(item) == True. Неименованная функцияlambda x: not x.startswith('_')возвращаетTrueв том случае, если строкаxне начинается с '_'. Т.е. вторая строка показывает только публичные методы/атрибуты объекта dbroot.
Пробую воспоьзоваться как обычным словарем:
>>> dbroot['key'] = ['Uno', 'Duo', 'Treo']
Чтобы изменения были записаны нужно подтвердить транзакцию:
>>> transaction.commit()
Если этого не сделать, то во-первых ничего в dbroot не измениться, а во-вторых, при попытке закрыть соединение вылезет:
>>> conn.close()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python2.3/site-packages/ZODB/Connection.py", line 232, in close
raise ConnectionStateError("Cannot close a connection joined to "
ZODB.POSException.ConnectionStateError: Cannot close a connection joined to a transaction
Смотрю, что же получилось:
>>> dbroot.keys()
['key']
>>> dbroot['key']
['Uno', 'Duo', 'Treo']
Ну и для разминки, попробую сохранить какой-нибудь класс и объект:
>>> class Foo(object):
... def foometh(self):
... print "foo"
...
>>> class Bar(object):
... def barmeth(self):
... print "bar"
...
>>> dbroot['class'] = Foo
>>> dbroot['object'] = Bar()
>>> import transaction
>>> transaction.commit()
И посмотреть результат:
>>> dbroot['class']
<class '__main__.Foo'>
>>> dbroot['object']
<__main__.Bar object at 0xb7a08d0c>
>>> f = dbroot['class']()
>>> f.foometh()
foo
>>> b = dbroot['object']
>>> b.barmeth()
bar
ZEO
Если попытаться одновременно открыть одно и то же хранилище, то у второго ничего не выйдет: в модуле FileStorage возникнет исключение IOError: [Errno 11] Resource temporarily unavailable ZEO же позволяет многим клиентам по сети соединяться с одним хранилищем. Т.е. ZEO выступает в роли "прокси". Работать с ZEO очень просто:
runzeo.py -a 3030 -f /tmp/zodb.fs
Опция -a указывает ZEO слушать соединения на 3030 порту, а опция -f - имя файла-хранилища.
В коде же клиентов вместо FileStorage надо будет использовать ClientStorage:
>>> from ZEO import ClientStorage
>>> storage = ClientStorage.ClientStorage(('localhost', 3030))
ZConfig
Зачастую для одного и того же кода требуется использование различных хранилищ. Например, на станции разработчика, это может быть FileStorage с тестовыми данными, а на рабочего сервере - ClientStorage с реальными данными. Поэтому удобно использовать конфигурации и конфигурационные файлы. С ZODB идет еще один "кусочек" Zope - ZConfig. Т.е. в коде клиентов нужно указать, какой конфигурационный файл будем использовать:
>>> from ZODB import config
>>> db = config.databaseFromURL('references.conf')
Если есть желание использовать ZODB.FileStorage, то в конфигурационном файле нужно указать:
<zodb>
<filestorage>
path /tmp/zodb.fs
</filestorage>
</zodb>
А если ZEO.ClientStorage, то:
<zodb>
<zeoclient>
server localhost:3030
</zeoclient>
</zodb>
Ограничения ZODB
У ZODB (как и у любого другого хранилища сериализированных объектов) есть ограничения:
-
При изменении объекта ZODB помечает его как "грязный" (dirty) и при
подтверждении транзакции записывает в хранилище только "грязные" объекты. При модификации изменяемого (mutable, например список, словарь) атрибута объекта, ZODB не помечает его как quot;грязный". Решение - либо вручную помечать, что объект стал "грязным" (атрибут
_p_changed), либо использовать не обычные списки и словари, а их "обертки" (PersistentList и PersistentMapping). Кстати говоря, корень ZODB - пример PersistentMapping -
Современные версии ZODB позволяют переопределять методы
__setattr__,__getattr__и__delattr__. (старые версии вообще не позволяли это делать), но в этом случае, нужно не забывать помечать объекты "грязными" при изменении атрибута. -
Объекты не должны имет метод
__del__.
Пример (справочники в ZODB)
Как я уже говорил, я использую ZODB для хранение справочников. Таких справочников некоторое количество, так что весьма расточительно хранить отдельный справочник в отдельном хранилище. Удобнее создать одно хранилище, и уже в нем хранить все справочники.
Для хранения большого количества данных обычные словари (тип dict) не эффективны. В ZODB есть пакет, реализующий B-tree. В составе пакета
BTreesпять разновидностей B-tree:IFBTree,IIBTree,IOBTree,OIBTree,OOBTree. Каждая из разновидностей оптимизирована для различных типов ключей и данных. Определить, где какую разновидность использовать, очень легко: в названии разновидности B-tree первая буква - тип ключа, вторая - тип ключевого значения. I - целое (int), F - с плавающей точкой (float), O - объект (object). Я использую в основном IOBTree (если ключи - целые, а так зачастую и бывает) и OOBTree (если ключи - строки).
Итак, реализация справочников в ZODB:
- класс ReferenceItem, наследованный от Persistent, для представления отдельного значения справочника;
- класс DataStorage, реализующий подключение и отключение ZODB;
- класс Reference, наследованный от DataStorage, реализующий создание нового справочника либо использование уже существующего;
- класс ReferenceById, наследованный от Reference. Предназначен для представления справочника, где ключи - числа, соответственно, использующего наиболее эффективный тип B-tree - IOBTree.
Код класса ReferenceItem я не буду приводить, при желании вы можете глянуть его в references.py на code.google.com.
А вот код классов для реализации справочников я приведу:
from ZODB import config
from BTrees import IOBTree
class DataStorage(object):
"""Data storage (ZODB) """
def __init__(self):
self.db = config.databaseFromURL('references.conf')
self.conn = self.db.open()
self.root = self.conn.root()
def finish(self):
self.conn.close()
self.db.close()
class Reference(DataStorage):
"""Reference"""
def __init__(self, referenceName, bTreeType):
DataStorage.__init__(self)
refname = "ref_%s" % referenceName
if refname not in self.root.keys():
self.root[refname] = bTreeType()
self.refname = refname
self.ref = self.root[refname]
def __getitem__(self, key):
return self.ref[key]
def __setitem__(self, key, value):
self.ref[key] = value
def delete(self):
del(self.root[self.refname])
self.ref = None
class ReferenceById(Reference):
"""Reference where keys are integers"""
def __init__(self, referenceName):
Reference.__init__(self, referenceName, IOBTree.IOBTree)
Ну и пример использования:
>>> from references import ReferenceById
>>> exref = ReferenceById('example')
>>> exref.refname
'ref_example'
>>> exref.ref
<BTrees._IOBTree.IOBTree object at 0xb76a9584>
>>> exref[1] = 'example value'
>>> exref[1]
'example value'
>>> exref.root.keys()
['ref_example']
>>> exref.delete()
>>> exref.root.keys()
[]
Конечно, это весьма общие наброски. К примеру, в рабочей версии у меня реализована проверка типа BTree у уже существующего справочника (например, создавали IOBTree, а на чтение обращаются как к OOBTree), но это не принципиальные детали, так что я их не стал включать в пример.
Весь код - на code.google.com
