Dokumente in CouchDB wieder herstellen

Das Dateiformat bei CouchDB ist append-only, d.h. Veränderungen an der Datenbank ändern niemals bestehende Daten, sondern es wird immer am Ende angehängt. Das hat den Vorteil, dass bei einem Absturz o.ä., Daten nicht korrupt gehen können, keine langen Konsistenz-Checks beim Starten durchgeführt werden müssen und kein Locking stattfindet und dadurch extrem skalierbar ist.

Aber wie werden dann Daten in CouchDB gelöscht?

Beim Löschen eines Dokuments (Datensatz) wird dieses mit dem zusätzlichen Attribut _deleted und dem Wert true an die Datenbank angehängt. Da bei jeder Änderung eines Dokuments sich die sogenannte Revision ID ändert, erhält man auch beim Löschen eines Dokuments eine neue Revision ID. Dieses neue (angehängte) Dokument ist jedoch nur noch ein stub (Stumpf). D.h. es beinhaltet (fast) nichts mehr von seinen ursprünglichen Daten.

Einzig die Attribute ID, Revision ID und _deleted sind erhalten. Endgültig gelöscht werden Daten erst, wenn man eine Datenbank in CouchDB compacted. Da wird dann altes Zeug ausgemistet.

Das bedeutet, man kann auf gelöschte Dokumente weiterhin zugreifen, wenn man vorherige Revision IDs verwendet und kann damit z.B. Datensätze wieder herstellen. Da die letzte Revision ja nur noch ein Stumpf ist, muss also eine vorherige verwendet verwenden und es darf vorher solange NICHT compacted werden. Erst nachdem der Un-Delete durchgeführt wurde, darf man compacten. Ansonsten sind die Daten weg...

Details auf der Mailing Liste

Hier mal ein Beispiel, wie man das mit dem Python Modul für CouchDB macht. Hilfe dazu bekam ich auf der Mailingliste. Das Anzeigen von Dokumenten hab ich hier noch mit curl in einer Shell gemacht, damit es anschaulicher ist. Lässt sich aber auch direkt mit dem Python Modul machen. Testumgebung ist eine Ubuntu Karmic Installation, die mit CouchDB 0.10 und CouchDB-Python 0.6 daherkommt...

Anlegen einer Datenbank und Dokument erstellen

import couchdb
server = couchdb.Server()
db = server.create('testdb')
db['somedoc'] = {'type': 'Subtitle'}

Dokument anzeigen

curl -X GET 127.0.0.1:5984/testdb/somedoc
{"_id":"somedoc","_rev":"1-1d0d2e2f53eebed9dc22b38e05f7b585","type":"Subtitle"}

Dokument löschen

curl -X DELETE 127.0.0.1:5984/testdb/somedoc?rev="1-1d0d2e2f53eebed9dc22b38e05f7b585"
{"ok":true,"id":"somedoc","rev":"2-3369b4ffb9474a2ae9c22beb238a53a4"}

Wie man sieht, sagt CouchDB mit "ok":true, dass das Dokument gelöscht wurde und auch eine neue Revision ID bekommen hat.

Alte Revisionen anschauen

curl -X GET 127.0.0.1:5984/testdb/somedoc?rev=2-3369b4ffb9474a2ae9c22beb238a53a4
{"_id":"somedoc","_rev":"2-3369b4ffb9474a2ae9c22beb238a53a4","_deleted":true}

Na, etwas aufgefallen? Unser Attribut "type":"Subtitle" fehlt. Das ist der Stumpf, den ich zuvor angesprochen habe. Um eine vorherige Version mit allen Daten zu erhalten, dürfen wir nicht die letzte Revision ID nehmen, sondern eine davor.

curl -X GET 127.0.0.1:5984/testdb/somedoc?rev=1-1d0d2e2f53eebed9dc22b38e05f7b585
{"_id":"somedoc","_rev":"1-1d0d2e2f53eebed9dc22b38e05f7b585","type":"Subtitle"}

Tadaa. Unser altes Dokument. Um es wieder herzustellen, müssen wir diese Revision ID nutzen.

Dokument wieder herstellen

doc = db.get('somedoc', rev='1-1d0d2e2f53eebed9dc22b38e05f7b585')
db[doc.id] = doc

Mal sehen, was passiert ist:

curl -X GET 127.0.0.1:5984/testdb/somedoc
{"_id":"somedoc","_rev":"2-489230fd027dd8b7a1f96d1a0bd3928a","type":"Subtitle"}

Und siehe da. Unser Dokument ist wieder da. Und zwar erneut mit einer neuen Revision ID...