Core Data by tutorials 笔记(二)

今天继续来重温 Raywenderlich 家的《Core Data by Tutorials》。学完前三章就掌握了 CoreData 的基本知识,现在我们来深入来研究下 Fetching 💪


Chapter 4: Intermediate Fetching

这一章主要深入地探究Fetching,学完这一章,你将会加满如下技能点:

  • fetch only what you need to;
  • refine your fetched results using predicates;
  • fetch in the background to avoid blocking the UI;
  • avoid unnecessary fetching by updating objects directly in the persistent store.

还是按作者的主要目录来梳理一下吧:

一、NSFetchRequest: the star of the show

明星登场,同样是创建 NSFetchRequest 的实例,并对其进行 configure 且执行。总而言之,繁重的工作都替你干了,任劳任怨的程度和 NSManagedObjectContext 有一拼。接着展示了四种创建 NSFetchRequest 实例的方式:

//1
let fetchRequest1 = NSFetchRequest()  
let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedObjectContext!)  
let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedObjectContext!)  
//2
let fetchRequest2 = NSFetchRequest(entityName: "Person")  
//3
let fetchRequest3 = managedObjectModel.fetchRequestTemplateForName("peopleFR")  
//4
let fetchRequest4 = managedObjectModel.fetchRequestFromTemplateWithName("peopleFR", substitutionVariables: ["NAME" : "Ray"])  

二、Introducing the Bubble Tea app

本章要实现一个泡泡🍵茶的 APP,作者提供了一个 Start project,快速浏览一下,主界面是一个 TableView,奶茶店等原始数据都保存 seed.json 之中。接下来要考虑如何将数据从 seed 中读取到 Core Data 的对象中去。

三、Stored fetch requests

这里可以在 data model 中设置一个 Fetch Request,并选择 Fetch 哪些对象,设置一些条件;紧接着你就可以使用模板中 fetchRequest 了:

fetchRequest = coreDataStack.model.fetchRequestTemplateForName("FetchRequest")  

这里要使用 coreData,并注意 FetchRequest 名称要和 model 中的匹配。

四、Fetching different result types

作者把 NSFetchRequest 比喻成了 Core Data framework 中的瑞士军刀,看来还是相当强大的。这里有四种 fetch request 的 result type:

  1. NSManagedObjectResultType: Returns managed objects (default value).
  2. NSCountResultType: Returns the count of the objects that match the fetch request.
  3. NSDictionaryResultType: This is a catch-all return type for returning the results of different calculations.
  4. NSManagedObjectIDResultType: Returns unique identifiers instead of full- fledged managed objects.

fetchRequest.resultType 设为 NSCountResultType 在查询大量对象数量时可以极大的优化性能。

另一种获取 count 的方式是 context 直接调用countForFetchRequest方法

let count = coreDataStack.context.countForFetchRequest(fetchRequest, error: &error)  

Core Data 提供了多种函数支持来支持计算,如 average, sum, min and max. 下面是一个求和的例子:

func populateDealsCountLabel() {  
    //1 指定result类型
    let fetchRequest = NSFetchRequest(entityName: "Venue") fetchRequest.resultType = .DictionaryResultType
    //2 创建表达式描述,指定个名称,以便将来在fetch的结果(字典类型)中找到
    let sumExpressionDesc = NSExpressionDescription() sumExpressionDesc.name = "sumDeals"
    //3 创建具体的表达式
    sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments:[NSExpression(forKeyPath: "specialCount")])
    sumExpressionDesc.expressionResultType = .Integer32AttributeType
    //4 设置fetch request 将要fetch sum
    fetchRequest.propertiesToFetch = [sumExpressionDesc]
    //5 执行,最后通过之前设置的表达式name得到最终结果
    var error: NSError?
    let result = coreDataStack.context.executeFetchRequest(fetchRequest, error: &error) as [NSDictionary]?
    if let resultArray = result {
        let resultDict = resultArray[0]
        let numDeals: AnyObject? = resultDict["sumDeals"] numDealsLabel.text = "\(numDeals!) total deals"
    } else {
        println("Could not fetch \(error), \(error!.userInfo)")
        } 
    }

现在还剩一种 .ManagedObjectIDResultType 类型,他返回一个包含 NSManagedObjectID 对象的数组,NSManagedObjectID 可以看成是 managed object 通用唯一标识,除了多线程的某些场景下,否则很少用到它。

关于性能优化 CoreData 通过以下几种方式来削减 income data:

  1. fetch batches,可以使用 fetchBatchSize, fetchLimit 和 fetchOffset 这些属性来削减 income data。
  2. 支持 faulting 这样的 占位符
  3. 使用 predicates
    最后要注意一下如果想要在运行时改变 predicate,就不能再用 model 来初始化 fetchRequest 了
override func viewDidLoad() {  
    super.viewDidLoad()
    // fetchRequest = coreDataStack.model.fetchRequestTemplateForName("FetchRequest")
    fetchRequest = NSFetchRequest(entityName: "Venue") fetchAndReload()
}

至于 predicates 的用法请查看官方文档

五、Sorting fetched results

排序主要用到了 NSSortDescriptor 这个类,值得注意的是该排序是真正发生在 SQLite层面 的,而不是在内存中的,因此该排序十分高效。

NSSortDescriptor 实例初始化需要三个参数:①.你要排序属性的 key path ②.指明升序 or 降序 ③. 一个可选的 selector
这里要注意一点就是 CoreData 不支持 block-based API 定义 NSSortDescriptor,以及基于 block-based 方式定义 NSPredicate。

//下面两个初始化方法不能用于CoreData中
+ (instancetype)sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending comparator:(NSComparator)cmptr
+ (NSPredicate *)predicateWithBlock:(BOOL (^)(id evaluatedObject, NSDictionary *bindings))block

下面创建了一个按名字排序 filter

lazy var nameSortDescriptor: NSSortDescriptor = {  
    var sd = NSSortDescriptor(key: "name",
        ascending: true,
        selector: "localizedStandardCompare:") 
    return sd
}()

这里注意一下第三个参数 localizedStandardCompare,传入这个参数就会按本地语言排序,这也是苹果官方推荐的一种做法。

得到一个反转排序的 sortDescriptor 也很简单,这样就可以了

selectedSortDescriptor = nameSortDescriptor.reversedSortDescriptor  

六、Asynchronous fetching

iOS 8 苹果推出全新的 API 来处理异步 fetch 问题, NSAsynchronousFetchRequest,这里不要被他的名字迷惑了,和 NSFetchRequest 没什么直接关系,他其实是 NSPersistentStoreRequest 的子类。下面是该类的定义及初始化方法:

@availability(iOS, introduced=8.0)
class NSAsynchronousFetchRequest : NSPersistentStoreRequest {  
    var fetchRequest: NSFetchRequest { get }
    var completionBlock: NSPersistentStoreAsynchronousFetchResultCompletionBlock? { get }
    var estimatedResultCount: Int
    init(fetchRequest request: NSFetchRequest, completionBlock blk: NSPersistentStoreAsynchronousFetchResultCompletionBlock?)
}

从该类的初始化方法可以将异步 fetchRequest 看做是对 fetchRequest 的一种包装,第一个参数是标准的 NSFetchRequest 对象,而第二个参数是一个 a completion handler,不过仅仅有 completion handler 是不够的,最后还是需要 executeRequest,下面是一个完整的例子:

//1
fetchRequest = NSFetchRequest(entityName: "Venue")  
//2
asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) {  
     [unowned self] (result: NSAsynchronousFetchResult! ) -> Void in
        self.venues = result.finalResult as [Venue]
        self.tableView.reloadData() 
}
//3
var error: NSError?  
let results = coreDataStack.context.executeRequest(asyncFetchRequest, error: &error)  
if let persistentStoreResults = results {  
    //Returns immediately, cancel here if you want
} else {
    println("Could not fetch \(error), \(error!.userInfo)")
}

这段代码注意 executeRequest 一旦执行立即返回一个 NSAsynchronousFetchResult 类型的结果,在这里你不用操心 fetch 到的数据和UI匹配,这些工作都在第二步 handle 那里处理了。另外 NSAsynchronousFetchResult 是有 cancel() 方法的。

仅仅有新 API 还不够,还要修改 context

context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)  

另外一种 .PrivateQueueConcurrencyType 将在后面多线程介绍

因为是异步 fetch,所以可能 tableview 初始化完毕后,fetch 的结果才到,这里给数据源可以设置一个空数组来 fix。

七、Batch updates: no fetching required

有的时候你需要一次更新很多属性,全部 fetch 到内存显然是不高效的,iOS 8 推出了全新的批量更新(batch updates)来解决这一痛点。这个类就是 NSBatchUpdateRequest,和上面提到的 NSAsynchronousFetchRequest 都是 NSPersistentStoreRequest 的子类,下面是这个类的定义:

@availability(iOS, introduced=8.0)
class NSBatchUpdateRequest : NSPersistentStoreRequest {  
    init(entityName: String)
    init(entity: NSEntityDescription)
    var entityName: String { get }
    var entity: NSEntityDescription { get }
    var predicate: NSPredicate?
    // Should the update include subentities? Defaults to YES.
    var includesSubentities: Bool
    // The type of result that should be returned from this request. Defaults to NSStatusOnlyResultType
    var resultType: NSBatchUpdateRequestResultType
    // Dictionary of NSPropertyDescription|property name string -> constantValue/NSExpression pairs describing the desired updates.
    // The expressions can be any NSExpression that evaluates to a scalar value.
    var propertiesToUpdate: [NSObject : AnyObject]?
    }

具体使用起来也很简单:

let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")  
batchUpdate.propertiesToUpdate = ["favorite" : NSNumber(bool: true)]  
batchUpdate.affectedStores = coreDataStack.psc.persistentStores  
batchUpdate.resultType = .UpdatedObjectsCountResultType  
var batchError: NSError?  
let batchResult = coreDataStack.context.executeRequest(batchUpdate, error: &batchError) as NSBatchUpdateResult?  
if let result = batchResult {  
    println("Records updated \(result.result!)")
} else {
    println("Could not update \(batchError), \(batchError!.userInfo)")
}

最后注意一点,就是批量更新跳过了 NSManagedObjectContext,直接对 persistentStore 进行更新,没有经过有效性验证,这个就要靠你自己确保更新的数据合法了。


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!