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