認識CoreData - 基礎使用

該文章屬于劉小壯原創,轉載請注明:劉小壯

配圖

第一篇文章中并沒有講CoreData的具體用法,只是對CoreData做了一個詳細的介紹,算是一個開始和總結吧。

這篇文章中會主要講CoreData的基礎使用,以及在使用中需要注意的一些細節。因為文章中會插入代碼和圖片,內容可能會比較多,比較考驗各位耐心。

文章中如有疏漏或錯誤,還請各位及時提出,謝謝!??`


創建自帶CoreData的工程

在新建一個項目時,可以勾選Use Core Data選項,這樣創建出來的工程系統會默認生成一些CoreData的代碼以及一個.xcdatamodeld后綴的模型文件,模型文件默認以工程名開頭。這些代碼在AppDelegate類中,也就是代表可以在全局使用AppDelegate.h文件中聲明的CoreData方法和屬性。

系統默認生成的代碼是非常簡單的,只是生成了基礎的托管對象模型、托管對象上下文、持久化存儲調度器,以及MOCsave方法。但是這些代碼已經可以完成基礎的CoreData操作了。

系統生成代碼

這部分代碼不應該放在AppDelegate中,尤其對于大型項目來說,更應該把這部分代碼單獨抽離出去,放在專門的類或模塊來管理CoreData相關的邏輯。所以我一般不會通過這種方式創建CoreData,我一般都是新建一個“干凈”的項目,然后自己往里面添加,這樣對于CoreData的完整使用流程掌握的也比較牢固。


CoreData模型文件的創建

構建模型文件

使用CoreData的第一步是創建后綴為.xcdatamodeld的模型文件,使用快捷鍵Command + N,選擇Core Data -> Data Model -> Next,完成模型文件的創建。

創建完成后可以看到模型文件左側列表,有三個選項EntitiesFetch RequestsConfigurations,分別對應著實體、請求模板、配置信息。

模型文件

添加實體

現在可以通過長按左側列表下方的Add Entity按鈕,會彈出Add EntityAdd Fetch RequestAdd Configuration選項,可以添加實體、請求模板、配置信息。這里先選擇Add Entity來添加一個實體,命名為Person

添加Person實體后,會發現一個實體對應著三部分內容,AttributesRelationshipsFetched Properties,分別對應著屬性、關聯關系、獲取操作。

空實體

現在對Person實體添加兩個屬性,添加age屬性并設置typeInteger 16,添加name屬性并設置typeString

添加屬性
實體屬性類型

在模型文件的實體中,參數類型和平時創建繼承自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協議

添加實體關聯關系

創建兩個實體DepartmentEmployee,并且在這兩個實體中分別添加一些屬性,下面將會根據這兩個實體來添加關聯關系。

創建實體

Employee實體添加關系,在Relationships的位置點擊加號,添加一個關聯關系。添加關系的名稱設為department,類型設置為DepartmentInverse設置為employee(后面會講解這個inverse的作用)。

添加Relationships

選擇Department實體,點擊Relationships位置的加號,添加關聯關系。(需要注意的是,inverse需要設置好Relationships之后才能設置)

Department實體添加Relationships的操作和Employee都一樣,區別在于用紅圈標出的Type,這里設置的To Many一對多的關系。這里默認是To One一對一,上面的Employee就是一對一的關系。也就符合一個Department可以有多個Employee,而Employee只能有一個Department的情況,這也是符合常理的。

添加Relationships

Relationships類似于SQLite的外鍵,定義了在同一個模型中,實體與實體之間的關系。可以定義為對一關系或對多關系,也可以定義單向或雙向的關系,根據需求來確定。如果是對多的關系,默認是使用NSSet集合來存儲模型。

Inverse是兩個實體在Relationships中設置關聯關系后,通過設置inverse為對應的實體,這樣可以從一個實體找到另一個實體,使兩個實體具有雙向的關聯關系。

Fetched Properties

在實體最下面,有一個Fetched Properties選項,這個選項用的不多,這里就不細講了。

Fetched Properties用于定義查詢操作,和NSFetchRequest功能相同。定義fetchedProperty對象后,可以通過NSManagedObjectModel類的fetchRequestFromTemplateWithName:substitutionVariables:方法或其他相關方法獲取這個fetchedProperty對象。

fetched Property

獲取這個對象后,系統會默認將這個對象緩存到一個字典中,緩存之后也可以通過fetchedProperty字典獲取fetchedProperty對象。

Data Model Inspector

選中一個實體后,右側的側邊欄(Data Model Inspector)還有很多選項,這些選項可以對屬性進行配置。根據不同的屬性類型,側邊欄的顯示也不太一樣,下面是一個String類型的屬性。

Data Model Inspector
屬性設置
  • default Value: 設置默認值,除了二進制不能設置,其他類型幾乎都能設置。

  • optional: 在使用時是否可選,也可以理解為如果設置為NO,只要向MOC進行save操作,這個屬性是否必須有值。否則MOC進行操作時會失敗并返回一個error,該選項默認為YES

  • transient: 設置當前屬性是否只存在于內存,不被持久化到本地,如果設置為YES,這個屬性就不參與持久化操作,屬性的其他操作沒有區別。transient非常適合存儲一些在內存中緩存的數據,例如存儲臨時數據,這些數據每次都是不同的,而且不需要進行本地持久化,所以可以聲明為transient的屬性。

  • indexed: 設置當前屬性是否是索引。添加索引后可以有效的提升檢索操作的速度。但是對于刪除這樣的操作,刪除索引后其他地方還需要做出相應的變化,所以速度會比較慢。

  • Validation: 通過Validation可以設置Max ValueMin Value,通過這兩個條件來約定數據,對數據的存儲進行一個驗證。數值類型都有相同的約定方式,而字符串則是約定長度,date是約定時間。

  • Reg. Ex.(Regular Expression): 可以設置正則表達式,用來驗證和控制數據,不對數據自身產生影響。(只能應用于String類型)

  • Allows External Storage: 當存儲二進制文件時,如果遇到比較大的文件,是否存儲在存儲區之外。如果選擇YES,存儲文件大小超過1MB的文件,都會存儲在存儲區之外。否則大型文件存儲在存儲區內,會造成SQLite進行表操作時,效率受到影響。

Relationships設置
  • delete rule: 定義關聯屬性的刪除規則。在當前對象和其他對象有關聯關系時,當前對象被刪除后與之關聯對象的反應。這個參數有四個枚舉值,代碼對應著模型文件的相同選項。

NSNoActionDeleteRule 刪除后沒有任何操作,也不會將關聯對象的關聯屬性指向nil。刪除后使用關聯對象的關聯屬性,可能會導致其他問題。
NSNullifyDeleteRule 刪除后會將關聯對象的關聯屬性指向nil,這是默認值。
NSCascadeDeleteRule 刪除當前對象后,會將與之關聯的對象也一并刪除。
NSDenyDeleteRule 在刪除當前對象時,如果當前對象還指向其他關聯對象,則當前對象不能被刪除。

  • Type: 主要有兩種類型,To OneTo Many,表示當前關系是一對多還是一對一。
實體
  • Parent Entity: 可以在實體中創建繼承關系,在一個實體的菜單欄中通過Parent Entity可以設置父實體,這樣就存在了實體的繼承關系,最后創建出來的托管模型類也是具有繼承關系的。注意繼承關系中屬性名不要相同。
    使用了這樣的繼承關系后,系統會將子類繼承父類的數據,存在父類的表中,所有繼承自同一父類的子類都會將父類部分存放在父類的表中。這樣可能會導致父類的表中數據量過多,造成性能問題。

Fetch Requests

在模型文件中Entities下面有一個Fetch Requests,這個也是配置請求對象的。但是這個使用起來更加直觀,可以很容易的完成一些簡單的請求配置。相對于上面講到的Fetched Properties,這個還是更方便使用一些。

Fetch Requests

上面是對Employee實體的height屬性配置的Fetch Request,這里配置的height小于2米。配置之后可以通過NSManagedObjectModel類的fetchRequestTemplateForName:方法獲取這個請求對象,參數是這個請求配置的名稱,也就是EmployeeFR

Editor Style

這是我認為CoreData最大的優勢之一,可視化的模型文件結構。可以很清楚的看到實體和屬性的關系,以及實體之間的對應關系。

Editor Style

一個.xcdatamodeld模型文件的展示風格有兩種,一種是列表的形式(Table),另一種是圖表的形式展示(Graph)。

圖表看起來更加直觀,而圖表在操作上也有一些比Table更方便的地方。例如在Table的狀態下添加兩個實體的關聯關系,如果只做一次關聯操作,默認是單向的關系。而在Graph的狀態下,按住Control對兩個圖表進行連線,兩個實體的結果就是雙向關聯的關系。

手動創建實體

假設不使用.xcdatamodeld模型文件,全都是純代碼,怎么在項目里創建實體啊?這樣的話就需要通過代碼創建實體描述、關聯描述等信息,然后設置給NSManagedObjectModel對象。而使用模型文件的話一般都是通過NSManagedObjectModel對象來讀取文件。

如果是純代碼的話,蘋果更推薦使用KVC的方式存取值,然后所有托管對象都用NSManagedObject創建。但是這樣存在的問題很多,開發成本比較大、使用不方便等等。最大的問題就是寫屬性名的key字符串,很容易出錯,而且這樣失去了CoreData原有的優點。所以還是推薦使用.xcdatamodeld模型文件的開發方式。

創建托管對象類文件

創建文件

創建實體后,就可以根據對應的實體,生成開發中使用的基于NSManagedObject類的托管對象類文件。

還是按照上面DepartmentEmployee的例子,先創建一個Department實體。因為Department實體有對多關系,生成托管對象類文件的關聯屬性不一樣,可以體現出和對一關系的區別,所以使用Department實體生成文件。

點擊后綴名為.xcdatamodeld的模型文件,選擇XcodeEditor -> Create NSManagedObject Subclass -> 選擇模型文件 -> 選擇實體,生成Department實體對應的托管對象類文件。

生成的托管對象類文件

可以看到上面生成了四個文件,以實體名開頭的.h.m文件,另外兩個是這個實體的Category文件。為什么生成Category文件?一會再說,先打開類文件進去看看。

Category

實體Category

可以看到類文件中有兩個Category,分別是CoreDataPropertiesCoreDataGeneratedAccessors。其中如果沒有設置對多關系的實體,只會有CoreDataProperties,而設置了對多關系的實體系統會為其生成CoreDataGeneratedAccessors

CoreDataProperties中會生成實體中聲明的AttributesRelationships中的屬性,其中對多關系是用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_tfloat這樣的基礎數據類型。

標量類型

更新文件

當前模型對應的實體發生改變后,需要重新生成模型Category文件。生成步驟和上面一樣,主要是替換Category文件,托管對象文件不會被替換。生成文件時不需要刪除,直接替換文件


CoreData增刪改查

下面關于CoreData的相關操作,還是基于上面DepartmentEmployee的例子。并且引入了Company當做.xcdatamodeld模型文件,前面兩個實體被包含在Company中。

先講講NSManagedObjectContext

iOS5之前創建NSManagedObjectContext對象時,都是直接通過init方法來創建。iOS5之后蘋果更加推薦使用initWithConcurrencyType:方法來創建,在創建的時候指定當前是什么類型的并發隊列,初始化方法參數是一個枚舉值。這里簡單說說MOC,后面多線程部分還會涉及MOC多線程相關的東西。

NSManagedObjectContext初始化方法的枚舉值參數主要有三個類型:

  • NSConfinementConcurrencyType 如果使用init方法初始化上下文,默認就是這個并發類型。在iOS9之后已經被蘋果廢棄,不建議用這個API,調用某些比較新的CoreDataAPI可能會導致崩潰。

  • 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的字樣,這是什么東西?

momd文件

在創建后綴為.xcdatamodeld的模型文件后,模型文件在編譯期將會被編譯為后綴為.momd的文件,存放在.app中,也就是Main Bundle中。在存在多個模型文件時,我們需要通過加載不同的.momd文件,來創建不同的NSManagedObjectModel對象,每個NSManagedObjectModel對應著不同的模型文件。

NSManagedObjectModel類中包含了模型文件中的所有entitiesconfigurationsfetchRequests的描述。雖然.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的方式。其中BinaryXML這兩種方式,在進行數據操作時,需要將整個文件加載到內存中,這樣對內存的消耗是很大的。

  • 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);
}   

通過NSEntityDescriptioninsert類方法,生成并返回一個Employee托管對象,并將這個對象插入到指定的上下文中。

MOC將操作的數據存放在緩存層,只有調用MOCsave方法后,才會真正對數據庫進行操作,否則這個對象只是存在內存中,這樣做避免了頻繁的數據庫訪問。

刪除操作

// 建立獲取數據的請求對象,指明對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);
}

首先獲取需要刪除的托管對象,遍歷獲取的對象數組,逐個刪除后調用MOCsave方法保存。

修改操作

// 建立獲取數據的請求對象,并指明操作的實體為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);
}

和上面一樣,首先獲取到需要更改的托管對象,更改完成后調用MOCsave方法持久化到本地。

查找操作

// 建立獲取數據的請求對象,指明操作的實體為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中。在最后調用MOCsave方法后,MOC會將操作交給PSC去處理,PSC將會將這個存儲任務指派給NSPersistentStore對象。

上面的增刪改查操作,看上去大體流程都差不多,都是一些最基礎的簡單操作,在下一篇文章中將會將一些比較復雜的操作。


好多同學都問我有Demo沒有,其實文章中貼出的代碼組合起來就是個Demo。后來想了想,還是給本系列文章配了一個簡單的Demo,方便大家運行調試,后續會給所有博客的文章都加上Demo

Demo只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo一起學習,只看Demo還是不能理解更深層的原理Demo中幾乎每一行代碼都會有注釋,各位可以打斷點跟著Demo執行流程走一遍,看看各個階段變量的值。

Demo地址劉小壯的Github


這兩天更新了一下文章,將CoreData系列的六篇文章整合在一起,做了一個PDF版的《CoreData Book》,放在我Github上了。PDF上有文章目錄,方便閱讀。

如果你覺得不錯,請把PDF幫忙轉到其他群里,或者你的朋友,讓更多的人了解CoreData,衷心感謝!??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容