Objektorientierte Datenbank ZODB
1 Objektorientierte Datenbank: ZODB
2 Begriffsdefinition
3 Never change a running system
4 Grundlagen
4.1   Serialisieren
4.2   Endloslange Select-Statements
4.3   Transaktionen
5 ZODB
5.1   Installation
5.2   Beispiel
5.3   _p_changed=1
5.4   BTrees
5.5   Subtransaction
5.6   Primär-Schlüssel und Indizes
5.7   Update von Objekten in der ZODB
6 Storage-Typen
7 ZEO
8 ZODB und Webanwendungen
9 Features die ich nicht verwende
10 Eigene Erfahrungen
11 Nachteile
12 FAQ
13 Links

1   Objektorientierte Datenbank: ZODB

Objektorientierte Programmiersprachen haben sich deutlich durchgesetzt: Doch die Datenbanken? Die meisten Datenbanken (Oracle, MySQL, Postgres, ...) bieten Erweiterungen zum relationalen Modell an, doch die will man aufgrund der schlechten Portabilität nicht nutzen.

2   Begriffsdefinition

  1. Objektorientiert:
    Was heißt eigentlich "Objektorientiert"? Das wichtigste Merkmal ist, dass Methoden und Daten zusammengefasst werden. Im folgenden ein Beispiel in der Syntax der Programmiersprache Python:
    class Benutzer:
        def __init__(self, id, name, vorname): # Konstruktor
            self.id=id
            self.name=name
            self.vorname=vorname
            self.warenkorb=[]
    
        def zuWarenkorb(self, ware):
            self.warenkorb.append(ware)
        
        def warenkorbBestellen(self):
            bestellung=[]
            bestellung_wert=0
            for ware in self.warenkorb:
               self.bestellung.append(ware)
               bestellung.append(ware)
               bestellung_ware+=ware.preis
    
  2. Datenbank:
    Was versteht man unter einer Datenbank? Eine Datenbank speichert Daten, so dass sie nach dem Neustart des Rechners wieder verfügbar sind. Das Dateisystem kann somit als eine einfache hierarchische Datenbank angesehen werden.

3   Never change a running system

Möchte man nun die obige Klasse "Benutzer" in einer herkömmlichen (relationalen) Datenbank speichern, so müsste man die Tabellen "Benutzer", "Warenkorb" und "Bestellung" anlegen. Aus einer Klasse werden in diesem einfachen Beispiel drei Tabellen. Bei komplexen Aufgabenstellungen werden oft mehrere hundert Tabellen benötigt.

4   Grundlagen

4.1   Serialisieren

In Python, sowie vielen anderen Programmiersprachen, gibt es ein Modul um Objekte zu serialisieren. Die Objekte werden zu einer Byte-Folge gewandelt, die dann z.B. in eine Datei geschrieben werden. Kleine Anwendungen lassen sich so leicht ohne Datenbank programmieren: Beim Start der Anwendung werden die serialisierten Daten eingelesen (unpickle) und beim Beenden werden die Daten wieder serialisiert (pickle).

Dieser Mechanismus geht solange gut, bis die Daten größer als der verfügbare Haupspeicher werden.

4.2   Endloslange Select-Statements

Speichert man seine Daten in einer relationalen Datenbank, braucht man ein objekt-relationales Mapping (OR-Mapping) um die Daten in die Objekte zu bekommen. Es gibt kommerzielle und freie Bibliotheken, die einem bei einem OR-Mapping behilflich sein sollen, doch früher oder später werden die Daten mittels langen Select-Anweisungen ("SELECT A, B, C, FROM MYTABLE WHERE ID=....") aus der Datenbank gelesen. Besonders unschön wird es, wenn man viele verschachtelte Datenstrukturen hat.

4.3   Transaktionen

Transaktionen werden nach dem ACID Prinzip definiert:

5   ZODB

5.1   Installation

Die neuste Version aus dem Internet herunterladen: http://www.zope.org/Products/StandaloneZODB
 cd ZODB???
 python setup.py --prefix=$HOME install
Ggf. den Pythonpath anpassen:
export PYTHONPATH=$HOME/lib/python2.2/site-packages

5.2   Beispiel

Beispiel: Benutzer.py
Trotz der Einfachheit des obigen Beispiels, sollte man folgendes bedenken: Diese einfache Datenbank kann Daten von mehreren Gigabyte verarbeiten. Wenn man weiß, was man braucht (in diesem Falls die Benuter-ID kennt) sind die Nutzer-Daten innerhalb von Bruchteilen von Sekunden verfügbar:
 gesuchterNuzter=userdb.get(12345)

5.3   _p_changed=1

Wird die Transaktion ausgeführt (commit()), sucht die Datenbank alle veränderten Daten, um sie persistent zu machen. Änderungen an nicht modifizierbare Datentypen (int, float, strings) werden automatisch erkannt. Änderungen an Listen und Dictionaries müssen markiert werden:
 myNutzer.warenkorb.append(ware)
 myNutzer._p_changed=1

5.4   BTrees

BTrees sind das Kern-Stück von ZODB. Sie ermöglichen eine effiziente Massdaten-Verwaltung. BTrees verhalten sich wie Dictionaries (Hash-Tables), die Daten werden jedoch soriert gespeichert. Die Suche von Einträger kann somit mit einem schnellen binären Suche durchgeführt werden. Beispiel:
# Schreibend:
mybtree[key]=value

# Lesend:
value=mybtree.get(key)
if value:
    # Der Schlüssel existiert
else:
    # Der Schlüssel existiert nicht
Anders als herkömmliche Dictionaries können BTrees Daten verwalten, die umfangreicher als der Hauptspeicher sind.

Damit es nicht zu Datenverlust kommt. Müssen alle Schlüssel, die in einem BTree verwendet werden, von einem Datentyp sein.

Möchte man Objekte als Schlüssel (Keys) verwenden, so muss man die __cmp__() Methode implementieren. Ansonsten wird der Schlüssel anhand der Hauptspeicher-Adresse gespeichert. Nach einem Neustart, hat das Objekt jedoch eine andere Hauptspeicher-Adresse, und die Sortierung im BTree ist defekt: The BTree is insane.
# Testen, ob die BTree-Struktur korrekt ist:
for key in mybtree.keys():
    if not mybtree.has_key(key):
        raise("BTree is insane: key not found: %s" % key)

5.5   Subtransaction

Möchte man z.B. mehrere tausend Datensätze aus einem anderen Datenbank in ZODB importieren, kann der Prozess zuviel Hauptspeicher beanspruchen. Hintergrund: Die Transaktion wird während sie ausgeführt wird, nicht auf Platte geschrieben, so dass alle Änderungen im Hauptspeicher gehalten werden. Bei einem Massen-Import kann der Hauptspeicher ggf. knapp werden. Mit subtransactions wird der Hauptspeicherverbrauch reduziert:
i=0
while 1:
    i++
    data=get_data_from_somewhere()
    myzodb.addData(data)
    if i>100000:
        #Subtransaktion auf Platte schreiben
        get_transaction().commit(1)
        i=0

5.6   Primär-Schlüssel und Indizes

Bei relationalen Datenbanken gibt es in der Regel zu jeder Tabelle einen Primär-Schlüssel und mehrere Indizes. Bei ZODB verhälten sich die Schlüssel in einem BTree wie ein Primärschlüssel:
 mybtree[id]=object_1
 mybtree[id]=object_X # object_1 wird überschrieben
 
Indizes gibt es in ZODB nicht. Man kann sich aber leicht behelfen. Will man z.B. in der obige Benutzer-Verwaltung alle Nutzer finden die mit Nachnamen "Meier" heißen, so könnte man das wie folgt lösen:
 for nutzer in userdb.values():
     if nutzer.name=="Meier":
         print "ID: %s" % nutzer.id
 
Bei 100.000 Nutzern, muss die Schleife 100.000 mal durchlaufen werden, bis alle Nutzer durchsucht wurden. Um eine schnellere Abfrage zu ermöglichen, kann man sich einen BTree anlegen, der alle Namen speichert:
class BenutzerContainer:
    def __init__(self):
        self.indexName=OOBTree()  

    def setName(self, name, id):
        # Alten Namen löschen
        old_name_dict=self.indexName[name]
        del(old_name_dict[id])

        # Neuen Namen indizieren
        new_name_dict=self.indexName.setdefault(name, {})
        new_name_dict[id]=1
Mit vertretbarem Aufwand wäre es auch möglich sich eine eigene Volltext-Recherche zu programmieren, doch es ist meist einfache ZCTextIndex einzusetzen.

5.7   Update von Objekten in der ZODB

Oft sind Änderungen in einer bestehenden Objektdatenbank nötig. Anstatt einer Email-Adresse, soll z.B. jeder Nutzer in der neuen Version mehrere Email-Adressen speichern können. Dementsprechend muss die Klasse Benutzer verändert werden. Aufgepasst! Die schon erstellten Objekte in der ZODB haben weiterhin die alten Attribute. Man muss als in einer Methode alle bestehenden Benutzer aktualisieren:
class BenutzerContainer:
    def update(self):
        for user in self.users:
            if type(user.email)==type(""):
                user.email=[user.email]
                user._p_changed=1

6   Storage-Typen

In den bisherigen Beispielen wurde immer FileStorage verwendet. Es existieren jedoch auch andere Storage-Typen: Ein detailierterer Vergleich ist hier: Storage Comparison

7   ZEO

ZEO ermöglicht es die Datenbank auf mehrere Rechner zu verteilen. Es existiert ein zentraler ZEO-Server und beliebig viele Datenbank-Clients. In dem bisherigen Beispiel, muss nur eine Zeile verändert werden, um die Daten auf einem ZEO-Server zu speichern:
storage=ClientStorage.ClientStorage(("myzeoserver.mydomain.de", 1975))
db=DB(storage)
ZEO ist dann sinnvoll, wenn die Client-Anwendungen hauptsächlich lesend auf die Daten zugreifen. Beim Schreiben, schickt der ZEO-Server an alle Clients Invalidation-Nachrichte, so dass bei vielen Schreibzugriffen, die Performance leidet.

Die Kommunikation zwischen den ZEO Client und den ZEO Server ist unverschlüsselt. Möchte man mittels ZEO verteilte Anwendungen schreiben, sollte man die Verbindung tunneln (stunnel oder ssh).

8   ZODB und Webanwendungen

ZODB ist Teil des Webapplication Server Zope. Aus meiner Sicht ist es jedoch einfacher mittels Quixote und Dulcinea Daten in einer ZODB im Web verfügbar zu machen. Interessant ist auch Medusa, ein performanter HTTP-Server für Python.

9   Features die ich nicht verwende

10   Eigene Erfahrungen

  1. 2001: Mit Zope (Web Application Server): Per Web-Administiert
  2. 2002: Kleine Arbeitsverwaltung (Workflow) mit Zope
  3. 2003: Email-Archiv nur ZODB
  4. 2003: Key-File Archiv nur ZODB

11   Nachteile

12   FAQ

13   Links