一、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从字符串改成浮点数:

  1. Xcode菜单选择 Editor → Add Mapping Model
  2. 选择源模型版本和目标版本
  3. 在生成的.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 // 翻页偏移量

替代方案:更推荐使用NSFetchRequestfetchBatchSize属性:

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 { /* 操作 */ }

五、总结

  1. 数据迁移:简单变更用自动迁移,复杂逻辑需手动映射
  2. 批量操作:优先选择NSBatchInsertRequest等批量API
  3. 查询优化:预抓取、分页、派生属性三件套
  4. 线程安全:严格遵循"线程限制"原则,使用objectID传递对象

适用场景

  • 需要本地持久化的iOS/macOS应用
  • 复杂数据关系管理
  • 需要高性能批量操作的场景

局限性

  • 不适合超大规模数据(超过百万级)
  • 跨平台支持较弱(相比SQLite直接操作)

最后提醒:所有Core Data操作都应该放在performperformAndWait中执行!