/ 
  
 
 niemand.leermann@thomas-guettler.de
Objektorientierte Datenbank ZODB
 
     
            1 Objektorientierte Datenbank ZODB
             [toc]
            
Objektorientierte Programmiersprachen haben sich deutlich durchgesetzt:
 
  - Turbo-Pascal heißt nun Delphi und ist objektorientiert.
  
- Visual Basic ist es inzwischen auch.
  
- Perl ist es ebenfalls.
  
- Aus C wurde C++.
  
- Lisp, Java und natürlich Python sind objektorientierte 
   Programmiersprachen
 
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]
            
 - 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
- 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]
            
            4.1 Serialisieren
             [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.
            4.2 Endloslange Select-Statements
             [toc]
            
 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
             [toc]
            
Transaktionen werden nach dem ACID Prinzip definiert:
 - A: Atomar. Transaktionen werden ganz oder garnicht durchgeführt.
 
- C: Consistency. Die Konsistenz der Daten muss nach der Transaktion gewährleistet sein.
 
- I: Isolation. Führt ein Thread eine Transaktion durch,
 darf ein zweiter Thread die geänderten Daten erst sehen, wenn die
 Transaktion abgeschlossen ist.
 
- D: Durability. Stürzt der Rechner während dem Betrieb ab,
 dürfen keine Daten verloren gehen.
5.1 Installation
             [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
            5.2 Beispiel
             [toc]
            
 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
             [toc]
            
 
 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
             [toc]
            
 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
             [toc]
            
 
 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
             [toc]
            
 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
             [toc]
            
 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.
            5.8 Lang laufende Transaktionen
             [toc]
            
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:
 
  - DirectoryStorage:Pro Objekt werden die Daten in eine Datei
  geschrieben. Dass hat den Vorteil, dass dieser Storage nicht von Zeit
  zu Zeit komprimiert werden muss.
  
- BerkleyStorage:Die Objekte werden in einer BerkleyDB
  gespeichert. Dieser Storage Typ sollte nicht verwendet werden, da er
  zur Zeit nicht weiterentwickelt wird.
  
- ClientStorage:Die Daten werden auf einem Server gespeichert. Siehe ZEO.
 
Ein detailierterer Vergleich ist hier: Storage
 Comparison
            
 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:
 
  - Zwischen Start und Ende einer Transaktion sollte möglichst wenig
  Zeit vergehen.
 
  
- Batch-Input (Einlesen vieler gleichartiger Datensätze) sollte
  aufgeteilt werden. Ein Commit nach jeweils N Datensätzen ist
  sinnvoll.
  
- Wenn eine Transaktion aufgrund eines ConflictErrors misslingt,
  vor dem erneuten Versuch "sync()" aufrufen, damit der Datenbestand
  des ZEO-Clients aktualisiert wird.
 
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]
            
 - Ape: Speichern der serialisierten Objekte in einer relationalen
 Datenbank. Aus meiner Sicht macht es die Anwendung komplexer und
 fehleranfälliger. FileStorage und ClientStorage gut getestet und
 stabil.
 
- Undo: ZODB unterstützt das Zurücknehmen von
 Transaktionen, was aus meiner Sicht keinen Sinn macht. Wenn man
 Fehler gemacht hat, muss man ein Backup der Datenbank benutzen und
 falls in der Anwendungs-Schicht eine Undo-Funktion benötigt wird,
 sollte man sich die selber programmieren.
 
- Versions: ZODB verwaltet in Zusammenhang mit der
 Undo-Funktionalität mehrere Versionen eines Objekts. Es ist möglich
 sich ältere Versionen eines Objekts aus der Datenbank zu holen. Es
 hat sich jedoch gezeigt, dass der Verlauf eines Objekts besser in der
 Anwendung anstatt in der ZODB verwaltet wird. ZODB Versions sollten
 nicht verwendet werden. Siehe auch: Mailingliste
 Juli 2004.
 
- Threads: ZODB unterstützt Threads, die Anwendung wird jedoch
 dadurch unnötig komplex. Besser ist es mit mehreren Worker-Prozessen
 zu arbeiten, die nur einmal eine Verbindung zur Datenbank aufbauen,
 und diese Verbindung wiederverwenden.
10 Eigene Erfahrungen
             [toc]
            
 
  - 2001: Mit Zope (Web Application Server): Per Web-Administiert
  
- 2002: Kleine Arbeitsverwaltung (Workflow) mit Zope
  
- 2003: Email-Archiv nur ZODB
  
- 2003: Key-File Archiv nur ZODB
 
11 Nachteile
             [toc]
            
            
 - Gibt es eine Abfragesprache, ähnlich wie SQL?
 ZODB verfügt über eine ausgereifte, vollständige Programmiersprache
  als Abfragesprache.
- Wie kommunizieren Client und Server?
 Wenn man FileStorage (default) verwendet gibt keinen Client und
  keinen Server. Man kann sich eine ZODB-Anwendung wie eine große
  objektorientierte Stored-Procedure vorstellen.
 
 Verwendet man ZEO kommunizieren der Client und der
  ZEO-Server über TCP oder über ein Unix-Socket.
- Ist der Quelltext auch in der Datenbank?
 Nein, der Quelltext wird nicht in der Datenbank gespeichert. Der
  Quelltext ist wie gewöhnlich im Dateisystem. Bei einem Backup der
  ZODB sollte man ebenfalls den Quelltext sichern, da die Datenbank
  nur in Zusammenhang mit dem Quelltext benutzbar ist.
- Gibt es ein Sprache zur Datendefinition
 Die Python-Klassen sind die Datendefinition.
- Warum verwendest du nicht Zope?
 
   - "Through the Web Development" ist langsam und nervig.
   
- Es ist umständlich Python-Produkte mit Zope zu programmieren.
   
- Ich benötige die von Zope zur Verfügung gestellte Oberfläche nicht
   
- Implizit Acquisition ist fehleranfällig: Zope3 verwendet es
   auch nicht mehr.
   
- KISS: Keep it Simple and Stupid. Die von ZODB bereitgestellte
   API ist einiges kleiner, als die von Zope. Der Nachteil, dass man
   Dinge wie Nutzerverwaltung selbst programmieren muss, ist für mich
   nicht bedeutend.
   
- WEB-DAV hält nicht was es verspricht.
  
 
  © 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