10

Click here to load reader

Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

  • Upload
    zuehlke

  • View
    573

  • Download
    3

Embed Size (px)

DESCRIPTION

NoSQL auf iOS-Devices Big iOS Data NoSQL-Datenbanken erfreuen sich im Webumfeld zunehmender Beliebtheit, doch kann man sie auch sinnvoll auf mobilen Geräten einsetzen? In diesem Artikel wollen wir uns anhand eines Beispiels die NoSQL-Datenbank CouchDB im Zusammenspiel mit iOS ansehen. Fazit In diesem Artikel haben Sie gesehen, wie CouchDB eingesetzt werden kann, um auf einfache Art und Weise eine asymmetrische Synchronisierung zwischen den Datenbeständen einer mobilen Anwendung und einer zentralen Anwendung zu implementieren. Insbesondere für Systeme, die zur Erfassung von großen Datenmengen gedacht sind, ist CouchDB durch seine Architektur eine gute Wahl. In vielen Fällen kommt man Dank des RESTbasierten Datenbank-APIs sogar ohne Application-Server aus. Wie kurz angedeutet wurde, kann CouchDB mithilfe von CouchApp sogar als Application-Server für Web-Apps dienen. In einem Folgeartikel werden wir auf den Einsatz von CouchDB auf Android eingehen und das Thema CouchApps vertiefen. Artikel von Peter Friese in der Mobile Technology 3/2012.

Citation preview

Page 1: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

CouchDB mobile | iOS 037

www.mobile360.de 3 | 2012 Mobile Technology

NoSQL auf iOS-Devices

NoSQL-Datenbanken erfreuen sich im Webumfeld zunehmen-

der Beliebtheit, doch kann man sie auch sinnvoll auf mobilen

Geräten einsetzen? In diesem Artikel wollen wir uns anhand

eines Beispiels die NoSQL-Datenbank CouchDB im Zusam-

menspiel mit iOS ansehen.

Big iOS Data

Page 2: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

038 iOS | CouchDB mobile

www.mobile360.deMobile Technology 3 | 2012

von Peter Friese

CouchDB ist eine Open-Source-NoSQL-Datenbank, die über einige interessante Eigenschaften verfügt, die eine Verwendung in mobilen Projekten besonders geeignet erscheinen lassen. Eine der wesentlichen Eigenschaften ist die durchgängige Verwendung von Webprinzipien: die Interaktion mit CouchDB kann vollständig über HTTP erfolgen – das gesamte API ist REST-basiert und kann im Prinzip mittels curl-Kommandos gesteuert wer-den. Wem das zu kryptisch ist, kann auf das browser-basierte Administrations-Frontend Futon ausweichen (Abb.!1).

Anders als relationale Datenbanken speichert Couch-DB Daten und Relationen nicht in Tabellen. Stattdessen werden die Daten schemalos in JSON-basierten Doku-menten abgespeichert. Abfragen werden in JavaScript formuliert und nutzen das MapReduce-Modell [1]. CouchDB ist übrigens eine Abkürzung und steht für „Cluster of unreliable Commodity Hardware“, was auf eine weitere Kerneigenschaft hinweist: CouchDB ist für die Implementierung von verteilten Datenbanksys-temen geschaffen worden und zeichnet sich durch ein sehr einfaches aber dennoch ef!zientes inkrementelles Replikationsverfahren aus. Eventuell bei der Replikati-on auftretenden Kon"ikte werden durch Multi-Version Concurrency Control (MVCC) abgefangen: hier ge-winnt einem ausgeklügelten Algorithmus folgend repro-duzierbar immer eine bestimmte Dokumentenrevision, die verlierenden Revisionen werden ebenfalls abgespei-chert und können vom Client bei Bedarf zur Au"ösung des inhaltlichen Kon"ikts herangezogen werden. Durch dieses Verfahren kann auf Locking verzichtet werden, was sich hinsichtlich der Performance positiv auswirkt. Durch den Einsatz des Replikationsverfahrens eignet sich CouchDB sehr gut für die Implementierung von An-wendungen, die of"inefähig sein müssen: Während der Of"inephase werden die Daten in einer lokalen Couch-DB gespeichert und anschließend mittels Replizierung mit der zentralen Instanz synchronisiert – ein Vorgehen, das wir in diesem Artikel demonstrieren werden.

Ein beliebtes Beispiel für die Verwendung von Couch-DB ist GroceryList, eine Einkaufsliste, die von mehreren Personen gleichzeitig eingesehen und bearbeitet werden kann. Obwohl mit diesem Beispiel die meisten Funk-tionsprinzipien von CouchDB sehr gut erklärt werden können, werden die Fähigkeiten von CouchDB,#große Datenmengen ef!zient zu verwalten, nur unzureichend dargestellt – wer hat schon Einkaufslisten mit mehreren tausend Einträgen?

Aus diesem Grund soll im vorliegenden Artikel eine Lauf-App entwickelt werden. Die von jedem Läufer bei Verwendung der App aufgezeichneten GPS-Koordina-ten sollen sowohl in einer lokalen CouchDB als auch in einer zentralen Instanz in der Cloud abgelegt werden. Schon bei einem 5-km-Lauf werden zirka 300 bis 400 Wegpunkte aufgezeichnet, sodass schon bei einer recht überschaubaren Anzahl von Benutzern mit einem nicht

ganz unerheblichen Datenvolumen zu rechnen ist. Darü-ber hinaus ergeben sich durch das Beispiel einige zusätz-liche Probleme, die einer Lösung bedürfen.

Architektur unseres BeispielsWerfen wir nun einen Blick auf die Architektur unserer Lauf-App: Die Erfassung der gelaufenen Strecke soll mit einem Smartphone erfolgen (zunächst iPhone, später auch Android). Je nachdem wo die Laufstrecke liegt, ist vielleicht keine permanente Verbindung zum Internet möglich, daher ist eine lokale Speicherung der aufge-zeichneten GPS-Daten unerlässlich. Um spätere Auswer-tungen zu ermöglichen, sollen die Daten aber auf jeden Fall auch in einer zentralen Datenbank in der Cloud ab-gelegt werden. Sobald das Smartphone also wieder mit dem Internet verbunden ist, müssen die in der lokalen Datenbank gespeicherten Daten mit der zentralen Da-tenbank synchronisiert werden.

Diese Anforderungen können wir mit einer recht ein-fachen Architektur umsetzen: Die beim Laufen aufge-zeichneten GPS-Koordinaten werden in einer lokalen CouchDB-Instanz auf dem Smartphone gespeichert. Um eine zentrale Auswertung zu ermöglichen, installieren wir auf einem Server im Web eine weitere CouchDB-Ins-tanz. Diese zentrale CouchDB dient den mobilen Clients als Synchronisationspunkt: sobald ein Client online ist, synchronisiert er den Inhalt seiner lokalen Datenbank mit der zentralen Instanz (Abb.!2). Dass es dabei nicht zu Kon"ikten kommt, ist der Tatsache zu verdanken, dass CouchDB UUIDs als Schlüssel für jeden Datensatz verwendet – die Gefahr einer Schlüsselkollision auch über mehrere Rechner hinweg ist extrem gering. Bei der Synchronisation ist zu beachten, dass nicht alle in der zentralen Datenbank gespeicherten GPS-Koordinaten mit den mobilen Clients synchronisiert werden dürfen. Einerseits wären das bereits nach kurzer Zeit zu viele Datensätze für den auf dem Smartphone limitierten Speicher. Andererseits darf jeder Benutzer natürlich nur seine eigenen Daten sehen. Die Synchronisation vom Server zu den mobilen Clients muss also durch einen Filter abgesichert werden. Erfreulicherweise muss die Synchronisation der Daten nicht selbst entwickelt wer-den: CouchDB verfügt über einen sehr einfachen, aber dennoch leistungsfähigen Replikationsmechanismus, den wir für unsere Zwecke einsetzen können. Auf diese Weise wird unsere App übrigens of"inefähig, ohne dass wir viel Mühe investieren müssen.

CouchDB auf iOSUnter [2] steht ein CouchDB Build zur Verfügung, der für die iOS-Plattform optimiert wurde. Sie werden sich jetzt vielleicht ein wenig wundern, aber wir wer-den diesen Build nicht verwenden. Stattdessen kommt TouchDB zum Einsatz. Ein paar Worte der Erklärung scheinen angebracht. Wie bereits angesprochen, ba-siert CouchDB auf Erlang. CouchDB für iOS besteht im Prinzip nur aus einem sehr dünnen Wrapper um die in Erlang geschriebene CouchDB Engine. Das hört sich

Page 3: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

www.mobile360.de

CouchDB mobile | iOS 039

zuerst einmal nach einer guten Idee an, hat aber zwei entscheidende Nachteile: die Erlang Runtime ist alleine schon zirka!4 bis!6 Megabyte groß und auch die Startup-Zeit liegt mit!5 bis 10 Sekunden außerhalb dessen, was auf einem mo-bilen Device toleriert werden kann. Aus diesem Grund hat das CouchDB-Team vor einiger Zeit damit begonnen, eine CouchDB-kompatible Engine speziell für mobile Devices zu schreiben. Das Ergebnis ist TouchDB [3], [4]. Die Designziele für TouchDB sind folgende:

geringe Codegröße (idealerweise < 256 KB)schnelle Startzeiten (idealerweise < 100 ms)geringer Hauptspeicherverbrauchausreichende Performance auf mobilen CPUs

Neben diesen Zielen sind zwei weitere Treiber maßgeblich für die Architektur von TouchDB verantwortlich: einerseits sind dies die Einschränkungen der iOS-Plattform (z.!B. können keine Third-Party-Prozesse im Hintergrund ausgeführt werden, was eine Architektur mit einem auf dem Smartphone ausgeführten lo-kalen Server ad absurdum führt), anderseits sollte das TouchDB API dem REST-basierten CouchDB API nachempfunden werden, um eine möglichst reibungslose Weiterverwendung von existierenden Clients zu ermöglichen. Um den REST-basierten Zugriff zu ermöglichen, implementiert TouchDB ein NSURLProtocol und registriert das URL-Schema touchdb. Dank dieser Implementierung kann ein iOS-Client ganz normal über REST-Aufrufe mit TouchDB kommunizieren. Wer lieber Cocoa-Idiome nutzt, sollte CouchCocoa [5] verwenden: eine ursprünglich für CouchDB geschriebene Zugriffsbibliothek, die aber auch für TouchDB ver-wendet werden kann.

CouchCocoaCocoaTouchDB speichert seine Daten lokal in einer SQLite-Datenbank ab. MapReduce-Funktionen werden bei TouchDB nicht in JavaScript geschrieben, sondern in Objective-C nativ implementiert, was wir später noch detailliert be-trachten werden.

Setup des ProjektsUm TouchDB und CouchCocoa in einem Projekt verwenden zu können, sind folgende Schritte auszuführen:

Abb. 1: Browserbasiertes Administrations-Frontend Futon

06_2012_mobile_tech_70x297_R.indd 1 20.06.12 17:07

leonor
Anzeige
Page 4: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

040 iOS | CouchDB mobile

www.mobile360.deMobile Technology 3 | 2012

Neues leeres Projekt anlegen (mit ARC)Im Projektverzeichnis ein Verzeichnis Vendor für die 3rd-Party-Bibliotheken anlegenTouchDB-iOS klonen:

- git clone git://github.com/couchbaselabs/TouchDB-iOS.git

Submodule aktualisieren: - cd TouchDB-iOS - git submodule init - git submodule updateTouchDB.xcodeproj öffnen und das Target iOS Framework bauen

- Vendor/TouchDB-iOS/build/Products/Debug-ios-universal/TouchDB.frameworkper per Drag and Drop auf die Frameworksgruppe des CouchTo5K-Projekts fallen lassen

Für das Target CouchTo5K unter B!"#$ S%&&"'() nun O&*%+ L"',%+ F#-() auf -ObjC setzen

Für das Target CouchTo5K unter B!"#$ P*-)%) die folgenden Frameworks aufnehmen:

- libz.dylib - libsqlite3.dylib - SystemCon!guration.framework - Security.framework - CFNetwork.framework - Wieder zu Vendor wechselnCouchCocoa klonen:

- git clone https://github.com/couchbaselabs/Couch-Cocoa.git

- cd CouchCocoa - git submodule init - git submodule update - CouchCocoa.xcodeproj öffnen und das Target

„iOS Framework“ bauen - Vendor/CouchCocoa/build/Products/Debug-ios-

universal/CouchCocoa.framework per Drag and Drop auf die Frameworkgruppe des CouchTo5K-Projekts fallen lassen

UI für das Tracken von GPS Waypoints bauenWie man unter iOS die aktuelle Position bestimmt und Positionsveränderungen abonniert, ist bereits in anderen Artikeln detailliert beschrieben worden. Dennoch stellt die Bestimmung der aktuellen Position die Grundlage für unsere Lauf-App dar, daher soll hier wenigstens die prinzipielle Herangehensweise kurz beschrieben werden. Um einen GPS-Track aufzuzeichnen, verwenden wir die Klasse CLLocationManager aus dem CoreLocation-Framework. Wir registrieren uns als CLLocationMana-gerDelegate, um über Positionsänderungen informiert zu werden. Da die Ermittlung von GPS-Koordinaten recht viel Energie verbraucht, sollte die Ermittlung von GPS-Koordinaten erst dann aktiviert werden, wenn sie tatsächlich benötigt wird. Durch einen Aufruf der Me-thode startUpdatingLocation wird die Ermittlung der aktuellen Position gestartet. Der Location-Manager ruft nun bei jeder Positionsänderung die Delegate-Methode locationManager:didUpdateToLocation:fromLocation: auf und liefert dabei sowohl die alte als auch die neue Position.

Daten lokal speichernDie so ermittelten GPS-Positionen speichern wir zu-nächst in einer lokalen CouchDB. An dieser Stelle sei noch einmal darauf hingewiesen, dass CouchDB eine schemalose Datenbank ist – wir können also beliebig strukturierte Daten speichern. Zunächst müssen wir eine Verbindung zur Datenbank aufbauen (Listing.1).

Die einzelnen Trackpoints speichern wir als individu-elle CouchDB-Dokumente ab. Jedes Dokument enthält natürlich Länge und Breite der aktuellen GPS-Position sowie den Zeitpunkt, an dem diese Information auf-gezeichnet wurde. Darüber hinaus speichern wir auch noch den Namen des Benutzers sowie die ID des aktu-ellen Laufs mit jedem Dokument ab. Diese Information benötigen wir später, um per Map Reduce verschiede-

Listing 2NSDictionary *trackpointProperties = [NSDictionary dictionaryWithObjectsAndKeys: self.runnerName, @"user", self.runName, @"run", [NSString stringWithFormat:@"%@", [NSNumber numberWithDouble:newLocation.coordinate.latitude]], @"lat", [NSString stringWithFormat:@"%@", [NSNumber numberWithDouble:newLocation.coordinate.longitude]], @"lon", [NSString stringWithFormat:@"%@", newLocation.timestamp] , @"time", nil];

CouchDocument *trackpointDocument =[database untitledDocument];RESTOperation* op = [trackpointDocument putProperties:trackpointProperties];[op onCompletion: ^{ if (op.error) NSLog(@"Couldn't save the new item");}];[op start];

Listing 1(CouchDatabase *)connect{ CouchTouchDBServer *server = [CouchTouchDBServer sharedInstance]; NSAssert(!server.error, @"Error initializing TouchDB server: %@", server.error); self.database = [server databaseNamed:@"couch25k"]; NSError *error; if (! [self.database ensureCreated:&error]) { // raise error self.connected = false; } self.connected = true; self.database.tracksChanges = YES; return self.database;}

Page 5: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

CouchDB mobile | iOS 041

www.mobile360.de 3 | 2012 Mobile Technology

ne Auswertungen über die gesamte Menge der in der Datenbank abgelegten Daten auszuführen, zum Beispiel um die Trackpoints eines ganz bestimmten Laufs zu er-mitteln oder die Liste aller Läufe eines einzelnen Läufers.

Um ein neues CouchDB-Dokument anzulegen, ver-wenden wir das CouchCocoa API. Die Methode untit-ledDocument der Klasse CouchDatabase erzeugt ein leeres Dokument. Dieses Dokument können wir nun mittels der Methode putProperties mit Informationen befüllen. Da CouchDB schemalos arbeitet, bietet es sich an, die Daten ganz zwanglos mittels eines NSDictionary zu übergeben.

Noch sind die Daten nicht in der Datenbank gespei-chert. Der Aufruf der Methode putProperties liefert als Ergebnis eine RESTOperation, die ausgeführt werden muss, um die Daten in der Datenbank zu speichern. Das erfolgt dann durch den Aufruf der Methode start auf der Operation. Eventuelle Probleme beim Speichern können mittels eines Completion Handlers behandelt werden (Listing!2). Mit diesem relativ einfachen Vorge-hen werden also alle Trackpoints des aktuellen Laufs in der lokalen CouchDB auf dem Smartphone des Läufers gespeichert. Am Ende des Laufs wird die Aufzeichnung der GSP-Position mittels [locationManager stopUp-datingLocation] beendet.

Darstellung der KarteNach dem Lauf möchte man sich vielleicht die zurück-gelegte Strecke auf einer Karte anschauen. Hierzu müs-sen wir alle GPS-Koordinaten des entsprechenden Laufs aus der lokalen Datenbank extrahieren und sie dann an-schließend in der richtigen Reihenfolge als Punkte eines Polygons auf einer Karte darstellen.

Datenbankfragen werden in CouchDB nicht mit SQL formuliert, stattdessen kommt Map Reduce

zum Einsatz [1]. Bei diesem zweistu"gen Vorgehen werden zunächst alle Daten der Datenbank mithilfe einer Map-Funktion entsprechend den Anforderun-gen der gewünschten Abfrage gruppiert. Das Ergeb-nis der Map-Funktion ist eine Liste von Tupeln. Wie diese Tupel aufgebaut sind, hängt stark von der ent-sprechenden Abfrage ab. Meistens werden jedoch ein oder mehrere Attribute der verarbeiteten Dokumente als Element der Tupel verwendet. Jede Map-Funktion verarbeitet im Prinzip alle Dokumente einer Daten-bank, allerdings liefert sie nicht für jedes Dokument ein Ergebnis. Bedenken Sie, dass eine Datenbank be-dingt durch die schemalose Natur von CouchDB be-liebige Dokumententypen enthalten kann. Also muss jede Map-Funktion überprüfen, ob das aktuelle Doku-ment verarbeitet werden soll oder nicht. Möchte man innerhalb eines Dokumententyps nur einen bestimm-ten Bereich der Daten analysieren, kann die Abfrage durch die Angabe einer oberen und unteren Schranke eingegrenzt werden. Diese Schranken sind wiederum Tupel, die genauso aufgebaut sein müssen wie die von der jeweiligen Map-Funktion produzierten Tupel. Hat man die gewünschten Daten auf diese Weise aufberei-tet und ge"ltert, kann mithilfe einer Reduce-Funktion in einem abschließenden Schritt eine Auswertung über diese Daten ausgeführt werden. Zum Beispiel könnte man die Anzahl der zurückgelieferten Datensätze zäh-

!"# $%&'"["run-peterfriese–5","2012–03–27 06:40:33 +0000"] {

"_id":"AB5FA179-D305–42D5-B863–0F50F32E54D5", "_rev":"1–81108A3D–051D–4139-B4C5–9F031185870E", "run":"run-peterfriese–5", "user":"peterfriese", "lon":"11.59023645278115", "time":"2012–03–27 06:40:33 +0000", "lat":"48.13086178722513"}

["run-peterfriese–5","2012–03–27 06:40:51 +0000"] { "_id":"AB5FA179-D305–42D5-B863–0F50F32E54D5", "_rev":"1–81108A3D–051D–4139-B4C5–9F031185870E", "run":"run-peterfriese–5", "user":"peterfriese", "lon":"11.59022621909413", "time":"2012–03–27 06:40:51 +0000", "lat":"48.13059268764777"}

Tabelle 1: Ergebnisse der Map-Funktion

In einer schemalosen Daten-bank lassen sich beliebig struk-turierte Daten speichern.

Page 6: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

042 iOS | CouchDB mobile

www.mobile360.deMobile Technology 3 | 2012

len oder die der Werte in einem bestimmten Attribut bilden.

Die Map-Funktion muss seiteneffektfrei sein und bei jedem Aufruf für die gleichen Eingabeparameter stets die gleichen Ergebnisse liefern. Auch die Reduce-Funk-tion muss seiteneffektfrei ausgeführt werden und darf nur von den Eingabeparametern abhängig sein. Diese Eigenschaften erlauben eine parallele und von einan-der unabhängige Ausführung der beiden Funktionen – Grundlage zum Beispiel für eine Verteilung der Be-rechnung auf mehrere Rechner. Außerdem wird so eine inkrementelle Verarbeitung der Datensätze ermöglicht: Die Map-Funktion wird bereits beim Einfügen eines neuen Datensatzes ausgeführt, sodass Abfragen später über die so ermittelten Schlüssel ausgeführt werden kön-nen – mit entsprechenden Geschwindigkeitsvorteilen. Für die Ermittlung der Trackpoints eines Laufs reicht es aus, eine Map-Funktion in Kombination mit einer obe-ren und unteren Schranke zu verwenden.

In CouchDB werden Map/Reduce-Funktionen üb-licherweise in JavaScript geschrieben. In TouchDB für iOS ist das jedoch derzeit nicht möglich; die Integration eines JavaScript-Interpreters würde den Designzielen von TouchDB widersprechen. Daher müssen wir die ge-schilderte Map-Funktion in Objective-C programmie-ren (Listing!3).

Zunächst wird ein so genanntes Designdokument angelegt. Wie bereits mehrfach erwähnt, ist CouchDB schemalos – nicht nur Daten werden als Dokumente in der Datenbank gespeichert, sondern auch die Abfragen. Ein Designdokument kann eine beliebige Anzahl von Map/Reduce-Funktionen aufnehmen. Normalerweise werden Designdokumente in der Datenbank gespei-chert, im Fall von TouchDB ist das anders: hier werden Designdokumente nicht persistiert, sondern müssen bei jedem Start der Anwendung neu angelegt werden. In dem soeben angelegten Designdokument wird anschlie-ßend eine Map-Funktion mit dem Namen waypoints_by_run de"niert. Die Map-Funktion wird durch einen Block implementiert. Die Signatur für MAPBLOCK sieht wie folgt aus:

#define MAPBLOCK(BLOCK) ^(NSDictionary* doc, void (^emit)(id key, id value)){BLOCK}

Der Block bekommt bei Ausführung also ein NSDic-tionary mit den Werten des aktuellen Dokuments sowie einen Zeiger auf eine emit-Funktion geliefert. Innerhalb des Blocks bauen wir nun ein Tupel der Form (Name-des-Laufs, Zeitstempel) auf. Dieses Tupel wird nun gemeinsam mit dem ursprünglichen Dokument an die emit-Funktion gesendet. Das Resul-tat ist eine View, die alle Dokumente über Schlüssel der Form (Names-des-Laufs, Zeitstempel) erreichbar macht. Tabelle!1 zeigt einen Ausschnitt aus der durch die Map-Funktion aufgebauten Struktur. Wie man se-hen kann, sind die Schlüssel Tupel, die aus dem Namen des Laufs sowie dem Zeitstempel aufgebaut sind. Als

Listing 3CouchDesignDocument *designViewWaypointsByRun = [self.database designDocumentWithName: @"couch25k"];[designViewWaypointsByRun defineViewNamed: @"waypoints_by_run" mapBlock: MAPBLOCK({ NSString *run = (NSString *)[doc objectForKey:@"run"]; id time = [doc objectForKey:@"time"]; NSMutableArray *key = [[NSMutableArray alloc] init]; [key addObject:run]; [key addObject:time]; emit(key, doc);}) version: @"1.1"];

Listing 4RESTOperation *fetch = [query start];[fetch onCompletion:^{ CLLocation *previous = nil; CLLocationDistance distance = 0; for (CouchQueryRow *row in query.rows) { id waypoint = row.value; CLLocationDegrees lat = [[waypoint objectForKey:@"lat"] doubleValue]; CLLocationDegrees lon = [[waypoint objectForKey:@"lon"] doubleValue]; CLLocation *location = [[CLLocation alloc] initWithLatitude:lat longitude:lon]; [self addNewLocation:location]; if (previous) { distance += [location distanceFromLocation:previous]; } previous = location; }}];

Listing 5- (CouchLiveQuery *)queryRuns{ // Create a 'view' containing list items sorted by date: CouchDesignDocument* design = [self.database designDocumentWithName: @"couch25k"]; [design defineViewNamed: @"runs" mapBlock: MAPBLOCK({ id run = [doc objectForKey:@"run"]; if (run) emit(run, nil); }) reduceBlock:REDUCEBLOCK({ return [NSNumber numberWithInt:values.count]; }) version: @"1.0"]; CouchLiveQuery* query = [[[self.database designDocumentWithName: @"couch25k"] queryViewNamed: @"runs"] asLiveQuery]; [query setGroupLevel:1]; query.descending = YES; return query;}

Page 7: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

www.mobile360.de

CouchDB mobile | iOS 043

Wert wird das jeweilige Dokument (d.!h. der GPS Trackpoint) zurückgegeben.Der Inhalt der View wird von der TouchDB Runtime in entsprechenden Tabel-

len in der zugrunde liegenden SQLite-Datenbank gespeichert, um später ef"zient darauf zurückgreifen zu können. Die Map-Funktion wird für jeden Datensatz nur ein einziges Mal aufgerufen, egal wie oft die entsprechende View abgefragt wird. Ändert sich später einmal ein Datensatz oder wird ein neuer Datensatz hinzuge-fügt, wird die Map-Funktion genau für diesen einen Datensatz erneut aufgerufen. So wird auf eine recht ef"ziente Weise sichergestellt, dass immer ein aktueller Index vorliegt.

Um alle Trackpoints eines bestimmten Laufs zu ermitteln, müssen wir Start- und Endschlüssel der Abfrage entsprechend setzen. Angenommen, der Lauf hat den Namen hamburg-17, so lautet der Startschlüssel folglich (hamburg-17). Als Endschlüssel geben wir das Tupel (hamburg-17, \ufff0) an, wobei es sich bei \ufff0 um ein sehr hohes Unicode-Zeichen handelt, was mit ziemlicher Wahrscheinlich-keit niemals Teil eines Schlüssels sein wird. So stellen wir sicher, dass die Abfrage alle Dokumente zurückliefert, deren Schlüssel den Namen des gewünschten Laufs enthält. Alle anderen Dokumente werden von der Abfrage ignoriert:

CouchQuery *query = [[self.database designDocumentWithName: @"couch25k"] queryViewNamed: @"waypoints_ by_run"];[query setStartKey:[NSArray arrayWithObjects:self.runKey, nil]];[query setEndKey:[NSArray arrayWithObjects:self.runKey, @"\ufff0", nil]];

Nun endlich kann die Abfrage ausgeführt werden. Das Ergebnis der Abfra-ge ist eine Liste von CouchQueryRows, die jeweils aus dem Schlüssel in der Form (Name-des-Laufs, Zeitstempel) und dem jeweiligen Dokument bestehen. CouchDB sortiert die Ergebnisse einer Abfrage stets anhand der Schlüssel und folgt dabei der unter [6] beschriebenen Sortierreihenfolge. Durch das Anhän-gen des Zeitstempels an die Schlüssel erreichen wir, dass die von der Abfrage zurückgelieferten Ergebnisse stets chronologisch aufsteigend sortiert sind. Die Wegpunkte müssen unbedingt in der chronologisch korrekten Abfolge geliefert werden, da ansonsten die Abbildung auf die Landkarte nicht der tatsächlichen Laufstrecke entspräche. Im schlimmsten Fall würden wir nur ein Zickzack-Muster erhalten.

Die Wegpunkte können nun sequenziell als Eckpunkte eines Polygons auf die Landkarte übertragen werden, wobei gleichzeitig die zurückgelegte Distanz be-rechnet werden kann (Listing!4).

Daten auf dem Client abfragenUm dem Benutzer eine Übersicht über sein Trainingspensum zu geben, benö-tigen wir noch eine Liste, in der alle bereits lokal abgespeicherten Läufe des Benutzers angezeigt werden. Zunächst benötigen wir dazu eine Map/Reduce View, die eine Liste aller Läufe des Benutzers aus der lokalen Datenbank zu-rückgibt (Listing!5).

Auch hier wird zunächst ein Designdokument für die Map- und Reduce-Funk-tionen angelegt. Die durch die Map/Reduce-Funktionen de"nierte View erhält den Namen runs. In der Map-Funktion wird als Schlüssel der Wert des Attributs run ausgegeben – das ist der symbolische Name des jeweiligen Laufs (in einer späteren Ausbaustufe sollte die App beziehungsweise die Datenbank sicherstel-len, dass keine doppelten Namen vergeben werden, in der aktuellen Fassung der Anwendung ist der Benutzer hierfür selbst zuständig). Als Wert wird nil aus-gegeben – wir interessieren uns ja nur für den Namen des Laufs, die anderen Attribute sind uns egal. Das Ergebnis der Map-Funktion ist eine Liste der Läufe; allerdings taucht der Name jedes Laufs gleich mehrmals auf, und zwar so oft, wie es Trackpoints für diesen Lauf gibt. Das ist natürlich nicht das, was wir wollen, aber wir können die Situation ausnutzen: Einerseits können wir mithilfe der Reduce-Funktion die Anzahl der Trackpoints eines Laufs ermittlen. Ande-rerseits können wir das Ergebnis gruppieren, um so die Duplikate zu entfernen.

leonor
Anzeige
Page 8: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

044 iOS | CouchDB mobile

www.mobile360.deMobile Technology 3 | 2012

Doch eins nach dem anderen: Zählen wir zunächst die Trackpoints. Dazu fügen wir der View runs eine Redu-ce-Funktion hinzu. Reduce-Funktionen werden auf der nach dem Schlüssel sortierten Ergebnismenge der je-weiligen Map-Funktion ausgeführt. Als Eingabe erhält

die Reduce-Funktion dazu eine Liste von Schlüssel und eine Liste von Werten. Wie viele Schlüssel beziehungsweise Werte die Reduce-Funktion bei jedem Aufruf er-hält, hängt vom Aufbau und Füllzustand des B-Baums ab, in dem das Ergebnis der Map-Funktion abgelegt ist. Detaillierte Informationen gibt es im (übrigens sehr empfehlenswerten) Buch „The De!niti-ve Guide To CouchDB“, das es sowohl als käu"ich zu erwerbende Papierversion [7] gibt als auch als kostenlos verfügbare Onlineversion [8]. Reduce und Re-Redu-ce werden zum Beispiel unter [9] erklärt. Um die Anzahl der Trackpoints eines Laufs zu ermitteln, zählen wir einfach die Anzahl der Elemente im Parameter keys der Reduce-Funktion (wir könnten auch die Anzahl der Elemente im Parameter values zählen). Die mehrfach auftreten-den Ergebnisse können wir eliminieren, indem wir das Ergebnis gruppieren. Da-bei aggregiert CouchDB die Ergebnisse

der Reduce-Funktion für jeden eindeutigen Schlüssel in der Ergebnismenge.

Die so ermittelten Daten sollen nun in einer Liste dar-gestellt werden. CouchCocoa unterstützt die Anzeige von Daten, die an ein Live-Query angebunden sind, mit der Klasse CouchUITableSource, mit dessen Hilfe ein UITableView schnell umgesetzt ist. Die relevanten Frag-mente des Quellcodes !nden Sie in Listing#6.

Daten zum Server synchronisierenBis jetzt haben wir uns hauptsächlich um den Teil der App gekümmert, der auf dem Smartphone läuft. Im nächsten Schritt sollen die aufgezeichneten Daten in eine zentrale Datenbank in der Cloud synchronisiert werden. Eine zentrale Speicherung der Daten ermöglicht eine ganze Reihe von interessanten Diensten: Zum Beispiel könnte man dem Anwender gra!sche Auswertungen seiner sportlichen Leistungen anzeigen, ähnlich wie das bei Runkeeper oder Nike+ möglich ist. Ein anderer inte-ressanter Einsatzzweck ist die Darstellung der aktuellen Läuferpositionen in einem Rennen. Da CouchDB die Daten nahezu live zwischen dem Smartphone und der zentralen Datenbank synchronisiert, kann so eine relativ präzise Darstellung der aktuellen Position eines Läufers ermittelt werden.

Die Synchronisierung von Daten zwischen zwei Da-tenbankinstanzen ist vielleicht eines der wichtigsten Features von CouchDB. Wer nun vermutet, dass die Synchronisierung von Daten zwischen zwei Datenbank-instanzen aufwändig und kompliziert einzurichten ist, der irrt. Das Aufsetzen der Replikation zwischen zwei Datenbankinstanzen ist mit CouchDB so einfach, dass das entsprechende Kapitel im CouchDB Guide nur schlappe vier Seiten umfasst [10].

In unserem Fall wollen wir eine asymmetrische Re-

Listing 6 (void)viewDidLoad{ [super viewDidLoad]; self.navigationItem.title = @"Run Log"; [self setupView];}

(void)setupView{ self.dataSource = [[CouchUITableSource alloc] init]; self.tableView.dataSource = self.dataSource; self.dataSource.tableView = self.tableView; self.dataSource.query = [[DatabaseAdapter sharedAdapter] queryRuns];}

(void)couchTableSource:(CouchUITableSource*)source willUseCell:(UITableViewCell*)cell forRow:(CouchQueryRow*)row{ cell.backgroundColor = [UIColor whiteColor]; cell.selectionStyle = UITableViewCellSelectionStyleGray; cell.textLabel.font = [UIFont fontWithName: @"Helvetica" size:18.0]; cell.textLabel.backgroundColor = [UIColor clearColor]; cell.textLabel.text = row.key;}

Abb. 2: Das browserbasierte Admin-Tool Futon

Page 9: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

CouchDB mobile | iOS 045

www.mobile360.de 3 | 2012 Mobile Technology

plikation einrichten: Alle auf dem Smartphone aufge-zeichneten Daten sollen in die Cloud synchronisiert werden, aber nicht alle in der Cloud gespeicherten Daten sollen auch wieder auf das Smartphone syn-chronisiert werden: Sobald mehrere Benutzer die App verwenden, werden auch ihre Daten in der Cloud ge-speichert. Diese Daten sollen natürlich nicht auf die Geräte der anderen Läufer synchronisiert werden. Einerseits natürlich aus Gründen des Datenschutzes, andererseits wäre die Datenmenge sicherlich bald zu umfangreich für eine lokale Datenbank. Um nur benut-zerspezi!sche Daten zu synchroniseren, werden wir die Synchronisierung auf dem Downstream mittels eines Filters auf genau die dem jeweiligen Läufer zugeordne-ten Daten einschränken.

Bevor wir aber überhaupt Daten synchronisieren kön-nen, benötigen wir eine CouchDB-Instanz. Installieren Sie dazu entweder eine lokale Instanz von CouchDB auf Ihrem Entwicklungsrechner [11] oder melden Sie sich kostenlos bei Iris Couch [12] an. Anschließend benö-tigen wir eine Datenbank, die wir entweder über die Kommandozeile mit curl -X PUT http://127.0.0.1:5984/couch25k oder über das browserbasierte Admin-Tool Futon anlegen können. Futon erreichen Sie auf jeder CouchDB-Instanz unter http://localhost:5984/_utils/ (Abb. 2).

Clients können angeben, dass für die Synchronisa-tion mit dem entfernten System eine Filterfunktion verwendet werden soll. Diese Filterfunktion muss auf dem entfernten System vorliegen, um eine ef!ziente Fil-terung zu ermöglichen – schließlich möchte man nicht erst alle Daten übertragen und dann !ltern, sondern nur die wirklich benötigten Daten übertragen. Filter-funktionen sind JavaScript-Funktionen, die genau wie Map/Reduce-Funktionen in einem Designdokument in einer CouchDB-Datenbank gespeichert werden. Lis-ting"7 zeigt einen Ausschnitt aus dem Designdokument für die couch25k-Datenbank, die wir für unser Beispiel verwenden.

Die Filter-Funktion by_user überprüft also, ob der in der Datenbankanfrage übergebene Benutzername mit dem Feld user des aktuellen Dokuments übereinstimmt. Wenn der Filter aktiv ist, werden folglich nur Dokumen-te ausgeliefert, für die diese Bedingung erfüllt ist.

Die Replikation wird clientseitig gestartet, indem die Methode replicateWithURL auf der Klasse Couch-Database aufgerufen wird. Als URL wird die Adresse der Datenbankinstanz angegeben, mit der wir syn-chronisieren wollen (also unsere eben gerade angelegte Instanz auf dem lokalen Rechner oder auf Iriscouch.com). Das Ergebnis dieses Aufrufs ist ein NSArray, das zwei CouchPersistentReplication-Objekte enthält, mit denen die Replikation vom entfernten System (Ele-ment an Position"0) beziehungsweise zum entfernten System (Element an Position"1) kon!guriert werden kann. Um den Downstream zu !ltern, geben wir also den Namen der Filterfunktion in der Form Daten-bankName/FilterName an. Außerdem übergeben wir

Listing 7{ "_id": "_design/couch25k", "_rev": "1-5542059a0466c96837142edc4802fe4a", "language": "javascript", "filters": { "by_user": "function(doc, rq) { if(doc.user == rq.query.username) { return true; } return false; }" }}

Listing 8 (void)startSync{ if (self.connected) { NSURL *url = [NSURL URLWithString:@"http://peterfriese.iriscouch:5984/couch25k"]; NSArray *replications = [self.database replicateWithURL:url exclusively: YES]; CouchPersistentReplication *from = [replications objectAtIndex:0]; from.continuous = YES; from.filter = @"couch25k/by_user"; NSDictionary *filterParams = [NSDictionary dictionaryWithObjectsAndKeys: @"peterfriese", @"username", nil]; from.query_params = filterParams; }}

Listing 9{"rows":[ {"key":"demo1","value":702}, {"key":"hellomtc17","value":351}, {"key":"hellotame","value":645}, {"key":"run-peterfriese-1","value":4}, {"key":"run-peterfriese-123","value":17}, {"key":"run-peterfriese-19","value":182}, {"key":"run-peterfriese-2","value":9}, {"key":"run-peterfriese-33","value":7}, {"key":"run-peterfriese-34","value":7}, {"key":"run-peterfriese-35","value":29}, {"key":"run-peterfriese-4","value":4}, {"key":"run-peterfriese-42","value":351}, {"key":"run-peterfriese-5","value":7}, {"key":"run-peterfriese-mtc1","value":53}, {"key":"tame-tribute","value":1827}, {"key":"test1","value":4}]}

Page 10: Big iOS Data - NoSQL auf iOS devices: Mobile Technology3/2012

www.mobile360.deMobile Technology 3 | 2012

046 iOS | CouchDB mobile

die Filterkriterien (hier den Benutzernamen) in einem Dictionary (Listing!8). Die Synchronisation läuft nun im Hintergrund, solange die App aktiv ist, und sorgt für einen zeitnahen Upload der lokal erfassten GPS-Wegpunkte.

Daten auf der zentralen Datenbankinstanz abfragenDie auf der zentralen Datenbankinstanz gespeicherten Daten können nun ganz regulär mit Map/Reduce Views abgefragt werden. Anders als unter TouchDB können CouchDB-Designdokumente in der Datenbank gespei-chert werden. Designdokumente erhalten eine ID, die mit _design/ beginnt, um sie von anderen Dokumenten besser unterscheiden zu können.

Um zum Beispiel eine Liste der Läufe zu ermitteln, benötigen wir die in Listing!8 dargestellte Map/Re duce View. Unter dem Namen byrun abgespeichert, kann sie dann zum Beispiel wie folgt aufgerufen werden: curl localhost:5984/couch25k/_design/couch25k/_view/by_run?group=true und liefert das in Listing!9 dargestellte Ergebnis.

Dieses Ergebnis können wir beispielsweise mittels eines geeigneten jQuery Scripts in einer Webseite als Liste anzeigen. CouchDB unterstützt mittels so ge-nannter CouchApps [13] die einfache Entwicklung von Webapplikationen, die innerhalb von CouchDB ausgeführt werden und somit direkten Zugriff auf die Datenbank und die Map/Reduce Views haben. Eine Ein-führung in die Welt der CouchApps würde den Rahmen des Artikels jedoch sprengen.

FazitIn diesem Artikel haben Sie gesehen, wie CouchDB ein-gesetzt werden kann, um auf einfache Art und Weise eine asymmetrische Synchronisierung zwischen den Da-tenbeständen einer mobilen Anwendung und einer zen-tralen Anwendung zu implementieren. Insbesondere für Systeme, die zur Erfassung von großen Datenmengen gedacht sind, ist CouchDB durch seine Architektur eine

gute Wahl. In vielen Fällen kommt man Dank des REST-basierten Datenbank-APIs sogar ohne Application-Ser-ver aus. Wie kurz angedeutet wurde, kann CouchDB mithilfe von CouchApp sogar als Application-Server für Web-Apps dienen. In einem Folgeartikel werden wir auf den Einsatz von CouchDB auf Android eingehen und das Thema CouchApps vertiefen.

Für Systeme, die zur Erfassung von großen Daten gedacht sind, ist CouchDB durch seine Architektur eine gute Wahl.

Peter Friese arbeitet als Software Engineering Consultant für Zühl-ke Engineering. Seine Schwerpunkte liegen auf der modellgetrie-benen Softwareentwicklung, der plattformübergreifenden Entwicklung von mobilen Anwendungen (u.!a. für iOS, Android, Windows Phone!7 und Mobile Web) sowie Eclipse als Plattform.

Peter bloggt auf http://www.peterfriese.de und twittert unter @peterfriese.

Links & Literatur

[1] Jeffrey Dean and Sanjay Ghemawat, MapReduce: Simpli"ed Data Processing on Large Clusters, http://research.google.com/archive/mapreduce.html

[2] https://github.com/couchbase/iOS-Couchbase/

[3] https://github.com/couchbaselabs/TouchDB-iOS

[4] https://github.com/couchbaselabs/TouchDB-Android

[5] https://github.com/couchbaselabs/CouchCocoa

[6] http://wiki.apache.org/couchdb/View_collation

[7] J.Chris Anderson, Jan Lehnardt, Noah Slater, CouchDB: The De"nitive Guide, O`Reilly Media 2010

[8] http://guide.couchdb.org/

[9] http://guide.couchdb.org/editions/1/en/views.html#reduce

[10] http://guide.couchdb.org/editions/1/en/replication.html

[11] http://wiki.apache.org/couchdb/Installation

[12] http://www.iriscouch.com

[13] http://couchapp.org/page/index

Game Development, Design & Business

29. – 30. Oktober 2012Rheingoldhalle Mainz

Blindtext

Game Development, Design & Business

29. – 30. Oktober 2012Rheingoldhalle Mainz

www.gamesdevcon.de

BarCamp

+ Konferenz

99 !!"#$%&'())))*+,)-."/0))1$)2#3,4