本地缓存

Core Data learning note with Swi

2015-11-23  本文已影响306人  扬仔360

CoreData Learning note

后续阅读:

https://www.objc.io/issue-4/core-data-overview.html
中文:http://www.cocoachina.com/ios/20130911/6981.html

01: 存取:

首先开头用最简单的小例子。

//MARK: - Save Data
  func saveName(name: String){
    let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
    let managedContext = appDelegate.managedObjectContext

    let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedContext)
    let person = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedContext)

    person.setValue(name, forKey: "name")

    do {
      try managedContext.save()
      people.append(person)
    } catch let error as NSError{
      print("Could not save \(error),\(error.userInfo)")
    }
  }
  
  //MARK: - Fetch Data
  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
    let managedContext = appDelegate.managedObjectContext

    let fetchRequest = NSFetchRequest(entityName: "Person")
    do {
      let results = try managedContext.executeFetchRequest(fetchRequest)
      people = results as! [NSManagedObject]
    } catch let error as NSError {
        print("Could not fetch \(error), \(error.userInfo)")
    }
  }

02.

可以使用BinaryData来存储图像,但是巨大的图像直接存储在数据库中会导致性能降低,选择类型为BD的Attribute,然后可以在右边的属性选择框中勾选Allows External Storage,类似所以存储在外部,提高数据访问性能。

Transformable类型用于存储那些实现了NSCoding protocol 的对象,比如 UIColor,UIColor 实现了NSSecureCoding

03. 不要过于依赖 NSManagedObject

虽然使用KVC很便捷,但是尽量不要过于依赖KVC。

04.自动创建Entity的子类:

打开。xcfatamodeld文件 -> Editor -> Create NSManagedObject Subclass

创建出来的每个 Entity 对应了两个文件,普通的文件仅仅包含了所有的 action 操作,Entity 的 所有属性都用 extension 的方式进行了实现。所有的属性在Extension中声明的时候前面都加上了@NSManaged,这个标示通知了编译器,这个 property 会在runtime的时候提供,而不是在编译的时候。

一般的模式下,property 是由内存中的实例变量实现的,但是在managedObject中,是由managed object context实现的,compile time 并不知道。

创建了属于自定义的EntityManaged Object的子类有两个好处:
· 隔离的KVC,编译器可以获取 Property,而不是通过 KVC 的方式,方便编写程序。
· 方便重写方法。当时注意Apple文档中标出了一些从来都不应该重写的方法。

05. 结合子类 NSManagedObject 去实现添加和 Retrieve 的 Demo:

这里也就是增加的Demo:

let bowtie = NSEntityDescription.insertNewObjectForEntityForName("Bowtie", inManagedObjectContext: managedObjectContext) as! Bowtie
    bowtie.name = "My bow tie"
    bowtie.lastWorn = NSDate()

    do {
      try managedObjectContext.save()
    } catch let error as NSError {
      print("Save error\(error.localizedDescription)")
    }

    //Retrieve test bow tie
    do {
      let request = NSFetchRequest(entityName: "Bowtie")
      let ties = try managedObjectContext.executeFetchRequest(request) as! [Bowtie]

      let sample : Bowtie = ties[0]
      print("Name = \(sample.name), Worn: \(sample.lastWorn)")
    }catch let error as NSError {
      print("Fetching error: \(error.localizedDescription)")
    }

06. Data Validation:

选择Entity的Property,然后右边可以设置该项属性的最大值、最小值和默认值。

07. 具体的操作对象:

具体这一部分可以添加常用的方法:会在后续添加。

http://justsee.iteye.com/blog/1881110

NSManagedObjectContext

表示被操作数据的上下文环境。类似于一种持续性的数据库连接,可以做增删查等操作。

而且只有在调用Save方法的时候,这些所有的改动才会被提交,否则是不会在持久层做任何改动的。

支持撤销和重做。

NSManagedObjectModel

表示被管理的数据模型,这里包含着所有对象的表格信息,所有数据结构,包括他们之间的关系。

在这里添加实体的属性,添加实体和实体之间的关系。

所以NSManagedObjectModel其实包含着NSEntityDescriptionNSPropertyDescription,前者相当于数据库中的一个表,后者相当于表中的一列。NSPropertyDescription可以描述实体的基本属性(Attributes)、实体之间的关系(Relationships)还有 查询属性(FetchedProperty)。

查询属性对应NSFetchedPropertyDescription对象。

Persistent Store

持久化存储层,是由文件或者外部数据库组成的,大多数情况下访问持久化层全部由上下文Context来完成。

CoreData 提供了四种持久化层的方案:

非原子访问的:NSQLiteStoreType
原子访问的:BSXMLStoreTypeNSBinaryStoreTypeNSInMemoryStoreType

XML是将数据存为XML文件,只在 OS X 平台下可用。Binary的方法是存为一个Data文件。InMemory的方式是不会对数据进行真正意义上的持久化,全部存储在内存中,当应用程序退出时,数据也就消失了。

NSPersistentStoreCoordinator

持久化存储助理的存在相当于和数据持久层的连接器,SQLite的话就是和数据库的连接,它设置着数据库的路径名字、位置、存储方式和存储的时机。
CoreData其实就相当于一个栈,栈的底层是持久化存储层,栈顶是Context,所以一般简单的需求我们只需要使用栈顶的Context,那么NSPersistentStoreCoordinator就是栈中层的一层存在,协调上栈的上下层关系。

负责理解NSManagedObjectModel和去NSPersistentStore中执行相应操作。

补充NSManagedObjectContext

NSManagedObjectContext就像内存中的便签纸,临时修改你的ManagedObject,所以知道你提交save(),否则持久化层是不会有变化的。

08. 创建自己的线程栈

创建:CoreDataStack.swift

第一部分创建 Model 的名字和 Document 的路径URL:
创建新的 .swift 文件

import CoreData
class CoreDataStack {

  let modelName = "Dog Walk"

  //Document's URL
  private lazy var applciationDocumentDirectory: NSURL = {
    let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
    return urls[urls.count-1]
  }()
}

第二步给类添加懒加载的三个属性 NSManagedObjectContextNSPersistentStoreCoordinatorNSManagedObjectModel

 //NSManagedObjectContext
 //Note: ConcurrencyType 的具体参数会在后面补充添加,暂时先使用 .MainQueueConcurrencyType
 //创建出来Context是完全没有意义的,直到设置了Context的PersistentStoreCoordinator
 lazy var context: NSManagedObjectContext = {
   var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType)
   managedObjectContext.persistentStoreCoordinator = self.psc
   return managedObjectContext
 }()
 
 //NSPersistentStoreCoordinator
 //对StoreCoordinator做懒加载,StoreCoordinator是介与PersistentStore(s)和ObejctModel之间的,所以至少需要一个PersistentStore。
 private lazy var psc: NSPersistentStoreCoordinator = {
   //coordinator init,传入Model,Model指所有的Entity和所有的relationship
   let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
//    PersisitentStore 的物理存储路径
   let url = self.applciationDocumentDirectory.URLByAppendingPathComponent(self.modelName)
   do{
//      一些Option配置:
     let options = [NSMigratePersistentStoresAutomaticallyOption : true]
//addPersistentStoreWithType 选择一个SQLite的type,使用SQLite作为存储模式。
     try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL:url, options: options)
   } catch {
     print("Error adding persistnet store.")
   }
   return coordinator
 }()
 
 //NSManagedObjectModel
 //这里包含着MainBundle里面的momb文件里面的 `.xcdatamodeld` 文件,就是Xcode图形化设计Entity和Relationship的那个文件,使用它来创建ManagedObjectModel
 private lazy var managedObjectModel: NSManagedObjectModel = {
   let modelURL = NSBundle.mainBundle().URLForResource(self.modelName, withExtension: "momd")!
   return NSManagedObjectModel(contentsOfURL: modelURL)!
 }()

添加的每 Property 都对应着 Core Data 的重要组件,使用Lazy Loading,每个组件都依赖其他的组件。

这些 Property 中只有NSManagedObjectContext是 Public 的 Property,其余的都是 private 修饰。private 修饰的组件外界不需要获取的。

另外,NSPersistentStoreCoordinator可以通过 NSManagedObjectContext的 Public property 获取。

而所有的NSManagedObjectModelNSPersistentStore都可以通过NSPersistentStoreCoordinator的Public property 来获取。

StoreCoordinator做懒加载,它是介与PersistentStore(s)ObejctModel之间的,所以至少需要一个PersistentStore

最后添加一个方法:

//保存的方法
func saveContent () {
  if context.hasChanges {
    do {
      try context.save()
    } catch let error as NSError {
      print("Error: \(error.localizedDescription)")
      abort()
    }
  }
}

AppDelegate中添加LazyLoading的iVar:

lazy var coreDataStack = CoreDataStack()

didFinishLaunchingWIthOption的方法中可以给rootViewController中的Context赋值了:

let navigationController = window!.rootViewController as! UINavigationController
let viewController = navigationController.topViewController as! ViewController
viewController.managedContext = coreDataStack.context

接下来在合适的时机调用SaveContent方法,这里就是在applicationDidEnterBackgroundapplicationWillTerminate代理方法中调用:

coreDataStack.saveContent()

到这里,一个CoreData的栈类就创建完成了,并且已经实现了在应用退出的时候对 Context 进行 saveing 的操作


09.高级查询

简单的查询通过创建 NSFetchRequest 来从 CoreData 中取得数据。

下面展示四种查询数据的方式:

//1
let fetchRequest1 = NSFetchRequest()
let entity = NSEntityDescription.entityForName("Person", inManagedObjectContext: managedObjectContext)! 
fetchRequest1.entity = entity

第一种方法通过默认初始化NSFetchRequest,从 managedContext 来创建 Person 类的 EntityDescription,然后设置fetchRequest的entity来完成。

//2
let fetchRequest2 = NSFetchRequest(entityName: "Person")

第二种方法可以在初始化NSFetchRequest的时候传入EntityName来完成,这是一种便捷的快速方法,在init的时候就制定了Entity。

//3
let fetchRequest3 = managedObjectModel.fetchRequestTemplateForName("peopleFR")

第三种通过调用managedObjectModel.fetchRequestTemplateForName方法来获取 NSFetchRequest。在Xcode的 Data Model Editor 界面中可以手动设置一些针对用户需求常用的fetch属性,可以使用这种方法来快速调用。

//4
let fetchRequest4 = managedObjectModel.fetchRequestFromTemplateWithName("peopleFR", substitutionVariables: ["NAME" :"Ray"])

第四种基于第三种,但是会在第三种的基础上使用substitutionVariables进行再次的筛选。


在Editor上创建 Fetch Template

在 Data Model Editor 界面上长时间按住 Add Entity 的那个按钮,选择Add Fetch Request

之后如果是查询一个实体的全部数据,就吧下拉框选为目标查询的实体。

几个小点:

NSFetchRequest 神奇の存在

在 CoreData 框架中,NSFetchRequest 就像一把多功能的瑞士军刀,你可以批量获取数据,可以获取单个数据,可以获取最大最小、平均值等、

那么他是如何实现这些的呢,FR 有一个property叫做 resultType,默认值是 NSManagedResultType

NSCountResultType 实现 count 计数

在获取了 FR 之后,需要给 FR 添加 predicate,predicate 在创建的时候支持 key path,比如:

lazy var cheapVenuePredicate: NSPredicate = {
  var predicate = NSPredicate(format: "priceInfo.priceCategory == %@", "$")
  return predicate
}()

上面代码中的 priceInfo.priceCategory 就是一个应用。

func populateCheapVenueCountLabel() {
  // $ fetch request
  let fetchRequest = NSFetchRequest(entityName: "Venue")
  fetchRequest.resultType = .CountResultType
  fetchRequest.predicate = cheapVenuePredicate
  do {
    let results = try coreDataStack.context.executeFetchRequest(fetchRequest) as! [NSNumber]
    let count = results.first!.integerValue
    firstPriceCategoryLabel.text = "\(count) bubble tea places"
  } catch let error as NSError {
    print("Could not fetch \(error), \(error.userInfo)")
  }
}

这个方法使用上面的 LazyLoading 的iVar - cheapVenuePredicate 作为 predicate,来做数据查询。

这里指明了 fetchRequest.resultType = NSCountResultType,所以结果会返回一个包含了一个 NSNumberNSArray。当然这个 NSNumber 是一个 NSInteger,它就是那个count。

除了上面的一种执行 executeFetchRequest 的方法获取Count的方法之外,还可以直接调用 context 的countForFetchRequest 方法来获取Count:

func populateExpensiveVenueCountLabel() {
  // $$$ fetch request
  let fetchRequest = NSFetchRequest(entityName: "Venue")
  fetchRequest.resultType = .CountResultType
  fetchRequest.predicate = expensiveVenuePredicate
  
  var error: NSError?
  let count = coreDataStack.context.countForFetchRequest(fetchRequest, error: &error)
  
  if count != NSNotFound {
    thirdPriceCategoryLabel.text = "\(count) bubble tea places"
  } else {
    print("Could not fetch \(error), \(error?.userInfo)")
  }
}

上面的代码中,使用的错误处理不是像之前使用的 try-catch 结构,而是使用 iOS SDK 中常见的 error 的结构,这是因为 countForFetchRequest 方法的第二个参数是一个 *NSError 类型,需要将一个 error 对象的指针传递进去。使用:

coreDataStack.context.countForFetchRequest(fetchRequest, error: &error)

来获取查询结果的 count。

DictionaryResultType 实现计算逻辑

前面说了,NSFetchRequest 可以左好多事情,这里包括了一些计算的逻辑。

对于某些需求,比如对查询的结构进行一些SUM、MIN、MAX这样的常见操作,常见一些低效率的代码把所有的数据全部查询出来,然后在程序中使用 For 循环来进行筛选,这样做又 Naive 又低效。

代码如下:

func populateDealsCountLabel() {
  //1 像之前一样普通的创建 NSFetchRequest,但是 resultType 指定为 .DictionaryResultType
  let fetchResult = NSFetchRequest(entityName: "Venue")
  fetchResult.resultType = .DictionaryResultType
  
  //2 创建一个 NSExpressionDescription 对象去请求 SUM,而且给它一个 name 标示“sumDeals”,在resultResults中就会有这个 name 标识的返回结果
  let sumExpressionDesc = NSExpressionDescription()
  sumExpressionDesc.name = "sumDeals"
  
  //3 上面仅仅给了 ExpressionDescription 一个name标示,但是只是一个String而已,真正让它清楚自己要做sum求和需要给ExpressionDesc对象的这个 .expression 对象做配置:
  //初始化一个 NSExpression 对象,function写上“sum”,还有好多,使用 command 键按进去方法描述下面一大堆,后面的 argument 参数指明对查询出来的结果的 specialCount 来进行逻辑计算。
  sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [NSExpression(forKeyPath: "specialCount")])
  //指定计算结果的数据类型
  sumExpressionDesc.expressionResultType = .Integer32AttributeType
  
  //4 配置好了 NSExpressionDescription 对象之后,将配置好的它 set 为 NSFetchRequest 对象的 .propertiesToFetch(看见没这里是ties,意思要接受数组,而且可以配置好多个,配置多个就返回多个喽~)
  fetchResult.propertiesToFetch = [sumExpressionDesc]
  
  //5 司空见惯的查询,看从 resultDic 中取得查询结果的那个 [sumDeals] 就是前面 NSExpressDescription.name。
  do {
    let results = try coreDataStack.context.executeFetchRequest(fetchResult) as! [NSDictionary]
    let resultDic = results.first!
    let numDeals = resultDic["sumDeals"]
    numDealsLabel.text = "\(numDeals!) total deals"
  } catch let error as NSError {
    print("Could not fetch \(error), \(error.userInfo)")
  }
}

ManagedObjectIDResultType 查询结果的唯一标示

四种类型前面已经说了三种,接下来的一种就是 ManagedObjectIDResultType,这种查询类型会返回查询结果的一个特殊标志,一种 universal identifier 每一个 ManagedObject 对应着一个特殊的 ID。

之前笔者认为它像 hashCode 但是明显又不一样,因为即便是两个内容相同的 ManagedObjcet,他们的 ID 也都是不一样的,但是 HashCode 确应该是一样的。

iOS 5 的时候取得 NSManagedObjectID 是线程安全的,但是现在已被弃用,但是有更好的并发模型提供给我们使用,以后会有 CoreData 与多线程并发。

另外提两点:

(如果使用了 Editor 配置的 predicate,那么在 Runtime 的时候就不能更改 predicate。)

其实 NSPredicate 不属于 Core Data 框架,它是属于 Foundation 框架中的,更多有关于 NSPredicate 的,可以看 苹果官方的文档

FetchResults 排序

NSFetchRequest 另外一个神奇的功能是它能把获取的数据进行排序,实现的机制是使用 NSSortDescription 类,而且这个排序的执行时机是在 数据存储层 也就是在 SQLite 层完成的,保证了排序的速度和效率。

案例:需要排序的模块中加入 NSSortDescription 的 lazy Var 如下:

lazy var nameSortDescriptor: NSSortDescriptor = {
  var sd = NSSortDescriptor(key: "name", ascending: true, selector: "localizedStandardCompare:")
  return sd
}()

lazy var distanceSortDescription: NSSortDescriptor = {
  var sd = NSSortDescriptor(key: "location.distance", ascending: true)
  return sd
}()

第一个 NSSortDescription 按照姓名来进行排序,排序的比较方法选择了 String 的 localizedStandardCompare:

第二个 NSSortDescription 按照 location.distance 进行排序。
ascending 参数表示是否要升序,true -> ascendingfalse -> descending

注意:

localizedStandardCompare 是什么,当你对用户看到的那些字符串进行排序的时候,Apple 都建议传递 localizedStandardCompare来当做排序的规则来对当前字符串进行排序。

如果要某个 SortDescriptor 的逆向排序,可以调用它的 .reversedSordDescriptor取得。

nameSortDescriptor.reversedSortDescriptor as? NSSortDescriptor

配置好了 SortDescription 之后,将 NSFetchRequest 的 sortDescriptors 属性设置为包含了 SortDescription 的数组:

fetchRequest.sortDescriptors = [sr]

异步Fetching(iOS 8 的特性)

如同很多其他 iOS 上的需求一样,当复杂且耗时长的工作放在主线程上,会造成线程阻塞,这个时候 UI 会处于一种假死的状态。同样的 Core Data 的 Fetch 也是一样的,在 Fetch 条件复杂、数据量很大的情况下,同样会造成线程阻塞。这个时候,这部分工作就应该异步执行。

Core Data 的异步执行被封装的相当简单,在已有的 FetchRequest 的基础上,使用 NSAsynchronousFetchRequest 来实现异步请求。

NSAsynchronousFetchRequest 的命名容易让人产生歧义,其实它并不是 FetchRequest 的 subclass,它跟 FetchRequest 一样,同样是 NSPersistentStoreRequest 的子类。也就是说实现异步的 Fetch 就是把 NSFetchRequest 替换为 NSAsynchronousFetchRequest

首先在模块原有的 FetchRequest 中模块中添加 iVar 变量:

var asyncFetchRequest: NSAsynchronousFetchRequest!

NSAsynchronousFetchRequest 就像是已经存在的 FetchRequest 的一个 Wrapper 一样。创建一个 NSAsynchronousFetchRequest 需要一个正常的 FetchRequest 和一个 Completion Handle。

执行请求的时候类似,不过请求的方法从 executeFetchRequest 变成了 executeRequest,传递进去的参数也从 FetchRequest 变成了 AsynchronousFetchRequest。

执行请求之后,返回的数据为:NSAsynchronousFetchResult

调用的方法如下:

fetchRequest = NSFetchRequest(entityName: "Venue")

asyncFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) {
//查询成功的处理
  [unowned self] (result: NSAsynchronousFetchResult!) -> Void in
  self.venues = result.finalResult as! [Venue]
  self.tableView.reloadData()
}
do {
  try coreDataStack.context.executeRequest(asyncFetchRequest)
  //Returns immediately, cancel here if you want.
} catch let error as NSError {
  print("Could not fetch \(error), \(error.userInfo)")
}

block 之内是对返回的数据做处理。

另外,如果要取消正在,asyncFetchRequest 可以调用 cancel() 方法来取消这次异步请求。

批量更新(不做查询,直接在Store层进行更新,iOS 8 的特性)

举例一种情况,如果我们要对十万条数据都进行同样的一个属性的更新,一般的做法我们需要取出这十万条数据,我们取出十多万条数据只是为了做一个很简单的更新。

经典的案例是电子邮箱类似的APP中 标记所有邮件为已读 这样的需求,难道要把上千封邮件全部请求出来吗,显然不是。

iOS 8 发布了支持 批量更新(Bench Update)NSBatchUpdateRequest,使用它可以在不做查询的情况下更新数据。

NSBatchUpdateRequest 实现的原理是完全绕开了 NSManagedObjectContext,直接去 NSPersistentStore 层去做 Update。

//这四行代码,在初始化的时候配置好EntityName, 配置影响的property和更改的值 以及 配置影响的Store,以及返回Result的数据类型。
//创建NSBatchUpdateRequest 的实例,entityName 作为初始化参数。
let batchUpdate = NSBatchUpdateRequest(entityName: "myEntityName")
//标明需要 Update 的 property 和 值
batchUpdate.propertiesToUpdate = ["favorite" : NSNumber(bool: true)]
//被影响的Stores 默认情况下这么写就可以,如果涉及比较多的PersistentStores 情况就更复杂了。
batchUpdate.affectedStores = coreDataStack.context.persistentStoreCoordinator!.persistentStores
//配置返回数据的类型,还可以是 UpdatedObjectIDsResultType。
batchUpdate.resultType = .UpdatedObjectsCountResultType

//执行批量更新
do {
  let batchResult = try coreDataStack.context.executeRequest(batchUpdate) as! NSBatchUpdateResult
  print("Records updated \(batchResult.result!)")
} catch let error as NSError {
    print("Could not update \(error), \(error.userInfo)")
}

在初始化的时候配置好EntityName, 配置影响的property和更改的值 以及 配置影响的Store,以及返回Result的数据类型。

另外提一下:如同这种在 Store 层的数据更新,数据删除也有同样的API,在 iOS 9 的时候苹果提供了一个类似 NSBatchUpdateRequest 的类来做在 Store 层的数据删除,使用 ---- NSBatchDeleteRequest,同样它们两个都是 NSPersistentStoreRequest 的子类。

注意:

额外需要注意的一点是,前面提到了「NSBatchUpdateRequest 实现的原理是完全绕开了 NSManagedObjectContext,直接去 NSPersistentStore 层去做 Update。」

所以做了批量 Update / Delete 之后,你之前请求的那部分数据已经失效了,因为它们跟数据库已经失去了同步性。

End.

上一篇 下一篇

猜你喜欢

热点阅读