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阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,511评论 6 13
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 当茫茫黑夜来临 将一切淹没在昏暗里 看不清 朦胧 朦胧 看不清 心不再飘向远方 而是 回到最初的地方 梦开始的地方...
    我是净叶不沉阅读 270评论 2 2
  • 1 周一照例是公司开周会的日子,会上老总嘚吧嘚吧一通培训,那他自己的话说,这是在给员工们洗脑。作为一个几经波折的创...
    奶油溜吖溜阅读 487评论 4 2
  • 近日,季熏遥身边悲惨世纪连连不断,季熏遥感觉整个人都不好了。小霉运都是不足为矣的,可这些就像是暴风雨前夕,真正让人...
    渡狱阅读 278评论 0 3