In objektorientiertem Programmcode wirken SQL-Abfragen wie Fremdkörper. Zudem unterscheiden sich die SQL-Dialekte verschiedener Datenbanken. Die Lösung: Der Objekt-Relational-Mapper SQL Object stellt ein einheitliches objektorientiertes Interface zur Verfügung.
Relationale Datenbanken und objektorientierte Programmiersprachen unterscheiden sich stark in der Art, wie sie Daten speichern. Eine Adressendatenbank enthält normalerweise eine eigene Tabelle für Personen und Adressen, einer Person lassen sich damit mehrere Adressen zuordnen.
Die einzelnen Tabellen wissen jedoch nichts übereinander. Erst durch eine mehr oder weniger komplexe SQL-Abfrage ist es möglich, die Daten zu einem logischen Datensatz zu verknüpfen. In objektorientierten Systemen hingegen ist die Zugehörigkeit von Objekten klarer festgelegt: Ein Adressen-Objekt ist einem oder beliebig vielen Personen-Objekten zugeordnet.
Diese unterschiedlichen Paradigmen erschweren das Arbeiten mit relationalen Datenbanken in objektorientierten Programmen. Abhilfe schaffen so genannte Object Relational Mapper. ORMs setzen die Tabellen einer relationalen Datenbank in Objekte um. Klassen entsprechen dabei Tabellen, die Datensätze erscheinen als Objekte, die Tabellenspalten als Objektattribute. Der ORM setzt Veränderungen an Klassen und Objekten im Hintergrund in SQL-Statements um und überträgt sie so in die Datenbank. Das Klasseninterface lässt sich also ohne SQL-Kenntnisse benutzen.
Die SQL-Dialekte der Datenbanksysteme unterscheiden sich trotz SQL-Standard oft erheblich. SQL Object abstrahiert von diesen SQL-Varianten und macht es auf diese Weise überflüssig, SQL-Code für verschiedene Datenbank-Engines von Hand zu schreiben.
SQL Object
Der für den Einsatz in Python konzipierte ORM SQL Object kommt bereits bei großen Projekten wie dem Ruby-On-Rails-Konkurrenten Turbogears [1] zum Einsatz. Der Python-Entwickler Ian Bicking hat SQL Object initiiert, inzwischen liegt Version 0.7 vor. Es unterliegt der LGPL und steht unter [2] zum Download bereit. Die Installation erfolgt mit »python setup.py install« oder mit dem Tool easy_install [3] über den Aufruf »easy_install SQLObject«.
SQL Object unterstützt weit verbreitete Datenbanken, darunter MySQL, PostgreSQL, SQLite und Firebird. Die Oracle-Unterstützung ist derzeit experimentell und noch nicht im Main-Branch enthalten.
Eine Musikdatenbank
Das folgende Beispiel verwendet SQLite als Datenbank-Backend. Wer eine andere Datenbank verwendet, muss aber den Quellcode bis auf die Connection-URL in Zeile 72 nicht verändern. Listing 1 enthält den Code für eine Musikdatenbank auf der Basis von SQL Object. Das einfache Python-Skript speichert Künstler mit die zugehörigen Genres und Alben in einer Datenbank.
|
Listing 1: |
|---|
01 from sqlobject import *
02 from optparse import OptionParser
03
04
05 class Artist(SQLObject):
06 name = StringCol(length=100)
07 albums = MultipleJoin('Album')
08 genres = RelatedJoin('Genre')
09
10 class Genre(SQLObject):
11 name = StringCol(length=100)
12 artists = RelatedJoin('Artist')
13
14 class Album(SQLObject):
15 name = StringCol(length=100)
16 artist = ForeignKey('Artist')
17 songs = MultipleJoin('Song')
18
19 class Song(SQLObject):
20 name = StringCol(length=100)
21 album = ForeignKey('Album')
22
23 def create_tables(option, opt_str, value, parser):
24 Artist.createTable(ifNotExists=True)
25 Genre.createTable(ifNotExists=True)
26 Album.createTable(ifNotExists=True)
27 Song.createTable(ifNotExists=True)
28
29 def add_artist(option, opt_str, value, parser):
30 artists = Artist.select(Artist.q.name==value)
31 artist_exists = len(list(artists)) != 0
32 if not artist_exists:
33 artist = Artist(name=value)
34
35 def del_artist(option, opt_str, value, parser):
36 artist = Artist.get(value)
37 artist.destroySelf()
38
39 def add_genre(option, opt_str, value, parser):
40 genres = list(Genre.select(Genre.q.name==value))
41 genre_exists = len(genres) != 0
42 genre = None
43 if not genre_exists:
44 genre = Genre(name=value)
45 else:
46 genre = genres[0]
47 artist = Artist.get(parser.values.artist_id)
48 artist.addGenre(genre)
49
50 def add_album(option, opt_str, value, parser):
51 artist = Artist.get(parser.values.artist_id)
52 album = Album(name=value, artist=artist)
53
54 def list_artists(option, opt_str, value, parser):
55 artists = Artist.select()
56 for artist in artists:
57 print '''[%d] %s:
58 Album(s): %s
59 Genre(s): %s''' % (artist.id, artist.name, ','.join([i.name for i in
60 artist.albums]), ','.join([i.name for i
61 in artist.genres]))
62
63 def find_artist_by_album(option, str, value, parser):
64 album = list(Album.select(Album.q.name==value))[0]
65 print 'Album artist:', album.artist.name
66
67 def find_artists_by_genre(option, str, value, parser):
68 genre = list(Genre.select(Genre.q.name==value))[0]
69 print 'Artists of Genre %s:n%s' % (value, 'n'.join([i.name for i in
69 genre.artists]))
70
71 if __name__ == '__main__':
72 connection = connectionForURI('sqlite:///home/michael/musikdb/sqlobject_example.sql')
73 sqlhub.processConnection = connection
74
75 parser = OptionParser()
76 parser.add_option('-c', '--create', action='callback', callback=create_tables,
77 help='Create necessary tables in the database')
78 parser.add_option('--add-artist', action='callback', callback=add_artist,
79 type='string',help='Add new artist to the database')
80 parser.add_option('--del-artist', action='callback', callback=del_artist,
81 type='int', help='Delete artist with the specified ID')
82 parser.add_option('--artist-id', dest='artist_id', help=
83 'The artist ID for the command following the ID parameter')
84 parser.add_option('--add-genre', action='callback', callback=add_genre,
85 type='string', help='Add a genre to an existing artist')
86 parser.add_option('--add-album', action='callback', callback=add_album,
87 type='string', help='Add an album to an existing artist')
88 parser.add_option('-l', '--list', action='callback', callback=list_artists,
89 help='List artists, their genres and albums')
90 parser.add_option('--artist-by-album', action='callback',
91 callback=find_artist_by_album, type='string',
92 help='Find the artist of a given album')
93 parser.add_option('--artists-by-genre', action='callback',
94 callback=find_artists_by_genre,
95 type='string', help='Find artists of a given genre')
96
97 (options, args) = parser.parse_args()
|
Die über Kommandozeilenargumente gesteuerte Anwendung erlaubt es, Daten einzugeben, auszulesen und zu durchsuchen. Auf eine Löschfunktion für Alben und Genres wurde der Einfachheit halber verzichtet. Abbildung 1 zeigt die Dateneingabe sowie einige Datenbankabfragen auf der Konsole.
Tabellen
Die Beispielanwendung verwendet drei über Python-Klassen definierte Tabellen: »Artist«, »Genre« und »Album«. Die Klasse »Song« dient ausschließlich zur Vervollständigung des Datenbankschemas, die Programmlogik greift sie nicht auf. Python-Klassen, die mit Hilfe von SQL Object eine Datenbanktabelle abbilden, leiten sich stets von »SQLObject« ab. Die Attribute der Klasse entsprechen den Spalten der Tabelle. Die »Artist«-Tabelle beispielsweise enthält die Spalte »name«, die im SQL-Object-Quelltext über »StringCol« definiert ist.
Neben »StringCol« steht noch eine Anzahl weiterer Typen zur Verfügung, zum Beispiel »BoolCol« für einen booleschen Wert, »DateCol« für ein Datum oder »IntCol« für einen Integer-Wert. Eine vollständige Liste der Attributtypen ist in der offiziellen SQL-Object-Dokumentation [4] unter [5] zu finden.
Listing 1 definiert keine Primärschlüssel. SQL Object fügt jedoch automatisch ein Attribut »id« als Primärschlüssel hinzu. Es lässt sich wie explizit definierte Felder auslesen. Die Zeilen 7 und 8 legen die Beziehungen zwischen dem Künstler und den Genres und Alben fest. Zwischen den Tabellen »Artist« und »Genre« besteht eine Many-to-many-Beziehung: Ein Künstler kann sowohl Rock\’n\’Roll als auch Blues spielen. Außerdem gibt es mehr als nur einen Blues-Musiker. Das Schlüsselwort »RelatedJoin« stellt diese Art von Beziehung her.
SQL Object erstellt eine Tabelle mit dem Namen »Klasse1_Klasse2«, um die Relation in der Datenbank zu realisieren, und erwartet die Definition der Relation in beiden beteiligten Klassen. Über den als Parameter übergebenen Namen der Zielklasse lassen sich damit auch Beziehungen zu SQL-Object-Klassen herstellen, deren Deklaration im Quellcode erst später erfolgt.
Für die Verknüpfung zwischen Künstlern und zugehörigen Alben kommt ein »MultipleJoin« oder eine One-to-many-Relation zum Einsatz. Eine entsprechende »MultipleJoin«-Deklaration findet sich in den Klassen »Artist« (Zeile 7) und »Album« (Zeile 16). Die Tatsache, dass ein Song von mehreren Künstlern gespielt und auch auf mehreren Alben erscheinen kann, berücksichtigt die einfach gehaltene Beispieldatenbank nicht.
Daten eingeben
SQL Object legt die Datenbanktabellen nach der Deklaration der Klassen nicht sofort an. Erst der Aufruf der Methode »createTable()« initialisiert die Datenbank. Der optionale Parameter »ifNotExists=True« weist SQL Object an nur solche Tabellen zu erzeugen, die nicht bereits existieren. Im Beispielskript erzeugt die Funktion »create_tables()« in den Zeilen 23 bis 28 alle Tabellen, falls der Aufruf des Skripts den Parameter »-c« enthält. Alle Aktionen des Beispielskripts lassen sich über Kommandozeilenswitches steuern (Abbildung 1). Es nutzt zur Verarbeitung der Argumente das Standardmodul »optparse«, das Python in Version 2.3 als High-Level-Replacement für »getopt« einführte.

Abbildung 1: Die Beispielanwendung setzt Kommandozeilenargumente in Datenbankabfragen um. Dank SQL Object ist datenbankspezifischer SQL-Code nicht erforderlich.
Die Methoden »add_artist()«, »add_genre()« und »add_album()« (Zeilen 29, 39 und 50) demonstrieren, wie sich mit SQL Object neue Einträge in die Tabellen schreiben lassen. »add_genre()« prüft in den Zeilen 40 bis 43 zunächst, ob ein Genre mit dem übergebenen Namen bereits vorhanden ist. Zeile 40 ruft über die von SQL Object vererbte Methode »select()« alle bereits vorhandenen Genres ab, die wie das hinzuzufügende Genre lauten. Die Methode »select()« gibt einen Generator zurück.
Durch die Umwandlung des Generators in eine Liste lässt sich am leichtesten überprüfen, ob das Genre bereits existiert: Dies ist der Fall, wenn die Länge der Liste ungleich 0 ist. Nur wenn ihre Länge 0 ist, legt das Skript einen neuen Genre-Eintrag an. Die Methode »get()« sucht den Künstler, der zu der auf der Kommandozeile übergebenen »id« passt. Die von SQL Object generierte Methode »addGenre()« schreibt das neue Genre in die Datenbank.
Das Anhängen von »?debug=t« an die Connection-URI in Zeile 72 aktiviert den Debug-Output für die Verbindung zur Datenbank. Listing 2 zeigt die Debug-Ausgabe für einen Aufruf von »add_genre()«. Zeilen, die mit »QueryR« beginnen, zeigen die Rückgabe des Resultats der zuvor ausgeführten Query. Die anderen »add_*()«-Methoden arbeiten ähnlich wie »add_genre()«. Die Methode »del_artist()« dient zum Löschen von Einträgen: Nach einem Aufruf von »destroySelf()« (Zeile 37) löscht SQL Object den entsprechenden Künstler.
|
Listing 2: |
|---|
01 1/Select : SELECT artist.id, artist.name FROM artist WHERE (artist.name = 'Dido')
02 1/QueryR : SELECT artist.id, artist.name FROM artist WHERE (artist.name = 'Dido')
03 1/COMMIT : auto
04 1/QueryIns: INSERT INTO artist (name) VALUES ('Dido')
05 1/COMMIT : auto
06 1/QueryOne: SELECT name FROM artist WHERE id = 3
07 1/QueryR : SELECT name FROM artist WHERE id = 3
08 1/COMMIT : auto
|
Auslesen der Datenbank
Die Methoden »list_artists()«, »find_artist_by_album()« und »find_artists_by_genre()« (Zeilen 54, 63, 67) zeigen das Auslesen aus der Datenbank. Ein Aufruf der »select()«-Methode ohne Parameter liefert einen Generator für alle Einträge der Tabelle. Die beiden »find_*()«-Methoden übergeben »select()« eine Bedingung, um bestimmte Einträge auszuwählen. Erhält »select()« zusätzlich den Parameter »orderBy«, fügt SQL Object der SQL-Abfrage ein entsprechendes »ORDER BY«-Statement hinzu.
Der Rückgabewert von »select()« ist vom Typ »generator« [6]. Dieser Generator ist lazy-evaluated, SQL Object führt die Datenbankabfrage dementsprechend erst aus, wenn eine Operation auf den Generator ausgeführt wird. Iteriert man über den Generator, statt ihn wie in den Methoden »add_artist()« und »add_genre()« mit der Funktion »list()« in eine Liste umzuwandeln (Zeilen 56 bis 61), ruft SQL Object die Resultate einzeln aus der Datenbank ab.
Bei großen Datenmengen ist es für die Performanz von Vorteil, dass dabei die Objekte nicht alle gleichzeitig im Speicher gehalten werden müssen. Andererseits führen die wiederholten Abfragen zu einer längeren Laufzeit. Dieser Nachteil lässt sich mit dem Aufruf »my_select()[:5]« vermeiden: SQL Object erstellt dann eine SQL-Anfrage mit dem Parameter »LIMIT 5«, die nur die benötigten Datensätze zurückgibt.
Die Klasse Sqlmeta
Die Klasse »sqlmeta« beeinflusst das Verhalten von SQL Object beim Umsetzen der Objekte und Attribute in Tabellen- und Spaltennamen. Unter anderem lässt sich hier der Name des Primärschlüssels anpassen. Wer auf bereits vorhandene Tabellen zugreifen möchte, kann außerdem über die Variable »fromDatabase« die Klasse mit den notwendigen Attributen füllen. Eine vollständige Liste der konfigurierbaren Parameter enthält die SQL-Object-Dokumentation [7].
Folgendes Beispiel demonstriert den Einsatz von »sqlmeta«:
class MetaTest(SQLObject):
class sqlmeta:
lazyUpdate = True
table = 'sqlmetatest'
Die Zuweisung von »lazyUpdate = True« bewirkt, dass SQL Object Veränderungen in den Objektattributen erst nach Aufruf von »sync()« in die Datenbank schreibt. Das »table«-Attribut überschreibt außerdem den Namen der Datenbanktabelle auf den vom Benutzer definierten Wert »sqlmetatest«.
SQL Object erleichtert die Arbeit von Python-Entwicklern, indem es eine einheitliche, objektorientierte Syntax für den Zugriff auf unterschiedliche Datenbanken ermöglicht. Damit besteht die Möglichkeit, auch den Anwendern ohne Mehraufwand eine entsprechende Auswahlmöglichkeit zu bieten.
Für die Zukunft
Der Object Relational Mapper unterstützt bereits viele verbreitete Datenbanksysteme. Bei der wachsenden Popularität ist für die nahe Zukunft mit der Unterstützung weiterer Datenbank-Backends, insbesondere der Oracle-Engine, zu rechnen. Hinzu kommt, dass SQL Object – anders als die meisten Alternativen aus der Python-Welt – unabhängig von einem Framework ist. (pkr)
|
Infos |
|---|
|
[1] Turbogears-Projektwebsite: [http://www.turbogears.org] [2] SQL-Object-Projektwebsite: [http://www.sqlobject.org] [3] Easy-install-Dokumentation:[http://peak.telecommunity.com/DevCenter/EasyInstall] [4] SQL-Object-Dokumentation:[http://sqlobject.org/SQLObject.html] [5] Liste der SQL-Object-Columntypes: [http://sqlobject.org/SQLObject.html#column-types] [6] Informationen zu Generatoren in Python: [http://docs.python.org/tut/node11.html#SECTION00111000000000000000000] [7] Sqlmeta-Attribute: [http://sqlobject.org/SQLObject.html#class-sqlmeta] |
|
Der Autor |
|---|
|
Michael Göttsche besucht den 13. Jahrgang eines Gymnasiums in Bargteheide. Daneben betreibt er die auf Projekthosting spezialisierte Firma Hosted-projects.com und beschäftigt sich mit verschieden Themen der Software-Entwicklung (Python, Webentwicklung C++ und Qt). |





