Fork me on GitHub
6/8/2006

Пробую 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

Комментарии

Все статьи