##language:zh ''' ZODB 讨论中 ''' ::-- hoxide [[[DateTime(2005-02-20T06:24:40Z)]]] [[TableOfContents]] = ZODB = ''简述'' == 起因 == * ["WoodpeckerClass/2005-02-19"] -- 公元2005年2月19日会课中, limodou提到用ZODB来完成知识存储的想法. 他挖坑,偶就先跳进去了. == 学习笔记 == 《ZODB/ZEO Programming Guide》一共才25页, 花了3小时看完, 先写点不算翻译也不算感想的东西吧. === ZODB的安装 === * windows版本从[http://zope.org/Products/ZODB3.2]下载. * BSD下直接在ports/databases/zodb3中安装 * ZODB主要包括了ZODB,ZEO,BTREE等几个重要都包, 他们可以独立于ZOPE运行的, 其实ZODB是ZOPE的地层, 整个ZOPE就架在ZODB上. === 基本概念 === [[Include(/BaseNotion)]] === 逐步分解 === * 连接数据库, 这个例子中使用普通文本: {{{ #!python from ZODB import FileStorage, DB storage = FileStorage.FileStorage('/tmp/test-filestorage.fs') db = DB(storage) conn = db.open() }}} * 建一个ZODB化的类User {{{ #!python import ZODB from Persistence import Persistent class User(Persistent): pass }}} * 获取数据库的根, 若没有userdb添加一个userdb实例 {{{ #!python dbroot = conn.root() # Ensure that a 'userdb' key is present # in the root if not dbroot.has_key('userdb'): from BTrees.OOBTree import OOBTree dbroot['userdb'] = OOBTree() userdb = dbroot['userdb'] }}} dbroot和userdb都是OOBTree的实例, 什么是BTree稍后解释, 你可以暂且认为是ZODB化的dict. 做userdb中插入一条User记录: {{{ #!python # Create new User instance newuser = User() # Add whatever attributes you want to track newuser.id = 'amk' newuser.first_name = 'Andrew' ; newuser.last_name = 'Kuchling' ... # Add object to the BTree, keyed on the ID userdb[newuser.id] = newuser # Commit the change get_transaction().commit() }}} 你也许会奇怪get_transaction()是哪来的, 有什么用? get_transaction是在import ZODB的时候加入到__builtins__里面的, 他获得一个事务. 事务有两个方法:'commit' 和'abord',分别是提交和废弃. * 关闭数据库连接 {{{ #!python conn.close() storage.close() }}} 不关闭数据库连接 test2就无法执行, FileStorage不支持多连接啊 * 读取数据 test2() * 先连接数据库, 和test1一样 {{{ #!python storage = FileStorage.FileStorage("test-filestorage.fs") db = DB(storage) conn = db.open() }}} 然后获取dbroot: {{{ #!python dbroot = conn.root() }}} * 因为ZODB是树状结构的, 所以我深度优先遍历这个棵树: {{{ #!python it = [dbroot] for t in it: print t for k, v in t.items(): if isinstance(v, OOBTree): print k, ':' it.append(v) elif isinstance(v, User): print 'Key:', k print 'ID:', v.id print 'first_name:', v.first_name print 'last_name:', v.last_name }}} 这个只是试验,证明test1的确在数据库中存放了数据. 至此例子分析完毕. === ZODB的关系模型 === ZODB除了提供Persistent类ZODB化python外还提供了 PersistentMapping, PersistentList, 顾名思义他们分别是用来模拟Mapping结构和List结构的(python中的dict和list). 为什么要提供这两种结构呢?因为ZODB不能有效得处理python中的可变对象(dict和list). 当改变ZODB对象时, 应该将对象标记为脏的(dirty),这样在commit时就知道到底哪些数据需要更新了. 在ZODB对象中用'_p_changed'属性标记脏数据. 但是在改变可变类型时_p_changed并不改变, 需要用户手动设置, 如: {{{ #!python userobj.friends.append(otherUser) userobj._p_changed = 1 }}} PersistentMapping, PersistentList只解决了正确性的问题. 而BTree则应该是真正ZODB化的解决方案. === BTree === 学过数据结构的应该都觉得BTree有点眼熟吧, 对, BTree就是平衡二叉树(balanced tree). 为了处理大很大的数据量, ZODB引进BTree作为Mapping的实现, 他在使用方法上类似于dict. * BTree是按需存取的, 他在使用时才会将数据读入内存, 这样就可以处理非常大的Mapping结构. * BTree是平衡二叉树, 因此在按key读取时速度非常快, 应该是O(log2(n))这个级别的时间复杂度. BTree包含了多种Mapping类供选择, 供了BTree, Bucket, Set,TreeSet四种数据结构, 按key和value的数据类型分为'I'和'O'分别表示整型(Int)和对象类型(Object), 用'I' 'O'修饰数据结构就得到了BTree中可用的类: {{{ OOBTree, OOBucket, OOSet, OOTreeSet, IOBTree, IOBucket, IOSet, IOTreeSet, OIBTree, OIBucket, OISet, OITreeSet, IIBTree, IIBucket, IISet, IITreeSet, }}} ==== 例子 ==== 直接拷贝了 {{{ #!python >>> from BTrees.OOBTree import OOBTree >>> t = OOBTree() >>> t.update({ 1: "red", 2: "green", 3: "blue", 4: "spades" }) >>> len(t) 4 >>> t[2] 'green' >>> s = t.keys() # this is a "lazy" sequence object >>> s >>> len(s) # it acts like a Python list 4 >>> s[-2] 3 >>> list(s) # materialize the full list [1, 2, 3, 4] >>> list(t.values()) ['red', 'green', 'blue', 'spades'] >>> list(t.values(1, 2)) ['red', 'green'] >>> list(t.values(2)) ['green', 'blue', 'spades'] >>> t.minKey() # smallest key 1 >>> t.minKey(1.5) # smallest key >= 1.5 2 }}} btree和tree set 类型的keys(),values()和items()方法返回的是"lazy" sequence, 即值在需要时才会获取. BTree同样有可变对象的问题, 看例子: {{{ #!python >>> L1, L2, L3 = [1], [2], [3] >>> from BTrees.OOBTree import OOSet >>> s = OOSet((L2, L3, L1)) # this is fine, so far >>> list(s.keys()) # note that the lists are in sorted order [[1], [2], [3]] >>> s.has_key([3]) # and [3] is in the set 1 >>> L2[0] = 5 # horrible -- the set is insane now >>> s.has_key([3]) # for example, it’s insane this way 0 >>> s OOSet([[1], [5], [3]]) }}} 不要用可变对象作为key啊. === 子事务 === 子事务的存在的主要任务是解决非常大的对象提交时的内存问题. 考察一个有200,000个对象被改变的事务, 所有对象的修改在事务被提交前都将保存在内存中, 这是因为ZODB能将对象从ZODB的cache中除去, 这样的内存使用是非常巨大的. 使用子事务, 一个提交可以被分割成多个提交, 例如每10,000个对象提交一次. 这样这10,000个对象就可以从cache中释放了. 用子事务提交代替一个完整的事务提交, 仅续传递一个为True的值给commit()和abord(), 例如: {{{ #!python # Commit a subtransaction get_transaction().commit(1) # Abort a subtransaction get_transaction().abort(1) }}} 下一个子查询将在提交后自动生成. * 尚未试验, 可以通过abort主事务来abort所有提交过的事务. === ZEO的使用 === ZEO是Zope Enterprise Objects, 他可以让ZODB使用在网络上的数据库. ZEO包括其测试在内大约6000行程序, 因为他仅包括了TCP/IP服务和一种新的存储的实现ClientStorage. ClientStorage实现了ZEO的远端调用, 使用方法和FileStorage类似. 启动ZEO服务器:使用ZEO/start.py脚本, 选项 -p XXXX 用来指定服务端口 ==== 例子 ==== 基于ZEO的简单聊天程序: {{{ #!python from ZEO import ClientStorage from ZODB import DB from Persistence import Persistent from BTrees.OOBTree import OOBTree from time import time as _time from time import sleep as _sleep class ChatSession(Persistent): def __init__(self, name): self.name = name # Internal attribute: _messages holds all the chat messages. self._messages = OOBTree() def add_message(self, message): """Add a message to the channel. message -- text of the message to be added """ while 1: try: now = _time() self._messages[now] = message get_transaction().commit() except ConflictError: # Conflict occurred; this process should pause and # wait for a little bit, then try again. _sleep(.2) pass else: # No ConflictError exception raised, so break # out of the enclosing while loop. break # end while def get_messages4time(self, T): new = [] get_transaction().commit() for t, message in self._messages.items(): if t > T: new.append((t, message)) return new from cmd import Cmd class chat(Cmd): prompt = 'Chat: ' def setchat(self, chatses, me): self._chatses = chatses self._me = me def emptyline(self): pass def default(self, line): self._chatses.add_message(self._me+': '+line) def do_exit(self, line): return True def do_quit(self, line): return True def pchat(conn, chatses, me): T = _time() while True: try: conn.sync() new = chatses.get_messages4time(T) if new: for t,l in new: if t > T: T = t if not l.startswith(me+':'): print print l else: _sleep(1) except SystemExit: break from sys import argv as _argv from thread import start_new_thread as _start_new_thread def main(): addr = ('localhost', 7000) storage = ClientStorage.ClientStorage(addr) db = DB(storage) conn = db.open() root = conn.root() name = _argv[1] me = _argv[2] if not root.has_key(name): root[name] = ChatSession(name) _start_new_thread(pchat, (conn, root[name], me) ) c = chat() c.setchat(root[name], me) c.cmdloop() if __name__ == '__main__': main() }}} ==== 图片 ==== attachment:lt.JPG ==== 解析 ==== * 连接ZEO: {{{ #!python addr = ('localhost', 7000) storage = ClientStorage.ClientStorage(addr) db = DB(storage) }}} * 在数据库中添加一条信息, 以时间为标记 {{{ #!python def add_message(self, message): """Add a message to the channel. message -- text of the message to be added """ while 1: try: now = _time() self._messages[now] = message get_transaction().commit() except ConflictError: # Conflict occurred; this process should pause and # wait for a little bit, then try again. _sleep(.2) pass else: # No ConflictError exception raised, so break # out of the enclosing while loop. break # end while }}} * 按时间获取新消息: {{{ #!python def get_messages4time(self, T): new = [] get_transaction().commit() for t, message in self._messages.items(): if t > T: new.append((t, message)) return new }}} = 反馈 = * 好也!又是一个耐不住的行者!不过建议使用 WikiName 规范的页面名称-- ZoomQuiet * 嗬嗬嗬!谢谢理解!使用标准 WikiName 好一些的,不过,对于很长的页面,推荐使用 {{{[[Include()]]]}}} 宏来分解页面,并且也使每节的内容页面容易维护是也乎! * 收到, 以后注意 -- hoxide * 你们好,以后在解释这些名词时,请给它加一个解释链接,如ZODB指的是什么?可以方便我们不了解这些内容时,可以查看相关的解释。谢谢! --lith * '''["TiosnG"]''' --俺什么也不说了…… ZoomQuiet