托管对象上下文 : NSManagedObejctContext
数据库中的数据行在刚被取出时是Array或Hash形式,这种数据结构并不直观,为了便于使用,通常会进行对象关系映射,即把类模拟为数据行的模板,取出数据行时,将数据行转换为该类的对象。
把数据行存进去是个相反的过程,向类的对象写数据,再把这个对象转为数据行存入数据库。
一进一出的过程并非凭空完成,它们由 NSManagedObejctContext 控制。
比如我们来存一条数据:
//Insert
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext as! NSManagedObejctContext
let student = NSEntityDescription.insertNewObject(forEntityName: "Student" , into: moc) as! Student
student.id = 1
student.name = "Alice"
student.age = 18
do {
try moc.save()
print("保存成功")
} catch {
fatalError("\(error)")
}
托管对象模型 : NSManagedObjectModel
简单地说,它用来描述数据库的结构、表关系。
它的实例化需要.momd文件,这个文件存在模拟器沙盒中,具体位置像这样:/Users/gaoya/Library/Developer/CoreSimulator/Devices/D07B71B5-8508-47F5-8181-6309D819F52B/data/Containers/Bundle/Application/3B9F46F7-74E1-44AB-8DCD-FBEC3C894EDC/test.app/test.momd
我猜这个文件是在安装app的时候,根据源文件中的.xcdatamodel文件生成的
实例化 NSManagedObjectModel:
let modelUrl = Bundle.main.url(forResource: "test.momd/test 2", withExtension: "mom")!
let managedObjectModel = NSManagedObjectModel.init(contentsOf: modelUrl)
持久化存储协调器 : NSPersistentStoreCooedinator
负责管理底层存储文件。它包括模型和真实数据。
模型参照上面的NSManagedObjectModel,真实数据的储存的方式可以有很多种,sqlite、二进制、xml、json等。无论用哪一种方式储存,真实数据都需要与模型的结构保持一致,持久化存储协调器的工作就在于此。
自定义上下文的数据库:
//获取momd文件
let modelUrl = Bundle.main.url(forResource: "test.momd/test 2", withExtension: "mom")!
//使用momd文件实例化托管对象模型
let managedObjectModel = NSManagedObjectModel.init(contentsOf: modelUrl)
//使用托管对象模型实例化持久化存储协调器
let coordinator = NSPersistentStoreCoordinator.init(managedObjectModel: managedObjectModel! )
//获取沙盒的Document路径
var applicationDocumentsDirectory: URL = {
let urls = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask)
return urls[urls.count - 1]
}()
let url = applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite")
//为持久化存储协调器添加真实数据
try! coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
//创建一个上下文
let managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
//为上下文设置持久化存储协调器
managedObjectContext.persistentStoreCoordinator = coordinator
获取沙盒的Document路径就像这样: /Users/gaoya/Library/Developer/CoreSimulator/Devices/D07B71B5-8508-47F5-8181-6309D819F52B/data/Containers/Data/Application/57E04E49-DF06-49BE-805D-48CD22D02EB1/Documents
,sqlite文件并非一定要储存在这里,只要能获取到就行。
实体描述 : NSEntityDescription
Entity相当于数据表,NSEntityDescription 代表一张数据表。
查询表结构:
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
//et 是 NSEntityDescription实例
for et in (moc.persistentStoreCoordinator?.managedObjectModel.entities)! {
print("表名:\(et.name!)")
print("字段:")
for (k,_) in et.attributesByName {
print(k)
}
}
抓取请求 : NSFetchRequest
NSFetchRequest代表将请求的内容,也代表优化过的sql语句
// Select
let request = NSFetchRequest(entityName: "Student")
request.sortDescriptors = [NSSortDescriptor(key: "id", ascending: true)] // 依 id 由小到大排序
let selectId = "1"
request.predicate = NSPredicate(format: "id = \(selectId )") //查找条件
do {
let results =
try moc.fetch(request) as! [Student]
for result in results {
print("查询结果:")
print("\(result.id). \(result.name!)")
print("年龄: \(result.age)")
}
} catch {
fatalError("\(error)")
}
迁移计划 : NSEntityMigrationPolicy
描述一次迁移计划,在映射模型MappingModel文件中指定Custom Policy为自定义的继承自NSEntityMigrationPolicy的类,Custom Policy处填写[工程名.类名],在该类中复写以下可选方法,则迁移时会相应调用它们。
当迁移将要开始时
func beginEntityMapping(mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool在旧数据上构建新的实例时
func createDestinationInstancesForSourceInstance(sInstance: NSManagedObject, entityMapping mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool在旧数据上构建新的实例结束时
func endInstanceCreationForEntityMapping(mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool构建新的RelationShips时
func createRelationshipsForDestinationInstance(dInstance: NSManagedObject, entityMapping mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool`构建新的RelationShips结束时
func endRelationshipCreationForEntityMapping(mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool验证,保存数据时
func performCustomValidationForEntityMapping(mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool迁移结束时
func endEntityMapping(mapping: NSEntityMapping, manager: NSMigrationManager, error: NSErrorPointer) -> Bool
数据迁移 : NSMigrationManager
当iOS App升级时,本地库可能会改动,如果你直接改动本地数据库结构,编译,安装(覆盖旧版本),打开。
不出意外App会崩掉,因为数据库结构改动了,而真实数据没改动。
此时,你应该迁移你的数据库,根据需求,选择一种迁移方式执行。
轻量迁移
当你对本地库改动不大时,使用轻量迁移最快,符合轻量迁移的修改条件有:
- 添加或删除属性
- 非可选属性变为可选属性,反之亦然(但要填写默认值)
- 重命名实体,属性或关系
本地库可以创建多个版本,选中.xcdatamodel文件 -> Editor -> Add Model Version
填入新版本名(Version name)、它的基础版本(Based on model)
创建完成,选择使用你刚创建的版本
修改符合轻量迁移的修改条件,比如增加一个表字段(height)
现在,我们可以直接编译、覆盖安装旧版本的App了,覆盖后的App将拥有最新的本地库版本,旧版本地库的数据将自动迁移到新版本,因为你只增加了一个表字段,程序可以自动识别如何将数据迁移到新版本。
轻量迁移前后的表结构log
=========前=========
表名:
Student
字段:
id
age
name
=========后=========
表名:
Student
字段:
age
height
name
id
重量迁移
当App版本不断更新,本地库的修改需求超过轻量迁移的能力范围时,使用重量迁移。
比如,现在需要新建一张Teacher表,表结构与Student一致(id、age、name、height),将原来的Student表里age超过18岁的人移到Teacher表,未超过的留在Student表。
具体步骤:
新建本地库版本、添加Teacher表,字段与Student保持一致,并切换版本到最新
指定迁移计划的方式,在Custom Policy属性后填写[工程名.类名]
新建的这个类让它继承NSEntityMigrationPolicy,并复写createDestinationInstances方法
迁移时,每一条旧本地库的数据都会调用一遍这个方法,sInstance就是旧数据,
manager可以获取新旧版本本地库的上下文
import Foundation
import CoreData
class V2_To_V3_Policy: NSEntityMigrationPolicy {
override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
if sInstance.entity.name == "Student"
{
let id = sInstance.primitiveValue(forKey: "id") as! Int
let name = sInstance.primitiveValue(forKey: "name") as! String
let age = sInstance.primitiveValue(forKey: "age") as! Int
let height = sInstance.primitiveValue(forKey: "height") as! Int
if age > 18 {
let tea = NSEntityDescription.insertNewObject(forEntityName: "Teacher",
into: manager.destinationContext)
tea.setValue(id, forKey: "id")
tea.setValue(name, forKey: "name")
tea.setValue(age, forKey: "age")
tea.setValue(height, forKey: "height")
} else {
let stu = NSEntityDescription.insertNewObject(forEntityName: "Student",
into: manager.destinationContext)
stu.setValue(id, forKey: "id")
stu.setValue(name, forKey: "name")
stu.setValue(age, forKey: "age")
stu.setValue(height, forKey: "height")
}
}
}
}
与轻量迁移后一样,直接编译、覆盖安装旧版本的App了
启动后,打印一下App的本地库情况
表名:
Student
字段:
age
height
name
id
==================
表名:
Teacher
字段:
age
height
name
id
=================
学生数据:
1. Alice1号
年龄: 6
身高:180
2. Alice2号
年龄: 7
身高:180
... ...
13. Alice13号
年龄: 18
身高:180
老师数据:
14. Alice14号
年龄: 19
身高:180
15. Alice15号
年龄: 20
身高:180
... ...
20. Alice20号
年龄: 25
身高:180
渐进式迁移
在不考虑轻量迁移的情况下,App从V1升级到V2,存在映射模型、指定了迁移计划,迁移过程会很顺利地完成,从V2升级到V3也是如此。但是当用户很久没更新,直接从V1升级到V3时,如果没有专门写V1到V3的迁移,那么App不出意外会崩掉。这就意味着每一次升级,你就需要为每一个历史版本提供一份迁移到最新版本的计划。
使用渐进式迁移,每次只迁移到离当前最近的一次版本,重复这个过程,直到成为最新版本。
待补充... ...
Relationship
使用CoreData你就不必再创建xx_id这种表字段了,这种字段可以使用relationship代替。
在xcdatamodel可视化界面中,relationships行有3个参数:relationship/名字、destination/目标表名、inverse/目标表的relationships字。
当两个表各自有一条relationships的destination和inverse指向对方,就建立一条关系,这个关系有一些参数可以选择,比如是否级联删除、关系类型一对一或是一对多等。
比如student表和teacher表建立了多对1的关系,那么student的数据行映射对象alice,可以直接使用像"alice.tea.name"获取老师名字,其中tea是student表对teacher表的多对一关系名称。
结尾
- 不要试图删除旧版本,要改动的话新增版本就行。
- xcode默认帮我们创建了表的同名类,因为表的Codgen属性默认为Class Definition。
- 如果创建了版本,又使用git了回退,发现项目编译不了。可以尝试在文件管理器中的项目目录下,找到*.xcdatamodel文件,右键 -> 显示包内容 -> 删除相关版本后再编译。