【2013-11-19 21:24:26 伯樂線上補充】:由於之前和譯者@shaojingkk 溝通不夠,我們在釋出這篇譯文之前並不知道譯者參考對照@answer-huang 之前的譯文版本,所以釋出文章時未能在文中特別說明,責任在我們,在此向@answer-huang 表示歉意。感謝大家對我們的批評和監督,另外也要感謝@answer-huang 的翻譯分享。
Core Data可能是OS X和iOS中最容易被誤解的框架之一了。為了幫助大家理解,我們將快速研究Core Data,來看一下它是關於什麼的。為了正確使用Core Data, 有必要理解其概念。幾乎所有Core Data引起的挫敗,都是因為不理解它能做什麼和它是怎麼工作的。讓我們開始吧。
Core Data是什麼?
大概8年前,在2005年4月,Apple釋出了OS X 10.4版本,第一次引入了Core Data框架。那時YouTube也剛釋出。
Core Data是模型層的技術。Core Data幫助你構建代表程式狀態的模型層。Core Data也是一種持久化技術,它可以將模型的狀態持久化到磁碟。但它更重要的特點是:Core Data不只是一個載入和儲存資料的框架,它也能處理記憶體中的資料。
如果你曾接觸過Object-relational mapping(O/RM),Core Data不僅是一種O/RM。如果你曾接觸過SQL wrappers, Core Data也不是一種SQL wrapper。它確實預設使用SQL,但是,它是一種更高層次的抽象概念。如果你需要一個O/RM或者SQL wrapper,那麼Core Data並不適合你。
>Core Data提供的最強大的功能之一是它的物件圖形管理。為了更有效的使用Core Data, 你需要理解這一部分內容。
還有一點需要注意:Core Data完全獨立於任何UI層的框架。從設計的角度來說,它是完全的模型層的框架。在OS X中,甚至在一些後臺駐留程式中,Core Data也起著重要的意義。
堆疊The Stack
Core Data中有不少元件,它是一種非常靈活的技術。在大多數使用情況裡,設定相對來說比較簡單。
當所有元件繫結在一起,我們把它們稱為Core Data Stack. 這種堆疊有兩個主要部分。一部分是關於物件圖管理,這是你需要掌握好的部分,也應該知道怎麼使用。第二部分是關於持久化的,比如儲存模型物件的狀態和再次恢復物件的狀態。
在這兩部分的中間,即堆疊中間,是持久化儲存協調器(Persistent Store Coordinator, PSC),也被朋友們戲稱做中心監視局。通過它將物件圖管理部分和持久化部分綁在一起。當這兩部分中的一部分需要和另一部分互動,將通過PSC來調節。
物件圖管理是你的應用中模型層邏輯存在的地方。模型層物件存在於一個context裡。在大多數設定中,只有一個context,所有的物件都放在這個context中。Core Data支援多個context,但是是針對更高階的使用情況。需要注意的是,每個context和其他context區分都很清楚,我們將要來看一點這部分內容。有個重要的事需要記住,物件和他們的context繫結在一起。每一個被管理的物件都知道它屬於哪個context,每一個context也知道它管理著哪些物件。
堆疊的另一部分是持久化發生的地方,比如Core Data從檔案系統讀或寫。在所有情況下,持久化儲存協調器(PSC)有一個屬於自己的的持久化儲存器(persistent store),這個store在檔案系統和SQLite資料庫互動。為了支援更高階的設定,Core Data支援使用多個儲存器附屬於同一個持久化儲存協調器,並且除了SQL,還有一些別的儲存型別可以選擇。
一個常見的解決方案,看起來是這個樣子的:
元件如何一起工作
我們來快速看一個例子,來說明這些元件是如何協同工作的。在我們a full application using Core Data的文章裡,我們正好有一個實體enity,即一種物件: 我們有一個Item實體對應一個title。每一個item可以有子items,因此我們有一個父子關係。
這是我們的資料模型。像我們在Data Models and Model Objects文章裡提到的,在Core Data中有一個特別型別的物件叫做Entity。在這種情況下,我們只有一個entity:一個Item entity. 同樣的,我們有一個NSManagedObject子類叫Item。這個Item entity對映到Item類。在data models的文章裡會詳細的談到這個。
我們的應用僅有一個根item。這裡面沒有什麼奇妙的地方。這只是個簡單的我們用在底層的item。這是一個我們永遠不會為其設定父類的item.
當app啟動,我們沒有任何item。我們需要做的第一件事是建立一個根item。你通過插入物件到context裡來新增可管理的物件。
建立物件
可能看起來有點笨重。我們通過NSEntityDescription的方法來插入:
1 2 |
+ (id)insertNewObjectForEntityForName:(NSString *)entityName inManagedObjectContext:(NSManagedObjectContext *)context |
我們建議你新增兩個方便的方法到模型類中:
1 2 3 4 5 6 7 8 9 10 |
+ (NSString *)entityName { return @“Item”; } + (instancetype)insertNewObjectInManagedObjectContext:(NSManagedObjectContext *)moc; { return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:moc]; } |
現在,我們可以插入我們的根物件:
1 |
Item *rootItem = [Item insertNewObjectInManagedObjectContext:managedObjectContext]; |
現在在我們的managed object context裡有了一個唯一的item. context知道這個新插入的被管理物件,這個被管理物件rootItem也知道這個context(它有 -managedObjectContext方法)。
儲存改變
到目前為止,雖然我們還沒有碰到持久化儲存協調器或者持久化儲存器。這個新的模型物件,rootItem,只是在記憶體中。如果我們想儲存我們模型物件的狀態(我們的情況裡就是rootItem),我們需要這樣儲存context:
1 2 3 4 |
NSError *error = nil; if (! [managedObjectContext save:&error]) { // Uh, oh. An error happened. :( } |
現在,有很多事將要發生。首先,managed object context算出改變的內容。Context有責任去記錄你對context裡任何被管理的物件做出的改變。在我們的例子裡,我們迄今為止唯一的改變是往裡插入了一個物件,我們的rootItem.
這個managed object context把這些變化傳遞給持久化儲存協調器,讓它把改變傳遞給store。持久化儲存協調器協調store(在我們的例子裡,是一個SQL儲存器)把我們新插入的物件寫入磁碟中的SQL資料庫裡。NSPersistentStore類管理著和SQLite的真正互動,並且生成需要被執行的SQL程式碼。持久化儲存協調器的角色只是簡單的協調store和context之間的互動。在我們的例子裡,這個角色相對簡單,但是更復雜的應用裡可以有多個store和多個context.
更新關係
Core Data的重要能力是它可以管理關係。我們看一個簡單的例子,加第二個item,把它設為rootItem的子item。
1 2 3 |
Item *item = [Item insertNewObjectInManagedObjectContext:managedObjectContext]; item.parent = rootItem; item.title = @"foo"; |
好了。再次注意,這些改變只是在managed object context裡面。一旦我們儲存了context,managed object context就會告訴持久化儲存協調器去把那個新建的物件新增到資料庫檔案中,像我們的第一個物件一樣。但是它也同樣會更新從我們第二個item到第一個的關係,或從第一個物件到第二個的關係。記住一個Item實體是如何有了父子關係。同時他們也有相反的關係。因為我們把第一個item設為第二個的父類,第二個就會是第一個的子類。Managed object context記錄了這些關係,持久化儲存協調器和store持久化(比如儲存)這些關係到磁碟。
弄清物件
假設我們已經使用了我們的app一段時間,並且已經新增了一些子items到根item,甚至一些子items到子items。我們再次啟動app,Core Data已經在資料庫檔案中儲存了這些item之間的關係,物件圖已經存在了。現在我們需要取出我們的根item, 這樣我們可以顯示底層items列表。我們有兩種辦法來實現這個,我們先來看一個簡單的。
當根Item物件建立並儲存後,我們可以獲取它的NSManagedObjectID。這是一個不透明的物件,只代表根Item物件。我們可以把它儲存到NSUserDefaults, 像這樣:
1 2 |
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setURL:rootItem.managedObjectID.URIRepresentation forKey:@"rootItem"]; |
現在,當我們的app執行中,我們可以像這樣取回這個物件:
1 2 3 4 5 |
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSURL *uri = [defaults URLForKey:@"rootItem"]; NSManagedObjectID *moid = [managedObjectContext.persistentStoreCoordinator managedObjectIDForURIRepresentation:uri]; NSError *error = nil; Item *rootItem = (id) [managedObjectContext existingObjectWithID:moid error:&error]; |
當然,在一個真正的app中,我們需要檢查NSUserDefaults是否返回了一個有效的值。
剛才發生的事情是,managed object context讓持久化儲存協調器去從資料庫裡獲取指定的物件。那個root物件現在就被恢復到了context中。但是所有其他的item還不在記憶體中。
rootItem有一個關係叫子關係,但是那兒還什麼也沒有。我們想顯示rootItem的所有子item,所以我們呼叫:
1 |
NSOrderedSet *children = rootItem.children; |
現在發生的是,context注意到要獲取rootItem有關聯的children,會得到一個所謂的故障。Core Data已經標記了這個關係作為一件需要被解決的事。既然我們已經在這個時候訪問了它,context現在會自動和持久化儲存協調器去協調,來載入這些子item到context裡。
這個聽起來可能是不重要的,但是事實上這個地方有很多事情發生。如果子物件中碰巧有一些已經在記憶體中了,Core Data需要保證它會重用這些物件。這就叫做唯一性。在context中,從來不會有多於一個物件對應於一個給定的item.
其次,持久化儲存協調器有它自己內部物件值的快取。如果context需要一個指定的物件(比如一個子item),並且持久化儲存協調器已經在記憶體中有它需要的值了,這個物件可以被直接加到context,不需要通過store。這個很重要,因為使用store意味著執行SQL語句,這要比直接用記憶體中的值慢很多。
我們繼續從一個item到子item再到子item,我們慢慢的把整個物件圖載入到managed object context。但是一旦它們都在記憶體中,操作物件,或者獲取它們的關係都是很快的,因為我們只是在managed object context中工作。我們完全不需要和持久化儲存協調器打交道。這時候獲取我們的Item物件的title, parent, children屬性都很快也很方便。
理解在這些情況中資料是怎麼獲取的很重要,因為它影響效能。在我們的這個例子裡,它不太重要,因為我們沒有使用很多資料。但是一旦你開始使用,你就需要理解背後發生了什麼。
當你獲取一種關係(比如我們的例子是父子關係),下面三種情況中的一個會發生: (1) 這個物件已經在context中,獲取基本上沒有開銷。(2)這個物件不在context中,但是因為你最近從store中獲取過這個物件,持久化儲存協調器快取了它的值。這種情況適當便宜一些(雖然一些操作會被鎖住)。開銷最大的情況是:(3)當這個物件被context和持久化儲存協調器都第一次訪問,這樣它需要被store從SQLite資料庫中取出來。這種情況要比1和2開銷大很多。
如果你知道你需要從store中獲取物件(因為你還沒有它們),當你限制一次取回多少個物件時,將會產生很大不同。在我們的例子裡,我們可能需要一次獲取所有的子item,而不是一個接一個。這可以通過一個特殊的NSFetchRequest來實現。但是我們一定要小心只是在我們需要的時候才執行一次取出請求。因為一個取出請求將會引起(3)發生,它總是需要通過SQLite資料庫來獲取。因此,如果效能很重要,檢查物件是否已經存在就很必要。你可以使用 -[NSManagedObjectContext objectRegisteredForID:]
來檢測一個物件是否已經存在。
改變物件的值
現在,比如我們要改變一個item物件的title:
1 |
item.title = @"New title"; |
當我們這麼做的時候,這個item的title就改變了。但是同時,managed object context會把這個item標記為已經改變,這樣當我們呼叫context的-save:,它將會通過持久化儲存協調器和相應的store儲存起來。context的一個重要職責就是標記改變。
context知道從上次儲存後,哪些物件已經被插入,改變和刪除。你可以通過-insertedObjects, -updateObjects, -deletedObject這些方法來獲取。同樣的,你也可以通過-changedValues方法問一個被管理的物件,它的哪些值變了。你可能從來都不需要這麼做。但是這是Core Data可以把改變儲存到支援的資料庫中的方式。
當我們插入一個新的Item物件,Core Data知道需要把這些改變存入store。現在,當我們改變title,也會發生同樣的事情。
儲存值需要和持久化儲存協調器還有持久化store依次訪問SQLite資料庫。當恢復物件和值時,使用store和資料庫比直接在記憶體中運算元據相對耗費資源。儲存有一個固定的開銷,不管你要儲存的變化有多少。並且每次變化都有成本,這只是SQLite的工作。當你改變很多值時,需要將變更打包,並批量更改。如果你每次改變都儲存,需要付出昂貴的代價,因為你需要經常儲存。如果你儲存次數少一些,你將會有有一大批更改交由SQLite來處理。
需要注意的是儲存是原子性的。它們都是事務。或者所有的改變都被提交到store/SQLite資料庫,或者任何改變都不會被儲存。當你實現自定義的NSIncrementalStore子類的時候,這點很重要應該要記住。你可以保證儲存永遠不會失敗(比如因為衝突),或者你的store子類需要在儲存失敗時恢復所有改變。否則,記憶體中的物件圖會和store中的不一致。
如果你只是使用簡單的設定,儲存通常不會失敗。但是Core Data允許多個context對應一個持久化儲存協調器,所以你可能會在持久化儲存協調器中遇到衝突。改變是每一個context的,另一個context可能會引入有衝突的改變。Core Data甚至允許完全不同的堆疊都訪問同一個在磁碟中的SQLite資料庫檔案。這顯然也可能引發衝突(比如,一個context想更新一個object的一個值,但是這個object已經被另一個context刪除了)。另一個導致儲存失敗的原因可能是校驗。Core Data支援對物件的複雜校驗規則。這是個高階的話題。一個簡單的校驗可以是,一個Item的title長度一定不能超過300字元。但是Core Data也支援對屬性的複雜的校驗規則。
結束語
如果Core Data看起來讓人畏縮,可能主要因為它可以讓你通過複雜的方式來靈活使用。始終記住:儘可能讓事情保持簡單。這會讓開發更容易,讓你和你的使用者免於麻煩。只在你確定會有幫助的情況下,才使用像background contexts這些更復雜的東西。
當你使用一個簡單的Core Data堆疊,並且你用我們在本文中提到的方法來使用managed objects,你會很快學會欣賞Core Data可以為你做的事情,和它怎麼幫你縮短開發週期。