ZODB 讨论中

::-- hoxide [2005-02-20 06:24:40]

1. ZODB

简述

1.1. 起因

  • WoodpeckerClass/2005-02-19 -- 公元2005年2月19日会课中, limodou提到用ZODB来完成知识存储的想法. 他挖坑,偶就先跳进去了.

1.2. 学习笔记

  • 《ZODB/ZEO Programming Guide》一共才25页, 花了3小时看完, 先写点不算翻译也不算感想的东西吧.

1.2.1. ZODB的安装

  • windows版本从http://zope.org/Products/ZODB3.2下载.

  • BSD下直接在ports/databases/zodb3中安装
  • ZODB主要包括了ZODB,ZEO,BTREE等几个重要都包, 他们可以独立于ZOPE运行的, 其实ZODB是ZOPE的地层, 整个ZOPE就架在ZODB上.

1.2.2. 基本概念

  • ZODB虽然是OODB, 但是任何有一些和关系数据库类似的概念
  • ZODB的数据存储形式, 是多选的, 可以是普通文件(FileStorage), DB4和ZEO连接.

  • Python类通过继承Persistent可以变为ZODB化的.
  • ZODB是基于"事务"的.

1.2.2.1. 例子

  • 先来看一个例子, 这个例子是可以运行的, 源于《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从数据库读数据.

1.2.3. 逐步分解

  • 连接数据库, 这个例子中使用普通文本:

   1 from ZODB import FileStorage, DB
   2 storage = FileStorage.FileStorage('/tmp/test-filestorage.fs')
   3 db = DB(storage)
   4 conn = db.open()
  • 建一个ZODB化的类User

   1 import ZODB
   2 from Persistence import Persistent
   3 
   4 class User(Persistent):
   5     pass
  • 获取数据库的根, 若没有userdb添加一个userdb实例

   1 dbroot = conn.root()
   2 # Ensure that a 'userdb' key is present
   3 # in the root
   4 if not dbroot.has_key('userdb'):
   5    from BTrees.OOBTree import OOBTree
   6    dbroot['userdb'] = OOBTree()
   7 userdb = dbroot['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',分别是提交和废弃.
  • 关闭数据库连接

   1 conn.close()
   2 storage.close()
  • 不关闭数据库连接 test2就无法执行, FileStorage不支持多连接啊

  • 读取数据 test2()
    • 先连接数据库, 和test1一样

   1     storage = FileStorage.FileStorage("test-filestorage.fs")
   2     db = DB(storage)
   3     conn = db.open()

然后获取dbroot:

   1 dbroot = conn.root()
  • 因为ZODB是树状结构的, 所以我深度优先遍历这个棵树:

   1     it = [dbroot]
   2     for t in it:
   3         print t
   4         for k, v in t.items():
   5             if isinstance(v, OOBTree):
   6                 print k, ':'
   7                 it.append(v)
   8             elif isinstance(v, User):
   9                 print 'Key:', k
  10                 print 'ID:', v.id
  11                 print 'first_name:', v.first_name
  12                 print 'last_name:', v.last_name

这个只是试验,证明test1的确在数据库中存放了数据.

至此例子分析完毕.

1.2.4. ZODB的关系模型

顾名思义他们分别是用来模拟Mapping结构和List结构的(python中的dict和list).

为什么要提供这两种结构呢?因为ZODB不能有效得处理python中的可变对象(dict和list). 当改变ZODB对象时, 应该将对象标记为脏的(dirty),这样在commit时就知道到底哪些数据需要更新了. 在ZODB对象中用'_p_changed'属性标记脏数据.

但是在改变可变类型时_p_changed并不改变, 需要用户手动设置, 如:

   1 userobj.friends.append(otherUser)
   2 userobj._p_changed = 1

PersistentMapping, PersistentList只解决了正确性的问题. 而BTree则应该是真正ZODB化的解决方案.

1.2.5. 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.2.5.1. 例子

直接拷贝了

   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啊.

1.2.6. 子事务

  • 子事务的存在的主要任务是解决非常大的对象提交时的内存问题. 考察一个有200,000个对象被改变的事务, 所有对象的修改在事务被提交前都将保存在内存中, 这是因为ZODB能将对象从ZODB的cache中除去, 这样的内存使用是非常巨大的. 使用子事务, 一个提交可以被分割成多个提交, 例如每10,000个对象提交一次. 这样这10,000个对象就可以从cache中释放了. 用子事务提交代替一个完整的事务提交, 仅续传递一个为True的值给commit()和abord(), 例如:

   1 # Commit a subtransaction
   2 get_transaction().commit(1)
   3 # Abort a subtransaction
   4 get_transaction().abort(1)

下一个子查询将在提交后自动生成.

  • 尚未试验, 可以通过abort主事务来abort所有提交过的事务.

1.2.7. ZEO的使用

  • ZEO是Zope Enterprise Objects, 他可以让ZODB使用在网络上的数据库.

    ZEO包括其测试在内大约6000行程序, 因为他仅包括了TCP/IP服务和一种新的存储的实现ClientStorage. ClientStorage实现了ZEO的远端调用, 使用方法和FileStorage类似. 启动ZEO服务器:使用ZEO/start.py脚本, 选项 -p XXXX 用来指定服务端口

1.2.7.1. 例子

  • 基于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()

1.2.7.2. 图片

1.2.7.3. 解析

  • 连接ZEO:

   1     addr = ('localhost', 7000)
   2     storage = ClientStorage.ClientStorage(addr)
   3     db = DB(storage)
  • 在数据库中添加一条信息, 以时间为标记

   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
  • 按时间获取新消息:

   1     def get_messages4time(self, T):
   2             new = []
   3             get_transaction().commit()
   4             for t, message in self._messages.items():
   5                 if t > T:
   6                     new.append((t, message))
   7             return new

2. 反馈

  • 好也!又是一个耐不住的行者!不过建议使用 WikiName 规范的页面名称-- ZoomQuiet

  • 嗬嗬嗬!谢谢理解!使用标准 WikiName 好一些的,不过,对于很长的页面,推荐使用 [[Include()]]] 宏来分解页面,并且也使每节的内容页面容易维护是也乎!

    • 收到, 以后注意 -- hoxide
    • 你们好,以后在解释这些名词时,请给它加一个解释链接,如ZODB指的是什么?可以方便我们不了解这些内容时,可以查看相关的解释。谢谢! --lith