iOS的CoreData技术笔记
作者丨风海铜锣君
来源丨搜狐技术产品(ID:sohu-tech)
本文字数:5914字
预计阅读时间:28分钟
为什么写这篇文章
SQLite
就可以解决了。SQLite
确实存在要自己设计数据库,处理逻辑编码,还有调试方面的种种繁琐问题。所以考虑使用iOS的Core Data方案。Swift
版本。于是萌生了自己写一篇文章来整理一遍思路的想法。尽可能让新人快速的上手,不但要知道其然,还要知道其设计的所以然,这样用起来才更得心应手。什么是Core Data
UserDefaults
,大部分时候是用来保存简单的应用配置信息;而NSKeyedArchiver
可以把代码中的对象保存为文件,方便后来重新读取。SQLite
就是更合适的方案了。关于数据库的知识,我这里就不赘述了,稍微有点技术基础的童鞋都懂。Core Data
比SQLite
做了更进一步的封装,SQLite
提供了数据的存储模型,并提供了一系列API,你可以通过API读写数据库,去处理想要处理的数据。但是SQLite
存储的数据和你编写代码中的数据(比如一个类的对象)并没有内置的联系,必须你自己编写代码去一一对应。Core Data
却可以解决一个数据在持久化层和代码层的一一对应关系。也就是说,你处理一个对象的数据后,通过保存接口,它可以自动同步到持久化层里,而不需要你去实现额外的代码。ORM
)。Core Data
还提供了很多有用的特性,比如回滚机制,数据校验等。数据模型文件 - Data Model
Core Data
时,我们需要一个用来存放数据模型的地方,数据模型文件就是我们要创建的文件类型。它的后缀是.xcdatamodeld
。只要在项目中选 新建文件→Data Model 即可创建。Model.xcdatamodeld
。下面我依然以 Model.xcdatamodeld
作为举例的文件名。数据模型中的“表格” - Entity
Model.xcdatamodeld
时,会看到苹果提供的编辑视图,其中有个醒目的按钮Add Entity
。Entity
呢?中文翻译叫“实体”,但是我这里就不打算用各种翻译名词来提高理解难度了。Entity
就相当于库里的“表格”。这么理解就简单了。Entity
就是让你定义数据表格类型的名词。Book
的Entity
。“属性” - Attributes
Book
的Entity
时,会看到视图中有栏写着Attributes
,我们知道,当我们定义一本书时,自然要定义书名,书的编码等信息。这部分信息叫Attributes
,即书的属性。Entity
:属性名 | 类型 |
---|---|
name | String |
isbm | String |
page | Integer32 |
Entity
描述。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
,却没有东西可以填,这是为什么?Book
的一栏,在Relationship
下添加新的borrowBy
,Destination
是Reader
,这时候点击Inverse
一栏,会发现弹出了borrow
,直接点上。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
了。Core Data框架的主仓库 -
NSPersistentContainer
Core Data
的数据类型信息后,我们并没有产生任何数据,就好比图书馆已经制定了图书的规范 - 一本书应该有名字、isbm、页数等信息,规范虽然制定了,却没有真的引进书进来。Core Data
打交道了。Core Data
的功能较为强大,必须分成多个类来处理各种逻辑,一次性学习多个类是不容易的,还容易混淆,所以后续我会分别一一列出。NSPersistentContainer
,因为它就是存放这多个类成员的“仓库类”。NSPersistentContainer
,就是我们通过代码和Core Data
打交道的第一个目标。它存放着几种让我们和Core Data
进行业务处理的工具,当我们拿到这些工具之后,就可以自由的访问数据了。所以它的名字 - Container
蕴含着的意思,就是 仓库、容器、集装箱。Core Data
框架的swift
文件开头引入这个框架:import CoreData
❝早期,在iOS 10之前,还没有 NSPersistentContainer
这个类,所以Core Data
提供的几种各司其职的工具,我们都要写代码一一获得,写出来的代码较为繁琐,所以NSPersistentContainer
并不是一开始就有的,而是苹果框架设计者逐步优化出来的较优设计。
图4: NSPersistentContainer和其他成员的关系
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
不直接在构造函数里完成数据库的加载?这就涉及到一个面向对象的开发原则,即构造函数的初始化应该是(原则上)倾向于原子级别,即简单的、低开销内存操作,而对于性能开销大的,内存之外的存储空间处理(比如磁盘,网络),应尽量单独提供成员函数来完成。这样做是为了避免在构造函数中出错时错误难以捕捉的问题。
表格属性信息的提供者 -
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
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
。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版)
数据获取的条件筛选 -
NSPredicate
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版)\"")
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)")
}
扩展和进阶主题的介绍
Core Data
的基础知识可以说已经掌握的差不多了。当然了,这部分基础对于日常开发已经基本够用了。Core Data
开发的进阶部分,我在这里简单列举一下:Relationship
部分的开发,事实上通过之前的知识可以独立完成。回滚操作,相关类: UndoManager
。Entity
的Fetched Property
属性。多个 context
一起操作数据的冲突问题。持久化层的管理,包括迁移文件地址,设置多个存储源等。
结语
Core Data
在圈内是比较出了名的“不好用”的框架,主要是因为其抽象的功能和机制较为不容易理解。本文已经以最大限度的努力试图从设计的角度去阐述该框架,希望对你有所帮助。
引用
https://developer.apple.com/documentation/coredata
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
面试题
】即可获取评论