ZODB 讨论中
::-- hoxide [2005-02-20 06:24:40]
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上.
基本概念
- ZODB虽然是OODB, 但是任何有一些和关系数据库类似的概念
ZODB的数据存储形式, 是多选的, 可以是普通文件(FileStorage), DB4和ZEO连接.
- Python类通过继承Persistent可以变为ZODB化的.
- ZODB是基于"事务"的.
例子
- 先来看一个例子, 这个例子是可以运行的, 源于《ZODB/ZEO Programming Guide》
1 from ZODB import FileStorage, DB
2 import ZODB
3 from Persistence import Persistent
4 from BTrees.OOBTree import OOBTree
5
6 class User(Persistent):
7 pass
8
9 def test1():
10 storage = FileStorage.FileStorage("test-filestorage.fs")
11 db = DB(storage)
12 conn = db.open()
13 dbroot = conn.root()
14 # Ensure that a 'userdb' key is present
15 # in the root
16 if not dbroot.has_key('userdb'):
17 dbroot['userdb'] = OOBTree()
18 userdb = dbroot['userdb']
19 # Create new User instance
20 newuser = User()
21 # Add whatever attributes you want to track
22 newuser.id = 'amk'
23 newuser.first_name = 'Andrew'
24 newuser.last_name = 'Kuchling'
25 # Add object to the BTree, keyed on the ID
26 userdb[newuser.id] = newuser
27 # Commit the change
28 get_transaction().commit()
29 conn.close()
30 storage.close()
31
32 def test2():
33 storage = FileStorage.FileStorage("test-filestorage.fs")
34 db = DB(storage)
35 conn = db.open()
36 dbroot = conn.root()
37 it = [dbroot]
38 for t in it:
39 for k, v in t.items():
40 if isinstance(v, OOBTree):
41 print k, ':'
42 it.append(v)
43 elif isinstance(v, User):
44 print 'Key:', k
45 print 'ID:', v.id
46 print 'first_name:', v.first_name
47 print 'last_name:', v.last_name
48
49 if __name__ == "__main__":
50 test1()
51 test2()
- test1向数据库写数据, test2从数据库读数据.
逐步分解
- 连接数据库, 这个例子中使用普通文本:
- 建一个ZODB化的类User
- 获取数据库的根, 若没有userdb添加一个userdb实例
dbroot和userdb都是OOBTree的实例, 什么是BTree稍后解释, 你可以暂且认为是ZODB化的dict.
做userdb中插入一条User记录:
1 # Create new User instance
2 newuser = User()
3 # Add whatever attributes you want to track
4 newuser.id = 'amk'
5 newuser.first_name = 'Andrew' ; newuser.last_name = 'Kuchling'
6 ...
7 # Add object to the BTree, keyed on the ID
8 userdb[newuser.id] = newuser
9 # Commit the change
10 get_transaction().commit()
你也许会奇怪get_transaction()是哪来的, 有什么用? get_transaction是在import ZODB的时候加入到builtins里面的, 他获得一个事务.
- 事务有两个方法:'commit' 和'abord',分别是提交和废弃.
- 关闭数据库连接
不关闭数据库连接 test2就无法执行, FileStorage不支持多连接啊
- 读取数据 test2()
- 先连接数据库, 和test1一样
然后获取dbroot:
1 dbroot = conn.root()
- 因为ZODB是树状结构的, 所以我深度优先遍历这个棵树:
这个只是试验,证明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并不改变, 需要用户手动设置, 如:
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,
例子
直接拷贝了
1 >>> from BTrees.OOBTree import OOBTree
2 >>> t = OOBTree()
3 >>> t.update({ 1: "red", 2: "green", 3: "blue", 4: "spades" })
4 >>> len(t)
5 4
6 >>> t[2]
7 'green'
8 >>> s = t.keys() # this is a "lazy" sequence object
9 >>> s
10 <OOBTreeItems object at 0x0088AD20>
11 >>> len(s) # it acts like a Python list
12 4
13 >>> s[-2]
14 3
15 >>> list(s) # materialize the full list
16 [1, 2, 3, 4]
17 >>> list(t.values())
18 ['red', 'green', 'blue', 'spades']
19 >>> list(t.values(1, 2))
20 ['red', 'green']
21 >>> list(t.values(2))
22 ['green', 'blue', 'spades']
23 >>> t.minKey() # smallest key
24 1
25 >>> t.minKey(1.5) # smallest key >= 1.5
26 2
btree和tree set 类型的keys(),values()和items()方法返回的是"lazy" sequence, 即值在需要时才会获取.
BTree同样有可变对象的问题, 看例子:
1 >>> L1, L2, L3 = [1], [2], [3]
2 >>> from BTrees.OOBTree import OOSet
3 >>> s = OOSet((L2, L3, L1)) # this is fine, so far
4 >>> list(s.keys()) # note that the lists are in sorted order
5 [[1], [2], [3]]
6 >>> s.has_key([3]) # and [3] is in the set
7 1
8 >>> L2[0] = 5 # horrible -- the set is insane now
9 >>> s.has_key([3]) # for example, it’s insane this way
10 0
11 >>> s
12 OOSet([[1], [5], [3]])
不要用可变对象作为key啊.
子事务
- 子事务的存在的主要任务是解决非常大的对象提交时的内存问题. 考察一个有200,000个对象被改变的事务, 所有对象的修改在事务被提交前都将保存在内存中, 这是因为ZODB能将对象从ZODB的cache中除去, 这样的内存使用是非常巨大的. 使用子事务, 一个提交可以被分割成多个提交, 例如每10,000个对象提交一次. 这样这10,000个对象就可以从cache中释放了. 用子事务提交代替一个完整的事务提交, 仅续传递一个为True的值给commit()和abord(), 例如:
下一个子查询将在提交后自动生成.
- 尚未试验, 可以通过abort主事务来abort所有提交过的事务.
ZEO的使用
- ZEO是Zope Enterprise Objects, 他可以让ZODB使用在网络上的数据库.
ZEO包括其测试在内大约6000行程序, 因为他仅包括了TCP/IP服务和一种新的存储的实现ClientStorage. ClientStorage实现了ZEO的远端调用, 使用方法和FileStorage类似. 启动ZEO服务器:使用ZEO/start.py脚本, 选项 -p XXXX 用来指定服务端口
例子
- 基于ZEO的简单聊天程序:
1 from ZEO import ClientStorage
2 from ZODB import DB
3 from Persistence import Persistent
4 from BTrees.OOBTree import OOBTree
5 from time import time as _time
6 from time import sleep as _sleep
7
8 class ChatSession(Persistent):
9 def __init__(self, name):
10 self.name = name
11 # Internal attribute: _messages holds all the chat messages.
12 self._messages = OOBTree()
13 def add_message(self, message):
14 """Add a message to the channel.
15 message -- text of the message to be added
16 """
17 while 1:
18 try:
19 now = _time()
20 self._messages[now] = message
21 get_transaction().commit()
22 except ConflictError:
23 # Conflict occurred; this process should pause and
24 # wait for a little bit, then try again.
25 _sleep(.2)
26 pass
27 else:
28 # No ConflictError exception raised, so break
29 # out of the enclosing while loop.
30 break
31 # end while
32 def get_messages4time(self, T):
33 new = []
34 get_transaction().commit()
35 for t, message in self._messages.items():
36 if t > T:
37 new.append((t, message))
38 return new
39
40 from cmd import Cmd
41 class chat(Cmd):
42 prompt = 'Chat: '
43 def setchat(self, chatses, me):
44 self._chatses = chatses
45 self._me = me
46 def emptyline(self):
47 pass
48 def default(self, line):
49 self._chatses.add_message(self._me+': '+line)
50 def do_exit(self, line):
51 return True
52 def do_quit(self, line):
53 return True
54
55 def pchat(conn, chatses, me):
56 T = _time()
57 while True:
58 try:
59 conn.sync()
60 new = chatses.get_messages4time(T)
61 if new:
62 for t,l in new:
63 if t > T:
64 T = t
65 if not l.startswith(me+':'):
66 print
67 print l
68 else:
69 _sleep(1)
70 except SystemExit:
71 break
72
73 from sys import argv as _argv
74 from thread import start_new_thread as _start_new_thread
75
76 def main():
77 addr = ('localhost', 7000)
78 storage = ClientStorage.ClientStorage(addr)
79 db = DB(storage)
80 conn = db.open()
81 root = conn.root()
82 name = _argv[1]
83 me = _argv[2]
84 if not root.has_key(name):
85 root[name] = ChatSession(name)
86 _start_new_thread(pchat, (conn, root[name], me) )
87 c = chat()
88 c.setchat(root[name], me)
89 c.cmdloop()
90
91 if __name__ == '__main__':
92 main()
图片
解析
- 连接ZEO:
- 在数据库中添加一条信息, 以时间为标记
1 def add_message(self, message):
2 """Add a message to the channel.
3 message -- text of the message to be added
4 """
5 while 1:
6 try:
7 now = _time()
8 self._messages[now] = message
9 get_transaction().commit()
10 except ConflictError:
11 # Conflict occurred; this process should pause and
12 # wait for a little bit, then try again.
13 _sleep(.2)
14 pass
15 else:
16 # No ConflictError exception raised, so break
17 # out of the enclosing while loop.
18 break
19 # end while
- 按时间获取新消息: