本文我們將會更加深入探討Core Data 的models以及managed object的類 。本文絕不是對 Core Data 的簡單概述,而是在實際運用中鮮為人知或不易記憶卻可以發揮奇效的那一部分的合集。如果你需要的是更詳細的概述,那么我推薦你去看“Apple’s Core Data Programming Guid”。
數據模型
Core Data數據模型(儲存在 *.xcdatamodel 文件里)中定義了數據類型 (在 Core Data 里的“實體”中)。大多數情況下,我們更偏向通過 Xcode 的圖形界面去定義一個數據模型,但同樣我們可以使用純代碼去完成這個工作。首先,你需要創建一個NSManagenObjectModel對象,然后創建NSEntitiyDesciption對象來表示一組實體,該實體通過NSAttributeDescription和NSRelationshipDescription對象來表示實體屬性和實體之間的關系。雖然你幾乎不需要去處理這些事情,但是知道這些類總有好處。
屬性 (Attributes)
一旦就創建了某個實體,我們就需要去定義該實體的一些屬性。屬性定義是非常簡單的,但是接下來我們要深入的研究屬性的某些特性。
默認的/可選的
每個屬性可被定義成可選的或者非可選(必須)的。如果一個被變更的對象的非可選屬性沒有設置的話,那么在存儲時將會失敗。同時,我們可以為每一個屬性設置默認值。沒有人阻止我們使用一個可選的屬性并且給它賦一個默認的值,但是當你進行深入思考的時候,你會發現這么做并沒有什么意義甚至引起混淆。所以,我們建議永遠不要使用帶有默認值的可選屬性。
瞬態 Transient
另一個經常被忽視的屬性的特性選項是它的transient。被聲明為transient的屬性除了不被持久化到本地之外,其余所有行為都與正常屬性類似。這也意味著可以對它們進行校驗,撤銷管理,故障處理等操作。當你將更復雜的數據模型邏輯映射到managed object subclasses的時候,transient屬性將會發揮出它的優勢。我們將會在后面繼續討論這個特性,以及我們更傾向于使用transient屬性而非實例變量的原因。
索引
如果你以前使用過關系數據庫,那么你對索引應該并不陌生。如果沒有,你可以認為屬性的索引可以提供一種大幅提高檢索速度的方法。但是有利有弊,索引在提高讀取速度的同時卻降低了寫入速度,因為每當數據變更的時候,索引就需要進行相應的更新。
當把一個屬性設置為indexed時,它將在SQLite中所對應的表的列中建立索引。我們能夠為任何屬性創建索引,但是請留意對寫性能的潛在影響。Core Data 當然也支持創建復合索引(在 entity 的 檢查器的 Indexs 部分中),就像那些橫跨了多個屬性的索引。當你在多屬性的場景下使用復合索引來獲取數據時可以對檢索效率進行提升。Daniel 有一個使用復合索引獲取數據的例子:fetching data。
標量類型
Core Data 支持包括整形、浮點型、布爾型在內的許多常見數據類型。但是數據模型編輯器默認以 NSNumber 生成這些屬性并內置于managed object子類中。這使得我們經常會在程序代碼中用調用floatValue,boolValue,integerValue等NSNumber的方法。
當然,我們同樣可以直接設置這些屬性為想要的標量類型,如int64_t,float_t或是BOOL,它們一樣可以正常運作。XCode甚至在生成NSManagedObject(原始數據類型使用標量屬性)對話框內有一個小選擇框可以為你進行強類型匹配。 取而代之,不再會是:
而會用如下聲明替換:
這就是我們在 Core Data 中進行獲取和保存標量類型所需要做的全部。在文檔中,仍然規定 Core Data 將不能自動的為標量生成存取方法,現在看來文檔似乎有點過時了了。
存儲其他類型對象
Core Data 并沒有約束我們只能對預定義類型進行存儲。事實上,對于任何遵守NSCoding協議的對象甚至到任何包含了大量功能的結構對象,我們都可以對其進行輕松的存儲。
我們可以通過使用transformable attributes來存儲遵守NSCoding協議的對象。我們需要做的僅僅是在下拉菜單中選擇 “Transformable” 選項。如果你生成了一個相應的managed object subclasses,你就會看到一個類似如下的屬性聲明:
我們可以手動將對象原有的id類型修改成任意我們想要儲存的類型來使編輯器進行強類型檢查。然而在使用 transformable 屬性的時候我們會遇到一個陷阱:如果我們想使用默認轉換器(最常用的),我們必須不能為它指定名字。甚至指定默認轉換器名字為其原始名字(NSKeyedUnarchiveFromDataTransformerName)都將會導致不好的事情。
不僅限于此,我們還可以創建自定義的值轉換器并使用它們去存儲任意的對象類型。只要我們能夠把要存儲的東西轉化為可支持的基本類型,我們就能存儲它。為了儲存比如結構體這種不支持的非對象的類型,基本的解決方式是創建一個未定義類型的transient屬性和一個持久化的已支持類型的影子屬性。然后,重寫transient屬性的存取方法,將值轉化為上述的持久化類型。這是很重要的,因為這些存取方法需要遵從KVC/KVO,同時還需要考慮到 Core Data 原始存取方法。請閱讀蘋果指南中的non-standard persistent attributes(非標準的持久屬性)這一部分的自定義代碼。
抓取屬性 (Fetched Properties)
在多個持久化存儲之間創建關系的時候我們經常會用到fetched屬性。由于使用多個持久化存儲本身已經是非常不常見、且高級的案例,因此fetched屬性幾乎也不會被使用。
當我們獲取一個抓取屬性的時候,Core Data 會在后端執行一個抓取請求并且緩存抓取結果。我們可以直接在 Xcode 中數據模型編輯器里通過指定目標實體類型和斷言來對抓去請求進行配置。這里的斷言是動態的而非靜態的,其通過 $FETCHSOURCE 和 $FETCHEDPROPERTY 兩個變量在程序運行態進行配置。更多細節可以參考蘋果官方文檔。
關系 (Relationships)
實體間的關系應該總是被定義成雙向的。這給予了 Core Data 足夠的信息為我們全面管理類圖。盡管定義雙向的關系不是一個硬性要求,但我還是強烈建議這么去做。
如果你對實體之間的關系很了解,你也能將實體定義成單向的關系,Core Data 不會有任何警告。但是一旦這么做了,你就必須承擔很多正常情況理應由 Core Data 管理的一些職責,包括確認圖形對象的一致性,變化跟蹤和撤銷管理。舉一個簡單的例子,我們有“書”和“作者”兩個實體,并設置了一個書到作者的單項關系。當我們刪除了“作者”的時候,和這個“作者”有關聯的“書”將無法收到這個“作者”被刪除的消息。此后,我們仍舊可以使用這本書“作者”的關系,只是我們將會得到一個指向空的錯誤。
很明顯單向關系帶來的弊端絕對不會是你想要的。雙向關系化可以讓你擺脫這些不必要的麻煩。
數據類型設計
在為 Core Data 設計數據模型的時候,一定要牢記 Core Data 不是一個關系數據庫。因此,我們在設計數據模型的時候只需要著眼于數據將要如何組織和展示即可,而不是像設計數據庫表一樣來進行設計。
在需要對某一數據進行展示的時候避免大規模的抓取該數據的關系數據,從這一點看通常數據模型的非規范化是有其價值的。再舉個例子,假如現有一個“作者”實體中有一個一對多的關系指向“書”實體的話,如果我們只需要展示作者寫的書的數量的話,再保存一個數字會是一個很好的做法。
這是因為,假設我們需要展示一張作者和其對應作品數量的表。如果取得每個作者名下作品的數量這條數據只能通過統計作者實體關聯的書實體的數量來獲取,則每一個作者單元格中必須進行一次抓取請求操作。這樣做性能不佳。我們可以使用relationshipKeyPathsForPrefetching對書對象進行預抓取,但當保存的書數據量大的時候,這同樣無法達到理想狀態。所以,如果我們為每個作者添加一個屬性來管理書籍數量,那么,一切所需信息都將在請求抓取作者信息的時候一并獲得。
當然,為保持冗余數據的同步,非規范化也會帶來額外的性能開銷。我們需要根據實際情況來權衡是否需要這么做。有時這么做不會有什么感覺,但有時其帶來的麻煩會讓你頭疼不已。這樣做非常依賴于特定的數據模型,比如應用有沒有需要去與后臺交互,或者是否要在多個客戶端之間使用點對點的形式同步數據。
通常情況下,這個數據模型已經被某個后臺服務定義過了,我們可能只需要將數據模型復制到應用程序即可。然而,即使在這種情況下,我們仍有權利在客戶端對數據模型進行一些修改,就比如我們可以為后臺數據模型定義一個清晰的映射。再拿“書”和“作者”舉例,僅在客戶端執行向作者實體添加一個作品數量屬性的小操作以實現檢索性能的優化而無需通知服務器。如果我們做了一些本地修改或從服務器接收到了新的數據,我們需要更新這些屬性并且保持其余的數據同步。
實際情況往往復雜的多,但就像上面的簡單優化,卻能緩解在處理標準關系數據庫數據模型的性能時的瓶頸問題。
實體層級 vs 類層級
Managed object models 可以允許創建實體層級,即我們可以指定一個實體繼承另外一個實體。雖然,實體間可以通用一些都有的屬性聽起來不錯,不過在實踐中我們幾乎不會這么去做。
這一切背后發生的事情其實是,Core Data 將所有帶有相同父實體的實體存儲在同一張表中。這樣做會迅速的建成一個含有大量屬性的數據表,并使性能降低。通常情況下,我們創建實體層級的目的僅僅是為了創建一個類層級,從而可以在實體基類中實現代碼并分享到多個子類實體中。當然,我們還有更好的方法來實現這個需求。
實體層級與NSManagedObject父類層級是相互獨立的。換言之,我們不需要去為了已有的實體層級而去創建一個類層級。
讓我們繼續用“作者”和“書”舉例。他們兩者間會有一些共有的字段,比如ID(identifier),創建時間(createdAt),修改時間(changedAt)。我們可以為這個例子構建如下的結構:
然而,我們可以壓縮實體的層級關系而保持類的層級關系不變。
這個類可能會被這樣聲明:
這樣做的好處是我們能夠將共同的代碼移動到父類中,同時避免了將所有實體放到放到一個表中引起的性能消耗。雖然我們在 Xcode 的管理對象生成器中無法根據實體層級來創建類層級,但是花費極少的代價去手動的去創建管理對象類將會給我們巨大好處,具體我們會在下面介紹。
配置與抓取請求模板
所有使用過 Core Data 的人肯定都與數據模型的“實體-模型”方面的功能打過交道。但是數據模型還有兩個相對少見少用的領域:配置 (configurations) 和 抓取請求模板 (fetch request templates)。
配置用來定義是哪個實體需要保存在哪個持久化存儲。持久化存儲協調器使用addPersistentStoreWithType:configuration:URL:options:error:來添加持久化存儲,其中配置參數定義了需要映射的持久化存儲。在目前所有的應用場景中,我們只會使用一個持久化儲存,因此不用考慮處理多個配置的情況。當創建好一個持久化存儲的時候默認的配置就已經為我們配置好了。其實還是有多存儲的極為少見的實例的,本話題的導入大數據集一文中對其進行了概述。
正如抓取請求模板的名字所暗示的那樣:預定義的抓取請求以managed object model的形式存儲,需要時可以執行fetchRequestFormTemplateWithName:substitutionVariables操作從而方便地使用。我們可以使用 Xcode 中數據模型編輯器或者代碼來定義這些模板。雖然 Xcode 的編輯器還不能夠支持NSFetchRequest的所有功能。
老實說我曾經有一段痛苦的經歷去說服別人使用抓取請求模板。其實一個好處是抓取請求的斷言將會被預先解析好,從而當你執行一條新的抓取請求的時候該步驟不用每次執行。雖然幾乎沒有什么聯系,任何頻繁的抓取都會使我們陷入麻煩之中。假如你在找一個定義你抓取請求的地方(你不應該將它們定義在視圖控制器中),也許考慮將它們儲存在于managed object model中將會是個不錯的選擇。
Managed Objects
任一使用 Core Data 的應用其核心就是managed objects。managed objects依賴managed object context而存在并反映我們的數據。managed objects理應在程序中至少穿透 model-controller 的壁壘,甚至會穿透 controller-view 的壁壘,而被分發。盡管后者頗具爭議但是我們可以更好的通過一個例子來進行抽象理解:定義一個協議,遵守該協議的對象可以被某個視圖使用,或者是通過在視圖的類別中實現配置方法來橋接數據對象與特定的視圖之間的間隙。
不管怎么說我都不能將managed objects限定于數據層,當我們想分發數據的時候,應該將它們及時抓取出來并放入不同結構中去。managed objects是 Core Data 應用中的一等公民,所以我們也要將它們用得適得其所。舉個例子,managed objects應該在兩個視圖控制器間進行傳遞,并為它們提供所需要的數據。
為了獲取managed objects context我們經常在代碼中看到如下代碼:
如果你已經給視圖控制器傳遞了一個模型對象,可以直接通過對象來獲取上下文:
這么做移除了application delegate的隱性依賴并且增強了代碼可讀性以及更便于測試。
使用 Managed object 子類
類似的,managed object的子類也應當這樣被使用。我們可以在這些類中實現自定義業務邏輯,驗證邏輯和輔助方法,同時創建層級以便于剝離出共同的代碼放進父類中。后者的實現非常簡單,因為類的層級和實體層級的解耦合在上面已經提過了。
你可能想知道,當 Xcode 在重新生成文件的時候總是覆蓋它們,那么如何在managed object子類中實現自定義代碼。其實,這個答案十分簡單,不要使用 Xcode 生成它們即可。如果你仔細想想,在這些類中被生成的代碼很瑣碎并且你自己也很容易能夠實現,當然你也可以只生成一次然后保證手動更新就好。因為生成的只是一堆屬性的聲明。
當然還有一些其他的解決方案,如將自定義代碼放到類別中,或者使用類似mogenerator這樣的工具。mogenerator 可以為每個實體和子類創建一個可以支持用戶代碼的基礎類。但是,上面的所有解決方案都不能根據實體層級靈活的創建類層級。所以我們還是建議你手動創建這些類,即使你需要自己去書寫幾行繁瑣的代碼。
Managed Object 子類中的實例變量
當我們開始使用managed object子類來實現業務邏輯的時候,我們可能會需要創建一些實例變量來緩存計算結果之類的東西。為了方便的達到這個目的,我們可以使用transient。因為managed object的生命周期與一般的對象有一點不同。Core Data 經常會對那些不再需要的對象執行faults操作。如果我們要使用實例變量,就必須將其手動加入進程并且釋放這些實例變量。然而,當我們換成transient屬性,這一切都不再需要我們去做了。
創建新對象
在模型類中可以加入一個類方法來將新的對象插入到 managed object 上下文中,這是在模型類中添加有用輔助方法的一個好例子。Core Data 創建新對象的 API 并不是非常的直觀:
萬幸的是,我們能夠輕易的在我們的子類中以一個優雅的方式解決這個問題:
在,創建一個“書”對象就簡單得多。
當然,如果我們將實際模型類從共同的父類中繼承下來,我們就應當將insertNewObjectIntoContext:和entityName這兩個方法移動到父類中。然后每個子類里面就只需要去重寫entityName就可以了。
一對多關系賦值
如果你用 Xcode 生成了一個含有一對多關系的managed object子類的話,系統將會為我們創建在這個關系中增刪對象的方法。
我們有一個更加優雅的方法來替代這些賦值方法,尤其是在我們沒有生成managed object子類的情況下。我們可以簡單的使用mutableSetValueForKey:方法獲取相關的可變對象的集合(或者對于有序關系的話,使用mutableOrderedSetValueForKey:)。這樣可以封裝成為一個更簡單的存取方法:
然后我們可以如同使用一般的集合那樣使用這個可變集合。Core Data 將會捕捉到這些變換,并且幫我們處理剩下的事情。
驗證
Core Data 支持多種數據驗證的方式。Xcode 的數據模型編輯器讓我們為屬性制定一些基本的需求,就像一個字符串的最大和最小長度,或者是一對多關系中最多和最少的對象個數。除此之外,使用代碼我們可以做更多的事情。
文檔中“Managed Object Validation”一節對本主題有更加深入的介紹。Core Data 通過實現validate:error:方法支持屬性層級驗證,以及通過validateForInsert:,validateForUpdate:,和validateForDelete:方法進行屬性內驗證。驗證將會在保存前自動進行,當然我們也可以在屬性層使用validateValue:forKey:error:方法手動觸發。
總結
Core Data 應用依賴于數據模型與模型對象。我們鼓勵大家不要去直接使用便利的封裝,而是去擁抱managed object子類和對象。當使用 Core Data 的時候,值得非常注意的是要十分清楚發生了什么,否則的話一旦你的應用變得復雜的時候,事情就會很糟糕了。
我們希望已闡述的幾個簡單的技術可以使你更容易的使用managed objects。另外我們還初步了解了幾個非常高級的特性,以便大家在使用數據對象的時候對我們能做到些什么有個大致的概念。請謹慎地運用這些技術,因為到頭來其實往往還是簡單才是王道。