YongSir

专业程序员伪装者

CoreData(三)

CoreData and Swift by YongSir🇨🇳🇨🇳

NSFetchRequest is the multi-function Swiss army knife of the Core Data framework!

这一部分,将集中介绍coreData中fetch的内容,在前边所有的fetch基本都是一个套路:create an instance of NSFetchRequest, configure it and hand it over to NSManagedObjectContext,很简单,事实上有4种持有请求的方式,为了不显突兀,都列举如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 1
let fetchRequest1 = NSFetchRequest()
let entity = NSEntityDescription.entityForName("XXX", inManagedObjectContext: managedObjectContext)
fetchRequest1.entity = entity!

// 2
let fetchRequest2 = NSFetchRequest(entityName: "XXX")

// 3
let fetchRequest3 = managedObjectModel.fetchRequestTemplateName(“peopleFR")

// 4
let fetchRequest4 = managedObjectModel.fetchRequestFromTemplateWithName("peropleFR", substitutionVariables:["NAME" : "Ray"])

其实区别并不像想象的大和突兀,对于request来说,跟重要的在于特定的一些配置,让我们不写SQL语句也能操作数据。使用DemoBubbleTeaFinder来具体说明吧


首先是创建项目,添加coreData Stack等等的准备工作,好在已经完成了
先介绍一种最直接的request方式,就是利用Xcode的data editer添加requset,然后再通过代码关联,用fetchRequest承接:

1
2
// 关联editor的request -- 通过model + Xcode辅助
fetchRequest = coreDataStack.model.fetchRequestTemplateForName("FetchRequest")

这种方式要注意的是:

  1. 它直接从NSManagedObjectModele,通过借助Xcode辅助而来,使用异常简单;
  2. 缺点在于形式固定(A drawback of stored fetch requests is that there is no way to specify a sort order for the results.),不能多余的配置,所有常用于需要反复多次相同的request时,这种情况很少遇到。如果执意要使用,那出现这种错误就不要奇怪了:
1
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Can't modify a named fetch request in an immutable model.'

如果只是单纯的以为request只是单纯的获取数据,那就大错特错了,它的功能不止如此 :You can use it to fetch individual values, compute statistics on your data such as the average, minimum and maximum, and more.它拥有一个可以规定返回类型的属性NSManagedObjectResultType ,有四种类型:

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

接下来就使用countResultType,Demo中的Filters界面,主要功能是按照给定的标记归档分类,给出当前最符合条件的店址,其中的PRICE部分,是根据价格分类的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  // 使用CountResultType方式,返回一个只包含总数目的数组
// 当然你也可以返回所有的[Venue],在得出数量,但当数据巨大时,
// CountResultType方式就是一种高效的选择(is more memory- efficient)
func populateCheapVenueCountLable() {
let fetchRequest = NSFetchRequest(entityName: "Venue")
fetchRequest.resultType = NSFetchRequestResultType.CountResultType
fetchRequest.predicate = cheapVenuePredicate
// 执行请求
do {
let results = try coreDataStack.context.executeFetchRequest(fetchRequest) as! [NSNumber]
// 从数组中取出
let count = results[0].integerValue
firstPriceCategoryLabel.text = "\(count) bubbke tea places"
}catch let error as NSError {
print("未能成功获取\(error) ,\(error.userInfo)")
}
}

需要注意的是,NSCountResultType会返回一个数组“[12]”,但这个数组只含有一个元素,此外不会将所有项目都加载,在处理大量数据时,是一种高效的得到数量的方式。

在Offering a deal条目中,我们想要得到交易数目,也就是JSON中各个不同店铺的specialCount字段下的值的和,当然把所有的店铺信息全部取出来然后通过一个for循环去求和是可以的,但考虑到大量数据的情形就不行了,所以coreData为我们提了NSDictionaryResultType这种类型,会直接返回我们想要的计算后的结果, has built-in support for a number of different functions such as average, sum, min and max.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  func populateDealsCountLabel() {

// 1 使用DictionaryResultType方式--即是以NSDictionary的格式组织返回 的数据
let fetchRequest = NSFetchRequest(entityName: "Venue")
fetchRequest.resultType = NSFetchRequestResultType.DictionaryResultType

// 2 创建NSExpressionDescription,并命名
let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"

// 3 截取specialCount字段,并做sum计算
sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [NSExpression(forKeyPath: "specialCount")])
sumExpressionDesc.expressionResultType = NSAttributeType.Integer32AttributeType

// 4 配置请求:propertiesToFetch属性赋值
fetchRequest.propertiesToFetch = [sumExpressionDesc]

// 5 执行请求 + 更新UI
do {
let results = try coreDataStack.context.executeFetchRequest(fetchRequest) as! [NSDictionary]
// 实际上会返回一个包含一个字典的数组 [{sumDeals = 12;}]
let resultDict = results[0]
print(results)
let numDeals: AnyObject? = resultDict["sumDeals"]
numDealsLabel.text = "\(numDeals!) total deals"
}catch let error as NSError{
print("未能成功获取折扣数量\(error),\(error.userInfo)")
}
}

其中,需要注意的是,需要创建和配置NSExpressionDescription,设置propertiesToFetch属性,执行请求返回的结果是一个包含一个字典的数组[{sumDeals = 12;}]。事实上,由于代码的不直观,除非处于性能上的考虑,这样的用法并不常见:

Fetching a calculated value from Core Data requires you to follow many, often unintuitive steps, so make sure you have a good reason for using this technique— like performance considerations.

至少现在已经用过了3种方式(好吧,其实是2种),还有最后一种NSManagedObjectIDResultType,会返回一组符合符合条件的查找到的托管的数据“[ID…]”,就像数据库中的id一样。在ios5之后的并发技术引入以后,这种方式就更少见到了,所以基本可以忽略。

When you fetch with this type, the result is an array of NSManagedObjectID objects rather the actual managed objects they represent. An NSManagedObjectID is a compact, universal identifier for a managed object. It works like the primary key in the database!


在补全Files中的剩余条目之前,先来思考一个问题:

You’ve gotten a taste of all the things a fetch request can do for you. But just as important as the information a fetch request returns is the information it doesn’t return. For practical reasons, you have cap the incoming data at some point.

Why? Imagine a perfectly connected object graph, one where each Core Data object is connected to every other object through a series of relationships. If Core Data didn’t put limits on the information a fetch request returned, you’d be fetching the entire object graph every single time! That’s not memory efficient.

所以必须要对获取的请求加以限制,好在我们有很多种手段:
There are ways you can manually limit the information you get back from a fetch request. For example, NSFetchRequest supports fetching batches. You can use the properties fetchBatchSize, fetchLimit and fetchOffset to control the batching behavior.

Yet another way to limit your object graph is to use predicates, as you’ve done to populate the venue count labels above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lazy var offeringDealPredicate: NSPredicate = {
var pr = NSPredicate(format: "specialCount > 0")
return pr
}()

lazy var walkingDistancePredicate: NSPredicate = {
var pr = NSPredicate(format: "location.distance < 500")
return pr
}()

lazy var hasUserTipsPredicate: NSPredicate = {
var pr = NSPredicate(format: "stats.tipCount > 0")
return pr
}()

同时补全tableViewDelegate中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  //MARK: - UITableViewDelegate methods
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.cellForRowAtIndexPath(indexPath)!
// 不同的cell,对应不用的predicate
switch cell {
// PRICE section
case cheapVenueCell:
selectedPredicate = cheapVenuePredicate
case moderateVenueCell:
selectedPredicate = moderateVenuePredicate
case expensiveVenueCell:
selectedPredicate = expensiveVenuePredicate
// POPULATE section
case offeringDealCell:
selectedPredicate = offeringDealPredicate
case walkingDistanceCell:
selectedPredicate = walkingDistancePredicate
case userTipsCell:
selectedPredicate = hasUserTipsPredicate
default:
print("default case")
} cell.accessoryType = UITableViewCellAccessoryType.Checkmark
}

借助代理传回到主控制器,控制刷新主界面的venue,这样就完成了按照指定要求获取对应数据的目的--也就是“查找”功能


接下来更进一步,在现实中,在查找之余能排序的需求更加广泛,所以coreData也为我们准备了方便的工具NSSortDescriptor,并且其用法跟单纯负责查找的NSPredicte是分类了,这里我们仍旧首先做lazy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//MARK: - lazy SortDescriptor

// 按姓名排序
lazy var nameSortDescriptor: NSSortDescriptor = {
var sd = NSSortDescriptor(key: "name", ascending: true, selector: "localizedStandardCompare:") // 就是要画蛇添足怎的
return sd
}()

// 按距离排序
lazy var distanceSortDescriptor: NSSortDescriptor = {
var sd = NSSortDescriptor(key: "location.distance", ascending: true)
return sd
}()

// 按假期排序
lazy var priceSortDescriptor: NSSortDescriptor = {
var sd = NSSortDescriptor(key: "piceInfo.priceCategory", ascending: true)
return sd
}()

To initialize an instance of NSSortDescriptor you need three things: a key path to specify the attribute by which you want to sort, a specification of whether the sort is ascending or descending and an optional selector.

NSSortDescriptor的实例话我们需要指明3个东西:key--表示对谁排序,布尔的ascending--指定降序还是升序,以及可选的selector--针对其他特殊要求。不使用可选的selector时,系统会默认调用localizedStandardCompare 方法做一些刚刚够用的排序,如果有特殊要求,那就自己实现selector吧!

此外额外的补充一点儿:coreData中的NSSortDescriptor和NSPredicate,都不支持在处理数组等集合中常见的的Block式的API,这是出于这样的考虑:排序和查找都是SQLite层级的处理,需要系统能快速的变成SQL语句。

The reason is related to the fact that filtering/sorting happens in the SQLite database, so the predicate/sort descriptor has to match nicely to something that can be written as an SQLite statement.

OK,还是说回来,继续完成排序功能,去添加排序实例然后并通过代理传递到主控制器。let’s go down to didSelectRowAtIndexPath and add the following cases to the end of the switch statement:

1
2
3
4
5
6
7
8
9
// SORT section
case nameAZSortCell:
selectedSortDescriptor = nameSortDescriptor
case nameZASortCell:
selectedSortDescriptor = nameSortDescriptor.reversedSortDescriptor as? NSSortDescriptor
case distanceSortCell:
selectedSortDescriptor = distanceSortDescriptor
case priceSortCell:
selectedSortDescriptor = priceSortDescriptor

截止目前为止,功能部分实现了,但是有一个坏消息是目前的request都是在主线程,这显然是一个优质应用应该避免的,现在的demo运行起来并不卡是应为过于简单,所以请求的异部化是必然的。


Asynchronous fetching

正是考虑到主线程占用的问题,所以在iOS8之后,苹果提供了一种新的请求: NSAsynchronousFetchRequest,看起来就像是对旧的NSFetchRequest一个异步的包装,因为它的实例化需要普通的NSFetchRequest,在下面的代码中就能看出,对应于异步请求,出现异步结果 NSAsynchronousFetchResult就是很自然了,同时还需要有主线程的回调,等不及了直接上代码,在主控制器的ViewDidLoad中替换原请求为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

// 1 创建请求
fetchRequest = NSFetchRequest(entityName: "Venue")

// 2 异步请求 + 完成回调更新UI
asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest, completionBlock: {
(results: NSAsynchronousFetchResult) -> Void in
self.venues = results.finalResult as! [Venue]
self.tableView.reloadData()
})
// 3 执行请求
do {
let results = try coreDataStack.context.executeRequest(asyncFetchRequest)
let persistentStoreResults = results
}catch let error as NSError {
print("未能成功获取\(error),\(error.userInfo)")
}

但如果此时编译运行会出现这样的错误:

1
NSConfinementConcurrencyType context <NSManagedObjectContext: 0x7f8eb3c130f0> cannot support asynchronous fetch request <NSAsynchronousFetchRequest: 0x7f8eb3e229e0> with fetch request <NSFetchRequest: 0x7f8eb3ea9580>.

实际上普通的NSManagedObjectContext并不能支持异步的请求方式,所以还有其他2个步骤需要变更

  • first,是coreDataStack 中的managedContext的实例话时,需要设置指定的并发类型:
1
2
3
4
5
6
7
8

// 指定并发类型的上下文实例
/*
NSManagedObjectContextConcurrencyType.ConfinementConcurrencyType // 默认
NSManagedObjectContextConcurrencyType.PrivateQueueConcurrencyType // 多线程
NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType // 主线程并发
*/
context = NSManagedObjectContext(concurrencyType: NSManagedObjectContextConcurrencyType.MainQueueConcurrencyType)

有3种类型供选择,默认的并发类型是“.ConfinementConcurrencyType”,但 The problem is that this concurrency type is an old pattern that is all but deprecated. 还有 “.PrivateQueueConcurrencyType"类型,是在多线程中的使用,先往后放。如果此时运行,就会报这样的错误:

fatal error: unexpectedly found nil while unwrapping an Optional value

  • second,这个错误是swift编程中的老面孔,原因就是尝试强制解包一个可选的类型,如果你自认为已经可以做到很注意类型的话,可能就能猜到原因,可惜在并发场景下我没能注意到,Since the original fetch request is asynchronous, it will finish after the table view does its initial load. The table view will try to unwrap the venues property but since there are no results yet, your app will crash.看来在swift的并发之中,需要长这样一个“心眼”。更改如下:
1
var venues: [Venue]! = []

You fix this issue by initializing venues to an empty array. This way, on first load, if there are no results yet, your table view will simply be empty.


Batch Updates:no fetch request

Sometimes, the only reason you fetch objects from Core Data is to mutate an attribute. Then, after you make your changes, you have to commit the Core Data objects back to the persistent store and call it a day. This is the normal process you’ve been following all along.
通常,从coredata获取对象的唯一理由是想更改其的一个属性,并在更改之后添加到上下文,再commit固化之,这是最常见的流程。

But What if you want to update a hundred thousand records all at once? It would take a lot of time and a lot of memory to fetch all of those objects just to update one attribute. No amount of tweaking your fetch request would save your user from having to stare at a spinner for a long, long time.
但是如果考虑一下场景:如果你需要一次性更新10万条纪录,这样量级的数据会费大量的时间和内存去请求所有的对象,所以调整请求是必须的。

幸运的是在iOS8之中,苹果提供了一种新的不需要请求和加载到内存的批量更新的方式,新技术允许越过上下文(NSManagedObjectContext)直接进行固化操作,let’s see this in practice:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MARK: 批量无请求

func batchNoFetch() {
// 批量请求:指明更新属性 指明作用存储 指明结果类型
let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate = ["favorite": NSNumber(bool: true)]
batchUpdate.affectedStores = coreDataStack.psc.persistentStores batchUpdate.resultType = NSBatchUpdateRequestResultType.UpdatedObjectsCountResultType

// 执行 - 返回NSBatchUpdateResult
do {
let result = try coreDataStack.context.executeRequest(batchUpdate) as! NSBatchUpdateResult

print("更新记录:\(result.result)")
}catch let error as NSError {
print("未能批量更新\(error),\(error.userInfo)")
}
}

使用起来还是相当方便的,指明操作属性,作用存储和设置类型着老一套,只是结果就是 NSBatchUpdateResult了,当然如果你叫板移动端没有这么大规模的数据那我也没办法了