在專案中建立和使用Core Data
Core Data包含的內容非常豐富,這篇文章是關於Core Data的比較基礎和常用的內容;包括Core Data的組成,和如何在應用中構建和使用Core Data。主要包括以下內容:
- 什麼是Core Data
- Core Data 棧
- 建立一個Core Data驅動的應用
- 使用Core Data操作資料
1、什麼是Core Data
Core Data是一種基於資料模型的資料管理解決方案,是蘋果提供的關係-物件對映的原生解決方案。在iOS系統架構中位於核心服務層,使用介面是一組Objective-C的類;它是設計用來與MVC設計模式協同工作的,部分工作可以用圖形化的方式進行編輯;
不同於關係型資料庫,Core Data並不在意值,它關注的是物件;從Core Data中取出資訊時,它會建立並返回一個裝有受控物件的陣列。Core Data會自動對結果資料的值進行包裝,封裝成應用中使用的模型物件,然後將這些物件當作獲取操作的結果返回。
2、Core Data棧
Core Data棧可由下面的Core Data架構示意圖展示,下圖將Core Data棧中的各種參與者以及它們之間的互動展示出來,描述了Core Data的工作過程。Core Data棧是由以下內容構成的:
- 受控物件模型
- 持久化儲存排程器
- 持久化儲存和儲存檔案
- 受控物件上下文
2.1、受控物件模型
受控物件模型(managed object model),簡稱物件模型,負責定義應用中的資料結構。物件模型儲存在視覺化檔案中,副檔名為.xcdatamodeld。它可以用一個圖形化介面工具來編輯,即Core Data資料模型編輯器(見下圖)。
在上圖中顯示了Core Data模型編輯器的工作區介面,圖中物件模型定義了一個Person實體,實體包含了幾個簡單的屬性。
在一個受控物件模型中,每個物件(Objective-C類)都被稱為一個實體(entity)。每個實體都有自己單獨的一個列表,其中列出了屬性(attribute)、關係(relationship)、和衍生屬性(fetched property)。
可以將屬性(attribute)看作是自己定義物件時使用的例項屬性,如上圖中的id、name等。關係定義了單個實體彼此之間的聯絡。
派生屬性也表示了物件模型中實體間的聯絡。關係和派生屬性的區別在於,關係是雙向的(雙方物件都知道關係的存在),而派生屬性只是單向的。
在圖形工作區建立完物件模型之後,就可以用Xcode來自動生成受控物件類了。在Xcode的選單中新建檔案,在新建的對話方塊中選擇Core Data檔案型別,選擇NSManagedObject作為基類並單擊Next(見下圖)。之後,可以在對話方塊中選擇所有需要建立物件類的實體,之後,就可以生成這些子類了,生成的類包括選中的實體類(如Person類),還有實體和Core Data屬性相關的分類。
2.2、持久化儲存排程器
Core Data棧中,其次重要的元素就是持久化儲存排程器。通過受控物件模型,可以建立一個持久化儲存排程器。這個物件模型定義的實體和關係會受該排程器的管理。
持久化儲存排程器在Core Data中基本上是一個自動的過程。除了一開始的建立過程之外,在應用的整個生命週期中,並不需要操作這個排程器。大多數使用core data的iOS應用,都是圍繞單個數據庫設計的。
但是如果一個應用擁有多個儲存檔案,排程器的用處就會凸顯出來,它會管理底層的儲存,而給開發者提供一個單一的受控物件上下文,使得使用起來更方便。
2.3、持久化儲存和儲存檔案
在使用Core Data時,需要在檔案系統中建立一個新的資料庫檔案的持久化儲存。持久化儲存其實就是對實際的資料庫檔案的一種Objective-C的表示方式。不用自己去建立新的持久化儲存,只要確定了儲存型別、配置、URL和選項之後,就可以將新的持久化儲存直接新增到一個已有的排程器中。
下面的程式碼建立了一個新的持久化儲存:
//獲取沙盒document路徑
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//為資料庫檔案建立一個URL
NSURL *storeURL = [[NSURL URLWithString:documentPath] URLByAppendingPathComponent:@"DemoCoreData.sqlite"];
//建立持久化儲存
NSError *error = nil;
NSPersistentStore *store;
//coordinator為NSPersistentStoreCoordinator的物件,屬於持久化儲存排程器
store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];
//如果建立失敗,則進行錯誤處理
if (!store) {
//錯誤處理
}
由於資料庫是一個.sqlite檔案,所以在新增儲存時,除了傳入儲存的URL之外,還要將儲存的型別設定為NSSQLiteStoreType。
這裡新增的儲存是一個SQLite資料庫。Core Data支援3種類型的儲存:NSSQLiteStoreType、NSBinaryStorageType、NSInMemoryStoreType。這3種類型的效能從速度上來說都差不多,但從資料模型中保留下來的資訊不一樣。SQLite型別的儲存只保留了部分關係圖,在處理大型資料集時更有效率,是iOS開發中最經常使用的。
2.4、受控物件上下文
有了受控物件模型,也以此初始化了持久化儲存排程器。讀取資料庫檔案的持久化儲存也新增到了這個排程器中。還剩下操作和使用資料,這部分由受控物件上下文負責。
Core Data獲取資料時返回的是物件,當物件被創建出來之後,它們就存在於一個受控物件上下文中。受控物件上下文的工作就是管理Core Data建立並返回的物件。
在建立了受控物件上下文後,需要為這個上下文設定持久化儲存排程器,這樣排程器就可以訪問實體了。
以下程式碼是通過受控物件上下文從資料庫中取得所有的人員(Person物件),可以用來說明受控物件上下文在core data棧中的角色:
//將獲取請求的實體設定為人員物件
NSEntityDescription *issueEntity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:objectContext];
//建立一個新的獲取請求
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:issueEntity];
//獲取結果
NSError *error = nil;
NSArray *fetchResults = [objectContext executeFetchRequest:request error:&error];
//遍歷所有結果
for (Person *person in fetchResults) {
NSLog(@"%@", person.name);
}
受控物件上下文建立的物件是受它自身管理的。在這個例子中,物件上下文會監視它返回的Person物件所受到的任何改變。如果修改了這些物件的屬性,物件上下文會自動紀錄下這些變化。當修改完畢後,只要呼叫上下文的儲存操作,就可以讓上下文將所有修改傳遞給排程器,然後儲存到持久化儲存中。
3、建立一個Core Data應用
在新建工程時如果選中了“Use Core Data”複選框,Xcode就會自動在工程中引入Core Data框架,並在應用代理類中加上建立和管理Core Data棧的方法。
如果新建工程是未選中“Use Core Data”複選框,而之後又希望使用Core Data作為儲存機制,那就需要手動在工程中建立Core Data棧,建立需要以下幾個步驟(對應Core Data棧的四個組成部分):
- 建立受控物件模型,在Xcode Core Data模型編輯器中進行。
- 建立持久化儲存排程器,初始化時用受控物件模型做引數。
- 為排程器新增一個持久化物件儲存。
- 建立新的受控物件上下文並設定儲存排程器。
3.1、建立受控物件模型
要新建一個受控物件模型,需要在工程中新增一個資料模型編輯器檔案(見下圖)。
資料模型檔案可以用Core Data模型編輯器來編輯和修改。在Core Data模型編輯器中定義好實體和關係之後,同時還需要生成NSmanagedObject的子類。
3.2、建立新的持久化儲存排程器
在模型編輯器建立好模型後,下一步就是用這個模型來生成持久化儲存排程器。下面的程式碼演示瞭如何新建一個排程器:
//獲得物件資料模型的引用
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
//用這個檔案的URL新建一個NSManagedObjectModel物件
objectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
//用這個objectModel物件新建NSPersistentStoreCoordinator物件
coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:objectModel];
初始化了持久化儲存排程器之後,剩下的事給他加上持久化儲存,在把這個排程器設定到一個受控物件上下文中。
3.3、新增新的持久化儲存
當獲得了儲存排程器之後,還需要向其中加入新的儲存,新增時要指定儲存的型別、配置、檔案URL和選項。新建儲存器需要以下程式碼:
//獲取沙盒document路徑
NSString *documentPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//為資料庫檔案建立一個URL
NSURL *storeURL = [[NSURL URLWithString:documentPath] URLByAppendingPathComponent:@"DemoCoreData.sqlite"];
//建立持久化儲存
NSError *error = nil;
NSPersistentStore *store;
//coordinator為NSPersistentStoreCoordinator的物件,屬於持久化儲存排程器
store = [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];
3.4、建立新的受控物件上下文
在建立新的受控物件上下文時,需要注意的是,必須在對應的分發佇列中建立和訪問這個上下文中的物件。受控物件上下文可以使用下列3種併發型別來進行初始化,分別是NSConfinementConcurrencyType、NSMainQueueConcurrencyType、NSPrivateQueueConcurrencyType三種。
NSConfinementConcurrencyType是傳統的初始化型別,在哪裡初始化,只能在對應的執行緒上使用;
NSMainQueueConcurrencyType和NSPrivateQueueConcurrencyType初始化受控物件上下文時,會在一個GCD分發佇列之內進行操作。當使用基於佇列的上下文時,該上下文建立的物件只在建立它的佇列內有效;在這種情況下,如果在一個事畢處理程式碼塊中或者是一個單獨的分發佇列中建立或者使用Core Data相關的物件,都是無效的。
受控物件上下文提供了兩種便捷方法,可以確保操作始終都在正確的佇列中完成。這些方法就是performBlock和performBlockAndWait。
使用GCD和程式碼塊處理過程更簡單高效和方便控制,因此推薦根據併發型別選用一個佇列。以下程式碼建立了一個新的受控物件上下文:
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
//執行程式碼塊中設定上下文
[moc performBlockAndWait:^{
//設定持久化儲存,coordinator為上一節建立的持久化儲存排程器
[moc setPersistentStoreCoordinator:coordinator];
}];
self.managedObjectContext = moc;
以下程式碼使用的併發型別是主佇列型別,在第一行分配了新的NSManagedObjectContext物件,指定併發型別為NSMainQueueConcurrencyType。在第四行用performBlockAndWait方法在受控物件上下文之上同步執行了一些操作,這些操作都會在正確的佇列中完成。
4、使用Core Data
現在已經在應用程式中建立起了一套Core Data棧,可以開始使用它操作其中的資料了。在通過受控物件上下文與Core Data持久化儲存進行互動的過程中,一般的操作包括以下4種類型之一:
- 新增新物件;
- 獲取物件並修改資料;
- 刪除物件;
- 撤銷、重做、回滾和復位。
4.1、新增新物件
我們和持久化儲存的所有互動都是轉交給受控物件上下文完成的,在進行所有這些互動時,可以把受控物件上下文想像成是持久化儲存的一個“工作副本”。對受控物件上下文所做的修改,直到使用正確的儲存操作提交之後,才會對資料產生影響。
以下程式碼展示瞭如何新建10個新的Person物件:
for (int i = 0; i < 10; i++) {
//向上下文中插入並返回一個新的Person物件
Person *newPerson = (Person *)[NSEntityDescription insertNewObjectForEntityForName:@"Person" inManagedObjectContext:self.managedObjectContext];
//設定新Person的名稱
[newPerson setName:[NSString stringWithFormat:@"Person#%i", i]];
}
//呼叫儲存上下文,將修改提交到持久化儲存中
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
NSLog(@"儲存出錯");
}
首先程式碼第三行在上下文中建立了一個新的物件,建立方法是:在上下文中通過一個實體的名稱插入一個新物件。由於每個實體都有自己的類名,所以這個方法返回的值是一個通用的id物件。在使用這個物件前,必須對返回值做型別轉換;之後,給這個物件設定了一個名稱。
所有物件新建處理完成以後,我們儲存了上下文,沒有必要在每次建立一個物件之後都儲存上下文。因為受控物件上下文會紀錄在兩次儲存動作之間發生的所有修改,所以沒有必要每次改動之後都呼叫儲存函式。實際上,那樣做可能會導致效能嚴重下降。可以把儲存操作當做是使用受控物件上下文的最後一步。應該先執行完當前任務需要對資料進行的一切修改,然後在最後一步儲存上下文。
4.2、獲取並修改物件
對於一個經過獲取操作返回的物件,其受到的任何修改也都會被受控物件上下文記錄下來。這意味著對這些物件所做的任何修改,在對建立這些物件的上下文呼叫儲存操作時,才會被提交到持久化儲存中。
在下面的程式碼中,我們在受控物件上下文執行了同樣的獲取操作。這裡我們為獲取操作定義了一個謂詞,用來將檢索引數限定為具有特別名稱的物件。一旦得到了結果,就會修改該物件的屬性,並儲存上下文來提交這些修改。
//將獲取請求的實體設定為Person物件
NSEntityDescription *personEntity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:managedObjectContext];
//建立一個新的請求
NSFetchRequest *request = [NSFetchRequest new];
[request setEntity:personEntity];
//為請求設定一個謂詞來限制請求的結果
//只需要具有指定名稱的期刊
NSPredicate *query = [NSPredicate predicateWithFormat:@"name = %@", name];
[request setPredicate:query];
//獲取結果
NSError *error = nil;
NSArray *fetchResults = [objectContext executeFetchRequest:request error:&error];
//如果得到了結果,就修改其屬性
if ([fetchResults count] > 0) {
Person *person = [fetchResults objectAtIndex:0];
person.address = @"ShangHai";
person.name = @"coredata";
}
//呼叫儲存上下文,將修改提交到持久化儲存中
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
NSLog(@"儲存出錯");
}
4.3、刪除物件
刪除一個現存物件與新增新物件很相似。在上下文上呼叫刪除操作時,將要刪除的物件作為引數傳遞過去,然後呼叫上下文的儲存操作就可以將修改提交到持久化儲存。
下面的程式碼展示瞭如何從持久化儲存中刪除一個物件:
Person *person = [self personWithName:name];
if (person) {
[self.managedObjectContext deleteObject:person];
}
if ([self.managedObjectContext hasChanges]) {
//儲存改動
NSError *error = nil;
if (![self.managedObjectContext save:&error]) {
NSLog(@"儲存出錯");
}
}
在第六行儲存上下文前,這裡檢查了受控物件上下文是否有需要儲存的修改。如果personWithName方法返回了nil值,那麼刪除物件的操作就不會執行。這樣,受控物件上下文將不會有需要儲存的新資訊。為了優化程式的效能,最好只在需要時才執行儲存操作。
4.4、撤銷、重做、回滾和復位
使用Core Data的一個好處就是,在應用的整個生命週期中,受控物件上下文會自動紀錄撤銷和重做狀態。
這項功能是透過NSUndoManager來達成的。NSUndoManager是iOS用來跟蹤資料變化的手段,用以管理撤銷操作。通常情況下,如果要在應用中支援撤銷操作,就必須建立自己的撤銷操作管理器(undo manager),並在事件發生時予以記錄。不過,如果用了Core Data,在使用者事件發生時,受控物件上下文自動在與其關聯的撤銷操作管理器中新增撤銷/重做的快照。
新增撤銷操作管理器
預設情況下iOS上的Core Data沒有自帶撤銷操作管理器,這主要是為了提高iOS的效能。因為iOS上,不是所有使用Core Data的應用程式都會用到撤銷操作,所以管理器預設被設定為nil。(在Mac OS X上,Core Data會自動為新建的受控物件上下文分配一個撤銷操作管理器。)
下面的程式碼展示瞭如何給一個受控物件上下文分配一個撤銷操作管理器:
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
//設定執行程式碼塊中設定上下文
[moc performBlockAndWait:^{
//新增撤銷操作管理器
NSUndoManager *undoManager = [NSUndoManager new];
[moc setUndoManager:undoManager];
//設定持久化儲存
[moc setPersistentStoreCoordinator:coordinator];
}];
self.managedObjectContext = moc;
預設情況下,一個受控物件上下文的撤銷操作管理器會設定為在收到使用者事件時記錄撤銷操作快照。使用者事件在這裡指的是由使用者觸發的控制元件事件、觸控事件或者晃動裝置的事件。
關於撤銷操作管理器,需要注意的一點是,它只有在使用者事件發生時才會記錄快照。如果沒有經過使用者操作,而是通過程式添加了物件的話,撤銷操作管理器是不會記錄這些改動的。撤銷操作管理器是用來撤銷使用者的動作,而不是撤銷開發者對受控物件上下文的全部操作的。
下表概括了在受控物件上下文上呼叫撤銷、重做、回滾和復位方法的效果。
方 法 名 | 描述 |
---|---|
undo | 撤銷自上次使用者控制元件事件發生以來,對資料所做的任何修改 |
redo | 重做對上下文所做的最後一組修改。只有在之前呼叫過undo方法時,這個方法才能重做被撤銷的修改 |
rollback | 將受控物件上下文的所有修改回滾,回到最後一次提交的狀態。這個操作同時也會清空紀錄在撤銷操作快取中的所有修改 |
reset | 清空紀錄在撤銷操作快取中的所有修改 |
5、總結
這邊文章整理記錄了Core Data的基礎知識,包含了Core Data棧的介紹,以及如何在一個專案中使用Core Data作為儲存和操作資料的機制;可以在新建工程時選擇使用Core Data框架,也可以自己在應用中建立一套Core Data棧。