Posts mit dem Label Datenbank werden angezeigt. Alle Posts anzeigen
Posts mit dem Label Datenbank werden angezeigt. Alle Posts anzeigen

Samstag, 19. Oktober 2013

Probleme mit CouchDB nach Upgrade auf Ubuntu 13.10

Gestern habe ich meinen Rechner von Ubuntu 13.04 auf 13.10 aktualisiert. Das hat auch soweit ohne Probleme geklappt (und dazu gibt's noch einen separaten Blogeintrag).

Auf dem Rechner ist auch CouchDB installiert, welches ich für die Speicherung von eine paar Daten einsetzen. Im Zuge des Upgrades wurde dann auch CouchDB von Version 1.2 auf 1.4 aktualisiert.

Erste Auffälligkeit nach dem 1. Booten von Ubuntu 13.10: Im Anmeldebildschirm erscheint ein Nutzer namens "CouchDB Administrator". Einloggen kann ich mich da aber nicht (was vielleicht daran liegt, dass ich CouchDB im "Admin-Party" Modus betreibe?).

Normal eingeloggt wurden dann Futon, die HTML-basierte GUI von CouchDB aufgerufen. Nächste Auffälligkeit: Die Datenbank wurde offensichtlich nicht auf automatisch auf die Version 1.4. migriert. Dafür gibt es einen Eintrag in der DB-Liste "1.2.0/name_der_db". Kurioser Weise kann man diese DB dann aber nicht öffnen! Den Versuch quittiert CouchDB mit der Fehlermeldung:

"Error: illegal_database_name
Name: '101/eigene_caches'. Only lowercase characters (a-z), digits (0-9), and any of the characters _, $, (, ), +, -, and / are allowed. Must begin with a letter."


Aha -  da stellt sich doch die Frage, warum die DB dann so umbenannt wurde...

Also, wie an die Daten kommen? Gut, ich habe diverse Kopien der DB auf einem anderen Laptop und meinen beiden Android-Geräten, von daher kann ich ein wenig entspannt experimentieren. Ein simples Kopieren der Datenbank-Datei und zugehörigen Views in Datenverzeichnis funktioniert nicht, CouchDB erkennt die Daten nicht als DB-Dateien.

Die Replikation von MobileFuton 1.2 (=CouchDB für Android) funktioniert ohne Problem. Die Daten sind jetzt wieder da.

Fazit: wer auch CouchDB unter Ubuntu nutzt und das Upgrade von 13.04 auf 13.10 macht - stellt vorher sicher, dass ihr eine aktuelle Sicherung der Datenbank habt bzw. repliziert das Datenbank(en) vorher auf ein anderes Gerät.

In der Doku von CouchDB habe ich keine Hinweise zu diesem Verhalten gefunden. Lediglich im Bugtracker von Debian gibt es einen Bugreport, dass die "database migration should be automatic" sein sollte (Link zum Bugreport).

Wer ähnliche Probleme hatte oder eine andere Lösung hat kann dies gerne in den Kommentaren zu diesem Blogeintrag posten.

Sonntag, 25. August 2013

sostore - eine simple Objekt-Datenbank für Python

Der Python Cheeseshop ist schon ein echte Fundgrube - zumindest mir geht es so, dass man da was sucht und dann zufällig auf was ganz anderes stößt, was man zwar eigentlich nicht braucht, aber trotzdem interessant ist.

Neulich bin ich so auf sostore gestoßen. Der Name steht für "SQLite Object Store" und das Projekt beschreibt sich als "An absurdly simple object "database" for Python".

Der Name ist vielleicht etwas irreführend weil: sostore speichert nicht wirklich Objekte (im Sinne von Python-Objekten), sondern "nur" Python Dictionaries.  Gespeichert wird dabei in eine SQLite Datenbank. Das Modul besteht nur aus zwei Dateien und hat keine Abhängigkeiten außer Python selber.

Dieser Blogeintrag beschreibt die Nutzung der zur Zeit aktuellste, stabilen Version 0.3.
(Nachtrag 17.9.2013: es gibt inzwischen die Version 0.4. Ein Update zu diesem Blogpost gibt es hier: Link)

Wie andere Python-Module auch lässt sich sostore via pip installieren:

$ pip install sostore

Danach steht das Modul unter dem Namen sostore zur Verfügung:

>>> import sostore

Die einzige Klasse, mit der man als Nutzer interagiert, heißt Collection. Diese enthält alle Daten und stellt alle Funktionen bereit.

Als erstes wird eine neue Collection angelegt:

>>> c = sostore.Collection('people')

Per Voreinstellung wird die im Hintergrund laufende SQLite-Datenbank als "in-memory" angelegt. Wer Daten persistent speichern möchte, der muss explizit das db-Argument angeben:

>>> c2 = sostore.Collection('people2', db='people.db')

Eine dritte Möglichkeit ist, beim Anlegen einer Instanz einer Collection als Argument eine bestehende DB-Connection anzugeben. Details hierzu sind in den Docstrings von sostore zu finden.

Als erstes werden der Collection ein paar Datensätze hinzugefügt:

>>> d = {'name':'Peter', 'surname':'Punk', 'job':'musician'}
>>> c.insert(d)
{'job': 'musician', '_id': 1, 'surname': 'Punk', 'name': 'Peter'}
>>> c.insert({'name':'Peter', 'surname': 'Pan', 'job': 'fairyhero'})
{'job': 'fairyhero', '_id': 2, 'surname': 'Pan', 'name': 'Peter'}
>>> c.insert({'name':'Otto', 'surname': 'Normal', 'job': 'lawyer'})
{'job': 'lawyer', '_id': 3, 'surname': 'Normal', 'name': 'Otto'}
>>> c.insert({'name':'Volker', 'surname': 'Racho', 'job': 'musicien'})
{'job': 'musicien', '_id': 4, 'surname': 'Racho', 'name': 'Volker'}


Wie zu sehen ist, wird für jedes hinzugefügt Objekt das Objekt sowie die automatisch angelegt ID (welches das Objekt in der SQLite-Datenbank als Primärschlüssel hat) zurück gegeben. Wer möchte, kann auch eine zufällige ID erzeugen, indem bei insert zusätzlich das Argument randomize=True angibt.

Das 'Aktualisieren eines Objekts innerhalb der Collection ist natürlich auch möglich. Im folgenden wird der Rechtschreibfehler beim Job des 4. Datensatzes korrigiert:

>>> d = c.get(4)
>>> d['job'] = 'musician'
>>> c.update(d)
{u'job': 'musician', '_id': 4, u'surname': u'Racho', u'name': u'Volker'}


Das Löschen eines Datensatzes erfolgt wie folgt:

>>> c.remove(3)

Die Anzahl der Datensätze in der Collection liefert count

>>> c.count
3


sostore bietet verschiedene Möglichkeiten, die Daten auszulesen. Um alle Datensätze zu sehen, genügt der Befehl:

>>> c.all()
[{u'job': u'musician', '_id': 1, u'surname': u'Punk', u'name': u'Peter'}, {u'job': u'fairyhero', '_id': 2, u'surname': u'Pan', u'name': u'Peter'}, {u'job': u'musician', '_id': 4, u'surname': u'Racho', u'name': u'Volker'}]


Aber natürlich sind die Datensätze auch einzeln selektierbar. Wer die ID kennt, kann sich diese Daten wie folgt anzeigen lassen:

>>> c.get(1)
{u'job': u'musician', '_id': 1, u'surname': u'Punk', u'name': u'Peter'}


Es ist auch möglich, mehr als eine ID anzugeben:

>>> c.get_many([1, 2])
[{u'job': u'musician', '_id': 1, u'surname': u'Punk', u'name': u'Peter'}, {u'job': u'fairyhero', '_id': 2, u'surname': u'Pan', u'name': u'Peter'}]


Optional kann man auch zusätzlich die Felder angeben, die zurück geliefert werden sollen:

>>> c.get_many([1, 2], fields='surname')
[{u'surname': u'Punk', u'name': u'Peter'}, {u'surname': u'Pan', u'name': u'Peter'}]


Das Argument field=... funktioniert übrigens auch beim weiter oben gezeigten Befehl all().

Nun besteht bei "real-life" Applikation natürlich auch öfters die Notwendigkeit, Daten anhand von Werten in den Datensätzen zu finden. Dies funktioniert so:

>>> c.find_one('surname', 'Punk')
{u'job': u'musician', '_id': 1, u'surname': u'Punk', u'name': u'Peter'}


Diese Funktion liefert immer nur den ersten Treffer zurück, wie der folgenden Befehl zeigt:

>>> c.find_one('name', 'Peter')
{u'job': u'musician', '_id': 1, u'surname': u'Punk', u'name': u'Peter'}


Jetzt gibt es aber zwei Datensätze mit dem Feld "name=Peter" in unserer Collection. Um alle Datensätze, die einem Suchkriterium entsprechen, zu finden, dient der Befehl:

>>> c.find_field('name', 'Peter')
[1, 2]


Das Ergebnis ist eine Liste von IDs. find_field kennt als optionales Argument noch compare_function, welches als Wert eine Vergleichsfunktion enthalten kann. Details findet man in den Docstrings.

Zwei weitere Funktionen von Collection sind random_entry() und random_entries(count=X), welche einen bzw. X zufällige gewählte Datensätze zurück liefern.

Das SQLite Object Store ist mit Sicherheit kein komplexes Modul. Aber es kann doch recht praktisch sein, wenn man Python-Dicts bzw. JSON-Objekt Speichern und Durchsuchen möchte. Außerdem bietet sostore die Möglichkeit die Daten persistent zu sichern.

Donnerstag, 13. September 2012

U1DB

U1DB ist Canonicals neue Datenbank zum Speichern und Synchroniseren von Daten. Synchronisieren heißt hier, dass "Desktop Daten" wie z.B. Kontaktdaten aus dem E-Mail Programm oder Kalenderdaten zwischen verschiedenen Rechner - und auch einem Serverdienst wie z.B. UbuntuOne - abgeglichen werden können. Wobei U1DB an sich die API beschreibt, und nicht die Datenbank selber.

Solche Dienste gibt es aber bekanntlich bereits - wozu programmiert Canonical also an einer eigenen Lösungen (die übrigens von Anfang an OpenSource ist und unter GNU LGPL v3 Lizenz steht)? Nun, neu ist die Synchronisation unter Ubuntu nicht. Genau genommen gibt es die schon länger, U1DB ist aber der 2. Anlauf. Grund: der 1. ist zwar nicht gescheitert, wurde aber wegen Problemen aufgegeben. Die 1. Variante setzte auch CouchDB als Datenbank-Server und lokal auf Desktopcouch. Dieser Ansatz war auch komplett in die Ubuntu Desktop Installationen integriert, wurde dann aber nach dem Release von Ubuntu 11.10 als "Auslaufmodell" deklariert, weil es bei Canonical zu Skalierungsproblemen mit CouchDB kam, die sich wohl nicht lösen ließen (wer darüber mehr erfahren möchte sollte einfach "ubuntu couchdb" als Suchbegriff in die Suchmaschine seiner Wahl eingeben). Der Nachfolger, und damit 2. Anlauf, ist jetzt U1DB. In Ubuntu 12.04 LTS konnte die Lösung noch nicht intergriert werden, dazu war die Zeit zu knapp. Für den kommenden Ubuntu Release 12.10 gibt es wohl eine Intergration, jedenfalls gibt es bereits fertige Pakete in den offiziellen Quellen.

Was heißt das jetzt für den Desktop-Nutzer? Nun, erst Mal nichts, da die Synchronisation der Daten transparent erfolgt, d.h. man selber hat keinen direkten Kontakt zu U1DB. Interessanter sind da schon die Pläne von Canonical, was die Integration in andere Betriebssysteme angeht. Diese sind nämlich recht ehrgeizig. U1DB soll - früher oder später - für alle gängigen Systeme verfügbar sein, also neben Linux auch MacOS, Windows, Android, iOS und eine Web-Schnittstelle. Dazu soll U1DB in Python, C, Vala, Go, C, C#, Objective C, Java und JavaScript umgesetzt werden. Die Implementierung in Python und C läuft, der aktuelle Release (Stand: 13.9.2012) ist 0.1.3. An der Vala und Go Umsetzung wird gearbeitet, es gibt aber noch keine Releases.

Schaut man in die API und Technik von U1DB, gibt es eine Reihe von Ähnlichkeiten mit CouchDB. Die Datenbank speichert text-basierte Daten im JSON-Format (ebenso wie CouchDB) - und ist alleine darauf ausgelegt. Binäre Daten (wie z.B. Bilder, Musik etc.) können nicht gespeichert werden. Des Weiteren beherrscht U1DB Mehrwege-Replikation inklusive Konfliktmanagement zwischen Datenbanken / Servern. Ein Feature, das CouchDB ebenfalls besitzt. Scheinbar war man bei Canonical mit der Funktionalität von CouchDB zufrieden und hat sich an diese angelehnt, wobei es keinerlei Kompatibilität gibt.

Als Speicher setzt U1DB lokal auf den meisten Plattformen auf die SQLite Datenbank. Was in sofern auch Sinn macht, als das Python, Android und iOS diese direkt mit an Bord haben. Auf der Projektseite wird aber betont, dass auch andere Datenbanken als Backend möglich sind. Die Go-Implementierung soll z.B. auf MongoDB setzen und auch über das Internet zugängliche U1DB-Server werden wohl eher auf MySQL und andere skalierbare Datenbanken setzen als auf SQLite.

Wie bereits erwähnt ist bisher "nur" die Python-Implementierung (mit in C geschriebenen Teilen fertig). Dies ist auch die Referenz-Implementierung. Wer sich also mit der API beschäftigen möchte, der sollte darauf zurück greifen. Das Python-Modul heißt einfach "u1db" und lässt sich auf bekanntem Wege via pip oder easy_install installieren. Übrigens ist U1DB für Python-Programmierer auch außerhalb von Ubuntu interessant, wenn eine einfache API zur persistenten Speicherung und Indizierung von JSON-Daten gesucht wird.

Zurück zu Ubuntu: U1DB wird unter Ubuntu sicherlich der kommende Standard zur Synchronisation von Daten. Wie gesagt, für den Nutzer transparent. Da sich U1DB noch in einem frühen Entwicklungsstadium befindet ist im Moment offen, wenn die API als stabil deklariert wird und die volle Integration in den Desktop abgeschlossen ist.

Sonntag, 2. September 2012

Redis und Python

Redis ist ein sehr schnelles Key-Value Store, zu Deutsch: Schlüssel-Werte Datenbank. Eine Besonderheit von Redis ist zusätzlich, dass es als Werte nicht nur einfache Strings kennt, sondern fünf verschiedenen Datentypen. Da die offizielle Dokumentation von Redis ganz hervorragend ist und auch in der September-Ausgabe von FreiesMagazin ein ausführlicher Artikel zu Redis vorhanden ist, wird an dieser Stelle auf eine weitere Beschreibung der Datenbank verzichtet und "nur" die Python-Anbindung näher beschrieben. In der Python-Welt erfreut sich Redis übrigens scheinbar recht großer Beliebtheit. Wird auf der Webseite des Python Package Index "redis" als Suchbegriff eingegeben, wird eine ziemliche lange Ergebnisliste angezeigt. So gibt es z.B. Anbindungen für Django, Flask und Celery an Redis sowie eine Vielzahl anderer Module, die Python auf die ein oder andere Weise mit Redis kombinieren.

Installation

Das empfohlene Python-Modul heißt einfach nur "redis" und lässt sich ganz einfach via pip oder easy_install installieren. Die die Ergebnisse der Abfragen des Redis-Servers werden von einem in Python implementierten Parser verarbeitet - was ohne Probleme funktioniert, aber nicht ultimativ schnell ist. Wer die volle Geschwindigkeit braucht, der installiert noch das Modul "hiredis" (via pip oder easy_install). So wird der in C geschriebene Parser inklusive Python-Bindings installiert. Die Entwickler nennen eine Geschwindigkeitssteigerung um das 10-fache, im Vergleich zum Python-Parser.

Mit dem Server verbinden

Das Verbinden mit einem laufenden Server ist einfach:

>>> r = redis.StrictRedis(host='localhost',port=6379,db=0)

Im Gegensatz zur z.B. Kommandozeile von Redis muss hier eine Datenbanknummer angegeben werden. Das Python-Modul erlaubt es übrigens nicht, die Datenbank im laufenden Betrieb zu wechseln (was z.B. via Redis Kommandozeile problemlos geht), weil die Datenbankverbindung dann nicht mehr "thread-safe" wäre. Wer also mehrere Datenbanken für sein Programm benötigt, der muss mehrere Instanzen von redis.StrictRedis(...) anlegen.

Connection Pooling

Im Hintergrund legt redis.StrictRedis(...) für jede neu Instanz einen neuen, eigenen Connection Pool an. Was je nach Anwendung aber gar nicht nötig ist. Aber es ist auch möglich, dass sich mehrere Instanzen einen Connection Pool teilen:

>>> pool = redis.ConnectionPool(host='localhost', port=6379, db=0)
>>> r = redis.Redis(connection_pool=pool)

Auch hier gilt: Ein Wechsel der Datenbank innerhalb eines Connection Pools ist nicht möglich bzw. - andersherum - ein Connection Pool kann immer nur für eine Datenbank gelten.

Datentypen

Natürlich unterstützt das Python-Modul allen Datentypen voll. Dabei werden so gut wie alle Befehle von Redis 1:1 in Python umgesetzt, so dass die Redis Befehlsreferenz bei Fragen zu Rate gezogen werden kann. Im folgenden ein paar einfache Beispiele zur Nutzung der Datentypen aus Python heraus: Strings sind der einfachste Datentyp:
>>> r.set('foo','bar')
True
>>> r.get('foo')
'bar'

Und ein paar Beispiele zu Listen in Redis:
>>> r.lpush('liste','foo')
1L
>>> r.lpush('liste','bar')
2L
>>> r.lrange('liste',0,-1)
['bar', 'foo']

Hashs lassen sich mit den bekannten Befehlen generieren:
>>> r.hset('hash','foo','bar')
1L
>>> r.hset('hash','spam','egg')
1L

Werden alle Werte innerhalb eines Hashs abgefragt, dann wird ein Python Dictionary zurück geliefert:
>>> r.hgetall('hash')
{'foo': 'bar', 'spam': 'egg'}

bei Einzelwerten logischerweise ein String:
>>> r.hget('hash','foo')
'bar'

Hashs lassen sich auch direkt aus einem Python Dictionary generieren:
>>> my_dict = {'Rhythmbox':'Audio','Totem':'Video'}
>>> r.hmset('progs',my_dict)
>>> r.hmget('progs','Rhythmbox','Totem')
['Audio', 'Video']

Anlegen eines Redis-Sets aus Python heraus:
>>> r.sadd('set','foo')
1
>>> r.sadd('set','bar')
1

Wird versucht, einen bereits vorhanden Wert zu einem Redis-Set hinzuzufügen, dann liefert die Datenbank einfach "0" (für "False") zurück:
>>> r.sadd('set','bar')
0
Werden alle Werte aus einem Redis-Set abgefragt, wird als Ergebnis ein Python-Set zurück geliefert:
>>> r.smembers('set')
set(['foo', 'bar'])
Ordered Sets werden wie folgt in Redis via Python angelegt:
>>> r.zadd('orderset',10,'bar')
1
>>> r.zadd('orderset',5,'foo')
1
Oder alternativ auch:
>>> r.zadd('orderset',spam=1.0,egg=2.0)
2
Die als Ergebnis einer Abfrage eines Ordered Sets wird entweder eine Liste von Strings oder eine Liste von Tuplen geliefert. Je nach dem, ob die Gewichtung der Werte mit abgefragt wird oder nicht:

>>> r.zrange('orderset',0,-1,withscores=True)
[('spam', 1.0), ('egg', 2.0), ('foo', 5.0), ('bar', 10.0)]
>>> r.zrange('orderset',0,-1,withscores=True,desc=True)
[('bar', 10.0), ('foo', 5.0), ('egg', 2.0), ('spam', 1.0)]

Transaktionen

Das Python Modul unterstützt auch das Zusammenfassen von mehreren Befehlen zu einer atomaren Transaktion. Allerdings setzt das Modul nicht die Redis-Befehle multi und exec um, sondern geht den Weg über Pipelines:

>>> pipe = r.pipeline()
>>> pipe.set('counter',1)
<redis.client.StrictPipeline object at 0x18cfb10>
>>> pipe.set('wort','irgendwas')
<redis.client.StrictPipeline object at 0x18cfb10>
>>> pipe.incr('counter')
<redis.client.StrictPipeline object at 0x18cfb10>
>>> pipe.execute()
[True, True, 2]

Alle Befehle werden also gepuffert und erst nach dem Aufruf von pipe.execute() im Block atomar ausgeführt. Das Ergebnis der Ausführung der einzelnen Befehle wird als Liste zurück geliefert.

Weitere Dokumentation

Hier im Blogeintrag wurden zwar die meisten, aber nicht alle Möglichkeiten des Python Redis-Moduls gezeigt. Diese sind aber natürlich in der Dokumentation aufgeführt.

Redis und ORM

Zu guter Letzt sei noch erwähnt, dass es auch auch eine Reihe von Object Relational Mapperns (ORM) für Python und Redis gibt. Diese tragen aber teilweise noch eine relativ niedrige Versionsnummer. Weiter entwickelt - wenn auch noch im Beta-Stadium - scheint Redisco zu sein. Andere ORMs bringt eine Suche im Python Package Index hervor. 

Alle Beispiele hier im Blogartikel sind unter Ubuntu 12.04, Python 2.7, redis-py 2.6.2 und dem Redis Server 2.4.16 getestet.

Sonntag, 4. September 2011

Cassandra und Python Teil 2 - der Objectmapper

Im ersten Teil des Artikels zur Nutzung von Cassandra mit Python wurde das pycassa-Modul beschrieben.

pycassa bringt zusätzlich auch noch einen Objectmapper mit, mit dessen Hilfe man Pythonklassen auf Column Families mappen kann. Als Beispiel Keyspace dient, wie im ersten Teil auch, "pydemo" und als Column Family wieder "pykontakt". Alle Beispiel werden in der Python-Shell ausgeführt.

Als erstes muss man natürlich pycassa wieder importieren:

>>> import pycassa

Danach legen wir die Klasse an, auf die später gemappt werden soll:

>>> class CFMap(object):
...        name = pycassa.String(default='nobody')
...        alter = pycassa.IntString(default=0)

Wie man sieht, muss man Datentypen aus pycassa / Cassandra zuordnen. Der Default wird immer dann eingesetzt, wenn man selber keine weiteren Vorgaben macht.

Jetzt wird einer Verbindung zum laufenden Cassandra-Server hergestellt sowie der Keyspace "pydemo" und Column Family "pykontakt" genutzt:

>>> pool = pycassa.connect('pydemo')
>>> cf = pycassa.ColumnFamily(pool,'pykontakt',
         autopack_values=False, autopack_names=False)

Wichtig sind die beiden Festlegungen von "autopack_values" und "autopack_names" auf "False" (der Default ist "True"), da im Falle von Mappings die automatische Konvertierung des Datentyps durch den Mapper erfolgt.

Jetzt kann man das eigentliche Mapping vornehmen:

>>> CFMap.objects = pycassa.ColumnFamilyMap(CFMap,cf)

Das Mapping kann man jetzt für neue Objekte nutzen:

>>> c = CFMap()
>>> c.key = 'User100'
>>> c.name = 'Susi'
>>> c.alter = 25

Das Objekt kann nun gespeichert werden:

>>> CFMap.objects.insert(c)
1315076828600806

und natürlich auch abgefragt:

>>> CFMap.objects.get(c.key).name
'Susi'
>>> CFMap.objects.get(c.key).alter
25

Die Defaultwerte werden immer dann eingesetzt, wenn man einer Spalte keinen Wert zuweist:

>>> c2 = CFMap()
>>> c2.key = 'User101'
>>> c2.name = 'Rainer'
>>> CFMap.objects.insert(c2)
1315077378894450
>>> CFMap.objects.get(c2.key).name
'Rainer'
>>> CFMap.objects.get(c2.key).alter
0

Über den Befehl multiget können mehrere Schlüssel auf einmal abgefragt werden, wobei das Ergebnis wie Dictionary abgefragt werden kann:

>>> erg = CFMap.objects.multiget([c2.key,c.key])
>>> erg
OrderedDict([('User101', <__main__.CFMap object at 0x7fe4bfa4ed90>), ('User100', <__main__.CFMap object at 0x7fe4bfa4edd0>)])
>>> erg['User100']
<__main__.CFMap object at 0x7fe4bfa4edd0>
>>> erg['User100'].name
'Susi'
>>> erg['User101'].alter
0

Über den Mapper könne auch Objekte gelöscht werden:

>>> CFMap.objects.remove(c2)
1315078335744352

Eine vollständige Übersicht über den Pycassa-Mapper findet man in der API-Referenz.

Hinweis: Alle Beispiele sind unter Python 2.6 mit Ubuntu 10.04, pycassa 1.1.1 und Cassandra 0.8.2 getestet.

Cassandra und Python

Ergänzend zum Cassandra-Artikel in FreiesMagazin 9/2011 werden hier noch zusätzliche Infos zur Nutzung von Cassandra mit Python gegeben.

Für alle, die den Artikel noch nicht gelesen haben sollten: Cassandra ist die Datenbank hinter Facebook (ist also für "großes" geschaffen), Open Source (in der Obhut der Apache Foundation unter Apache Lizenz) und hat ein interessantes, weil strukturiertes aber trotzdem flexibles Datenmodell.

Und natürlich gibt es auch ein Python-Modul namens "pycassa", mit dem man auf Cassandra Datenbanken zugreifen kann.

Installation

Die Installation ist mittels easy_install schnell erledigt:

sudo easy_install thrift
sudo easy_install pycassa

pycassa braucht das thrift-Modul, löst es als Abhängigkeit aber nicht selber auf, so dass man es selber händisch installieren muss.

Jetzt kann man via Python auf Cassandra zugreifen. Dazu muss man das Modul wie üblich importieren:

>>> import pycassa

Keyspace und Column Family anlegen

Um mit pycassa ein Verbindung zum Server herstellen, ein Keyspace anzulegen usw. benötigt man den "system_manager":

#Verbindung zum Server herstellen
>>> sys = pycassa.system_manager.SystemManager('localhost:9160')
#Keyspace "pydemo" anlegen, Replikation "SimpleStrategy" (=1)
>>> sys.create_keyspace('pydemo',1)
#Column Family "pykontakt" anlegen
>>> sys.create_column_family('pydemo','pykontakt')

Die gezeigten Befehle sind die "Minimalversion". Für eine vollständige Übersicht sollte man einen Blick in die zugehörige API-Dokumentation werfen.

Mit der Datenbank arbeiten

Jetzt kann man mit der Column Family arbeiten.

#Verbindung zur Datenbank und Keyspace "pydemo" herstellen
>>> pool = pycassa.connect('pydemo')
#Column Family "pykonakt" auswählen
>>> cf = pycassa.ColumnFamily(pool,'pykontakt')
#einige Spalten mit Wert eingeben
>>> cf.insert('User_1',{'name':'Susi'})
>>> cf.insert('User_1',{'alter':'25'})
#es ist auch möglich, mehrere Spalten auf einmal zu schreiben
>>> cf.insert('User_2',{'name':'Otto','alter':30})
>>> cf.insert('User_3',{'name':'Rainer','alter':'25'})

Es wurden also drei Schlüssel, "User_1", User_2", "User_3", angelegt und jeweils ein Wert in die Spaten "name" und "alter" geschrieben.

Die Werte können natürlich auch abgefragt werden:

#User_1 abfragen
>>> cf.get('User_1')
OrderedDict([('alter', '25'), ('name', 'Susi')])
#umgekehrte Reihenfolge
>>> cf.get('User_1',column_reversed=True)
OrderedDict([('name', 'Susi'), ('alter', '25')])
#nur eine Spalte abfragen
>>> cf.get('User_1',columns=['name'])
OrderedDict([('name', 'Susi')])
#Zeitstempel mit Abfragen
>>> cf.get('User_1',include_timestamp=True)
OrderedDict([('alter', ('25', 1312827347304141)), ('name', ('Susi', 1312827336484155))])

Wie man sieht ist das Ergebnis einer Abfrage vom Datentype OrderedDict, es kann darauf also wie auf ein Dictionary zugegriffen werden.

Im direktem Vergleich zum Cassandra-CLI übernimmt pycasssa dankenswerterweise die Konvertierung des Datentyps, d.h. beim Schreiben in die Datenbank wird alles automatisch nach "ByteType" konvertiert und beim Lesen wieder zurück nach UTF-8 usw.

Spalten indizieren

Cassandra unterstützt auch Abfragen über einen Index. Dazu muss die entsprechende Spalte aber zuerst explizit indiziert werden, was wieder über den weiter oben erwähnten SystemManager erfolgen kann:

>>> sys.create_index('pydemo','pykontakt',
'alter',pycassa.system_manager.BYTES_TYPE,index_name='alter_idx')

Hiermit wird im Keyspace "pydemo" aus der Column Family "pykontakt" die Spalte "alter" indiziert. pycassa.system_manager.BYTES_TYPE legt den Typ der Spalte fest, in diesem Fall BYTES_TYPE, den Defaulttyp. Die Angabe des Indexnames ist optional.

Mit einer so indizierten Spalte kann eine entsprechende Abfrage gestartet werden. Im folgenden wird nach Einträgen gesucht, bei denen das Alter gleich 25 ist.

#Suchausdruck festlegen
>>> alter_expr = pycassa.index.create_index_expression('alter','25')
>>> clause = pycassa.index.create_index_clause([alter_expr])
#Abfrage durchführen
>>> for key,user in cf.get_indexed_slices(clause):
...     print 'Schlüssel: %s' %key
...     print 'Name: %s, Alter: %s' %(user['name'],user['alter'])
...
Schlüssel: User_3
Name: Rainer, Alter: 25
Schlüssel: User_1
Name: Susi, Alter: 25

Natürlich ist es auch möglichen, Spalten oder ganze Schlüssel zu löschen:

#Spalte "alter" für "User_3" löschen
>>> cf.remove('User_3',['alter'])
1312829216114192
>>> cf.get('User_3')
OrderedDict([('name', 'Rainer')])
#Schüssel "User_3" komplett löschen
>>> cf.remove('User_3')
1312829235495355
>>> cf.get('User_3')
Traceback (most recent call last):
File "", line 1, in
File "/usr/local/lib/python2.6/dist-packages/pycassa-1.1.1-py2.6.egg/pycassa/columnfamily.py", line 409, in get
raise NotFoundException()
pycassa.cassandra.c08.ttypes.NotFoundException: NotFoundException()

weiterführende Hinweise

Wie oben bereits erwähnt bietet pycassa wesentlich mehr Möglichkeiten als hier gezeigt. Für Interessierte sei ein Blick in die Dokumentation oder das ebenfalls online verfügbare, etwas ausführlichere, englischsprachige Tutorial (an dem sich dieses Tutorial hier auch orientiert) empfohlen.

Im zweiten Teil dieses Blogeintrags wird der ebenfalls in pycasse enthaltene Mapper gezeigt.

Hinweis: Alle Beispiele sind unter Python 2.6 mit Ubuntu 10.04, pycassa 1.1.1 und Cassandra 0.8.2 getestet.