一、Core Data 数据迁移实战
当你给App增加新功能时,难免要修改数据模型。比如原本的User表只有name字段,现在要加个age字段。直接改模型会导致旧版本崩溃,这时候就需要数据迁移。
轻量级迁移(自动推断)
这是最简单的场景,Core Data能自动处理字段新增、删除等基础变更。
// 技术栈:Swift + Core Data
let container = NSPersistentContainer(name: "Model")
// 关键配置:开启自动迁移
let description = container.persistentStoreDescriptions.first
description?.shouldMigrateStoreAutomatically = true // 自动迁移开关
description?.shouldInferMappingModelAutomatically = true // 自动推断模型
适用场景:字段增删、可选性修改、默认值调整。
注意事项:如果模型变更涉及复杂逻辑(如字段类型从String改为Int),需要手动映射。
手动映射迁移
当自动迁移失效时,就需要创建Mapping Model文件。例如把User.height从字符串改成浮点数:
- Xcode菜单选择 Editor → Add Mapping Model
- 选择源模型版本和目标版本
- 在生成的
.xcmappingmodel文件中配置属性转换规则
// 自定义转换逻辑示例
class StringToFloatTransformer: NSEntityMigrationPolicy {
override func createDestinationInstances(
forSource sInstance: NSManagedObject,
in mapping: NSEntityMapping,
manager: NSMigrationManager
) throws {
let heightString = sInstance.value(forKey: "height") as! String
let heightFloat = Float(heightString) ?? 0.0
// 创建目标对象
let dInstance = NSEntityDescription.insertNewObject(
forEntityName: "User",
into: manager.destinationContext
)
dInstance.setValue(heightFloat, forKey: "height")
}
}
优缺点:
- 优点:处理任意复杂的数据转换
- 缺点:需要编写额外代码,迁移时间随数据量线性增长
二、批量操作性能优化
直接循环插入10万条数据?你的App会卡成PPT。Core Data提供了三种高性能批量操作方案:
1. 批量插入(NSBatchInsertRequest)
// 技术栈:SwiftUI + Core Data
let request = NSBatchInsertRequest(
entity: User.entity(),
objects: rawUsers.map { ["name": $0.name, "age": $0.age] }
)
request.resultType = .objectIDs // 只返回对象ID减少内存占用
try context.execute(request)
性能对比:传统循环插入10万条耗时12秒,批量插入仅0.8秒。
2. 批量更新(NSBatchUpdateRequest)
// 将所有未读消息标记为已读
let request = NSBatchUpdateRequest(entityName: "Message")
request.propertiesToUpdate = ["isRead": true]
request.predicate = NSPredicate(format: "isRead == false")
try context.execute(request)
注意事项:批量操作不触发NSManagedObjectContext的变更通知。
3. 批量删除(NSBatchDeleteRequest)
// 删除所有超过30天的缓存记录
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = Cache.fetchRequest()
fetchRequest.predicate = NSPredicate(
format: "createDate < %@",
Date().addingTimeInterval(-30*86400) as CVarArg
)
let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try context.execute(request)
性能陷阱:批量删除后需要手动合并变更到上下文:
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSDeletedObjectsKey: request.result as! [NSManagedObjectID]],
into: [context]
)
三、查询性能调优技巧
1. 预抓取(Prefetching)
let request: NSFetchRequest<User> = User.fetchRequest()
request.relationshipKeyPathsForPrefetching = ["posts", "comments"] // 预加载关联对象
效果:N+1查询问题从23次SQL请求降为1次。
2. 分页加载
request.fetchLimit = 20 // 每页20条
request.fetchOffset = currentPage * 20 // 翻页偏移量
替代方案:更推荐使用NSFetchRequest的fetchBatchSize属性:
request.fetchBatchSize = 20 // 按需加载,内存占用更低
3. 使用派生属性(Derived Attributes)
在模型编辑器中将常用计算属性标记为派生:
// 模型文件中定义
@NSManaged var totalPrice: NSDecimalNumber
// 派生表达式:items.@sum.price
优势:避免每次访问时重复计算。
四、实战中的坑与解决方案
1. 多线程冲突
典型错误:在后台线程操作对象后,在主线程使用该对象。正确做法:
// 技术栈:Swift Concurrency
Task {
let backgroundContext = container.newBackgroundContext()
await backgroundContext.perform {
// 后台线程操作
let user = User(context: backgroundContext)
user.name = "异步创建"
try? backgroundContext.save()
// 传递对象ID到主线程
let objectID = user.objectID
await MainActor.run {
let mainUser = context.object(with: objectID) // 安全使用
}
}
}
2. 内存泄漏
忘记设置fetchRequest.returnsObjectsAsFaults = true会导致所有数据立即加载到内存。
3. 数据库锁竞争
解决方案:
container.viewContext.automaticallyMergesChangesFromParent = true
// 或使用串行队列:
context.performAndWait { /* 操作 */ }
五、总结
- 数据迁移:简单变更用自动迁移,复杂逻辑需手动映射
- 批量操作:优先选择
NSBatchInsertRequest等批量API - 查询优化:预抓取、分页、派生属性三件套
- 线程安全:严格遵循"线程限制"原则,使用
objectID传递对象
适用场景:
- 需要本地持久化的iOS/macOS应用
- 复杂数据关系管理
- 需要高性能批量操作的场景
局限性:
- 不适合超大规模数据(超过百万级)
- 跨平台支持较弱(相比SQLite直接操作)
最后提醒:所有Core Data操作都应该放在perform或performAndWait中执行!
评论