Stanford iOS10 笔记-L10 Core Data

Core Data

本节介绍了Core Data的基本概念。Core Data是Apple出品的面向对象的数据库,但无法跨平台使用,再加上使用稍有些繁琐,所以在我所知的范围内,使用Core Data的少之又少。但是,Core Data可以和iCloud紧密使用,对于专注iOS/OS X平台的开发者,非常有必要深入学习并使用Core Data。

基本框架

使用Xcode创建一个使用Core Data的工程,可以一窥Core Data的大致结构。涉及到的概念主要有:model文件, NSPersistentContainerNSManagedObjectContextNSFetchedResultsControllerNSFetchRequest等。

model文件

指后缀 xcdatamodeld的文件,在这里可以进行类似数据库schema设计。

  • Entity: 类似一个table,在项目中对应一个继承自NSManagedObject的class。 其中的 attribute对应属性,relationship则会定义指向其他class的指针。
  • Fetch Request: 可以理解为一个SQL查询语句
  • Configuration:

在创建Entity的时候,右侧属性栏会有一些设置。比如 Codegen-Class Definition,这表示项目会自动生成一个 Class——虽然在项目工程中看不到,但在编译的时候,Xcode会自动生成,可以在derivedData文件夹中找到。注意Codegen中 manual/Nono的选项一般不使用,因为这意味着要使用类似 setValue:forKey: 的形式。
Entity的Attribute对应于属性,可以设置数据类型。
relationship表明了某些Entity间的关系,可以一一对应也可以一对多,关系也是继承自 NSManagedObject。在右侧的工具栏中,可以设置属性。比如,设置当对象删除后,所指关系的对象如何处理。

NSPersistentContainer

在AppDelegate中生成该对象,顾名思义,是一个 container,包括数据读取持久化等。但是,我们以后操作的如增删改查等操作,都是通过 NSManagedContext 进行的,该contenxt则是 NSPersistentContainer 的一个属性。在模版文件里,我们可以看到该container的创建,以及如何存储改动。因为所有变动都是内存操作,所以需要将变化及时持久化。这里需要注意,saveContext 何时调用(涉及应用生命周期),但这并不是说只在此时进行saveContext。

NSManagedContext

绝大多数操作都需要用到context,或者是作为参数,或者是使用context的方法。Core Data在设计的时候,就考虑到了访问速度。在设计Contetext的时候,是基于queue线程池的模型,在大多数情况下访问Data都极快。同时,这也意味着 NSManagedContext天然地不是线程安全的。如果需要线程安全访问,那么可以

context.performBlock { // or performBlockAndWait until it finishes
// do stuff with context (this will happen in its safe Q (the Q it was created on)) }

注意,该block必定不是在 main queue中,所以不要有UI操作。

此外,恰当的数据库操作是在后台进行,如

AppDelegate.persistentContainer.performBackgroundTask { context in
// do some CoreData stuff using the passed-in context
// this closure is not the main queue, so dont do UI stuff here (dispatch back if needed) // and dont use AppDelegate.viewContext here, use the passed context
// you dont have to use NSManagedObjectContexts perform method here either
// since youre implicitly doing this block on that passed contexts thread
try? context.save() // dont forget this (and catch errors if needed)
}

增删

以插入数据为例,如

let tweet: NSManagedObject =
  NSEntityDescription.insertNewObject(forEntityName: Tweet, into: context)

通过NSEntityDescription来实现insert,context则是上文所说的 NSManagedContext。返回的对象是一个 NSManagedObject——永远记住Core Data中的 model都是 NSManagedObject。
如果要删除,

managedObjectContext.delete(_ object: tweet)

通过context来删除。如果需要的话,可以在 tweet 类文件中实现 prepareForDeletion,以便在删除前做些处理。

查询 NSFetchRequest

即model文件中的 Fetch request。想象sql查询语言的要素, NSFetchRequest 需要涉及 Entity, NSSortDescriptor 和 NSPredicate。比如,

let request: NSFetchRequest<Tweet> = Tweet.fetchRequest()
//
let sortDescriptor = NSSortDescriptor(
key: screenName, ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)) // can skip this
)
//
let predicate = NSPredicate(format: tweeter = %@ && created > %@”, joe, aDate)
let andPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
//
request.sortDescriptors = [sortDescriptor]
request.predicate = andPredicate
//
let recentTweeters = try? context.fetch(request)

查询得到的对象,比如说含有100条tweet,并不意味着recentTweeters就包含了这100条数据。只有当你去访问返回的对象属性时,core data才去真正的读取。这个特性叫做 "Faulting"。
这里 NSPredicate 可能比较陌生,但在iOS支持正则表达式前, 在对数组进行查找时,NSPredicate 是用得最多的。

访问对象

在codegen中选择了 manual/None,那么获取/设置对象属性使用:

func value(forKey: String) -> Any?
func setValue(Any?, forKey: String)

如:

let username = tweet.value(forKeyPath: tweeter.name) as? String

注意,这里是获取relationship的对象的属性,所以用了 keyPath。 keypath或key的值,须是model文件中定义的;另外,注意类型的转换。

如果选择了class,那么直接使用property的访问形式,这样Xcode还能代码自动完成,方式出错。 如果选择了 Category/Extension,那么意味着我们需要自己实现该类——这也是多数情况下我们的选择。在新建类的时候,注意名字需要和Entity一模一样。而Xcode会通过Extension的方式,把属性和关系创建好。其中, @NSManaged 意味着该属性的 setter/getter 交由 NSManagedObject处理。

除了上述内容,Core Data的特性可以是本鸿篇巨著,本节内容没有展开。

NSFetchedResultsController

NSFetchedResultsController 和 UITableViewController 是好基友(文中似乎忘记了UICollectionViewController)。它可以将fetch request的内容,便捷的传递给Controller的 dataSource,而且性能极优。

注意,本节纯粹概念介绍,下节有demo。