該文章屬于劉小壯原創,轉載請注明:劉小壯
第一篇文章中并沒有講
CoreData
的具體用法,只是對CoreData
做了一個詳細的介紹,算是一個開始和總結吧。這篇文章中會主要講
CoreData
的基礎使用,以及在使用中需要注意的一些細節。因為文章中會插入代碼和圖片,內容可能會比較多,比較考驗各位耐心。文章中如有疏漏或錯誤,還請各位及時提出,謝謝!??`
創建自帶CoreData的工程
在新建一個項目時,可以勾選Use Core Data
選項,這樣創建出來的工程系統會默認生成一些CoreData
的代碼以及一個.xcdatamodeld
后綴的模型文件,模型文件默認以工程名開頭。這些代碼在AppDelegate
類中,也就是代表可以在全局使用AppDelegate.h
文件中聲明的CoreData
方法和屬性。
系統默認生成的代碼是非常簡單的,只是生成了基礎的托管對象模型、托管對象上下文、持久化存儲調度器,以及MOC
的save
方法。但是這些代碼已經可以完成基礎的CoreData
操作了。
這部分代碼不應該放在AppDelegate
中,尤其對于大型項目來說,更應該把這部分代碼單獨抽離出去,放在專門的類或模塊來管理CoreData
相關的邏輯。所以我一般不會通過這種方式創建CoreData
,我一般都是新建一個“干凈”的項目,然后自己往里面添加,這樣對于CoreData
的完整使用流程掌握的也比較牢固。
CoreData模型文件的創建
構建模型文件
使用CoreData
的第一步是創建后綴為.xcdatamodeld
的模型文件,使用快捷鍵Command + N,選擇Core Data -> Data Model -> Next
,完成模型文件的創建。
創建完成后可以看到模型文件左側列表,有三個選項Entities
、Fetch Requests
、Configurations
,分別對應著實體、請求模板、配置信息。
添加實體
現在可以通過長按左側列表下方的Add Entity
按鈕,會彈出Add Entity
、Add Fetch Request
、Add Configuration
選項,可以添加實體、請求模板、配置信息。這里先選擇Add Entity
來添加一個實體,命名為Person
。
添加Person
實體后,會發現一個實體對應著三部分內容,Attributes
、Relationships
、Fetched Properties
,分別對應著屬性、關聯關系、獲取操作。
現在對Person
實體添加兩個屬性,添加age
屬性并設置type
為Integer 16
,添加name
屬性并設置type
為String
。
實體屬性類型
在模型文件的實體中,參數類型和平時創建繼承自NSObject
的模型類大體類似,但是還是有一些關于類型的說明,下面簡單的列舉了一下。
- Undefined: 默認值,參與編譯會報錯
-
Integer 16: 整數,表示范圍
-32768 ~ 32767
-
Integer 32: 整數,表示范圍
-2147483648 ~ 2147483647
-
Integer 64: 整數,表示范圍
–9223372036854775808 ~ 9223372036854775807
-
Float: 小數,通過
MAXFLOAT
宏定義來看,最大值用科學計數法表示是0x1.fffffep+127f
-
Double: 小數,小數位比
Float
更精確,表示范圍更大 -
String: 字符串,用
NSString
表示 -
Boolean: 布爾值,用
NSNumber
表示 -
Date: 時間,用
NSDate
表示 -
Binary Data: 二進制,用
NSData
表示 -
Transformable:
OC
對象,用id
表示。可以在創建托管對象類文件后,手動改為對應的OC
類名。使用的前提是,這個OC
對象必須遵守并實現NSCoding
協議
添加實體關聯關系
創建兩個實體Department
和Employee
,并且在這兩個實體中分別添加一些屬性,下面將會根據這兩個實體來添加關聯關系。
給Employee
實體添加關系,在Relationships
的位置點擊加號,添加一個關聯關系。添加關系的名稱設為department
,類型設置為Department
,Inverse
設置為employee
(后面會講解這個inverse
的作用)。
選擇Department
實體,點擊Relationships
位置的加號,添加關聯關系。(需要注意的是,inverse
需要設置好Relationships
之后才能設置)
Department
實體添加Relationships
的操作和Employee
都一樣,區別在于用紅圈標出的Type
,這里設置的To Many
一對多的關系。這里默認是To One
一對一,上面的Employee
就是一對一的關系。也就符合一個Department
可以有多個Employee
,而Employee
只能有一個Department
的情況,這也是符合常理的。
Relationships
類似于SQLite
的外鍵,定義了在同一個模型中,實體與實體之間的關系。可以定義為對一關系或對多關系,也可以定義單向或雙向的關系,根據需求來確定。如果是對多的關系,默認是使用NSSet
集合來存儲模型。
Inverse
是兩個實體在Relationships
中設置關聯關系后,通過設置inverse
為對應的實體,這樣可以從一個實體找到另一個實體,使兩個實體具有雙向的關聯關系。
Fetched Properties
在實體最下面,有一個Fetched Properties
選項,這個選項用的不多,這里就不細講了。
Fetched Properties
用于定義查詢操作,和NSFetchRequest
功能相同。定義fetchedProperty
對象后,可以通過NSManagedObjectModel
類的fetchRequestFromTemplateWithName:substitutionVariables:
方法或其他相關方法獲取這個fetchedProperty
對象。
獲取這個對象后,系統會默認將這個對象緩存到一個字典中,緩存之后也可以通過fetchedProperty
字典獲取fetchedProperty
對象。
Data Model Inspector
選中一個實體后,右側的側邊欄(Data Model Inspector
)還有很多選項,這些選項可以對屬性進行配置。根據不同的屬性類型,側邊欄的顯示也不太一樣,下面是一個String
類型的屬性。
屬性設置
default Value: 設置默認值,除了二進制不能設置,其他類型幾乎都能設置。
optional: 在使用時是否可選,也可以理解為如果設置為
NO
,只要向MOC
進行save
操作,這個屬性是否必須有值。否則MOC
進行操作時會失敗并返回一個error
,該選項默認為YES
。transient: 設置當前屬性是否只存在于內存,不被持久化到本地,如果設置為
YES
,這個屬性就不參與持久化操作,屬性的其他操作沒有區別。transient
非常適合存儲一些在內存中緩存的數據,例如存儲臨時數據,這些數據每次都是不同的,而且不需要進行本地持久化,所以可以聲明為transient
的屬性。indexed: 設置當前屬性是否是索引。添加索引后可以有效的提升檢索操作的速度。但是對于刪除這樣的操作,刪除索引后其他地方還需要做出相應的變化,所以速度會比較慢。
Validation: 通過
Validation
可以設置Max Value
和Min Value
,通過這兩個條件來約定數據,對數據的存儲進行一個驗證。數值類型都有相同的約定方式,而字符串則是約定長度,date
是約定時間。Reg. Ex.(
Regular Expression
): 可以設置正則表達式,用來驗證和控制數據,不對數據自身產生影響。(只能應用于String
類型)Allows External Storage: 當存儲二進制文件時,如果遇到比較大的文件,是否存儲在存儲區之外。如果選擇
YES
,存儲文件大小超過1MB
的文件,都會存儲在存儲區之外。否則大型文件存儲在存儲區內,會造成SQLite
進行表操作時,效率受到影響。
Relationships設置
- delete rule: 定義關聯屬性的刪除規則。在當前對象和其他對象有關聯關系時,當前對象被刪除后與之關聯對象的反應。這個參數有四個枚舉值,代碼對應著模型文件的相同選項。
NSNoActionDeleteRule 刪除后沒有任何操作,也不會將關聯對象的關聯屬性指向nil
。刪除后使用關聯對象的關聯屬性,可能會導致其他問題。
NSNullifyDeleteRule 刪除后會將關聯對象的關聯屬性指向nil
,這是默認值。
NSCascadeDeleteRule 刪除當前對象后,會將與之關聯的對象也一并刪除。
NSDenyDeleteRule 在刪除當前對象時,如果當前對象還指向其他關聯對象,則當前對象不能被刪除。
-
Type: 主要有兩種類型,
To One
和To Many
,表示當前關系是一對多還是一對一。
實體
-
Parent Entity: 可以在實體中創建繼承關系,在一個實體的菜單欄中通過
Parent Entity
可以設置父實體,這樣就存在了實體的繼承關系,最后創建出來的托管模型類也是具有繼承關系的。注意繼承關系中屬性名不要相同。
使用了這樣的繼承關系后,系統會將子類繼承父類的數據,存在父類的表中,所有繼承自同一父類的子類都會將父類部分存放在父類的表中。這樣可能會導致父類的表中數據量過多,造成性能問題。
Fetch Requests
在模型文件中Entities
下面有一個Fetch Requests
,這個也是配置請求對象的。但是這個使用起來更加直觀,可以很容易的完成一些簡單的請求配置。相對于上面講到的Fetched Properties
,這個還是更方便使用一些。
上面是對Employee
實體的height
屬性配置的Fetch Request
,這里配置的height
要小于2米。配置之后可以通過NSManagedObjectModel
類的fetchRequestTemplateForName:
方法獲取這個請求對象,參數是這個請求配置的名稱,也就是EmployeeFR
。
Editor Style
這是我認為CoreData
最大的優勢之一,可視化的模型文件結構。可以很清楚的看到實體和屬性的關系,以及實體之間的對應關系。
一個.xcdatamodeld
模型文件的展示風格有兩種,一種是列表的形式(Table
),另一種是圖表的形式展示(Graph
)。
圖表看起來更加直觀,而圖表在操作上也有一些比Table
更方便的地方。例如在Table
的狀態下添加兩個實體的關聯關系,如果只做一次關聯操作,默認是單向的關系。而在Graph
的狀態下,按住Control
對兩個圖表進行連線,兩個實體的結果就是雙向關聯的關系。
手動創建實體
假設不使用.xcdatamodeld
模型文件,全都是純代碼,怎么在項目里創建實體啊?這樣的話就需要通過代碼創建實體描述、關聯描述等信息,然后設置給NSManagedObjectModel
對象。而使用模型文件的話一般都是通過NSManagedObjectModel
對象來讀取文件。
如果是純代碼的話,蘋果更推薦使用KVC
的方式存取值,然后所有托管對象都用NSManagedObject
創建。但是這樣存在的問題很多,開發成本比較大、使用不方便等等。最大的問題就是寫屬性名的key
字符串,很容易出錯,而且這樣失去了CoreData
原有的優點。所以還是推薦使用.xcdatamodeld
模型文件的開發方式。
創建托管對象類文件
創建文件
創建實體后,就可以根據對應的實體,生成開發中使用的基于NSManagedObject
類的托管對象類文件。
還是按照上面Department
和Employee
的例子,先創建一個Department
實體。因為Department
實體有對多關系,生成托管對象類文件的關聯屬性不一樣,可以體現出和對一關系的區別,所以使用Department
實體生成文件。
點擊后綴名為.xcdatamodeld
的模型文件,選擇Xcode
的Editor -> Create NSManagedObject Subclass -> 選擇模型文件 -> 選擇實體
,生成Department
實體對應的托管對象類文件。
可以看到上面生成了四個文件,以實體名開頭的.h
和.m
文件,另外兩個是這個實體的Category
文件。為什么生成Category
文件?一會再說,先打開類文件進去看看。
Category
可以看到類文件中有兩個Category
,分別是CoreDataProperties
和CoreDataGeneratedAccessors
。其中如果沒有設置對多關系的實體,只會有CoreDataProperties
,而設置了對多關系的實體系統會為其生成CoreDataGeneratedAccessors
。
CoreDataProperties
中會生成實體中聲明的Attributes
和Relationships
中的屬性,其中對多關系是用NSSet
存儲的屬性,如果是對一的關系則是非集合的對象類型屬性。再看.m
文件中,所有屬性都用@dynamic
修飾,CoreData
會在運行時動態為所有Category
中的屬性生成實現代碼,所以這里用@dynamic
修飾。
對多屬性生成的CoreDataGeneratedAccessors
,是系統自動生成管理對多屬性集合的方法,一般都是一個屬性對應四個方法,方法的實現也是在運行時動態實現的,方法都是用來操作集合對象的。
托管對象類文件
點擊系統生成的托管對象類文件,此類是繼承自NSManagedObject
類的。可以看到里面非常干凈,沒有其他邏輯代碼。
根據蘋果的注釋代碼:Insert code here to declare functionality of your managed object subclass
,提示應該在這個文件中編寫此類相關的邏輯代碼。這里就是編寫此類邏輯代碼的地方,當然也可以什么都不寫,看需求啦。
任意類型屬性
實體支持創建任意繼承自NSObject
類的屬性,例如項目中手動創建的類。項目中創建的類在下拉列表中并不會體現,可以在屬性類型選擇transformable
類型,然后生成托管對象類文件的時候,系統會將這個屬性聲明為id
類型,在創建類文件后,可以直接手動更改這個屬性的類型為我們想要的類型。
對于手動設置的屬性有一個要求,屬性所屬的類必須是遵守NSCoding
協議,因為這個屬性要被歸檔到本地。
標量類型
創建托管對象類文件時,實體屬性的類型無論是選擇的integer32
還是float
,只要是基礎數據類型,最后創建出來的默認都是NSNumber
類型的,這是Xcode
默認的。
如果需要生成的屬性類型是基礎數據類型,可以在創建文件時勾選Use scalar properties for primitive data types
選項,這樣就告訴系統需要生成標量類型屬性,創建出來的屬性就是int64_t
、float
這樣的基礎數據類型。
更新文件
當前模型對應的實體發生改變后,需要重新生成模型Category
文件。生成步驟和上面一樣,主要是替換Category
文件,托管對象文件不會被替換。生成文件時不需要刪除,直接替換文件。
CoreData增刪改查
下面關于CoreData
的相關操作,還是基于上面Department
和Employee
的例子。并且引入了Company
當做.xcdatamodeld
模型文件,前面兩個實體被包含在Company
中。
先講講NSManagedObjectContext
在iOS5
之前創建NSManagedObjectContext
對象時,都是直接通過init
方法來創建。iOS5
之后蘋果更加推薦使用initWithConcurrencyType:
方法來創建,在創建的時候指定當前是什么類型的并發隊列,初始化方法參數是一個枚舉值。這里簡單說說MOC
,后面多線程部分還會涉及MOC
多線程相關的東西。
NSManagedObjectContext
初始化方法的枚舉值參數主要有三個類型:
NSConfinementConcurrencyType 如果使用
init
方法初始化上下文,默認就是這個并發類型。在iOS9
之后已經被蘋果廢棄,不建議用這個API
,調用某些比較新的CoreData
的API
可能會導致崩潰。NSPrivateQueueConcurrencyType 私有并發隊列類型,操作都是在子線程中完成的。
NSMainQueueConcurrencyType 主并發隊列類型,如果涉及到
UI
相關的操作,應該考慮使用這個參數初始化上下文。
如果還使用init
方法,可能會對后面推出的一些API
不兼容,導致多線程相關的錯誤。例如下面的錯誤,因為如果沒有顯式的設置并發類型,默認是一個已經棄用的NSConfinementConcurrencyType
類型,就會導致新推出的API
發生不兼容的崩潰錯誤。
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSConfinementConcurrencyType context
創建MOC
下面是根據Company
模型文件,創建了一個主隊列并發類型的MOC
。
// 創建上下文對象,并發隊列設置為主隊列
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
// 創建托管對象模型,并使用Company.momd路徑當做初始化參數
NSURL *modelPath = [[NSBundle mainBundle] URLForResource:@"Company" withExtension:@"momd"];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelPath];
// 創建持久化存儲調度器
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
// 創建并關聯SQLite數據庫文件,如果已經存在則不會重復創建
NSString *dataPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
dataPath = [dataPath stringByAppendingFormat:@"/%@.sqlite", @"Company"];
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:nil error:nil];
// 上下文對象設置屬性為持久化存儲器
context.persistentStoreCoordinator = coordinator;
這段代碼創建了一個MOC
,我們從上往下看這段代碼。
momd文件
關于MOC
的并發隊列類型上面已經簡單說了,MOC
下面出現了momd
的字樣,這是什么東西?
在創建后綴為.xcdatamodeld
的模型文件后,模型文件在編譯期將會被編譯為后綴為.momd
的文件,存放在.app
中,也就是Main Bundle
中。在存在多個模型文件時,我們需要通過加載不同的.momd
文件,來創建不同的NSManagedObjectModel
對象,每個NSManagedObjectModel
對應著不同的模型文件。
NSManagedObjectModel
類中包含了模型文件中的所有entities
、configurations
、fetchRequests
的描述。雖然.momd
文件是支持存放在.app
中的,其他人可以通過打開.app
包看到這個文件。但是這個文件是經過編碼的,并不會知道這個.momd
文件中的內容,所以這個文件是非常安全的。通過NSManagedObjectModel
獲取模型文件描述后,來創建和關聯數據庫,并交給PSC
管理。
如果不指定NSManagedObjectModel
對應哪個模型文件,直接使用init
方法初始化NSManagedObjectModel
類,系統會默認將所有模型文件的表都放在一個SQLite
數據庫中。所以需要使用mainBundle
中的不同.momd
文件,對不同的NSManagedObjectModel
進行初始化,這樣在創建數據庫時就會創建不同的數據庫文件。
持久化存儲調度器(PSC)
在NSManagedObjectModel
下面就是NSPersistentStoreCoordinator
,這個類在CoreData
框架體系中起到了“中樞”的作用。對上層起到了提供簡單的調用接口,并向上層隱藏持久化實現邏輯。對下層起到了協調多個持久化存儲對象(NSPersistentStore
),使下層只需要專注持久化相關邏輯。
addPersistentStoreWithType: configuration: URL: options: error:
方法是PSC
創建并關聯數據庫的部分,關聯本地數據庫后會返回一個NSPersistentStore
類型對象,這個對象負責具體持久化存儲的實現。可以看到這個方法是一個實例方法,也就是可以添加多個持久化存儲對象,并且多個持久化存儲對象都關聯一個PSC
,這是允許的,在上面的圖中也看到了這樣的結構。但是這樣的需求并不多,而且管理起來比較麻煩,一般都不會這樣做。
PSC
有四種可選的持久化存儲方案,用得最多的是SQLite
的方式。其中Binary
和XML
這兩種方式,在進行數據操作時,需要將整個文件加載到內存中,這樣對內存的消耗是很大的。
- NSSQLiteStoreType : SQLite數據庫
- NSXMLStoreType : XML文件
- NSBinaryStoreType : 二進制文件
- NSInMemoryStoreType : 直接存儲在內存中
插入操作
// 創建托管對象,并指明創建的托管對象所屬實體名
Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:context];
emp.name = @"lxz";
emp.height = @1.7;
emp.brithday = [NSDate date];
// 通過上下文保存對象,并在保存前判斷是否有更改
NSError *error = nil;
if (context.hasChanges) {
[context save:&error];
}
// 錯誤處理
if (error) {
NSLog(@"CoreData Insert Data Error : %@", error);
}
通過NSEntityDescription
的insert
類方法,生成并返回一個Employee
托管對象,并將這個對象插入到指定的上下文中。
MOC
將操作的數據存放在緩存層,只有調用MOC
的save
方法后,才會真正對數據庫進行操作,否則這個對象只是存在內存中,這樣做避免了頻繁的數據庫訪問。
刪除操作
// 建立獲取數據的請求對象,指明對Employee實體進行刪除操作
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 創建謂詞對象,過濾出符合要求的對象,也就是要刪除的對象
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"];
request.predicate = predicate;
// 執行獲取操作,找到要刪除的對象
NSError *error = nil;
NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error];
// 遍歷符合刪除要求的對象數組,執行刪除操作
[employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[context deleteObject:obj];
}];
// 保存上下文
if (context.hasChanges) {
[context save:nil];
}
// 錯誤處理
if (error) {
NSLog(@"CoreData Delete Data Error : %@", error);
}
首先獲取需要刪除的托管對象,遍歷獲取的對象數組,逐個刪除后調用MOC
的save
方法保存。
修改操作
// 建立獲取數據的請求對象,并指明操作的實體為Employee
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 創建謂詞對象,設置過濾條件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"];
request.predicate = predicate;
// 執行獲取請求,獲取到符合要求的托管對象
NSError *error = nil;
NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error];
[employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.height = @3.f;
}];
// 將上面的修改進行存儲
if (context.hasChanges) {
[context save:nil];
}
// 錯誤處理
if (error) {
NSLog(@"CoreData Update Data Error : %@", error);
}
和上面一樣,首先獲取到需要更改的托管對象,更改完成后調用MOC
的save
方法持久化到本地。
查找操作
// 建立獲取數據的請求對象,指明操作的實體為Employee
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 執行獲取操作,獲取所有Employee托管對象
NSError *error = nil;
NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error];
[employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"Employee Name : %@, Height : %@, Brithday : %@", obj.name, obj.height, obj.brithday);
}];
// 錯誤處理
if (error) {
NSLog(@"CoreData Ergodic Data Error : %@", error);
}
查找操作最簡單粗暴,因為是演示代碼,所以直接將所有Employee
表中的托管對象加載出來。在實際開發中肯定不會這樣做,只需要加載需要的數據。后面還會講到一些更高級的操作,會涉及到獲取方面的東西。
總結
在CoreData
中所有的托管對象被創建出來后,都是關聯著MOC
對象的。所以在對象進行任何操作后,都會被記錄在MOC
中。在最后調用MOC
的save
方法后,MOC
會將操作交給PSC
去處理,PSC
將會將這個存儲任務指派給NSPersistentStore
對象。
上面的增刪改查操作,看上去大體流程都差不多,都是一些最基礎的簡單操作,在下一篇文章中將會將一些比較復雜的操作。
好多同學都問我有Demo
沒有,其實文章中貼出的代碼組合起來就是個Demo
。后來想了想,還是給本系列文章配了一個簡單的Demo
,方便大家運行調試,后續會給所有博客的文章都加上Demo
。
Demo
只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo
一起學習,只看Demo
還是不能理解更深層的原理。Demo
中幾乎每一行代碼都會有注釋,各位可以打斷點跟著Demo
執行流程走一遍,看看各個階段變量的值。
Demo地址:劉小壯的Github
這兩天更新了一下文章,將CoreData
系列的六篇文章整合在一起,做了一個PDF
版的《CoreData Book》,放在我Github上了。PDF
上有文章目錄,方便閱讀。
如果你覺得不錯,請把PDF幫忙轉到其他群里,或者你的朋友,讓更多的人了解CoreData,衷心感謝!??