轉自:http://www.cocoachina.com/ios/20160802/17260.html
之前兩篇文章都比較偏理論,文字表達比較多一些,但都是幹貨!學習時先了解理論知識,才能更好的幫助後面的了解。在這篇文章中,将會涉及關于CoreData的一些複雜操作,這些操作會涉及分頁查詢、模糊查詢、批處理等進階操作。通過這些操作可以更好的使用CoreData,提升CoreData性能。文章中将會出現大量示例代碼,通過代碼的方式更有助于了解。
文章内容還會比較多,希望各位耐心看完。文章中如有疏漏或錯誤,還請各位及時提出,謝謝!
NSPredicate
概述
在iOS開發過程中,很多需求都需要用到過濾條件。例如過濾一個集合對象中存儲的對象,可以通過Foundation架構下的NSPredicate類來執行這個操作。
CoreData中可以通過設定NSFetchRequest類的predicate屬性,來設定一個NSPredicate類型的謂詞對象當做過濾條件。通過設定這個過濾條件,可以隻擷取符合過濾條件的托管對象,不會将所有托管對象都加載到記憶體中。這樣是非常節省記憶體和加快查找速度的,設計一個好的NSPredicate可以優化CoreData搜尋性能。
文法
NSPredicate更加偏向于自然語言,不像SQLite一樣有很多固定的文法,看起來也更加清晰易懂。例如下面需要查找條件為年齡30歲以上,并且包括30歲的條件。
1 | |
過濾集合對象
可以通過NSPredicate對iOS中的集合對象執行過濾操作,可以是NSArray、NSSet及其子類。
對不可變數組NSArray執行的過濾,過濾後會傳回一個NSArray類型的結果數組,其中存儲着符合過濾條件的對象。
1 | |
對可變數組NSMutableArray執行的過濾條件,過濾後會直接改變原集合對象内部存儲的對象,删除不符合條件的對象。
1 | |
複合過濾條件
謂詞不隻可以過濾簡單條件,還可以過濾複雜條件,設定複合過濾條件。
1 | |
當然也可以通過NSCompoundPredicate對象來設定複合過濾條件,傳回結果是一個NSPredicate的子類NSCompoundPredicate對象。
1 | |
枚舉值NSCompoundPredicateType參數,可以設定三種複合條件,枚舉值非常直覺很容易看懂。
- NSNotPredicateType
- NSAndPredicateType
- NSOrPredicateType
基礎文法
下面是列舉的一些NSPredicate的基礎文法,這些文法看起來非常容易了解,更複雜的用法可以去看蘋果的官方API。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yMyMTM2UTM2IDOzADM3QTMvwVMwgDM2EDMy8CXzRWYvxGc19CXpBXYvwVbvNmLn1Waj92YuM2Yvw1LcpDc0RHaiojIsJye.png)
正規表達式
NSPredicate中還可以使用正規表達式,可以通過正規表達式完成一些複雜需求,這使得謂詞的功能更加強大,例如下面是一個手機号驗證的正規表達式。
1 2 | |
模糊查詢
NSPredicate支援對資料的模糊查詢,例如下面使用通配符來比對包含lxz的結果,具體CoreData中的使用在下面會講到。
1 | |
keyPath
NSPredicate在建立查詢條件時,還支援設定被比對目标的keyPath,也就是設定更深層被比對的目标。例如下面設定employee的name屬性為查找條件,就是用點文法設定的keyPath。
1 | |
設定查詢條件
在之前的文章中,執行下面MOC的fetchRequest方法,一般都需要傳入一個NSFetchRequest類型的參數。這個request參數可以做一些設定操作,這樣就可以以較優的性能擷取指定的資料。
1 | |
NSFetchRequest
在執行fetch操作前,可以給NSFetchRequest設定一些參數,這些參數包括謂詞、排序等條件,下面是一些基礎的設定。
- 設定查找哪個實體,從資料庫的角度來看就是查找哪張表,通過fetchRequestWithEntityName:或初始化方法來指定表名。
- 通過NSPredicate類型的屬性,可以設定查找條件,這個屬性在開發中用得最多。NSPredicate可以包括固定格式的條件以及正規表達式。
- 通過sortDescriptors屬性,可以設定擷取結果數組的排序方式,這個屬性是一個數組類型,也就是可以設定多種排序條件。(但是注意條件不要沖突)
- 通過fetchOffset屬性設定從查詢結果的第幾個開始擷取,通過fetchLimit屬性設定每次擷取多少個。主要用于分頁查詢,後面會講。
MOC執行fetch操作後,擷取的結果是以數組的形式存儲的,數組中存儲的就是托管對象。NSFetchRequest提供了參數resultType,參數類型是一個枚舉類型。通過這個參數,可以設定執行fetch操作後傳回的資料類型。
- NSManagedObjectResultType: 傳回值是NSManagedObject的子類,也就是托管對象,這是預設選項
- NSManagedObjectIDResultType: 傳回NSManagedObjectID類型的對象,也就是NSManagedObject的ID,對記憶體占用比較小。MOC可以通過NSManagedObjectID對象擷取對應的托管對象,并且可以通過緩存NSManagedObjectID參數來節省記憶體消耗
- NSDictionaryResultType: 傳回字典類型對象
- NSCountResultType: 傳回請求結果的count值,這個操作是發生在資料庫層級的,并不需要将資料加載到記憶體中
設定擷取條件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
這裡設定NSFetchRequest對象的一些請求條件,設定查找Employee表中name為lxz的資料,并且将所有符合的資料用height值升序的方式排列。
有實體關聯關系
一個模型檔案中的不同實體間,可以設定實體間的關聯關系,這個在之前的文章中講過。實體關聯關系分為對一或對多,也可以設定是否雙向關聯。
這裡示範的實體隻是簡單的To One的關系,并且下面會給出設定是否雙向關聯的差別對比。
插入實體
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
上面建立了四個實體,并且将Employee都關聯到Department上,完成關聯操作後通過MOC存儲到本地。
可以看到上面所有的托管對象建立時,都使用NSEntityDescription的insert方法建立,并和上下文建立關系。這時就想問了,我能直接采用傳統的init方法建立嗎?
會崩的!建立托管對象時需要指定MOC,在運作時動态的生成set、get方法。但是直接通過init方法初始化的對象,系統是不知道這裡是需要系統自身生成set、get方法的,而且系統也不知道應該對應哪個MOC,會導緻方法未實作的崩潰。是以就出現了開發中經常出現的錯誤,如下面崩潰資訊:
1 | |
雙向關聯
在上一篇文章中提到過雙向關聯的概念,也就是設定Relationship時Inverse是否為空。下面是Employee和Department在資料庫中,設定inverse和沒有設定inverse的兩種資料存儲,可以很清晰的對比出設定雙向關聯的差別。
測試代碼還是用上面插入實體的代碼,隻是更改inverse選項。
設定雙向關聯
Employee
Department
未設定雙向關聯
Employee
Department
從圖中可以看出,未設定雙向關聯的實體,Department關聯Employee為屬性并存儲後,Department表中的關系是存在的,但Employee表中的關系依然是空的。而設定雙向關聯後的實體,在Department關聯Employee為屬性并存儲後,Employee在表中自動設定了和Department的關系。
雙向關聯的關系不隻展現在資料庫中,在程式運作過程中托管對象的關聯屬性,也是随着發生變化的。雙向關聯的雙方,一方的關聯屬性設定關系後,另一方關聯屬性的關系也會發生變化。用下面的代碼列印一下各自的關聯屬性,結果和上面資料庫的變化是一樣的。
1 | |
查詢操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
查找Department實體,并列印實體内容。就像上面講的雙向關系一樣,有關聯關系的實體,自己被查找出來後,也會将與之關聯的其他實體也查找出來,并且查找出來的實體都是關聯着MOC的。
分頁查詢
在從本地存儲區擷取資料時,可以指定從第幾個擷取,以及本次查詢擷取多少個資料,聯合起來使用就是分頁查詢。當然也可以根據需求,單獨使用這兩個API。
這種需求在實際開發中非常常見,例如TableView中,上拉加載資料,每次加載20條資料,就可以利用分頁查詢輕松實作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
上面是一個按照身高升序排序,分頁擷取搜尋結果的例子。查找Employee表中的實體,将結果按照height字段升序排序,并從結果的第六個開始查找,并且設定擷取的數量也是六個。
模糊查詢
有時需要擷取具有某些相同特征的資料,這樣就需要對查詢的結果做模糊比對。在CoreData執行模糊比對時,可以通過NSPredicate執行這個操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
上面是使用通配符的方式進行模糊查詢,NSPredicate支援多種形式的模糊查詢,下面列舉一些簡單的比對方式。模糊查詢條件對大小寫不敏感,是以查詢條件大小寫均可。
- 以lxz開頭
1 | |
- 以lxz結尾
1 | |
- 其中包含lxz
1 | |
- 查詢條件結果包含lxz
1 | |
加載請求模闆
在之前的文章中談到在模型檔案中設定請求模闆,也就是在.xcdatamodeld檔案中,設定Fetch Requests,使用時可以通過對應的NSManagedObjectModel擷取設定好的模闆。
.... 省略上下文建立步驟 ....
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
擷取結果Count值
開發過程中有時需要隻擷取所需資料的Count值,也就是執行擷取操作後數組中所存儲的對象數量。遇到這個需求,如果像之前一樣MOC執行擷取操作,擷取到數組然後取Count,這樣對記憶體消耗是很大的。
對于這個需求,蘋果提供了兩種常用的方式擷取這個Count值。這兩種擷取操作,都是在資料庫中完成的,并不需要将托管對象加載到記憶體中,對記憶體的開銷也是很小的。
- 方法1,設定resultType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
方法1中設定NSFetchRequest對象的resultType為NSCountResultType,擷取到結果的Count值。這個枚舉值在之前的文章中提到過,除了Count參數,還可以設定其他三種參數。
- 方法2,使用MOC提供的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
MOC提供了專門擷取請求結果Count值的方法,通過這個方法可以直接傳回一個NSUInteger類型的Count值,使用起來比上面的方法更友善點,其他都是一樣的。
位運算
假設有需求是對Employee表中,所有托管對象的height屬性計算總和。這個需求在資料量比較大的情況下,将所有托管對象加載到記憶體中是非常消耗記憶體的,就算批量加載也比較耗時耗記憶體。
CoreData對于這樣的需求,提供了位運算的功能。MOC在執行請求時,是支援對資料進行位運算的。這個操作依然是在資料庫層完成的,對記憶體的占用非常小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
執行結果:
從執行結果可以看到,MOC對所有查找到的托管對象height屬性執行了求和操作,并将結果放在字典中傳回。位運算主要是通過NSFetchRequest對象的propertiesToFetch屬性設定,這個屬性可以設定多個描述對象,最後通過不同的name當做key來取出結果即可。
NSExpression類可以描述多種運算,可以在NSExpression.h檔案中的注釋部分,看到所有支援的運算類型,大概看了一下有二十多種運算。而且除了上面NSExpression調用的方法,此類還支援點文法的位運算,例如下面的例子。
1 | |
批處理
在使用CoreData之前,我和公司同僚也讨論過,假設遇到需要大量資料處理的時候怎麼辦。CoreData對于大量資料處理的靈活性肯定不如SQLite,這時候還需要自己使用其他方式優化資料處理。雖然在移動端這種情況很少出現,但是在持久層設計時還是要考慮這方面。
當需要進行資料的處理時,CoreData需要先将資料加載到記憶體中,然後才能對資料進行處理。這樣對于大量資料來說,都加載到記憶體中是非常消耗記憶體的,而且容易導緻崩潰的發生。如果遇到更改所有資料的某個字段這樣的簡單需求,需要将相關的托管對象都加載到記憶體中,然後進行更改、儲存。
對于上面這樣的問題,CoreData在iOS8推出了批量更新API,通過這個API可以直接在資料庫一層就完成更新操作,而不需要将資料加載到記憶體。除了批量更新操作,在iOS9中還推出了批量删除API,也是在資料庫一層完成的操作。關于批處理的API很多都是iOS8、iOS9出來的,使用時需要注意版本相容。
但是有個問題,批量更新和批量删除的兩個API,都是直接對資料庫進行操作,更新完之後會導緻MOC緩存和本地持久化資料不同步的問題。是以需要手動重新整理受影響的MOC中存儲的托管對象,使MOC和本地統一。假設你使用了NSFetchedResultsController,為了保證界面和資料的統一,這一步更新操作更需要做。
批量更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
上面對Employee表中所有的托管對象height值做了批量更新,在更新時通過設定propertiesToUpdate字典來控制更新字段和更新的值,設定格式是字段名 : 新值。通過設定批處理對象的predicate屬性,設定一個謂詞對象來控制受影響的對象。
還可以對多個存儲區(資料庫)做同樣批處理操作,通過設定其父類的affectedStores屬性,類型是一個數組,可以包含受影響的存儲區,多個存儲區的操作對批量删除同樣适用。
MOC在執行請求方法時,發現方法名也不一樣了,執行的是executeRequest: error:方法,這個方法是從iOS8之後出來的。方法傳入的參數是NSBatchUpdateRequest類,此類并不是繼承自NSFetchRequest類,而是直接繼承自NSPersistentStoreRequest,和NSFetchRequest是平級關系。
批量删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
大多數情況下,涉及到托管對象的操作,都需要将其加載到記憶體中完成。是以使用CoreData時,需要注意記憶體的使用,不要在記憶體中存在過多的托管對象。在已經做系統相容的情況下,進行大量資料的操作時,應該盡量使用批處理來完成操作。
需要注意的是,refreshAllObjects是從iOS9出來的,在iOS9之前因為要做版本相容,是以需要使用refreshObject: mergeChanges:方法更新托管對象。
異步請求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
上面通過NSAsynchronousFetchRequest對象建立了一個異步請求,并通過block進行回調。如果有多個請求同時發起,不需要擔心線程安全的問題,系統會将所有的異步請求添加到一個操作隊列中,在前一個任務通路資料庫時,CoreData會将資料庫加鎖,等前面的執行完成才會繼續執行後面的操作。
NSAsynchronousFetchRequest提供了cancel方法,也就是可以在請求過程中,将這個請求取消。還可以通過一個NSProgress類型的屬性,擷取請求完成進度。NSAsynchronousFetchRequest類從iOS8開始可以使用,是以低版本需要做版本相容。
需要注意的是,執行請求時MOC并發類型不能是NSConfinementConcurrencyType,這個并發類型已經被抛棄,會導緻崩潰。