/
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