From 33411b2def90a2ae1f8d4ec399b2594a2c223a55 Mon Sep 17 00:00:00 2001 From: John Estropia Date: Wed, 3 Jun 2015 01:40:40 +0900 Subject: [PATCH] Created Fetching and querying (markdown) --- Fetching-and-querying.md | 266 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 Fetching-and-querying.md diff --git a/Fetching-and-querying.md b/Fetching-and-querying.md new file mode 100644 index 0000000..fd962a3 --- /dev/null +++ b/Fetching-and-querying.md @@ -0,0 +1,266 @@ +Before we dive in, be aware that CoreStore distinguishes between *fetching* and *querying*: +- A *fetch* executes searches from a specific *transaction* or *data stack*. This means fetches can include pending objects (i.e. before a transaction calls on `commit()`.) Use fetches when: + - results need to be `NSManagedObject` instances + - unsaved objects should be included in the search (though fetches can be configured to exclude unsaved ones) +- A *query* pulls data straight from the persistent store. This means faster searches when computing aggregates such as *count*, *min*, *max*, etc. Use queries when: + - you need to compute aggregate functions (see below for a list of supported functions) + - results can be raw values like `NSString`'s, `NSNumber`'s, `Int`'s, `NSDate`'s, an `NSDictionary` of key-values, etc. + - only specific attribute keys need to be included in the results + - unsaved objects should be ignored + +The search conditions for fetches and queries are specified using *clauses*. All fetches and queries require a `From` clause that indicates the target entity type: +```swift +let people = CoreStore.fetchAll(From(MyPersonEntity)) +// CoreStore.fetchAll(From()) works as well +``` +`people` in the example above will be of type `[MyPersonEntity]`. The `From(MyPersonEntity)` clause indicates a fetch to all persistent stores that `MyPersonEntity` belong to. + +If the entity exists in multiple configurations and you need to only search from a particular configuration, provide the `From` clause the configuration name for the destination persistent store: +```swift +let people = CoreStore.fetchAll(From("Config1")) // ignore objects in persistent stores other than the "Config1" configuration +``` +or if the persistent store is the auto-generated "Default" configuration, specify `nil`: +```swift +let person = CoreStore.fetchAll(From(nil)) +``` +Now we know how to use a `From` clause, let's move on to fetching and querying. + +#### Fetching + +There are currently 5 fetch methods you can call from `CoreStore`, from a `DataStack` instance, or from a `BaseDataTransaction` instance. All of the methods below accept the same parameters: a required `From` clause, and an optional series of `Where`, `OrderBy`, and/or `Tweak` clauses. + +- `fetchAll(_:_:)` - returns an array of all objects that match the criteria. +- `fetchOne(_:_:)` - returns the first object that match the criteria. +- `fetchCount(_:_:)` - returns the number of objects that match the criteria. +- `fetchObjectIDs(_:_:)`` - returns an array of `NSManagedObjectID`'s for all objects that match the criteria. +- `fetchObjectID(_:_:)` - returns the `NSManagedObjectID`'s for the first objects that match the criteria. + +Each method's purpose is straightforward, but we need to understand how to set the clauses for the fetch. + +**`Where` clause** + +The `Where` clause is CoreStore's `NSPredicate` wrapper. It specifies the search filter to use when fetching (or querying). It implements all initializers that `NSPredicate` does (except for `-predicateWithBlock:`, which Core Data does not support): +```swift +var people = CoreStore.fetchAll( + From(MyPersonEntity), + Where("%K > %d", "age", 30) // string format initializer +) +people = CoreStore.fetchAll( + From(MyPersonEntity), + Where(true) // boolean initializer +) +``` +If you do have an existing `NSPredicate` instance already, you can pass that to `Where` as well: +```swift +let predicate = NSPredicate(...) +var people = CoreStore.fetchAll( + From(MyPersonEntity), + Where(predicate) // predicate initializer +) +``` +`Where` clauses also implement the `&&`, `||`, and `!` logic operators, so you can provide logical conditions without writing too much `AND`, `OR`, and `NOT` strings in the conditions: +```swift +var people = CoreStore.fetchAll( + From(MyPersonEntity), + Where("age > %d", 30) && Where("gender == %@", "M") +) +``` +If you do not provide a `Where` clause, all objects that belong to the specified `From` will be returned. + +**`OrderBy` clause** + +The `OrderBy` clause is CoreStore's `NSSortDescriptor` wrapper. Use it to specify attribute keys in which to sort the fetch (or query) results with. +```swift +var mostValuablePeople = CoreStore.fetchAll( + From(MyPersonEntity), + OrderBy(.Descending("rating"), .Ascending("surname")) +) +``` +As seen above, `OrderBy` accepts a list of `SortKey` enumeration values, which can be either `.Ascending` or `.Descending`. The associated value for the `SortKey` enumeration is the attribute key string. + +You can use the `+` and `+=` operator to append `OrderBy`'s together. This is useful when sorting conditionally: +```swift +var orderBy = OrderBy(.Descending("rating")) +if sortFromYoungest { + orderBy += OrderBy(.Ascending("age")) +} +var mostValuablePeople = CoreStore.fetchAll( + From(MyPersonEntity), + orderBy +) +``` + +**`Tweak` clause** + +The `Tweak` clause lets you, well, *tweak* the fetch (or query). `Tweak` exposes the `NSFetchRequest` in a closure where you can make changes to its properties: +```swift +var people = CoreStore.fetchAll( + From(MyPersonEntity), + Where("age > %d", 30), + OrderBy(.Ascending("surname")), + Tweak { (fetchRequest) -> Void in + fetchRequest.includesPendingChanges = false + fetchRequest.returnsObjectsAsFaults = false + fetchRequest.includesSubentities = false + } +) +``` +The clauses are evaluated the order they appear in the fetch/query, so you typically need to set `Tweak` as the last clause. +`Tweak`'s closure is executed only just before the fetch occurs, so make sure that any values captured by the closure is not prone to race conditions. + +Do note that while `Tweak` lets you micro-configure its `NSFetchRequest`, don't forget that CoreStore already preconfigured that `NSFetchRequest` to suitable defaults. Only use `Tweak` when you know what you are doing! + +#### Querying +One of the functionalities overlooked by other Core Data wrapper libraries is raw properties fetching. If you are familiar with `NSDictionaryResultType` and `-[NSFetchedRequest propertiesToFetch]`, you probably know how painful it is to setup a query for raw values and aggregate values. CoreStore makes querying easy by exposing the 2 methods below: + +- `queryValue(_:_:_:)` - returns a single raw value for an attribute or for an aggregate value. If there are multiple results, `queryValue(...)` only returns the first item. +- `queryAttributes(_:_:_:)` - returns an array of dictionaries containing attribute keys with their corresponding values. + +Both methods above accept the same parameters: a required `From` clause, a required `Select` clause, and an optional series of `Where`, `OrderBy`, `GroupBy`, and/or `Tweak` clauses. + +Setting up the `From`, `Where`, `OrderBy`, and `Tweak` clauses is similar to how you would when fetching. For querying, you also need to know how to use the `Select` and `GroupBy` clauses. + +**`Select` clause** + +The `Select` clause specifies the target attribute/aggregate key and the return type: +```swift +let johnsAge = CoreStore.queryValue( + From(MyPersonEntity), + Select("age"), + Where("name == %@", "John Smith") +) +``` +The example above queries the "age" property for the first object that matches the `Where` condition. `johnsAge` will be bound to type `Int?`, as indicated by the `Select` generic type. For `queryValue(...)`, the following are allowed as the return type (and as the generic type for `Select`): +- `Bool` +- `Int8` +- `Int16` +- `Int32` +- `Int64` +- `Double` +- `Float` +- `String` +- `NSNumber` +- `NSString` +- `NSDecimalNumber` +- `NSDate` +- `NSData` +- `NSManagedObjectID` +- `NSString` + +For `queryAttributes(...)`, only `NSDictionary` is valid for `Select`, thus you are allowed omit the generic type: +```swift +let allAges = CoreStore.queryAttributes( + From(MyPersonEntity), + Select("age") +) +``` + +If you only need a value for a particular attribute, you can just specify the key name (like we did with `Select("age")`), but several aggregate functions can also be used as parameter to `Select`: +- `.Average(...)` +- `.Count(...)` +- `.Maximum(...)` +- `.Median(...)` +- `.Minimum(...)` +- `.StandardDeviation(...)` +- `.Sum(...)` + +```swift +let oldestAge = CoreStore.queryValue( + From(MyPersonEntity), + Select(.Maximum("age")) +) +``` + +For `queryAttributes(...)` which returns an array of dictionaries, you can specify multiple attributes/aggregates to `Select`: +```swift +let personJSON = CoreStore.queryAttributes( + From(MyPersonEntity), + Select("name", "age") +) +``` +`personJSON` will then have the value: +```json +[ + [ + "name": "John Smith", + "age": 30 + ], + [ + "name": "Jane Doe", + "age": 22 + ] +] +``` +You can also include an aggregate as well: +```swift +let personJSON = CoreStore.queryAttributes( + From(MyPersonEntity), + Select("name", .Count("friends")) +) +``` +which returns: +```swift +[ + [ + "name": "John Smith", + "count(friends)": 42 + ], + [ + "name": "Jane Doe", + "count(friends)": 231 + ] +] +``` +The `"count(friends)"` key name was automatically used by CoreStore, but you can specify your own key alias if you need: +```swift +let personJSON = CoreStore.queryAttributes( + From(MyPersonEntity), + Select("name", .Count("friends", As: "friendsCount")) +) +``` +which now returns: +```swift +[ + [ + "name": "John Smith", + "friendsCount": 42 + ], + [ + "name": "Jane Doe", + "friendsCount": 231 + ] +] +``` + +**`GroupBy` clause** + +The `GroupBy` clause lets you group results by a specified attribute/aggregate. This is only useful only for `queryAttributes(...)` since `queryValue(...)` just returns the first value anyway. +```swift +let personJSON = CoreStore.queryAttributes( + From(MyPersonEntity), + Select("age", .Count("age", As: "count")), + GroupBy("age") +) +``` +this returns dictionaries that shows the count for each `"age"`: +```swift +[ + [ + "age": 42, + "count": 1 + ], + [ + "age": 22, + "count": 1 + ] +] +``` + + +## Contents +- [[Architecture]] +- [[Setting up]] +- [[Saving and processing transactions]] +- [[Fetching and querying]] +- [[Logging and error handling]] +- [[Observing changes and notifications]]