CoreData’s not SQL – dieser Slogan begegnet dem iOS-Entwickler, der in die CoreData-Welt einsteigt, an vielen Stellen – vor allem wenn es um die Selektion von Objekten/Entitäten geht. Zurück bleibt oft die Frage: Und was ist nun CoreData? Oder wie selektiere ich die Objekt, die mich interessieren?

Da viele Entwickler es gewohnt sind, über SQL auf eine Datenbank zuzugreifen, und in „SQL denken“, ist die Verwendung von Prädikaten für die Selektion zunächst ungewohnt. Dieser Artikel zeigt anhand von Beispielen, wie einfache und auch zunehmend komplexere Abfragen in CoreData realisiert werden können. Dabei darf und soll man SQL im ersten Moment wirklich vergessen. Wenn es später um Performance-Optimierung geht, kann es ganz hilfreich sein, hinter die Kulissen zu schauen und festzustellen, wie die Prädikate in SQL-Statements umgesetzt werden.

Das Datenmodell, das für die Beispiele verwendet wird, beinhaltet die Entitäten
Marke <–>> Motorrad <–>> Ersatzteil

Beginnen wir mit einer ganz simplen Abfrage, die alle Entitäten – in diesem Beispiel alle Motorräder – liefern soll.

NSManagedObjectContext* moc = [self managedObjectContext];
NSEntityDescription *entityDescription =
 [NSEntityDescription entityForName:@"Bike" inManagedObjectContext:moc];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSError *error;
NSArray* fetchResult = [moc executeFetchRequest:request error:&error];
if (fetchResult == nil) {
 // Deal with error...
}
// do something with the managed objects of type Bike in "fetchResult"

Siehe dazu auch die Dokumentatio von Apple: http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CoreData/Articles/cdFetching.html

Kleiner Exkurs

MagicalRecord ist eine sehr praktische Bibliothek, die dem Entwickler an vielen Stellen das Leben mit CoreData viel einfacher macht, und mit der sich z.B. obiger Code auf folgende Zeile reduziert:

NSArray* fetchResult = [Bike MR_findAll];
// do something with the managed objects of type Bike in "fetchResult"

MagicalRecord ist auf GitHub zu finden: https://github.com/magicalpanda/MagicalRecord

Selektion über ein Attribut

Im nächsten Schritt interessieren wir uns nur für Motorräder mit mehr als 1000 ccm Hubraum.

Dazu erstellen wir ein Prädikat, das wir obigem Request vor seiner Ausführung zuweisen. Mit Hilfe von Prädikaten werden die Selektionskriterien definiert. Apple stellt als Dokumentation hierfür den Predicate Programming Guide zur Verfügung.

...
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"cubicCapacity > 1000"];
[request setPredicate: predicate];
...

Oder via MagicalRecord:

NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"cubicCapacity > 1000"];
NSArray* fetchResult = [Bike MR_findAllWithPredicate: predicate];

Eine Auflistung der verwendbaren Operatoren innerhalb eines Prädikats befindet sich ebenfalls in der Dokumentation von Apple:
http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Predicates/Articles/pSyntax.html

Selektion über mehrere Attribute

In diesem Beispiel werden alle Motorräder mit mehr als 1000 ccm Hubraum selektiert, die außerdem zur Kategorie „Allrounder“ gehören. Die untere Grenze für den Hubraum wird dieses Mal nicht direkt im Prädikat formuliert, sondern als Variable übergeben.

// Request with entity "Bike"
NSNumber* minCubicCapacity = @1000;
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"cubicCapacity > %@ AND category == 'Allrounder'", minCubicCapacity];

Außerdem kann es teilweise sehr hilfreich sein, nicht nur Werte innerhalb von Prädikaten durch Variablen zu ersetzen, sondern Bezeichner von Attributen als Parameter zu übergeben:

// Request with entity "Bike"
NSString* attribute1 = @"cubicCapacity";
NSNumber* value1 = @1000;
NSString* attribute2 = @"category";
NSString* value2 = @"Allrounder";
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"%K > %@ AND %K == %@", attribute1, value1, attribute2, value2];

Selektion über Attribute von Entitäten einer to-one-Beziehung

Soll eine Abfrage auch Eigenschaften von Entitäten berücksichtigen, die mit den gesuchten Entitäten über eine to-one-Beziehung in Relation stehen, können die Attribute dieser Entitäten nach dem Keypath-Muster referenziert werden – auch über mehrere Ebenen hinweg.

Alle Motorräder der Marke Kawasaki erhält man mittels dieses Prädikats:

// Request with entity "Bike"
NSPredicate* predicate = [NSPredicate predicateWithFormat:
 @"brand.title == 'Kawasaki'"];

Alle Ersatzteile für Motorräder der Marke Kawasaki liefert folgende Abfrage:

// Request with entity "SparePart"
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"bike.brand.title == 'Kawasaki'"];

Selektion über Attribut von Entitäten einer to-many-Beziehung

Wenn für eine Selektion auch Attribute von Entitäten einer to-many-Beziehung relevant sind, kommen Mengen-Operatoren zum Einsatz.
Im folgenden Beispiel sollen alle Marken gefunden werden, die Motorräder mit mehr als 1000 ccm Hubraum haben. Der ANY-Operator (oder alternativ: SOME) prüft, ob es mindest ein Objekt innerhalb der referenzierten Relation gibt, auf das die nachfolgende Expression zutrifft.

// Request with entity "Brand"
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"ANY bikes.cubicCapacity > 1000"];

Alternativ kann das Prädikat auch mit einer Subquery formuliert werden. Allerdings ist ein Mengenoperator wenn möglich vorzuziehen, da dieser im Normalfall schnellere Abfragen liefert.

// Request with entity "Brand"
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"SUBQUERY(bikes, $bike, $bike.cubicCapacity > 1000).@count != 0"];

Innerhalb der SUBQUERY-Funktion stellt das erste Argument die Relation dar (also eine Menge von Entitäten), das zweite Argument ist die Definition eines Parameters, der für die Iteration durch die Menge von Entitäten verwendet wird. Das dritte Argument ist das Prädikat der Subquery, das auf jede Entität der im ersten Argument angegebenen Menge von Entitäten angewendet wird. Innerhalb dieses Prädikats kann die aktuelle Entität über den im zweiten Argument definierten Parameter referenziert werden. Das Ergebnis der Subquery ist die Menge der Entitäten, auf die das Prädikat zutrifft.

NONE

Für eine List der Marken, die kein Motorrad mit mehr als 1000 ccm Hubraum haben, wäre laut Dokumentation NONE der richtige Operator, welcher gleichzusetzen ist mit NOT ANY. Beide Varianten liefern aber momentan nicht das erwartete Ergebnis!

// Request with entity "Brand"
predicate = [NSPredicate predicateWithFormat:
  @"NONE bikes.cubicCapacity > 1000"];
predicate = [NSPredicate predicateWithFormat:
  @"NOT (ANY bikes.cubicCapacity > 1000)"];

Daher lässt sich bis zu einem Bugfix an dieser Stelle die Verwendung der „teureren“ Subquery nicht umgehen:

// Request with entity "Brand"
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"SUBQUERY(bikes, $bike, $bike.cubicCapacity > 1000).@count == 0"];

Der dritte der Mengenoperatoren – ALL – ist im Zusammenhang mit einer SQL-basierten Datenbasis nicht verwendbar.

Selektion über mehrere Attribute von Entitäten einer to-many-Beziehung

Die Mengenoperatoren ANY oder SOME, NONE, ALL und IN dürfen in einem Prädikat nur einmal vorkommen und können sich nur auf eine Expression beziehen. Konstrukte wie „ANY (bikes.cubicCapacity > %@ AND bikes.category == ‚Allrounder‘)“ werden vom Prädikat-Parser nicht akzeptiert.
Wollen wir nun alle Marken finden, die Motorräder der Kategorie „Allrounder“ mit mehr als 1000 ccm haben, müssen wir andere Wege gehen.

Es gibt verschiedene Lösungsmöglichkeiten:

1. Subquery

// Request with entity "Brand"
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"SUBQUERY(bikes, $bike, $bike.cubicCapacity > 1000
  AND $bike.category == 'Allrounder').@count != 0"];

Bewertung:
Das Ergebnis ist eine Menge von Marken-Entitäten.
Subqueries bieten flexible Abfragemöglichkeiten, aber für jede Entität des abgefragten Typs ist im Hintergrund eine eigene Agfrage erforderlich, wodurch die Performance bei großen Datenmengen leidet.
Wenn zusätzliche Expressions im Prädikat verwendet werden, sollte die Subquery aus Performancegründen die letzte Komponente des Prädikats sein, damit die Menge der Entitäten, auf welche die Subquery angewendet wird, durch die vorangegangenen Expressions bereits eingeschränkt wird.

2. Zwei einfache Requests

In einem ersten Request werden die entsprechenden Motorräder selektiert (siehe Beispiel „Mehrere Attribute“). Aus dem Ergebnis-Array, das alle passenden Motorräder enthält, erstellen wir über eine Keypath-Methode ein Array, das nur noch die Marken der Motorräder enthält.
Nun führen wir einen zweiten Request auf Marken aus, in dem wir die Marken selektieren, die in unserem Marken-Array enthalten sind.

NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"cubicCapacity > 1000 AND category == 'Allrounder'"];
// fetch bikes the MagicalRecord way:
NSArray fetchResult = [Bike MR_findAllWithPredicate: predicate];
NSArray* brands = [fetchResult valueForKey: @"brand"];
predicate = [NSPredicate predicateWithFormat: @"self IN %@", brands];
fetchResult = [Brand MR_findAllWithPredicate: predicate];

Bewertung:
Das Ergebnis ist wieder die gewünschte Menge von Marken-Entitäten.
Es werden keine Subqueries benötigt, die mit steigender Zahl an Entitäten die Performance stark in den Keller drücken, sondern es werden genau zwei Requests ausgeführt. Hinzu kommt noch die In-memoy-Erstellung des „Brand“-Arrays, das aber bereits nur noch die passenden Motorräder enthält.

3. Request eines Dictionaries

Die dritte und in vielen Fällen schnellste Alternative ist es oft, von der anderen Seite der Relation her zu kommen und nicht eine Entität, sondern lediglich ein Dictionary mit den benötigten Werten als Ergebnis der Abfrage zu wählen.
Wenn die Abfrage so geformt wird, dass der Request auf die Entität zielt, für die es die Einschränkungen zu formulieren gilt, und die eigentlich gewünschte Ergebnis-Entität über Keypath erreicht werden kann, lässt sich ein Request erstellen, der mit einem Select im Hintergrund auskommt und damit wesentlich schneller ist als Varianten mit Subqueries.

Für unser Beispiel erstellen wir also einen Request auf Bikes mit dem bereits bekannten Prädikat. Als Ergebnis wählen wir den ResultType „NSDictionaryResultType“ und geben die Properties/Attribute (von der Entität Bike her gesehen) an, die wir im Ergebnis benötigen. Wichtig ist noch die Angabe, dass das Ergebnis „distinct results“ liefern, also Dubletten aussondern soll.

NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"cubicCapacity > 1000 AND category == 'Allrounder'",
  minCubicCapacity];
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity: [NSEntityDescription entityForName:@"Bike"
  inManagedObjectContext: [self managedObjectContext]];
[fetchRequest setResultType:NSDictionaryResultType];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setPredicate: predicate];
[fetchRequest setPropertiesToFetch:@[@"brand.title", @"brand.icon"]];
NSArray* fetchResult = [[NSManagedObjectContext MR_defaultContext]
  executeFetchRequest:fetchRequest error:&error];
if (fetchResult == nil) {
  // Deal with error...
}
// do something with the dictionaries, that each stand for a brand and
// contain values for the keys brand.title and brand.icon

Bewertung:
Das Ergebnis enthält Dictionaries mit den gewünschten Attributen.
Es wird nur ein Request benötigt, was bei größeren Datenmengen meist die performanteste Lösung darstellt.

Selektion über Attribute von Entitäten in to-many-Beziehungen über mehrere Stufen

Je mehr Relationen innerhalb einer Abfrage betroffen sind, desto wichtiger wird die Frage nach der Performance und damit die Wahl einer geeigneten Lösung.
In diesem Beispiel wollen wir nochmals 2 Varianten mit ihren Auswirkungen betrachten. Wir interessieren uns hier für alle Marken, für die es Motorradersatzteile im Online-Shop gibt.

Variante 1 mit Subqueries

// Request with entity "Brand"
NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"SUBQUERY(bikes, $bike, SUBQUERY($bike.spareParts, $sparePart,
  $sparePart.isAvailableOnline == TRUE).@count != 0).@count != 0"];

Diese Lösung erzeugt geschachtelte Subqueries, so dass die Anzahl der dahinterstehenden SQL-Selects exponentiell steigt. Es gibt Fälle, in denen noch weitere Expressions innerhalb des Prädikats enthalten sein müssen, die eine andere Lösung schwierig machen. Wenn möglich sollte aber auf eine Reduzierung der Subqueries geachtet werden.

Variante 2 mit Dictionary als Ergebnistyp

Diese Variante ist nach dem Muster von Lösungsvorchlags 3 im vorigen Beispiel gestrickt. Variante 1 ist für viele wahrscheinlich die augenscheinlichere und logisch direktere Variante. Die Ausführungszeit von Variante 2 ist aber bei größeren Datenmengen um ein Vielfaches schneller als Variante 1.

NSPredicate* predicate = [NSPredicate predicateWithFormat:
  @"isAvailableOnline == TRUE"];
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
// get entity description the MagicalRecord way:
[fetchRequest setEntity: [SparePart MR_entityDescription]];
[fetchRequest setResultType: NSDictionaryResultType];
[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setPredicate: predicate];
[fetchRequest setPropertiesToFetch:@[@"bike.brand.title"]];
NSArray* fetchResult = [[NSManagedObjectContext MR_defaultContext]
  executeFetchRequest:fetchRequest error:&error];
if (fetchResult == nil) {
 // Deal with error...
}
// do something with the dictionary

Logging

Im Zuge der Performance-Optimierung ist es dann doch wieder interessant, wie die Prädikate bei Verwendung eines SQL-Stores in SQL-Selects umgesetzt werden. Hierbei ist wichtig zu wissen, dass das Mapping von Entitäten in Tabellen nach eigenen Regeln geschieht und die Interpretation der geloggten Statements mit Vorsicht vorgenommen werden sollte. Aber mit etwas Erfahrung sind die Log-Messages für die Optimierung hilfreich.

Das Logging wird über zwei runtime-Parameter aktiviert:
Erster Parameter ist -com.apple.CoreData.SQLDebug, der zweite ist eine 1

Siehe auch Screenshot „Manage Schemes“:

Fazit

Wichtigster Punkt ist, sich mit NSPredicate vertraut zu machen, das Keypath-Pattern im Hinterkopf zu haben und sich dessen bewusst zu sein, ob die in der Abfrage beteiltigten Relationen vom Typ to-one oder to-many sind.

Bei komplexeren Abfragen wird anhand der Beispiele schnell klar, dass es teilweise verschiedene Möglichkeiten gibt, die gewünschten Informationen zu selektieren. Um die performanteste Lösung für die jeweilige Situation zu finden, ist Kreativität gefragt und für eine abschließende Bewertung manchmal ein Test der Alternativen das Mittel der Wahl.

0 Kommentare

Dein Kommentar

An Diskussion beteiligen?
Hinterlasse uns Deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.