Core Data基本操作

1 前言

CoreData不僅僅是數據庫,而是蘋果封裝的一個更高級的數據持久化框架,SQLite只是其提供的一種數據存儲方法。CoreData對數據的查找做了很大的優化,提供了大量的API。CoreData的遺憾之一是不能設置唯一主鍵,但是它可以設置實體的UserInfo(relatedByAttribute)來實現主鍵的功能,另外UserInfo字典也擴展了CoreData的功能,可以通過代碼取得這個字典進行自定義操作。但是通常并不會完全自己建立CoreData,常用的第三方框架是MagicRecord,在其GitHub主要上有設置唯一主鍵及其他詳盡的使用方法。

2 重要的類

2.1 NSManagedObjectModel

NSManagedObjectModel可以認為是對整個數據庫中各個表的各個字段和表之間聯系的描述,它不僅包含每個模型對象的屬性,還包含該模型對象和其他模型對象之間的關系即relationship。

2.2 NSPersistentStore

NSPersistentStore代表真正存儲的數據,CoreData提供了SQLite等四種存儲模式。除SQLite外另外三種模式在對數據庫操作時需要將所有的數據全部讀入。同時SQLite是默認的存儲方式。除了四種默認的存儲模式外,CoreData還允許開發者通過創建NSIncrementalStore來自定義存儲格式。

2.3 NSPersistentStoreCoordinator

NSPersistentStoreCoordinator是模型文件描述NSManagedObjectModel和數據存儲NSPersistentStore之間的橋梁。前者只關心整個數據庫各個表結構及表之間的聯系,是一個抽象的概念。后者只關心數據的實際存儲而并不關心數據對應的對象。Coordinator作為橋梁將數據庫文件轉化為具體的對象。

2.4 NSManagedObjectContext

NSManagedObjectContext是操作上下文,也是我們的工作區,通常APP中會有一個版本的模型文件描述NSManagedObjectModel,一個對應的數據存儲文件NSPersistentStore及一個它們之間的橋梁NSPersistentStoreCoordinator。和多個工作區NSManagedObjectContext。大多數時候只需要維護一個工作區即可,對數據庫的所有編輯操作都將被保存在這個工作區中,只有當其執行完save操作時,數據庫才會被更改。多工作區的情況見CoreData多上下文操作。另外NSManagedObjectContext需要注意以下幾點。

  • 1)NSManagedObjectContext管理它創建或者抓取的對象的生命周期。
  • 2)一個對象必須依靠一個Context存在,每個對象都會引用管理它的Context可以通過object.managedObjectContext獲取,這是一個weak弱引用。
  • 3)一個對象在整個生命周期內都只會對一個Context保持引用。
  • 4)一個應用程序可以擁有多個Context。
  • 5)Context是線程不安全的,對每個的對象,創建、修改和刪除操作必須在同一個線程中完成。

2.5 NSPersistentStoreDescription

NSPersistentStoreDescription是在iOS10后新增的,其主要用于為NSPersistentContainer配置數據遷移和數據存儲的URL等信息。

2.6 NSPersistentContainer

在iOS10后,CoreDataStack概念被引入,NSPersistentContainer也是在iOS10過后新增的,它可以有效的將前四個類的對象結合起來,在程序中只用通過指定name創建一個NSPersistentContainer的實例,然后為它配置NSPersistentStore。CoreData會自動創建其他相關的實例進行數據庫初始化。初始化完成后的數據庫URL和主工作區都可以通過其persistentStoreDescription屬性中的URL方法拿到。另外也可以手動指定其persistentStoreDescription屬性配置數據遷移和數據存儲URL等信息。

3 建立CoreData

3.1 初始化CoreData

CoreData的初始化工作需要在AppDelegate的applicationDidFinishLaunching中以同步方式在主線程中進行,因為如果數據庫無法正確初始化,整個程序的運行都將無意義。

在iOS項目中使用CoreData持久化存儲數據可以在創建項目時勾選CoreData選項,此時系統會在Appdelegate中自動生成NSPersistentContainer及其相關代碼,此外工程中也將多一個以項目名稱命名的.xcdatamodeld文件。但是通常不會使用系統自帶的這個功能而是手動建立CoreData,并且通常使用MagicRecord第三方框架。

iOS10中蘋果引入了CoreDataStack堆棧的概念,因此下面分別介紹iOS10之前、iOS10之后、使用MagicRecord初始化CoreData的方法。

iOS10之前
lazy var managerContext: NSManagedObjectContext = {
  let context = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
  let model = NSManagedObjectModel(contentsOfURL: NSBundle.mainBundle().URLForResource("Person", withExtension: "momd")!)!
  let coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
  let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).last! +  "/person.db"
  let url = NSURL(fileURLWithPath: path)
  try! coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
  context.persistentStoreCoordinator = coordinator
  return context
}()

static let sharedCoreDataManager = HYFCoreDataManager()
iOS10之后
var storeURL : URL {
  let storePaths = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)
  let storePath = storePaths[0] as NSString
  let fileManager = FileManager.default
  
  do {
    try fileManager.createDirectory(
      atPath: storePath as String,
      withIntermediateDirectories: true,
      attributes: nil)
  } catch {
    print("Error creating storePath \(storePath): \(error)")
  }
  
  let sqliteFilePath = storePath
    .appendingPathComponent(storeName + ".sqlite")
  return URL(fileURLWithPath: sqliteFilePath)
}

private lazy var storeContainer: NSPersistentContainer = {
  let container = NSPersistentContainer(name: self.modelName)
  //指定Descriptions可以指定數據庫的存儲位置,但是一般不設置,由CoreData設置默認值,
  //默認設置為支持數據庫遷移,支持自動推斷映射模型,默認SQLite存儲,其默認的URL可以通過
  //Container的persistentStore屬性中的URL方法拿到。
  container.persistentStoreDescriptions = [self.storeDescription]
  container.loadPersistentStores { (storeDescription, error) in
    if let error = error as NSError? {
      print("Unersolved error \(error), \(error.userInfo)")
    }
  }
  return container;
}()

lazy var managedContext: NSManagedObjectContext = {
  return self.storeContainer.viewContext
}()

lazy var storeDescription: NSPersistentStoreDescription = {
  let description = NSPersistentStoreDescription(url: self.storeURL)
  description.shouldInferMappingModelAutomatically = true
  description.shouldMigrateStoreAutomatically = true
  //description.type = NSInMemoryStoreType
  return description
}()
MagicRecord中
//默認為支持數據遷移支持自動推斷映射模型,其數據存儲URL可以通過MagicRecord類方法獲得
[MagicalRecord setupCoreDataStackWithAutoMigratingSqliteStoreNamed:@"Database.sqlite"];

3.2 新建模型文件

在工程中新建一個CoreData分類下的DataModel文件,現在只考慮單個版本的模型文件,多版本管理和數據遷移在后續文章中介紹。首先看到的是三個部分,左側列出了所有的實體,中間列出了某個實體的所有屬性以及和其他實體之間的關系。右側為通過工具面板,當選中左側的某個實體或者中間的某個實體的某個屬性來進行更詳盡的編輯。

3.2.1 新建實體

實體可以理解為對某個對象的描述,它在SQLite數據庫中具體體現為一張表。選中實體時右側通用工具欄中最后一個Data Model Inspector可以進行給更多細節的編輯。

Entity描述中,這里的Abstract Entity表示抽象實體,意味著不會創建具體的實體,通常一個抽象實體是多個具體實體的父實體,如抽象實體Attachment可以對應幾個具體的子實體ImageAttachment和VideoAttachment。

Class描述中,通常Codegen選中Manual/None,表示由開發者手動建立實體類的OC文件,否則由CoreData自動生成。手動建立具體通過選中某個模型文件,在XCode菜單的Editor中選則創建Create NSManagedObject Subclass...。此時工程中會為某個實例生成兩個文件,分別為【實例名+CoreDataProperties.swift】和【實例名+CoreDataClass.swift】,如果需要生成OC文件需在選中模型文件后在右側的通用工具欄的第一項菜單下將Code Generation改為Objective-C。兩個文件作用在后續創建NSManagedObject類中介紹。此時Class區域內Name會被自動填充為實體名,Swift中涉及到命名空間問題,需要將其Module設置為當前Module。

UserInfo描述,它可以自定用用戶信息,擴展CoreData功能,在MagicRecord中通過relatedByAttribute設置唯一主鍵。

Versioning描述,這里應該是關于實體版本控制的,但在數據遷移中主要判斷的是模型的版本而不是實體的版本,暫時未用到,具體用法還行參考官網描述。

3.2.2 新建屬性

Attribute Type
新建屬性時,CoreData支持的類型和在代碼中映射的類型對應關系為,【Integer16 - NSNumber】、【Integer32 - NSNumber】、【Integer64 - NSNumber】、【Decimal - NSDecimalNumber】、【Double - NSNumber】、【Float - NSNumber】、【String - String】、【Boolean - Bool】、【Date - NSDate】、【Binary Data - NSData】、【Transformable - NSObject】。

Interger:在指定整形的Attribute type時需根據樣本的實際情況選擇合適類型。

  • Integer 16 有符號的占2字節整數 -32768~32767
  • Integer 32 有符號的占4字節整數 -2147483648 ~ 2147483647
  • Integer 64 有符號的占8字節整數 ...

Decimal:它表示一種科學計數法,具體為10^exponent,exponent is an integer from –128 through 127。

NSDecimalNumber *number = [NSDecimalNumber decimalNumberWithMantissa:1234 exponent:-2 isNegative:NO];   //12.34
number = [NSDecimalNumber decimalNumberWithMantissa:1234 exponent:2 isNegative:YES];   //-123400

Binary Data:其映射類型為NSData用于存儲類似于圖片、PDF文件或者其他任何能被編碼為二進制的資源。這里不需要擔心從內存中加載大量二進制數據問題,因為Core Data已經對這個問題作出了優化。在一個實體中,該類型的屬性右方的Attribute設置面板中勾選Allows External Storage選項,這時Core Data會自動對每一個實體對象檢查判斷是否將改資源作為二進制數據存儲到數據庫中,或者單獨將其存儲在主存中并為其創建一個指向該資源的通用標識符URI,并在數據庫中存儲URI。至于什么時候會存儲URI,在官方文檔中暫時未找到合理解釋,在論壇中有見提到是根據要存儲的數據大小來決定的,但是在很多案例中,這依然是個可行而高效的方法。在存儲時需手動轉換為NSData進行存儲,讀取時也應手動從NSData轉換為相應的變量類型。

這里需要注意的是,大多數開發者都指出了當使用Allows External Storage這種方式儲存資源在做數據庫遷移時有一個隱含bug,誘因是數據庫遷移后新的統一存儲該資源文件的文件夾會被刪除并重新創建,從而導致數據丟失,以下是一種解決方案。

- (NSPersistentStoreCoordinator*)persistentStoreCoordinator {
  if (_persistentStoreCoordinator !=nil) {
    return _persistentStoreCoordinator;
  }
  NSURL*storeURL =[[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataBinaryBug.sqlite"];
  NSError*error =nil;
  NSDictionary*sourceMetadata =[NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
  URL:storeURL
  error:&error];
  //Check if the new model is compatible with any previously stored model
  BOOL isCompatibile = [self.managedObjectModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata];
  BOOL needsMigration =!isCompatibile;
  NSFileManager*fileManager =[NSFileManager defaultManager];
  //Prepare a temporary path to move CoreData's external data storage folder to if automatic model migration is required
  NSString*documentsPath =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
  NSString*tmpPathToExternalStorage =[documentsPath stringByAppendingPathComponent:@"tmpPathToReplacementData"];
  NSString*pathToExternalStorage =[documentsPath stringByAppendingPathComponent:@".CoreDataBinaryBug_SUPPORT/_EXTERNAL_DATA"];
  if (needsMigration) {
    if ([fileManager fileExistsAtPath:pathToExternalStorage]) {
    //Move Apple's CoreData external storage folder before it's nuked by the migration bug
    [fileManager moveItemAtPath:pathToExternalStorage toPath:tmpPathToExternalStorage error:nil];
  }
  }
  NSDictionary*options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],NSMigratePersistentStoresAutomaticallyOption,[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
  _persistentStoreCoordinator =[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
  if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
     
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  } else {
    if (needsMigration) {
    //Apple's automatic migration is now complete. Replace the default external storage folder with the version pre upgrade
    [[NSFileManager defaultManager] removeItemAtPath:pathToExternalStorage error:nil];
    [[NSFileManager defaultManager] moveItemAtPath:tmpPathToExternalStorage toPath:pathToExternalStorage error:nil];
    }
  }
  return _persistentStoreCoordinator;
}

Transformable:只要遵守NSCoding協議的對象都能被以Transformable類型的方式存儲,默認的映射類型為NSObject,可以直接將從數據庫中獲得的對象進行強制類型轉變為當前類。同時這類型實體還允許在Data Model Inspector中通過Value Transfo...關聯一個類并實現以下操作輔助將該對象轉化為另外一個符合NSCoding協議對象來存儲。當再模型文件中直接引用工程中的文件時,必須指定Module,通常是項目名稱。NSCoding用法

class ImageTransformer: ValueTransformer {
  override class func transformedValueClass() -> AnyClass {
    return NSData.self
  }

  override class func allowsReverseTransformation() -> Bool {
    return true
  }
  
  override func reverseTransformedValue(_ value: Any?) -> Any? {
    guard let data = value as? Data else { return nil }
    return UIImage(data: data)
  }

  override func transformedValue(_ value: Any?) -> Any? {
    guard let image = value as? UIImage else { return nil }
    return UIImagePNGRepresentation(image)
  }
}

小結:在選擇屬性類型時,通常字符串、數字、Bool類型數據都有直接與之對應的類型,但是對于UIImage,UIcolor以及自定義類等數據類型并沒有直接與之匹配的屬性類型,此時可以將某個類型分開存儲,使用時再將其合成,如UIColor可以分解為RGBA四個部分整數分開存儲,但更為有效的是在Binary Data和Transformable類型中選擇合適的類型。

Attribute通用工具欄設置
選中一個屬性時,右側也會出現三個可選界面,分別是File Inspector、Quick Help Inspector和Data Model Inspector,在Data Model Inspector中也可以對屬性進行高級設置,不同類型屬性的高級設置面板有部分變動,以下以Integer 32類型為例。

Attribute描述:其中Properties中Optional表示該屬性是否為必有屬性,對應Swift中的必選屬性,當指定為必選屬性時需為其指定默認值,兩位兩個屬性暫未用過。Validation表示對數據的校驗,這里可以設置數據校驗規則,它負責對數據進行校驗,不在此范圍內的數據不會被存儲到Core Data中,這個錯誤將在調用Context的Save方法時候拋出。如果后期版本迭代時此處發生改變,需要進行輕量級數據遷移。同時這里可以設置最大最小和默認值。Advanced中兩個勾選框暫未用過。

User Info描述:這里添加的字點再代碼中都可以拿到,用于擴展CoreData的功能,在MagicRecord中,可以添加mappedKeyName-Value來將后來返回的Value字段轉換為Attribute本身。

Versioning描述,同樣這里應該是關于屬性版本控制的,但在數據遷移中主要判斷的是模型的版本而不是屬性的版本,暫時未用到,具體用法還行參考官網描述。

3.2.3 新建關系

在CoreData中,表之間的聯系需要設置為關系,比如一個公司實體Company擁有很多雇員實體Employee。這里Company和Employee在數據庫中分別為兩張表,而實現上述需求需要在Company實體中添加一個Destination為Employee的employees關系。

在CoreData中關系分為三類,一對一,一對多和多對多。需要特別注意的是無論哪種類型的關系,關系都是成對出現的,并且必須設置Inverse。并且關系可以指向實體自身。

一對一的關系只需要在實體A中添加指向實體B的to-one類型關系,并且在食堂B中同樣添加指向A的to-one類型關系,同時將兩個關系互相設置為inverse。一對多的關系需要在實體A中添加指向實體B的to-many類型關系,并且在食堂B中同樣添加指向A的to-one類型關系,同時將兩個關系互相設置為inverse。多對多的關系需要在實體A中添加指向實體B的to-many類型關系,并且在食堂B中同樣添加指向A的to-many類型關系,同時將兩個關系互相設置為inverse。

關系高級設置選項中,properties通常保留默認值為optional,type根據需要選擇,Delete Rule通常選擇為Nullify,其余選項下面介紹。Count可以設置最大最小的數量,Advanced暫未用過,保留默認值即可。User InfoVersioning描述同前文類似。Arrangement表示是否排序,當選擇to-Many類型關系時,關系中的元素將以集合Set形式組織數據而非數組。勾選ordered會使用有序集合NSInorderedSet,此時生成的【實例名+CoreDataProperties.swift】文件中CoreData會自動生成集合的操作方法。在MagicRecord中可以向UserInfo中添加relatedByAttribute字段指定排序屬性。

幾種關系的刪除規則Delete Rule:

  • Nullify(作廢):當A對象的關系指向的B對象被刪除后,A對象的關系將被設為nil。對于To Many關系類型,B對象只會從A對象的關系的容器中被移除。
  • Cascade(級聯):當B對象的關系指向的C對象被刪除后,B對象也會被刪除。B對象關聯(以Cascade刪除規則)的二級對象A也會被刪除。以此類推。
  • Deny(拒絕):如果刪除A對象時,A對象的關系指向的B對象仍存在,則刪除操作會被拒絕。
  • NO Action:當A對象的關系指向的B對象被刪除后,A對象保持不變,這意味著A對象的關系會指向一個不存在的對象。如果沒有充分的理由,最好不要使用。

3.3 創建NSManagedObject實體類

選中某個模型文件NSManagedObjectModle后,在XCode的菜單欄中選擇Editor可以為模型創建NSManagedObject實體類,當創建一個實例的類時,系統自行生成兩個不同文件【實例名 +CoreDataProperties.swift 】和【實例名+CoreDataClass.swift】。其中第一個只包含所有屬性,第二個包含所有操作。這樣設計的目的是,當后期為實例增加屬性時再從Editor選項中創建對應類時只會重新生成CoreDataProperties.swift文件,避免對CoreDataClass.swift文件的修改。

4 操作數據庫

4.1 簡單的操作

插入數據

let walk = Walk(context: managedContex)
walk.date = NSDate()
currentDog?.addToWalks(walk)

do {
  try managedContex.save()
} catch let error as NSError {
  print("Save error: \(error), description: \(error.userInfo)")
}

刪除數據,只有執行save后才會被真正刪除,注意刪除數據必須是一件謹慎的事情,在iOS9之前必須將程序中所有對被刪除數據的引用置為nill,否則會引發CoreData異常導致程序崩潰,幸運的是在iOS9過后,NSMnagedObjectContex對象有一個默認為True的屬性shouldDeleteInaccessibleFaults,當其為True時,對被刪除的數據操作將返回nil。

guard let walkToRemove = currentDog?.walks?[indexPath.row] as? Walk, editingStyle == .delete else {
  return
}

managedContex.delete(walkToRemove)

do {
  try managedContex.save()
} catch let error as NSError {
  print("Saving error: \(error), description: \(error.userInfo)")
}

查詢數據

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
        return;
    }
    
    let managedContext = appDelegate.persistentContainer.viewContext
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")
    
    do {
        people = try managedContext.fetch(fetchRequest)
    } catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
    }
}

錯誤處理
對于一個NSError的正確處理應該是檢查錯誤的Domain和Error code,標準的處理流程見官網。

4.2 查詢操作

4.2.1 NSFetchRequest基本使用

創建一個FetchRequest的方式有5種

let fetchRequest1 = NSFetchRequest<Dog>()
let entity = NSEntityDescription.entity(forEntityName: "Dog", in: managedContex)
fetchRequest1.entity = entity

let fetchRequest2 = NSFetchRequest<Dog>(entityName: "Dog")

let fetchRequest3: NSFetchRequest<Dog> = Dog.fetchRequest()

let fetchRequest4 = managedObjectModel.fetchRequestTemplate(forName: "venueFR")

let fetchRequest5 = managedObjectModel.fetchRequestTemplate(forName: "venueFR", substitutionVariables: ["NAME" : "Vivi bubble Tea"])

其中4、5方法都需要在.xcdatamodeld文件中的添加實體按鈕下拉選項中選擇添加可視化的fetchRequest。并通過Coordinator的managedObjectModel生成fetchRequest對象。注意其中的name參數必須嚴格與CoreData Editor中的fetchRequest名字一致,否則程序將會崩潰。
當程序中對于某個對象存在大量復雜查找時,并不注重排序時可以通過以上兩種方法,其優點是少些代碼,缺點是不能排序。

guard let model = coreDataStack.managedContext.persistentStoreCoordinator?.managedObjectModel, 
      let fetchRequest = model.fetchRequestTemplate(forName: "FetchRequest") as? NSFetchRequest<Venue> else {
  return
}

NSFetchRequest的resultType有四個值,其中.managedObjectResultType為默認值,返回的是滿足條件的對象,.countResultType返回的是滿足條件對象的個數,.dictionaryResultType返回了一個字典,其中包含平均值、最大最小值等統計信息,.managedObjectIDResultType返回滿足條件的唯一標識符。仔細選擇返回類型,在某些時候會極大提升程序運行效率。需要注意的是,當設置了某個具體resultType時,NSFetchRequest的范形需要與之對應。.managedObjectIDResultType返回的是一個NSManagedObjectID對象數組,因為這個屬性是線程安全的,在iOS5以前經常使用,但是在之后很少使用,因為CoreData提供了更好的處理方式。

查找符合某個條件的對象數量時,方法1示例如下:

let fetchRequest = NSFetchRequest<NSNumber>(entityName: "Venue")
fetchRequest.resultType = .countResultType

另外也可以不設置請求類型,直接調用context的方法,方法2如下:

let count = try coreDataStack.managedContext.count(for: fetchRequest)

dictionaryResultType:查找一個類所有數據某個屬性的統計結果用法很多,關于統計的可選函數列表見NSExpression文檔,下面只展示兩個實例。

案例一:求和

let fetchRequest = NSFetchRequest<NSDictionary>(entityName: "Venue")
fetchRequest.resultType = .dictionaryResultType

let sumExpressionDesc = NSExpressionDescription()
sumExpressionDesc.name = "sumDeals"

let specialCountExp = NSExpression(forKeyPath: #keyPath(Venue.specialCount))
sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [specialCountExp])
sumExpressionDesc.expressionResultType = .integer32AttributeType

fetchRequest.propertiesToFetch = [sumExpressionDesc]

do {
  let results = try coreDataStack.managedContext.fetch(fetchRequest)
  let resultDict = results.first!
  let numDeals = resultDict["sumDeals"]!
  numDealsLabel.text = "\(numDeals) total deals"
} catch let error as NSError {
  print("Count not fetch \(error), \(error.userInfo)")
}

案例二:計數

func totalEmployeesPerDepartmentFast() -> [[String: String]] {
  //1 創建NSExpressionDescription命名為“headCount”
  let expressionDescreption = NSExpressionDescription()
  expressionDescreption.name = "headCount"
  
  //2 創建函數統計每個"department"的成員數量,更多的函數關鍵字如average,sum,count,min等見NSExpression文檔
  expressionDescreption.expression =
    NSExpression(forFunction: "count:",
                 arguments: [NSExpression(forKeyPath: "department")])
  
  //3 通過設置propertiesToFetch初始化fetch的內容,這樣CoreData就不會查尋每條記錄的所有數據,這里只查詢"department"屬性,并通過expressionDescreption函數記錄不同"department"的數量。
  let fetchRequest: NSFetchRequest<NSDictionary> = NSFetchRequest(entityName: "Employee")
  // 這兩個參數都是必須的,第一個"department"只會關注對應的屬性并不會關注統計,其對應結果是【"department":name】的字典,第二個參數expressionDescreption只關注統計結果并不關注具體是哪一個department,其結果是【"headCount":value】的字典
  fetchRequest.propertiesToFetch = ["department", expressionDescreption]
  //查詢結果以"department"分組,這樣將返回一個數組
  fetchRequest.propertiesToGroupBy = ["department"]
  fetchRequest.resultType = .dictionaryResultType
  
  //4 執行查詢操作
  var fetchResults: [NSDictionary] = []
  do {
    fetchResults = try coreDataStack.mainContext.fetch(fetchRequest)
  } catch let error as NSError {
    print("ERROR: \(error.localizedDescription)")
    return [[String: String]]()
  }
  //5 查詢的結果是一個[NSDictionary],其中元素個數取決于fetchRequest.propertiesToGroupBy的分組個數,每個字典的元素個數取決于fetchRequest.propertiesToFetch中的個數。在上述兩個屬性都未設置時,其結果為[NSManagedObject]。
  return fetchResults as! [[String: String]]
}
4.2.2 NSPredicate限制NSFetchRequest

在數據庫中抓取數據的時候,CoreData會順著每一個實體的relationships去查詢相關實體,當這種關系非常復雜,或者查詢的實體自身數量龐大的時候,這會十分消耗性能。幸運的是,CoreData可以通過以下三種方式來優化效率1)CoreData支持分批查找,可以設置NSFetchRequest的fetchBatchSize、fetchLimit和fetchOffset屬性進行控制。2)CoreData使用faulting來優化內存效率,一個fault是一個占位對象,表示還沒有完全加載入內存的一個類型。3)使用NSPredicate限制查詢范圍。

NSPredicate條件可以通過AND,OR,NOT等各種條件限制,這個類是Foundation的內容,具體使用可以查詢官網。NSDescriptor也是屬于Foundation的內容。根據文檔中介紹,這兩個屬性是在SQLite level這一層生效。NSDescriptor有很多API可以得到一個comparator,NSPredicate也有很多實例化方法,但是CoreData并不會全部支持,因為該語法在SQLite生效,部分高效的方法無法轉化為SQLite的語法。

lazy var nameSortDescriptor: NSSortDescriptor = {
  let compareSelector = #selector(NSString.localizedStandardCompare(_:))
  return NSSortDescriptor(key: #keyPath(Venue.name), ascending: true, selector: compareSelector)
}()

lazy var dsitanceSortDescriptor: NSSortDescriptor = {
  return NSSortDescriptor(key: #keyPath(Venue.location.distance), ascending: true)
}()
4.2.3 異步抓取

和NSFetchRequest是NSPersistentStoreRequest的子類一樣,NSAsynchronousFetchRequest也是NSPersistentStoreRequest的子類,它可以在子線程對大量數據抓取。它需要一個NSFetchResult的對象進行初始化,包含一個完成回調,在managedContext中調用execute執行查詢操作。另外異步抓取請求可以通過NSAsynchronousFetchRequest對象的cancel()方法撤銷。異步抓取也可以使用Context執行perform方法來實現。兩種方案任選其一即可,暫未發現它們之間的本質區別。

fetchRequest = Venue.fetchRequest()

asyncFetchRequest = NSAsynchronousFetchRequest<Venue>(fetchRequest: fetchRequest, completionBlock: { [unowned self] (result: NSAsynchronousFetchResult) in
  guard let venues = result.finalResult else {
    return
  }
  self.venues = venues
  self.tableView.reloadData()
})

do {
  try coreDataStack.managedContext.execute(asyncFetchRequest)
} catch let error as NSError {
  print("Could not fetch \(error), \(error.userInfo)")
}

4.3 批量操作

4.3.1 批量更新

有時可能需要批量改變數據庫中某一個實體所有對象的單個屬性,首先傳統的將所有符合條件的對象從數據庫中加載到內存中能夠實現。但是當需要處理成千上萬條記錄的時候,這樣將會極大的浪費內存,降低效率。在iOS8以后,CoreData提供了更有效的操作,NSBatchUpdateRequest可以繞過加載到內存的操作,直接對數據庫中的數據進行批量更新。如郵件app中標記所有為已讀。

let batchUpdate = NSBatchUpdateRequest(entityName: "Venue")
batchUpdate.propertiesToUpdate = [#keyPath(Venue.favorite) : true]
batchUpdate.affectedStores = coreDataStack.managedContext.persistentStoreCoordinator?.persistentStores
batchUpdate.resultType = .updatedObjectsCountResultType

do {
  let batchResult = try coreDataStack.managedContext.execute(batchUpdate) as! NSBatchUpdateResult
  print("Records updated \(batchResult.result!)")
} catch let error as NSError {
  print("Could not update \(error), \(error.userInfo)")
}
4.3.1 批量刪除

同樣的NSBatchDeleteRequest也是NSPersistentStoreRequest的一個子類,它和NSBatchUpdateRequest一樣直接對數據庫進行操作,注意這兩個類的操作將不會把對象和Context進行關聯,因此也不會對數據進行校驗,因此當執行這兩個操作時需要手動進行數據校驗。

4.4 NSFetchedResultsController

NSFetchedResultsController是蘋果特地為支持UITableView從數據庫讀取數據設計的一個類。NSFetchedResultsController可以通過fetchRequest、managedObjectContext、sectionNameKeyPath和cacheName四個參數實例化一個對象。

其中第三個參數sectionNameKeyPath為分組的屬性字段,注意它不僅可以取一級屬性,還可以取多級屬性,如Team.qualifyZone.lon...。但是需要注意的是真正想讓數據分組展示必須使用相應的NSSortDescriptor賦值給NSFetchedResultsController進行數據排序。只要fetchRequest的NSSortDescriptor的sortDescriptors屬性數組中的首個元素和NSFetchedResultsController初始化時的分組使用同一鍵值,那么這里的升序或者降序不會對分組結果造成影響,只會影響排序。

第四個參數cacheName用于緩存分組相關信息,注意盡管這里并不是在數據庫中分類存儲,但是當重啟程序后分組信息依然有效,這是因為其具體緩存位置在主存Disk中。另外,當某個Request的條件改變時或者查詢另外一個實體時,如果使用了同一個name進行緩存,需要調用deleteCatch(withName:)或者使用另外一個不同的name,因此name盡量取得更有意義。

override func viewDidLoad() {
  super.viewDidLoad()

  let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest()

  let zoneSort = NSSortDescriptor(key: #keyPath(Team.qualifyingZone), ascending: true)
  let scoreSort = NSSortDescriptor(key: #keyPath(Team.wins), ascending: false)
  let nameSort = NSSortDescriptor(key: #keyPath(Team.teamName), ascending: true)

  fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]

  fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                        managedObjectContext: coreDataStack.managedContext,
                                                        sectionNameKeyPath: #keyPath(Team.qualifyingZone),
                                                        cacheName: "worldCup")

  fetchedResultsController.delegate = self

  do {
    try fetchedResultsController.performFetch()
  } catch let error as NSError {
    print("Fetching error: \(error), \(error.userInfo)")
  }
}

獲取抓取到的數據

func numberOfSections(in tableView: UITableView) -> Int {
  guard let sections = fetchedResultsController.sections else {
    return 0
  }
  return sections.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  guard let sectionInfo = fetchedResultsController.sections?[section] else {
    return 0
  }
  return sectionInfo.numberOfObjects
}

let team = fetchedResultsController.object(at: indexPath)

監聽數據改變
當某個fetchedResultsController的context對數據進行改變時,這個信息會發到它的delegate中。這里需要注意的是,當你點擊了tableview的某一行,可能導致數據更新Update同時還導致了數據排序Move,CoreData會將這兩個操作合并為一個操作,并只調用一次didChange 方法并且NSFetchedResultsChangeType = Move。

extension ViewController: NSFetchedResultsControllerDelegate {
  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()
  }
  
  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .insert:
      tableView.insertRows(at: [newIndexPath!], with: .automatic)
    case .delete:
      tableView.deleteRows(at: [indexPath!], with: .automatic)
    case .update:
      let cell = tableView.cellForRow(at: indexPath!) as! TeamCell
      configure(cell: cell, for: indexPath!)
    case .move:
      tableView.deleteRows(at: [indexPath!], with: .automatic)
      tableView.insertRows(at: [newIndexPath!], with: .automatic)
    }
  }
  
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates()
  }

  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
    let indexSet = IndexSet(integer: sectionIndex)
    switch type {
    case .insert:
      tableView.insertSections(indexSet, with: .automatic)
    case .delete:
      tableView.deleteSections(indexSet, with: .automatic)
    default:
      break
    }
  }
}

需要注意的是每次內容改變第二和第四個方法只會調用一個,當不會新增或者刪除分區Section時,CoreData調用第一、第二和第三個方法,當發生新增或者刪除分區Section時,CoreData調用第一、第三和第四個方法。當某個分區只有一個對象時,這個對象被刪除后就會調用類型為.delete的方法四。或者添加了一個包含新的分區的對象,就會調用類型為.insert的方法四。

同樣的,NSFetchedResultsController同樣對UICollectionview有很好的支持,不同的是,CollectionView并沒有beginUpdate和EndUpdate方法,因此需要在NSFetchedResultsController代理中didchanged中進行UI更新。
在使用NSFetchedResultsController的代理時,應注意,只要是它管理的實體數據庫發生一點改變,其代理都會被調用。

5 小結

通常并不會手動從0開始初始化CoreData,更常用的是使用MagicRecordRecord進行數據庫初始化。但是其并不能處理復雜的數據遷移,因此我們需要在調用它的初始化方法之前先手動進行數據庫遷移。關于MagicRecord詳細用法見其Github主頁.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,064評論 6 13
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,807評論 18 139
  • 當茫茫黑夜來臨 將一切淹沒在昏暗里 看不清 朦朧 朦朧 看不清 心不再飄向遠方 而是 回到最初的地方 夢開始的地方...
    我是凈葉不沉閱讀 293評論 2 2
  • 1 周一照例是公司開周會的日子,會上老總嘚吧嘚吧一通培訓,那他自己的話說,這是在給員工們洗腦。作為一個幾經波折的創...
    奶油溜吖溜閱讀 556評論 4 2
  • 近日,季熏遙身邊悲慘世紀連連不斷,季熏遙感覺整個人都不好了。小霉運都是不足為矣的,可這些就像是暴風雨前夕,真正讓人...
    渡獄閱讀 309評論 0 3