创建方式
1.创建项目时勾选使用CoreData
此时,AppDelegate 文件会自动包含CoreData的相关代码,项目里也会有*.xcdatamodeld文件可以直接使用。
2.给项目手动添加CoreData
em,直接把1里面的代码copy一遍就好:
import CoreData
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreDataTest")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
//这里需要用合适的方式处理错误信息,开发环境下可以直接fatalError,线上最好处理下错误
/*
这里出现错误的典型原因包括:
* 父目录不存在,无法创建或不允许写入。
* 由于设备被锁定时的权限或数据保护,持久化存储是不可访问的。
* 设备空间不足。
* 存储器不能迁移到当前的模型版本。
检查错误消息以确定实际的问题是什么
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
//保存失败时最好处理下相应的错误信息,线上最好不要直接fatalError
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
感觉CoreData的代码放在AppDelegate里面有些冗余,可以自己创建一个专门用来处理数据库的单例,然后放里面。这样便于封装管理
xcdatamodeld文件解释
Attribute属性解释
oc里面属于值类型的属性,在生成的类文件里面都是必不为空的属性;其它的如String,UUID,Binary Data 等引用类型的属性,都是可为空的属性:虽然String在swift里面也是值类型。
@NSManaged public var content: String?
@NSManaged public var createTime: NSNumber?
@NSManaged public var id: UUID?
@NSManaged public var title: String?
@NSManaged public var image: Data?
@NSManaged public var type1: Int16
@NSManaged public var type3: NSNumber?
Optional
是否可选;勾选后,保存对象到数据库时,如果该值为空,则会报错。
Transient
临时属性;勾选后,该字段和其它字段使用时没有区别,只是不会保存到持久存储器中。每次生成到context时,都会恢复成初始值。
Derived
派生属性; iOS13以后才有的。由Sqlite直接计算,不经过swift或者oc的代码,效率高,但是支持的表达式有限。
派生属性是只读的属性,需要同时勾选Optional,否则保存数据时会报错。
派生属性需要配合Derivation属性来使用:
派生属性会在每次保存时更新一次,即viewContext.save()时
Derivation
派生属性的描述表达式,对应的类是NSDerivedAttributeDescription,使用方法如下:
仅复制内容
复制的属性类型没有特别的限制。
比如一个职位Position对应一个公司Company,在RelationsShips里面配置好了company一对一的关系。
当我们要获取公司id时,我们需要Position.company.id
此时可以在Position里面新建一个派生属性companyId, 勾选Optional和Derived,Derivation设置:
company.id
就可以直接使用Position.companyId来获取公司id了。
字符串转换
仅支持字符串类型的属性
转换成大写:
uppercase:(name)
转换成小写:
lowercase:(name)
转换成不区分大小写和变音符号的字符串:
canonical:(name)
一对多关系时的聚合操作
例如一个公司Company发布了多个职位Position,在RelationsShips里面配置好了positions一对多的关系。
计算个数:
positions.@count
求和:
只能针对计算类型属性。比如Position有一个在招人数personCount的Int16属性,一个公司有多个职位,现在给公司增加一个派生属性totalPersonCount来统计总共需要招的人数:
positions.personCount.@sum
当前时间
比如Postion有一个Date类型的属性updateTime,用于保存最近更新职位信息的时间,如果不用派生属性,则需要在每次修改职位信息后手动修改一次更新时间,再保存到数据库。使用派生属性,则会在每次保存时自动修改更新时间:
now()
Allows External Storage
是否允许文件外存储,Binary Data 时可用,当二进制文件数据超过1M时,CoreData会把数据保存在Sqlite存储区之外。
Store in External Record File
启用该选项之后,系统会把持久化存储区里的数据复制成XML格式,并保存在存储区外。
Default Value
默认值,设置后可以给属性指定一个默认值。
但是在生成的类文件里面,可为空的属性依然是可为空的属性,不会变成去掉?
Use Scalar Type
使用标量类型,在数字类型(IntX, Double, Float等)的属性时会出现并默认勾选,取消勾选后,生成的类文件里面,数字类型会变成NSNumber?
Decimal 类型的属性,无论是否勾选,生成的类文件里面对应的属性类型都是NSDecimalNumber。
Validation
数据校验,如果是字符串类型的,可以限制长度区间;数值类型的属性,可以限制值区间。
Reg.Ex
正则表达式校验,仅用于字符串类型的属性。
Constraints
唯一约束,可以添加某个属性作为唯一约束,比如Position的id,同一id只允许存在一条数据。
Relationship 属性解释
Destination
目标类型。比如Company添加一个Position的Relationship,命名为positions,此时Position就是目标
Inverse
反向关联目标。想要有反向关联,需要在Position里面,同样添加一个Company的Relationship,命名为company。否则就是No Inverse
Delete Rule
关联删除规则,决定了从数据库删除当前对象时,它对应的Relationship里的关联对象该如何处理。总共有四种选项:
比如,公司A发布有3个职位,职位1,职位2,职位3,当我删除公司A时:
- No Action 啥也不做。需要自己手动维护3个职位里面的company属性;
- Nullify 设为nil。3个职位里面对应的company属性都设为nil;
- Cascade 同时删除关联对象。3个职位同时被删除;
- Deny 如果存在关联对象则无法删除。删除公司A失败,需要先删除职位123,才能删除公司A。
Type
对应关系。一对一或者一对多,一对多时,属性类型会是Set。
使用探索
创建对象
NSManagedObject对象无法直接使用init()方法来创建,需要使用NSEntityDescription相关方法,封装一次如下:
/// 创建一个实例对象,写入上下文,保存时会直接保存到数据库
public func createInContext<T: NSManagedObject>() -> T {
let name = "\(T.self)"
let model = NSEntityDescription.insertNewObject(forEntityName: name, into: persistentContainer.viewContext) as! T
return model
}
调用此方法,会在上下文中插入一个NSManagedObject对象,此处支持泛型。
保存对象
直接保存上下文中的所有修改
/// 保存上下文
public func saveContext() throws {
if persistentContainer.viewContext.hasChanges {
try persistentContainer.viewContext.save()
}
}
保存单个对象
无法只保存单个对象,但是可以创建一个新的上下文来写入单个对象,然后保存上下文
let context = NSManagedObjectContext.init(.mainQueue)
//指定一个存储调度器
context.persistentStoreCoordinator = viewContext.persistentStoreCoordinator
// 或者指定父context,这样可以直接使用父context的存储调度器,不可同时指定两个,会报错
context.parent = generalViewContext;
创建新的上下文,需要指定存储调度器,可以和其他上下文公用一个调度器,如果不指定persistentStoreCoordinator或者parent,调用context.save()时依旧无法保存数据
persistentStoreCoordinator或者parent不可同时指定,会导致crash
补充:后来在新创建的context里面,保存一个带Data的对象到数据库时,每次对象保存成功,但是Data数据在数据库查看就是Null,而使用默认的persistentContainer.viewContext不会出现这种问题,最后改成下面的代码创建新的context:
let context = persistentContainer.newBackgroundContext()
这样就不需要设置其它属性,直接使用。
使用多个NSManagedObjectContext
有很多场景下,如果我们直接使用NSManagedObject来处理用户交互数据,就可能需要用到多个Context。比如:
我打开记账页面记一笔账,但是必输的信息没有输入,然后又从记账页面跳转到新增银行卡的页面去录入新的银行卡信息;此时,context会有一个Record对象,缺少关键字段,后面又多了一个需要保存的Card对象。此时如果直接调用context.save,会报错
查询操作NSFetchRequest
查询符合条件的对象
封装方法:
public func query<T:NSManagedObject>(_ predicateString: String = "") throws -> [T] {
let fetchRequest:NSFetchRequest<T> = T.fetchRequest() as? NSFetchRequest<T> ?? NSFetchRequest<T>.init(entityName: "\(T.self)")
if !predicateString.isEmpty {
fetchRequest.predicate = NSPredicate.init(format: predicateString,"")
}
let results = try viewContext.fetch(fetchRequest)
return results
}
直接使用NSManagedObject.fetchRequest()可以快捷的构造一个NSFetchRequest对象;
筛选
查询筛选条件使用NSPredicate传入:
fetchRequest.predicate = NSPredicate.init(format: "userId==%@","123456")
或者直接把参数写在完整的字符串里面,方便使用一个字符串进行传参,就像封装方法里面的那样:
fetchRequest.predicate = NSPredicate.init(format: "userId=='123456'","")
如果是userId是字符串,需要使用单引号进行包裹,或者使用转义双引号\",否则查询时会出错
NSPredicate的使用
| 运算符 | 作用 | 示例 |
|---|---|---|
| > 、< 、== 、>= 、<= 、!= | 比较运算 | age > 18 |
| IN | 被包含 | name IN {'张三','李四'} |
| BETWEEN | 在区间内 | age BETWEEN {18,65} |
| BEGINSWITH | 开头是 | name BEGINSWITH '张' |
| ENDSWITH | 结尾是 | name ENDSWITH '三' |
| CONTAINS | 包含有 | name CONTAINS '三' |
| LIKE | 通配符 *和? | name LIKE '*三' |
| MATCHES | 正则 | name MATCHES '\(regex)' |
排序
除了筛选以外,NSFetchRequest还支持使用sortDescriptors参数进行排序,还可以传入多个NSSortDescriptor进行多重排序:
let sort = NSSortDescriptor.init(key: "id", ascending: true)
fetchRequest.sortDescriptors = [sort]
分页
NSFetchRequest支持进行分页查询:
fetchRequest.fetchLimit = pageSize
fetchRequest.fetchOffset = pageNum * pageSize
fetchLimit参数,限制每次查询的最大条数
fetchOffset参数,表示每次查询的数据的起始位置,
比如查第一页10条数据,就是:
fetchRequest.fetchLimit = 10
fetchRequest.fetchOffset = 0
再接着查第二页的10条数据:
fetchRequest.fetchLimit = 10
fetchRequest.fetchOffset = 10
...
查询某一列或几列数据
此时需要把NSFetchRequest<ResultType> 的返回类型设置为**NSFetchRequest<NSDictionary>**
修改NSFetchRequest的如下属性:
fetchRequest.propertiesToFetch = properties
fetchRequest.resultType = .dictionaryResultType
properties为属性key的数组;例如查询用户的姓名和年龄,那么:
let properties: [String] = ["name","age"]
此时查询获得的结果是[NSDictionary]类型,直接控制台输出如下:
(
{
name = "张三",
age = 18
},
{
name = "李四",
age = 19
},
{
name = "李四",
age = 19
}
...
)
数据去重
NSFetchRequest有专门的去重属性:
fetchRequest.returnsDistinctResults = true
returnsDistinctResults用于决定查询时是否只返回不相同的结果,默认为false;开启后,会自动去重:
(
{
name = "张三",
age = 18
},
{
name = "李四",
age = 19
}
...
)
删除对象
删除对象的操作,其实就是查询到符合要求的对象,然后进行删除:
@discardableResult
/// 删除
public func delete<T:NSManagedObject>(_ predicateString: String = "") throws -> [T] {
let results: [T] = try query(predicateString)
results.forEach { item in
viewContext.delete(item)
}
try saveContext()
return results
}
修改对象
修改和删除同理:
@discardableResult
/// 修改
public func modify<T:NSManagedObject>(_ predicateString: String = "", closure: @escaping ([T]) -> Void) throws -> [T] {
let results: [T] = try query(predicateString)
closure(results)
try saveContext()
return results
}
批量操作
如果数据量比较大时,可以使用批量操作
NSBatchDeleteRequest 批量删除
批量删除是iOS9+新增的类,有两种初始化方式:
使用指定的NSFetchRequest初始化:
public init(fetchRequest fetch: NSFetchRequest<NSFetchRequestResult>)
使用NSFetchRequest时,可以和常规操作一样,查询特定的数据进行批量删除,因为不关心NSFetchRequest的返回数据处理,所以我们只需要设置好查询条件即可:
@discardableResult
/// 删除
public func delete<T:NSManagedObject>(_ predicateString: String = "") throws -> [NSManagedObjectID] {
let fetchRequest:NSFetchRequest<T> = T.fetchRequest() as? NSFetchRequest<T> ?? NSFetchRequest<T>.init(entityName: "\(T.self)")
if !predicateString.isEmpty {
fetchRequest.predicate = NSPredicate.init(format: predicateString,"")
}
let batchDeleteRequest = NSBatchDeleteRequest.init(fetchRequest: fetchRequest as! NSFetchRequest<NSFetchRequestResult>)
batchDeleteRequest.resultType = .resultTypeObjectIDs
let result = try persistentContainer.viewContext.execute(batchDeleteRequest) as! NSBatchDeleteResult
let changedIds = result.result as? [NSManagedObjectID] ?? []
return changedIds
}
需要注意的是,批量删除的操作是直接在SQL层进行,不会同步到context里面,例如:
先创建一个user,保存到数据库
let user: LXUser = XXDataBase.createInContext()
user.id = "1"
XXDataBase.save()
如果通过常规删除:
viewContext.delete(item)
try? viewContext.save()
print("userId = \(user.id)")
此时控制台输出userId = nil,
如果通过批量删除:
XXDataBase.batchDelete(LXUser.self)
print("userId = \(user.id)")
控制台依然会输出userId的值:userId = Optional("1")
因此,如果使用批量删除时,需要在删除后进行一次上下文同步:
...
let changedIds = result.result as? [NSManagedObjectID] ?? []
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: changedIds
]
// 把所有删除的id,合并到上下文中,这样就会刷新上下文里面的对象并发送通知
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [persistentContainer.viewContext])
try saveContext()
同步之后再执行批量删除操作,则会输出userId = nil
使用特定的[NSManagedObjectID]数组初始化:
public convenience init(objectIDs objects: [NSManagedObjectID])
此时使用时和上面一样,删除特定的一组NSManagedObjectID的对象。
需要注意:[NSManagedObjectID]里面的id,需要来自同一个的Entity
NSBatchInsertRequest 批量插入
批量插入是iOS13以后新增的类,可以绕过内存直接把一组[String: Any]对象存入数据库,因为不经过context,数据变更时不会触发Notification,节省了大量时间
@discardableResult
/// 插入
public func batchInsert<T:NSManagedObject>(_ entity: T.Type, objects: [[String: Any]]) throws -> [NSManagedObjectID] {
let insert = NSBatchInsertRequest.init(entityName: "\(T.self)", objects: objects)
insert.resultType = .objectIDs
let result = try persistentContainer.viewContext.execute(insert) as! NSBatchInsertResult
return result.result as? [NSManagedObjectID] ?? []
}
这样批量保存数据时非常有优势,而且不用去上下文创建一个个对象:
let ids = XXDataBase.batchInsert(LXUser.self, objects: [
["id":"0001","name":"张三","age":2],
["id":"0002","name":"张四","age":6],
["id":"0003","name":"张五","age":8],
["id":"0004","name":"张六","age":16],
["id":"0005","name":"张七","age":18],
])
ios14还新增了一套用block方式来进行批量插入初始化的api。
合并冲突处理
如果LXUser里面,id设置了唯一约束,但是需要添加的数据里面,有相同的id,如
[
["id":"0006","name":"张八","age":16],
["id":"0006","name":"张九","age":18],
]
context里面有一个合并策略属性mergePolicy,
viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
下面列举各个合并策略以及结果:
| 策略单例 | 说明 | 数据库最终结果 |
|---|---|---|
| NSErrorMergePolicy | 默认合并策略,冲突时返回错误,错误信息包含(NSInsertedObjectsKey, NSUpdatedObjectsKey) | 张八 |
| NSMergeByPropertyStoreTrumpMergePolicy | 单独属性合并,当外部修改和内存修改同时发生,外部修改覆盖内存修改 | 张八 |
| NSMergeByPropertyObjectTrumpMergePolicy | 单独属性合并,当外部修改和内存修改同时发生,内存修改覆盖外部修改 | 张九 |
| NSOverwriteMergePolicy | 将当前的对象的状态写入数据库 | 张九 |
| NSRollbackMergePolicy | 丢弃冲突中所有的修改,保持数据库版本不变 | 张八 |
NSBatchUpdateRequest批量更新
可以直接批量更新符合指定条件的数据库数据的某一列或者多列:
@discardableResult
/// 批量更新
public func batchUpdate<T:NSManagedObject>(_ entity: T.Type, propertiesToUpdate: [AnyHashable : Any], predicateString: String = "") throws -> [NSManagedObjectID] {
let update = NSBatchUpdateRequest.init(entityName: "\(T.self)")
if !predicateString.isEmpty {
update.predicate = NSPredicate.init(format: predicateString,"")
}
update.resultType = .updatedObjectIDsResultType
update.propertiesToUpdate = propertiesToUpdate
let result = try persistentContainer.viewContext.execute(update) as! NSBatchUpdateResult
return result.result as? [NSManagedObjectID] ?? []
}
比如,把年龄小于10的对象名字都改为李四:
let user: [LXUser] = XXDataBase.query("age < 10")
print("批量更新前\(user.map({$0.name ?? ""}))")
let ids = XXDataBase.batchUpdate(LXUser.self, propertiesToUpdate: ["name":"李四"], predicateString: "age < 10")
print("批量更新后\(user.map({$0.name ?? ""}))")
打印日志如下:
批量更新前["张三", "张四", "张五"]
批量更新后["张三", "张四", "张五"]
根据日志可以发现,和批量删除时情况一致,上下文中的对象并不会改变,需要手动去更新上下文:
@discardableResult
/// 批量更新
public func batchUpdate<T:NSManagedObject>(_ entity: T.Type, propertiesToUpdate: [AnyHashable : Any], predicateString: String = "") throws -> [NSManagedObjectID] {
let update = NSBatchUpdateRequest.init(entityName: "\(T.self)")
if !predicateString.isEmpty {
update.predicate = NSPredicate.init(format: predicateString,"")
}
update.resultType = .updatedObjectIDsResultType
update.propertiesToUpdate = propertiesToUpdate
let result = try persistentContainer.viewContext.execute(update) as! NSBatchUpdateResult
let changedIds = result.result as? [NSManagedObjectID] ?? []
let changes: [AnyHashable: Any] = [
NSUpdatedObjectsKey: changedIds
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [persistentContainer.viewContext])
try saveContext()
return changedIds
}
需要注意:此处changes键值对里面的key,不是NSDeletedObjectsKey而是NSUpdatedObjectsKey。修改后输出日志如下:
批量更新前["张三", "张四", "张五"]
批量更新后["李四", "李四", "李四"]
因为批量操作可能导致context和数据库里面的内容不一致的问题,能用常规操作尽量使用常规操作。
之前看网上的文章说,批量删除操作并不会遵循实体的relationships规则,
即,如果我删除一个user,user的relationships关联了一组messages,即使设置的关联删除Cascade,数据库也不会自动删除messages,从而破坏表结构导致报错。
但是实际测试时(iPhone11, iOS15),关联的messages是会自动删除的,不知道其它系统版本是不是这样。
总结
CoreData本来也是拿着就用,没有想过那么多,以为掌握基础用法基本就够了。只是在实际使用场景中,使用了很多低效的数据处理方式。比如经常先查询拿到数据,再进行排序等低效操作。还有就是使用多个context去实现单独保存某一个对象,避免与全局context里面的数据冲突等操作。确实使用CoreData给开发工作带来了很多便利,同样也有很多地方需要深入研究,比如需要去重的时候,网上没找到资料,一个个属性查找才找到对应的属性。因为是自己摸索的,如果有什么错误或者疏漏,欢迎交流指正。