www.thomas-guettler.de / Vorträge
niemand.leermann@thomas-guettler.de
Objektorientierte Datenbank ZODB
   Objektorientierte Datenbank ZODB
   Begriffsdefinition
   Never change a running system
   Grundlagen
4.    Serialisieren
4.    Endloslange Select-Statements
4.    Transaktionen
   ZODB
5.    Installation
5.    Beispiel
5.    _p_changed=1
5.    BTrees
5.    Subtransaction
5.    Primär-Schlüssel und Indizes
5.    Update von Objekten in der ZODB
5.    Lang laufende Transaktionen
   Storage-Typen
   ZEO
   ZODB und Webanwendungen
   Features die ich nicht verwende
10    Eigene Erfahrungen
11    Nachteile
12    FAQ
13    Links

1 Objektorientierte Datenbank ZODB [toc]

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 [toc]

  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 [toc]

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 [toc]

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. 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. Transaktionen werden nach dem ACID Prinzip definiert:

5 ZODB [toc]

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
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)
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
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)
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
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. 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
Es ist auch möglich spezielle __getstate__ uns __setstate__ Methoden von persistenten Objekten zu definieren und somit das Objekt automatisch zu aktualisieren, falls aus dem in der Datenbank gespeicherten "Pickle" ein Objekt erstellt wird. Ein explizites Update-Script ist jedoch zu bevorzugen, da nach dem einmalige Lauf alle Objekte aktualisiert sind. Bei der automatische Aktualisierung kann es sein, dass selten benutzte Objekte sehr lange nicht auf den neuen Stand gebracht werden. Bei großen Datenmengen stoßen Zugriffe, die alle persistenten Objekte "anfassen" an die Grenzen der Hardware: Der Hauptspeicher wird meist knapp. Der Ausweg: Der Zugriff sollte zwischendurch gesichert (commit) oder abgebrochen (abort) werden. Im folgenden Fall ist 'objects' ein BTree der sehr viele Objekte enthält:
i=0
for objektid, object in root.objects.items():
    i+=1
    if i%10000==0:
        get_transaction().commit() # Bei einem readonly Zugriff abort()
        connection.sync()
    ...

6 Storage-Typen [toc]

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

7 ZEO [toc]

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

ZEO kann auch genutzt werden, damit mehrere Prozesse auf einem Rechner auf die Datenbank zugreifen können. Dabei bietet es sich an den ZEO Server auf einer Socket-Datei "lauschen" zu lassen. Ein TCP/IP Port ist somit nicht nötig.

Wenn mehrere Prozesse auf die Datenbank zugreifen kann es zu Zugriffskonflikten kommen. Beispiel:
  Zeit    |  Aktion
 ------------------
 12:00:00 | Prozess 1 liest Daten (Start der Transaktion)
 12:01:00 | Prozess 2 liest Daten und ruft sofort "commit()" auf
 12:02:00 | Prozess 1 will die seit zwei Minuten bearbeiteten Daten speichern
          |  --> ConflictError, da die Daten inzwischen geändert wurden.
Damit Zugriffkonflikte vermieden werden sollte man sich an folgende Regeln halten:

8 ZODB und Webanwendungen [toc]

ZODB ist Teil des Webapplication Server Zope. Aus meiner Sicht ist es jedoch einfacher mittels Quixote Daten einer ZODB im Web verfügbar zu machen. Wenn die Start-Up Zeit von reinem CGI zu langsam ist, weil für jeden Request der Python-Interpreter gestartet werden muss, die Module geladen werden und die Verbindung zur Datenbank aufgebaut wird, sollte man SCGI verwenden. Mit der Suchmaschine seiner Wahl findet man auch schnell Berichte, die beschreiben warum SCGI besser als mod_python oder FastCGI ist.

UPDATE: Ich bin inzwischen zu mod_wsgi (anstatt SCGI) gewechselt.

9 Features die ich nicht verwende [toc]

10 Eigene Erfahrungen [toc]

  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 [toc]

12 FAQ [toc]

13 Links [toc]


© 2002-2005 Thomas Güttler. Der Text darf nach belieben kopiert und modifiziert werden, solange dieser Hinweis zum Copyright und ein Links zu dem Original unter www.thomas-guettler.de erhalten bleibt. Es wäre nett, wenn Sie mir Verbesserungsvorschläge mitteilen: guettli@thomas-guettler.de