一、Core Data 简介
① 什么是 Core Data?
CoreData 是 iOS SDK 里的一个很强大的框架,允许开发者以面向对象的方式存储和管理数据,使用 CoreData 框架,开发者可以轻松有效地通过面向对象的接口管理数据。 CoreData 是一个模型层的技术,可以帮助建立代表程序状态的模型层。CoreData 也是一种持久化技术,能将模型对象的状态持久化到磁盘,但它最重要的特点是:不仅是一个加载和保存数据的框架,还能和内存中的数据很好的共事。在数据操作过程中,Core Data 无需编写任何 SQL 语句。 CoreData 使用包括实体和实体间的关系,以及查找符合某些条件实体的请求等内容。开发者可以在纯对象层上查找与管理这些数据,而不必担心存储和查找的实现细节。 CoreData 框架最早出现在 Mac OS 10.4 Tiger 与 iOS 3.0 系统,经过成千上万的应用程序以及数以百万用户的反复验证,CoreData 确实已经是一套非常成熟的框架,它利用 Objective-C 语言和运行时,巧妙地集成 CoreFoundation 框架,是一个易于使用的框架,不仅可以优雅地管理对象图,而且在内存管理方面表现异常优异。 CoreData 不是一个数据库,不要用数据库的眼光去看待 CoreData,它不是应用程序的数据库,也不是将数据持久化保存到数据库的 API,它是一个用于管理对象图的框架,可以把对象图写入磁盘从而持久化保存。
② Core Data 特点
支持对象改变过程管理,支持撤销和重做; 关系维护(例如删除一张照片的同时,会在拍照者中删除对应的指针); 惰性加载,部分加载来降低内存(在需要使用数据的时候再加载); 属性值检查(例如:保证年龄在0-200岁),保证数据有意义; Schema 迁移:Schema 用来描述对象(例如:name,age,sex 描述一个人),CoreData 能够适应 Schema 的改变; NSFetchedResultsController 来更好支持 Tableview; 完整的支持 KVC/KVO; 支持复杂的数据查询; 支持 Merge policies(例如:两个线程对同一个数据改变的情况)。
③ Core Data 架构
分析:
NSManagedObjectContext 可以理解为是一个容器,从持久化存储(文件)中查询的数据在这个容器中形成对象图,对这些对象图中的对象操作都会存储在这个容器里,直到发出指令让容器中的内容同步到磁盘;
NSManagedObject 是 NSManagedObjectContext 对象图中的实际对象,由 NSManagedObjectContext 管理,NSManagedObjectContext 会存储这些对象的变化来支持重做和撤销;
NSPersistent Store 负责把对象图中的信息 map 到实际的存储信息,NSPersistentStoreCordinator 存储调度器:负责将数据保存到磁盘的;
SQLite 和 FileSystem 是保存到持久化存储的文件,CoreData 支持 SQLite 的数据格式,但是需注意:coreData 不是 DBMS,并不能管理 SQLite。
④ Core Data 与应用、磁盘存储的关系
Core Data 比 SQLite 做了更进一步的封装,SQLite 提供了数据的存储模型,并提供了一系列 API,可以通过 API 读写数据库,去处理想要处理的数据。但是 SQLite 存储的数据和编写代码中的数据(比如一个类的对象)并没有内置的联系,必须我们自己编写代码去一一对应。 而 Core Data 却可以解决一个数据在持久化层和代码层的一一对应关系,也就是说,处理一个对象的数据后,通过保存接口,它可以自动同步到持久化层里,而不需要去实现额外的代码。这种“对象→持久化”方案叫“对象→关系映射”(英文简称 ORM)。 Core Data 还提供了很多有用的特性,比如回滚机制,数据校验等,它与应用、磁盘存储的关系如下:
二、数据模型
① 数据模型文件:Data Model
当使用 Core Data 时,需要一个用来存放数据模型的地方,数据模型文件就是要创建的文件类型,它的后缀是 .xcdatamodeld,在创建工程的时候,勾选 Use Core Data 创建:
或者在项目中选新建文件→Data Model 即可创建:
系统默认提供的命名为 Model.xcdatamodeld,以 Model.xcdatamodeld 作为示例的文件名,这个文件就相当于数据库中的“库”。通过编辑这个文件,就可以去添加定义想要处理的数据类型。
② 数据模型中的表格:Entity
当在 Xcode 中点击 Model.xcdatamodeld 时,会看到苹果提供的编辑视图,其中有个醒目的按钮 Add Entity:
什么是 Entity 呢?中文翻译叫“实体”,如果把数据模型文件比作数据库中的“库”,那么 Entity 就相当于库里的“表格”。简单理解,Entity 就是定义数据表格类型的名词。例如,这个数据模型是用来存放图书馆信息的,那么很自然的,会想建立一个叫 Book 的 Entity。 注意:创建 Entity 实体的首字母必须为大写。
③ 属性 Attributes
当建立一个名为 Book 的 Entity 时,会看到视图中有栏写着 Attributes,我们知道,当定义一本书时,自然要定义书名、书的编码等信息,这部分信息叫 Attributes,即书的属性:
属性名 类型 name String isbm String page Integer32
同理,也可以再添加一个读者:Reader 的 Entity 描述,如下:
属性名 类型 name String idCard String
④ 关系 Relationship
在使用 Entity 编辑时,除了看到 Attributes 一栏,还看到下面有 Relationships 一栏,这栏是做什么的? 回到示例中来,当定义图书馆信息时,刚书籍和读者的信息,但这两个信息彼此是孤立的,而事实上它们存在着联系。比如一本书,它被某个读者借走了,这样的数据该怎么存储呢?直观的做法是再定义一张表格来处理这类关系,但是 Core Data 提供了更有效的办法:Relationship。 从 Relationship 的思路来思考,当一本书 A 被某个读者 B 借走,可以理解为这本书 A 当前的“借阅者”是该读者 B,而读者 B 的“持有书”是 A。从以上描述可以看出,Relationship 所描述的关系是双向的,即 A 和 B 互相以某种方式形成了联系,而这个方式是我们来定义的。 在 Reader 的 Relationship 下点击 + 号键,然后在 Relationship 栏的名字上填 borrow,表示读者和书的关系是“借阅”,在 Destination 栏选择 Book,这样,读者和书籍的关系就确立了,如下所示:
对于第三栏,Inverse 却没有东西可以填,这是为什么?因为现在定义了读者和书的关系,却没有定义书和读者的关系。因为关系是双向的,就好比定义了 A 是 B 的父亲,那也要同时去定义 B 是 A 的儿子一个道理,计算机不会帮我们打理另一边的联系。 理解了这点,开始选择 Book 的一栏,在 Relationship 下添加新的 borrowBy,Destination 是 Reader,这时候点击 Inverse 一栏,会发现弹出了borrowBy,直接点上,这是因为在定义 Book 的 Relationship 之前,已经定义了 Reader 的 Relationship 了,所以电脑已经知道了读者和书籍的关系,可以直接选上。而一旦选好了,那么在 Reader 的 Relationship 中,我们会发现 Inverse 一栏会自动补齐为 borrowBy,这是因为电脑这时候已经完全理解了双方的关系,自动做了补齐。
⑤ “一对一”和“一对多”:to one 和 to many
建立 Reader 和 Book 之间的联系的时候,发现它们的联系逻辑之间还漏了一个环节:假设一本书被一个读者借走了,它就不能被另一个读者借走,而当一个读者借书时,却可以借很多本书,也就是说,一本书只能对应一个读者,而一个读者却可以对应多本书,这就是“一对一→to one”和“一对多→to many”。 Core Data 允许配置这种联系,具体做法就是在 RelationShip 栏点击对应的关系栏,它将会出现在右侧的栏目中(栏目如果没出现可以在 Xcode 右上角的按钮调出,如果点击后栏目没出现 Relationship 配置项,可以多点击几下,这是 Xcode 的 bug)。 在 Relationship 的配置项里,有一项项名为 Type,点击后有两个选项,一个是 To One(默认值),另一个就是 To Many。
Book 与 Reader 的 Relationship 如下:
Reader 与 Book 的 Relationship 如下:
通过改变实体的展示样式,能够帮助我们更加直观的看到它们之间的关系:
三、Core Data 的主仓库
① 主仓库 NSPersistentContainer 说明
当配置完 Core Data 的数据类型信息后,并没有产生任何数据,就好比图书馆已经制定了图书的规范:一本书应该有名字、isbm、页数等信息,规范虽然制定,却没有真的引进书进来,那么怎么才能产生和处理数据呢?这就需要通过代码真刀真枪的和 Core Data 打交道了。 由于 Core Data 的功能较为强大,必须分成多个类来处理各种逻辑,一次性学习多个类是不容易的,还容易混淆。要和这些各司其职的类打交道,不得不提第一个要介绍的类 NSPersistentContainer,因为它就是存放这多个类成员的“仓库类”。 这个 NSPersistentContainer 就是通过代码和 Core Data 打交道的第一个目标,它存放着几种和 Core Data 进行业务处理的工具,当拿到这些工具之后,就可以自由的访问数据,所以它的名字 Container 蕴含着的意思,就是“仓库、容器、集装箱”。 NSPersistentContainer 和其它成员的关系:
进入正式的代码编写的第一步,先要在使用 Core Data 框架的 Swift 文件开头引入这个框架:
import CoreData
② NSPersistentContainer 的初始化
在新建的 UIKit 项目中,找到 AppDelegate 类,写一个成员函数(即方法,后面直接用函数这个术语替代):
private func createPersistentContainer ( ) {
let container = NSPersistentContainer ( name: "Model" )
}
这样,NSPersistentContainer 类的建立就完成了,其中 Model 字符串就是建立的 Model.xcdatamodeld 文件,但是输入参数的时候,不需要(也不应该)输入 .xcdatamodeld 后缀。 当创建了 NSPersistentContainer 对象时,仅仅完成了基础的初始化,而对于一些性能开销较大的初始化,比如本地持久化资源的加载等,都还没有完成,必须调用 NSPersistentContainer 的成员函数 loadPersistentStores 来完成它。
private func createPersistentContainer ( ) {
let container = NSPersistentContainer ( name: "Model" )
container. loadPersistentStores { ( description, error) in
if let error = error {
fatalError ( "Error: \( error) " )
}
print ( "Load stores success" )
}
}
从代码设计的角度看,为什么 NSPersistentContainer 不直接在构造函数里完成数据库的加载?这就涉及到一个面向对象的开发原则,即构造函数的初始化应该是(原则上)倾向于原子级别,即简单的、低开销内存操作,而对于性能开销大的,内存之外的存储空间处理(比如磁盘,网络),应尽量单独提供成员函数来完成,这样做是为了避免在构造函数中出错时错误难以捕捉的问题。
③ Core Data 表格属性信息的提供者 NSManagedObjectModel
现在已经持有并成功初始化了 Core Data 的仓库管理者 NSPersistentContainer,接下去就可以使用向这个管理者索取信息了,我们已经在模型文件里存放了读者和书籍这两个 Entity 了,如何获取这两个 Entity 的信息呢?这就需要用到 NSPersistentContainer 的成员,即 managedObjectModel,该成员就是标题所说的 NSManagedObjectModel 类型。 为了了解 NSManagedObjectModel 能提供什么,通过以下函数来提供说明:
private func parseEntities ( container: NSPersistentContainer ) {
let entities = container. managedObjectModel. entities
print ( "Entity count = \( entities. count ) \n" )
for entity in entities {
print ( "Entity: \( entity. name! ) " )
for property in entity. properties {
print ( "Property: \( property. name) " )
}
print ( "" )
}
}
为了执行上面这个函数,需要修改 createPersistentContainer,在里面调用 parseEntities:
private func createPersistentContainer ( ) {
let container = NSPersistentContainer ( name: "Model" )
container. loadPersistentStores { ( description, error) in
if let error = error {
fatalError ( "Error: \( error) " )
}
self . parseEntities ( container: container)
}
}
在这个函数里,通过 NSPersistentContainer 获得了 NSManagedObjectModel 类型的成员 managedObjectModel,并通过它获得了文件 Model.xcdatamodeld 中配置好的 Entity 信息,即图书和读者。由于配置了两个 Entity 信息,所以运行正确的话,打印出来的第一行是:
Entity count = 2
container 的成员 managedObjectModel 有一个成员叫 entities,它是一个数组,这个数组成员的类型叫 NSEntityDescription,这个类名是专门用来处理 Entity 相关操作的,这里就没必要多赘述。 示例代码里,获得了 entity 数组后,打印 entity 的数量,然后遍历数组,逐个获得 entity 实例,接着遍历 entity 实例的 properties 数组,该数组成员是由类型 NSPropertyDescription 的对象组成。 关于名词 Property,在 Core Data 的术语环境下,一个 Entity 由若干信息部分组成,之前已经提过的 Entity 和 Relationship 就是,而这些信息用术语统称为 property。NSPropertyDescription 看名字就能知道,就是处理 property 用的。
Entity count = 2
Entity : Book
Property : isbm
Property : name
Property : page
Property : borrowedBy
Entity : Reader
Property : idCard
Property : name
Property : borrow
可以看到,打印出来配置的图书有 4 个 property,最后一个是 borrowedBy,明显这是个 Relationship,而前面三个都是 Attribute,这和刚刚对 property 的说明是一致的。
④ Entity 对应的类
Core Data 是一个“对象-关系映射”持久化方案,现在在 Model.xcdatamodeld 已经建立了两个 Entity,那么如果在代码里要操作它们,是不是会有对应的类?答案是确实如此,而且还不需要自己去定义这个类。 如果点击 Model.xcdatamodeld 编辑窗口中的 Book 这个 Entity,打开右侧的属性面板,属性面板会给出允许编辑的关于这个 Entity 的信息,其中 Entity 部分的 Name 就是命名的 Book,而下方还有一个 Class 栏,这一栏就是跟 Entity 绑定的类信息,栏目中的 Name 就是要定义的类名,默认它和 Entity 的名字相同,也就是说,类名也是 Book,所以改与不改,看个人思路以及团队的规范。 所有 Entity 对应的类,都继承自 NSManagedObject。为了检验这一点,可以在代码中编写这一行作为测试:
var book: Book ! // 纯测验代码,无业务价值
如果写下这一行编译通过了,那说明开发环境已经生成了 Book 这个类,不然它就不可能编译通过。测试结果,完美编译通过,说明不需要自己编写,就可以直接使用这个类。 说明:
关于类名,官方教程里一般会把类名更改为 Entity 名 + MO,比如这个 Entity 名为 Book,那么如果是按照官方教程的做法,可以在面板中编辑 Class 的名字为 BookMO,这里 MO 大概就是 Model Object 的简称。但是这里为简洁起见,就不做任何更改了,Entity 名为 Book,那么类名也一样为 Book。
另外,也可以自己去定义 Entity 对应的类,这样有个好处是可以给类添加一些额外的功能支持,这部分 Core Data 提供了编写的规范,但是大部分时候这个做法反而会增加代码量,不属于常规操作。
四、数据业务的操作
① 数据操作管理类 NSManagedObjectContext
接下来,隆重介绍 NSPersistentContainer 麾下的一名工作任务最繁重的大将,成员 viewContext,接下去和实际数据打交道,处理增删查改这四大操作,都要通过这个成员才能进行。 viewContext 成员的类型是 NSManagedObjectContext,顾名思义,它的任务就是管理对象的上下文。从创建数据,对修改后数据的保存、删除数据、修改数据,无一不是以它为入口。 现在开始,正式从“定义数据”的阶段,正式进入到“产生和操作数据”的阶段。
② 数据的插入
“数据插入”的调用方法:NSEntityDescription.insertNewObject。 先尝试创建一本图书,用一个 createBook 函数来进行,示例代码如下:
private func createBook ( container: NSPersistentContainer ,
name: String , isbm: String , pageCount: Int ) {
let context = container. viewContext
let book = NSEntityDescription . insertNewObject ( forEntityName: "Book" ,
into: context) as ! Book
book. name = name
book. isbm = isbm
book. page = Int32 ( pageCount)
if context. hasChanges {
do {
try context. save ( )
print ( "Insert new book(\( name) ) successful." )
} catch {
print ( "\( error) " )
}
}
}
在这个代码里,最值得关注的部分就是 NSEntityDescription 的静态成员函数 insertNewObject,通过这个函数来进行所要插入数据的创建工作。 insertNewObject 对应的参数 forEntityName 就是要输入的 Entity 名,这个名字当然必须是之前创建好的 Entity 有的名字才行,否则就出错,因为要创建的是书,所以输入的名字就是 Book。 而 into 参数就是处理增删查改的大将 NSManagedObjectContext 类型。insertNewObject 返回的类型是 NSManagedObject,如前所述,这是所有 Entity 对应类的父类,因为要创建的 Entity 是 Book,我们已经知道对应的类名是 Book,所以可以放心大胆的把它转换为 Book 类型。接下来就可以对 Book 实例进行成员赋值,可以惊喜的发现 Book 类的成员都是在 Entity 表格中编辑好的,真是方便极了。 那么问题来了,当把 Book 编辑完成后,是不是这个数据就完成了持久化了,其实不是的。这里要提一下 Core Data 的设计理念:懒原则,Core Data 框架之下,任何原则操作都是内存级的操作,不会自动同步到磁盘或者其它媒介里,只有开发者主动发出存储命令,才会做出存储操作,这么做自然不是因为真的很懒,而是出于性能考虑。 为了真的把数据保存起来,首先通过 context (即 NSManagedObjectContext 成员)的 hasChanges 成员询问是否数据有改动,如果有改动,就执行 context 的 save 函数(该函数是个会抛异常的函数,所以用 do→catch 包裹起来)。至此,添加书本的操作代码就全部写完,接下来把它放到合适的地方运行。 对 createPersistentContainer 稍作修改:
private func createPersistentContainer ( ) {
let container = NSPersistentContainer ( name: "Model" )
container. loadPersistentStores { ( description, error) in
if let error = error {
fatalError ( "Error: \( error) " )
}
//self.parseEntities(container: container)
self . createBook ( container: container,
name: "算法(第4版)" ,
isbm: "9787115293800" ,
pageCount: 636 )
}
}
Insert new book ( 算法(第4 版)) successful.
至此,书本的插入工作顺利完成,因为这个示例没有去重判定,如果程序运行两次,那么将会插入两条书名都为“算法(第4版)”的 book 记录。
③ 数据的获取
private func readBooks ( container: NSPersistentContainer ) {
let context = container. viewContext
let fetchBooks = NSFetchRequest < Book > ( entityName: "Book" )
do {
let books = try context. fetch ( fetchBooks)
print ( "Books count = \( books. count ) " )
for book in books {
print ( "Book name = \( book. name! ) " )
}
} catch {
}
}
处理数据处理依然是数据操作主力 context,而处理读取请求配置细节则是交给一个专门的类 NSFetchRequest 来完成,因为处理读取数据有各种各样的类型,所以 Core Data 设计了一个泛型模式,只要对 NSFetchRequest 传入对应的类型,比如 Book,它就知道应该传回什么类型的对应数组,其结果是可以通过 Entity 名为 Book 的请求直接拿到 Book 类型的数组,真是很方便。 打印结果:
Books count = 1
Book name = 算法(第4 版)
④ 数据获取的条件筛选
通过 NSFetchRequest 可以获取所有的数据,但是很多时候需要的是获得想要的特定的数据,通过条件筛选功能,可以实现获取出想要的数据,这时候需要用到 NSFetchRequest 的成员 predicate 来完成筛选,如下所示,要找书名叫“算法(第4版)”的书,在代码示例里,在之前实现的 readBooks 函数代码里略作修改:
private func readBooks ( container: NSPersistentContainer ) {
let context = container. viewContext
let fetchBooks = NSFetchRequest < Book > ( entityName: "Book" )
fetchBooks. predicate = NSPredicate ( format: "name = \"算法(第4版)\"" )
do {
let books = try context. fetch ( fetchBooks)
print ( "Books count = \( books. count ) " )
for book in books {
print ( "Book name = \( book. name! ) " )
}
} catch {
print ( "\( error) " )
}
}
fetchBooks. predicate = NSPredicate ( format: "name = \"算法(第4版)\"" )
从书籍中筛选出书名为 算法(第4版) 的书,因为之前已经保存过这本书,所以可以正确筛选出来。筛选方案还支持大小对比,如:
fetchBooks. predicate = NSPredicate ( format: "page > 100" )
这样将筛选出 page 数量大于 100 的书籍。
⑤ 数据的修改
当要修改数据时,比如说需要把 isbm = “9787115293800” 这本书的书名修改为“算法(第5版)” ,可以按照如下代码示例:
let context = container. viewContext
let fetchBooks = NSFetchRequest < Book > ( entityName: "Book" )
fetchBooks. predicate = NSPredicate ( format: "isbm = \"9787115293800\"" )
do {
let books = try context. fetch ( fetchBooks)
if ! books. isEmpty {
books[ 0 ] . name = "算法(第5版)"
if context. hasChanges {
try context. save ( )
print ( "Update success." )
}
}
} catch {
print ( "\( error) " )
}
上面的例子里,遵循了“读取→修改→保存”的思路,先拿到筛选的书本,然后修改书本的名字,当名字被修改后,context 将会知道数据被修改,这时候判断数据是否被修改(实际上不需要判断便也知道被修改了,只是出于编码规范加入了这个判断),如果被修改,就保存数据,通过这个方式,成功更改了书名。
⑥ 数据的删除
数据的删除,依然遵循“读取→修改→保存”的思路,找到想要的思路,并且删除它。删除的方法是通过 context 的 delete 函数。 如下所示,删除所有 isbm=“9787115293800” 的书籍:
let context = container. viewContext
let fetchBooks = NSFetchRequest < Book > ( entityName: "Book" )
fetchBooks. predicate = NSPredicate ( format: "isbm = \"9787115293800\"" )
do {
let books = try context. fetch ( fetchBooks)
for book in books {
context. delete ( books[ 0 ] )
}
if context. hasChanges {
try context. save ( )
}
} catch {
print ( "\( error) " )
}