為什么寫這篇文章
最近因為新項目想用到數據持久化,本來這是很簡單的事情,復雜數據一般直接SQLite
就可以解決了。
但是一直以來使用SQLite
確實存在要自己設計數據庫,處理邏輯編碼,還有調試方面的種種繁瑣問題。所以考慮使用iOS的Core Data方案。
上網查了一堆資料后,發現很多代碼都已經是陳舊的了。甚至蘋果官方文檔提供的代碼樣例都未必是最新的Swift
版本。于是萌生了自己寫一篇文章來整理一遍思路的想法。盡可能讓新人快速的上手,不但要知道其然,還要知道其設計的所以然,這樣用起來才更得心應手。
什么是Core Data
我們寫app肯定要用到數據持久化,說白了,就是把數據保存起來,app不刪除的話可以繼續讀寫。
iOS提供數據持久化的方案有很多,各自有其特定用途。
如果你正在面試,或者正準備跳槽,不妨看看我精心總結的面試資料:https://gitee.com/Mcci7/i-oser 來獲取一份詳細的大廠面試資料 為你的跳槽加薪多一份保障
比如很多人熟知的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
當在xcode中點擊Model.xcdatamodeld
時,會看到蘋果提供的編輯視圖,其中有個醒目的按鈕Add Entity
。
什么是Entity
呢?中文翻譯叫“實體”,但是我這里就不打算用各種翻譯名詞來提高理解難度了。
如果把數據模型文件比作數據庫中的“庫”,那么Entity
就相當于庫里的“表格”。這么理解就簡單了。Entity
就是讓你定義數據表格類型的名詞。
假設我這個數據模型是用來存放圖書館信息的,那么很自然的,我會想建立一個叫Book
的Entity
。
“屬性” - Attributes
當建立一個名為Book
的Entity
時,會看到視圖中有欄寫著Attributes
,我們知道,當我們定義一本書時,自然要定義書名,書的編碼等信息。這部分信息叫Attributes
,即書的屬性。
Book的Entity
:
屬性名 | 類型 |
---|---|
name | String |
isbm | String |
page | Integer32 |
其中,類型部分大部分是大家熟知的元數據類型,可以自行查閱。
同理,也可以再添加一個讀者:Reader的Entity
描述。
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
一欄,會發現彈出了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
并不是一開始就有的,而是蘋果框架設計者逐步優化出來的較優設計。
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
我們看到,打印出來我們配置的圖書有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
。
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版)\"")
我們從書籍中篩選出書名為 算法(第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
作者:風海銅鑼君
鏈接:http://www.lxweimin.com/p/132c2e2713c7