初識CoreData
這其實是一篇 WWDC 2015 Session 220 的學習筆記,順便整理了下 Core Data 批量操作和聚合操作的小技巧.
批量操作
Core Data 把數(shù)據(jù)庫封裝成了”object graph(對象圖)”,雖然對于面向?qū)ο缶幊虂碚f有了管理 Model 間繼承與關(guān)系的便利性,但同樣也犧牲了性能.比如批量操作時就需要將每條記錄作為NSManagedObject 對象讀取到內(nèi)存中,修改之后再存入數(shù)據(jù)庫.然而用 SQL 語句執(zhí)行既方便又高效.
于是蘋果在 iOS8 發(fā)布時順便弄了個”Batch Updates”,在 iOS9 發(fā)布時又弄了個”Batch Deletions”.這兩個”新技術(shù)”說白了就是直接操作持久層數(shù)據(jù)庫,然后還需要手動更新/刪除內(nèi)存中的 context 好使得我們的 UI 從 context 讀取的內(nèi)容不會出錯.這樣做的好處就是省去了向內(nèi)存的一次寫操作和查找操作,而越過 context 直接操作持久層,最后我們需要自己手動將持久層的變更結(jié)果(BatchResult)重新寫入 context.只有當需要更新/刪除大批量數(shù)據(jù)的時候才需要用到這兩個技術(shù).
然而蘋果至今未提供二者的文檔,關(guān)于”Batch Updates”我在CoreData處理海量數(shù)據(jù)中給出了用法和例子.看了 WWDC2015 Session 220 后覺得 “Batch Deletions” 應該與 “Batch Updates” 用法類似,并且坑爹. PS: 我在 iOS9 上測試 “Batch Updates” 發(fā)現(xiàn)了一個 bug, 每次更新 context 都會漏掉一條記錄,這讓我十分郁悶.
聚合操作
說完了批量操作,再談談聚合操作.在 SQL 語法中有一類聚合函數(shù),比如count(),sum(),max(),min(),avg() 等,它們一般搭配著 group by 甚至 having來使用.然而在號稱”object graph”的 Core Data 中,這種聚合操作在 NSFetchRequest 中也是有替代品的.下面的例子取自CORE DATA AND AGGREGATE FETCHES IN SWIFT:
我們想計算出每條產(chǎn)品線的銷售量和退貨量,可以用下面的 SQL 語句搞定:
- SELECT ProductLine, SUM(Sold) as SoldCount, SUM(Returned) as ReturnedCount FROM Products GROUP BY ProductLine
NSFetchRequest 有個 propertiesToGroupBy 屬性,正好對應著 group by 語句:
- // Build out our fetch request the usual way
- let request = NSFetchRequest(entityName: self.entityName)
- // This is the column we are grouping by. Notice this is the only non aggregate column.
- request.propertiesToGroupBy = ["productLine"]
下面還需要映射 SQL 語句中聚合函數(shù)及其計算后的結(jié)果,此時我們需要用到NSExpressionDescription 和 NSExpression 來替換 SQL 中的ProductLine, SUM(Sold) as SoldCount, SUM(Returned) as ReturnedCount:
- // Create an array of AnyObject since it needs to contain multiple types--strings and
- // NSExpressionDescriptions
- var expressionDescriptions = [AnyObject]()
- // We want productLine to be one of the columns returned, so just add it as a string
- expressionDescriptions.append("productLine")
- // Create an expression description for our SoldCount column
- var expressionDescription = NSExpressionDescription()
- // Name the column
- expressionDescription.name = "SoldCount"
- // Use an expression to specify what aggregate action we want to take and
- // on which column. In this case sum on the sold column
- expressionDescription.expression = NSExpression(format: "@sum.sold")
- // Specify the return type we expect
- expressionDescription.expressionResultType = .Integer32AttributeType
- // Append the description to our array
- expressionDescriptions.append(expressionDescription)
- // Create an expression description for our ReturnedCount column
- expressionDescription = NSExpressionDescription()
- // Name the column
- expressionDescription.name = "ReturnedCount"
- // Use an expression to specify what aggregate action we want to take and
- // on which column. In this case sum on the returned column
- expressionDescription.expression = NSExpression(format: "@sum.returned")
- // Specify the return type we expect
- expressionDescription.expressionResultType = .Integer32AttributeType
- // Append the description to our array
- expressionDescriptions.append(expressionDescription)
NSExpressionDescription 是用于表示那些抓取結(jié)果中實體中不存在的列名,比如我們這次用的聚合函數(shù)所計算的結(jié)果并不能在實體中找到對應的列,于是我們就得給它起個新名字,這就相當于 SQL 中的 as,這里對應著 NSExpressionDescription 的 name 屬性.而聚合函數(shù)表達式就需要用 NSExpression 對象來表示,比如 NSExpression(format: "@sum.returned")就是對”returned”這列求和.
像本例中這樣初始化 NSExpression 需要對格式化語法較為熟悉(比如"@sum.returned"),初學者建議看看官方的例子,使用容易理解的構(gòu)造方法一步步拼湊成想要的結(jié)果:Core Data Programming Guide
將以上這三個”列描述”依次添加到 expressionDescriptions 數(shù)組中,最后要賦值給NSFetchRequest 的 propertiesToFetch 屬性:
- // Hand off our expression descriptions to the propertiesToFetch field. Expressed as strings
- // these are ["productLine", "SoldCount", "ReturnedCount"] where productLine is the value
- // we are grouping by.
- request.propertiesToFetch = expressionDescriptions
propertiesToFetch 屬性其實是個 NSPropertyDescription 類型數(shù)組,能表示屬性,一對一關(guān)系和表達式.既然是個大雜燴,NSPropertyDescription 也就有一些子類:NSAttributeDescription,NSExpressionDescription,NSFetchedPropertyDescription,NSRelationshipDescription.我們這里用到的便是 NSExpressionDescription.
在設(shè)定 propertiesToFetch 屬性之前必需要設(shè)定好 NSFetchRequest 的 entity 屬性,否則會拋出 NSInvalidArgumentException 類型的異常.并且只有當 resultType 類型設(shè)為NSDictionaryResultType 時才生效:
- // Specify we want dictionaries to be returned
- request.resultType = .DictionaryResultType
最終結(jié)果:
- [
- ["SoldCount": 48, "productLine": Bowler, "ReturnedCount": 4],
- ["SoldCount": 142, "productLine": Stetson, "ReturnedCount": 27],
- ["SoldCount": 50, "productLine": Top Hat, "ReturnedCount": 6]
- ]
WWDC2015 Core Data 的一些新特性
蘋果號稱有超過40萬個 APP 使用 Core Data,并能讓開發(fā)者少寫50%~70%的代碼.并在內(nèi)存性能上強調(diào)卓越的內(nèi)存拓展和主動式惰性加載,炫耀了它跟 UI 良好的綁定機制,還提供了幾種多重寫入的合并策略.然而這不能阻止開發(fā)者對 Core Data 的吐槽,畢竟建立于持久層之上的”object graph”還做不到像 SQL 那樣面面俱到,于是今年針對 Core Data 新增的 API 更像是查缺補漏,并沒有帶來重大功能更新.
NSManagedObject 新增 API
hasPersistentChangedValues
- var hasPersistentChangedValues: Bool { get }
用此屬性可確定 NSManagedObject 的值與 “persistent store” 是否相同.
objectIDsForRelationshipNamed
- func objectIDsForRelationshipNamed(_ key: String) -> [NSManagedObjectID]
適用于大量的多對多關(guān)系.由于我們不想將整個關(guān)系網(wǎng)絡加載到內(nèi)存中,所以這個方法僅返回相關(guān)聯(lián)的 ID.下面是一個例子:
- let relations = person.objectIDsForRelationshipNamed("family")
- let fetchFamily = NSFetchRequest(entityName:"Person")
- fetchFamily.fetchBatchSize = 100
- fetchFamily.predicate = NSPredicate(format: "self IN %@", relations)
- let batchedRelations = managedObjectContext.executeFetchRequest(fetchFamily)
- for relative in batchedRelations {
- //work with relations 100 rows at a time
- }
通過給出的關(guān)系名稱 “family” 來獲取對應的 ID, 并每次遍歷100行記錄,實現(xiàn)了內(nèi)存占用的可控性.
#p#
NSManagedObjectContext 新增 API
refreshAllObjects
- func refreshAllObjects()
正如其名字所描述的那樣,它的功能就是刷新 context 中所有對象,但會保留未保存的變更.相比reset 方法不同的是它會依然保留 NSManagedObject 對象的有效性,我們無需重新抓取任何對象.正因如此,它很適用于打破一些因遍歷雙向關(guān)系循環(huán)而產(chǎn)生的保留環(huán).
mergeChangesFromRemoteContextSave
- class func mergeChangesFromRemoteContextSave(_ changeNotificationData: [NSObject : AnyObject], intoContexts contexts: [NSManagedObjectContext])
在 store 中使用多個 coordinator 時,這個方法將會從一個 coordinator 接受一個通知,并將其應用到另一個 coordinator 中的 context 上.這使得我們可以在所有 context 中存有最新的數(shù)據(jù),Core Data 會維護好所有的 context.
shouldDeleteInaccessibleFaults
- var shouldDeleteInaccessibleFaults: Bool
Core Data 偶爾會拋異常,但Core Data 不能加載故障, 因為它的主動式惰性加載對象使得內(nèi)存中只保留對象圖中的一部分.所以很有可能當我遍歷關(guān)系時要試圖回到磁盤上查找,但此時對象早已被刪除了.于是 shouldDeleteInaccessibleFaults 屬性應運而生,默認值為 YES.
如果我們在某處遇到了故障,我們會將其標記為已刪除.任何丟失的屬性將會被設(shè)為null,nil或0.這就使得我們的 app 繼續(xù)運行,并認為發(fā)生故障的對象已被刪除.這樣程序就不會再崩潰.
NSPersistentStoreCoordinator 新增 API
增加這兩個新的 API 的原因是很多開發(fā)者繞過 Core Data 的 API 來直接操作底層數(shù)據(jù)庫文件.因為NSFileManager 和 POSIX 對數(shù)據(jù)庫都不友好,并且如果此時文件的 open 連接沒關(guān)閉的話會損壞文件.
destroyPersistentStoreAtURL
- func destroyPersistentStoreAtURL(_ url: NSURL, withType storeType: String, options options: [NSObject : AnyObject]?) throws
傳入的選項與 addPersistentStoreWithType 方法要一樣,刪除對應類型的 persistent store.
replacePersistentStoreAtURL
- func replacePersistentStoreAtURL(_ destinationURL: NSURL, destinationOptions destinationOptions: [NSObject : AnyObject]?, withPersistentStoreFromURL sourceURL: NSURL, sourceOptions sourceOptions: [NSObject : AnyObject]?, storeType storeType: String) throws
與上面的 destroy 一個套路,就是 replace 而已.如果目標位置不存在數(shù)據(jù)庫,那么這個 replace 就相當于拷貝操作了.
Unique Constraints
很多時候我們在創(chuàng)建一個對象之前會查看它是否已經(jīng)存在,如果存在的話就會更新它,否則就創(chuàng)建對象.這很可能產(chǎn)生一個競態(tài)條件,如果多線程同時執(zhí)行下面這段代碼, 很可能就創(chuàng)建了多個重復的對象:
- managedObjectContext.performBlock {
- let createRequest = NSFetchRequest(entityName: "Recipe")
- createRequest.resultType = ManagedObjectIDResultType
- let predicate = NSPredicate(format: "source = %@", source)
- let results = managedObjectContext.executeFetchRequest(createRequest)
- if (results.count) {
- //update it!
- } else {
- //create it!
- }
- }
現(xiàn)在 Core Data 可以搞定這個事情了.我們設(shè)定屬性的值唯一,類似于 SQL 中的 unique 約束.諸如電子郵件,電話號, ISBN 等場景都適用此.同時別忘了 Core Data 的對象圖中實體的繼承關(guān)系,這里規(guī)定子類會從父類繼承到具有 Unique 約束的屬性,并可以將更多的屬性設(shè)為 Unique.
為實體設(shè)置 Unique 屬性十分簡單,只需要在 Xcode 中選中對應的實體,打開 “Data Model inspector” 就可以看到 “Constraints”, 點擊加號添加就好:
Model Caching
這是個輕量級的數(shù)據(jù)版本自動遷移解決方案.它會緩存舊版本數(shù)據(jù)中已創(chuàng)建的NSManagedObject 對象會被緩存到 store 中,并被遷移到合適的 store 中.
Generated Subclasses
在 Xcode7 中,自動創(chuàng)建 NSManagedObject 子類時將不再在對應實體子類文件中自動填充模板代碼,而是同時創(chuàng)建Category(Objective-C文件) 或 extension(Swift文件),并將模板代碼自動填寫進去.這樣帶來的好處是將我們自己寫的代碼跟 Xcode 生成的模板代碼分開,更易于更新維護.