虽然在 WWDC 2023 上,苹果将首要精力放在介绍新的数据框架 SwiftData 上,但作为 SwiftData 的基石,Core Data 也得到了必定程度上的功用增强。本文将介绍本年 Core Data 取得的新功用。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的大众号:【肘子的Swift记事本】

复合特点( Composite attributes)

复合特点是 Core Data 为实体(Entity)供给的一种新的自界说特点(Attribute)。经过它,开发者能够将杂乱的数据类型经过自界说的办法封装在一起。

举个例子,咱们有一个餐厅(Restaurant)实体:

public class Restaurant:NSManagedObject {
    @NSManaged public var address: String?
    @NSManaged public var name: String?
    @NSManaged public var phoneNumber: String?
    @NSManaged public var rating: Double
}

在复合特点呈现之前,咱们有三种可选计划来为该餐厅增加其经纬度信息:

  1. 别离创立经度和纬度特点,并创立一个核算特点 location 以改进代码的可读性。
  2. 创立一个 Location 实体,包含经度和纬度两个特点,并在 Restaurant 实体与 Location 实体之间创立1对1联系。
  3. 创立一个 Location 结构,在 Restaurant 实体中将其声明为 Transformable 特点。

这三种计划都有各自的优缺点:

  • 计划一:功用最佳,经纬度特点都能够独自作为谓词的判别条件。但当有多个实体都有相同的需求时,需求为每个实体进行重复的设置作业。复合类型(例如:Location)越杂乱,需求进行的重复操作也越多。
  • 计划二:经纬度特点都能够独自作为谓词的判别条件,但相对于第一种计划,在检索时功用稍有下降。
  • 计划三:经纬度不能作为谓词的判别条件(数据已转换为不行检索的状态),在保存和读取数据时,会因编解码而有必定的功用损失。

Composite attributes 为开发者供给了一个全新的选择。

首要咱们需求在 Xcode 的 Data Model Editor 中自界说一个 Composite Attributes。

WWDC 2023 Core Data 有哪些新变化

然后,选用与界说 Entity 相似的办法,在自界说的 Composite Attributes 中增加特点。

WWDC 2023 Core Data 有哪些新变化

在界说 Composite Attributes 时,咱们能够运用 Core Data 为 Entity 供给的任意特点,例如 String、Double、Date 等,一起也能够运用其他现已界说好的 Composite Attributes。支持嵌套也是 Composite Attributes 的一个非常明显的特点。

终究,咱们便能够在 Entity 中,像运用其他 Core Data 供给的预置特点相同,运用自界说的 Composite Attributes 了。

WWDC 2023 Core Data 有哪些新变化

需求留意的是,自界说的 Composite Attributes 仅仅一种对 Entity 特点类型的抽象描绘,Core Data 并不会在代码中为其生成对应的类型。在 SQLite 中,Composite Attributes 选用了与计划一相同的存储形式(在 Entity 对应的表中,将 Composite Attributes 的一切特点打开,并为其创立独立的字段)。

WWDC 2023 Core Data 有哪些新变化

在代码中 Composite Attributes 被声明为 [String:Any]? 类型:

public class Restaurant:NSManagedObject {
    @NSManaged public var address: String?
    @NSManaged public var name: String?
    @NSManaged public var phoneNumber: String?
    @NSManaged public var rating: Double
    @NSManaged public var location: [String: Any]?
}

目前,咱们仍需经过字典的办法在保管目标中设置和读取该特点的内容:

let newRestaurant = Restaurant(context: viewContext)
newRestaurant.address = address
newRestaurant.name = name
newRestaurant.phoneNumber = phoneNumber
newRestaurant.rating = rating
newRestaurant.location = [
    "latitude": 39.90469,
    "longitude": 116.40528,
]

但是,在设置谓词时,能够直接运用带有命名空间的 keyPath 办法进行拜访:

let predicate = NSPredicate(format:"location.latitude > %f AND location.latitude < %f",31.3,40.0)

提示:在官方有关 Composite Attributes 的 文档 中,呈现了如下的演示代码。咱们希望在之后的更新中,能够经过这种办法直接拜访 Composite Attributes 中的子特点。

// Use property-like setters and getters to manage the underlying attributes directly.
quake.magnitude.richter = 4.6
print(quake.magnitude.richter)

在 Core Data 中运用新的 Predicate

长久以来,Core Data 开发人员一直希望能够以愈加 Swift 的办法创立安全易懂的 Predicate。在本年,这个愿望总算因 Foundation 的 Swift 化得以完成。

开发人员能够经过以下办法为 SwiftData 创立谓词:

let today = Date()
let tripPredicate = #Predicate<Trip> { 
    $0.destination == "New York" &&
    $0.name.contains("birthday") &&
    $0.startDate > today
}

幸亏的是,在这次 Predicate 的更新中,Core Data 没有被扔掉。开发者能够经过新的 NSPredicate 构建办法,将 Predicate 转换为 NSPredicate。

例如:

let p = #Predicate<Restaurant>{
    $0.rating > 3.5
}
let predicate = NSPredicate(p)

需求留意两点:

  • 只有为 NSObject 的子类创立的 Predicate 才能转换为 NSPredicate,也就是说,为 SwiftData 创立的 Predicate 是无法转换成 Core Data 对应的保管目标可用的谓词。
  • 目前还无法在 Predicate 中直接经过 keyPath 的办法拜访 Composite Attributes 的特点。

VersionChecksum

在本年,NSManagedObjectModel 增加了一个新的特点 versionChecksum。该特点对应该数据模型的 Base64 编码的 128 位模型版别哈希值。

此值也可在版别化模型的VersionInfo.plist文件和 Xcode 的构建日志中找到。

这个值有两个作用:

  • 用于在阶段式搬迁中,为不同版别的数据模型创立 NSManagedObjectModelReference,下文中有更多阐明。
  • 在 SwiftData 与 Core Data 并行的项目中,用于比对两者是否运用相同版别的数据模型。

例如,咱们能够经过下面的代码获取 SwiftData 当时运用的模型 versionChecksum 值,然后在 CoreDataStack 中,经过与该值进行比较,就能够知道两者是否运用相同的数据模型。

@main
struct PredicateTestApp: App {
    let container = try! ModelContainer(
        for: Item.self
    )
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    if let versionChecksum = container.schema.makeManagedObjectModel()?.versionChecksum {
                        print(versionChecksum)
                    }
                }
        }
        .modelContainer(container)
    }
}

推迟搬迁(Deferred migration)

在 Core Data 进行数据模型搬迁的过程中,假如数据量很大,搬迁操作很杂乱,应用会呈现 UI 无法响应的情况,给用户带来欠好的用户体会。

在本次 Core Data 的更新中,苹果为 Core Data 增加了推迟搬迁(Deferred migration)的功用,能够在某种程度上缓解因上述原因导致的运用者不适。

留意事项:

  • 推迟搬迁只能针对轻量级搬迁过程中的部分操作。
  • 任何或许导致数据模型不兼容的操作都不能被推迟。
  • 推迟搬迁仅适用于 SQLite 存储类型。
  • 推迟搬迁具备向后兼容性,可追溯到 iOS 14 以及 Big Sur。
  • 推迟搬迁相同适用于本年新增的阶段式搬迁。

用更容易了解的办法来说:在敞开推迟搬迁功用后,Core Data 会从轻量级搬迁的操作中判别哪些操作即便在搬迁的过程中暂不进行,也不会影响应用对终究完结的数据模型版别数据库的操作(例如,更新索引、删去现已不需求的特点,从有序联系更改为非有序联系等操作)。Core Data 将先越过这些操作,直到开发者在应用中找到合适的机遇,经过代码显式地履行这些“善后”操作。

要敞开推迟搬迁功用,需求在存储选项中将 NSPersistentStoreDeferredLightweightMigrationOptionKey 设置为 true。

let options = [
    NSPersistentStoreDeferredLightweightMigrationOptionKey: true,
    NSMigratePersistentStoresAutomaticallyOption: true,
    NSInferMappingModelAutomaticallyOption: true
]
let store = try coordinator.addPersistentStore(
    ofType: NSSQLiteStoreType,
    configurationName: nil,
    at: storeURL,
    options: options
)

在必要的搬迁操作完结后,开发者能够在恰当的机遇经过调用finishDeferredLightweightMigration办法履行”整理”作业(苹果建议在BGProcessingTask中进行):

let metadata = coordinator.metadata(for: store)
if (metadata[NSPersistentStoreDeferredLightweightMigrationOptionKey] == true) {
    coordinator.finishDeferredLightweightMigration()
}

阶段式搬迁( Staged migration )

在当时版别的 Core Data 中,开发者最常运用以下三种数据搬迁办法:

  • 轻量级搬迁

假如两个数据模型版别之间的更改很简单,Core Data 能够自行揣度映射模型,那么无需开发者供给更多的信息,Core Data 将主动在两个版别中进行数据搬迁。

  • 自界说映射模型

假如开发者对数据模型做了更深层次的调整,导致 Core Data 无法主动揣度映射模型,此时,开发者能够经过 Xcode Model Editor 创立一个针对两个特定版别之间的映射模型( Mapping Model),经过自界说映射模型中供给的额定信息,帮助 Core Data 完结在两个版别间的数据搬迁操作。

  • 自界说实体映射战略

假如自界说映射模型供给的表达式仍无法满意搬迁的需求,开发者则需求创立自界说实体映射战略(NSEntityMigrationPolicy 的子类),NSEntityMigrationPolicy 供给了一些办法,用于掩盖默认的数据搬迁操作。

虽然 Core Data 自身供给了一种高度可控的渐进式搬迁办法,但因为其对开发人员不太友爱,需求编写大量代码,因而在实际开发中很少运用。

因为 SwiftData 并不运用 Xcode 的 Model Editor,因而苹果需求为其供给一种不依赖 Mapping Model 文件的搬迁办法。一起原有的编写自界说实体映射战略的办法对开发者也不太友爱。为此,SwiftData 运用了根据阶段式搬迁(Stage migration)的数据搬迁办法。作为 SwiftData 的基础,Core Data 自然也新增了该搬迁形式。

本文不会对阶段式搬迁进行详尽的阐明,未来会经过其他文章对其进行深入探讨。

阶段式搬迁包含两种搬迁形式:轻量级搬迁(NSLightweightMigrationStage)和自界说搬迁(NSCustomMigrationStage)。它鼓舞开发者将非轻量级搬迁的搬迁使命分化一系列的轻量级搬迁的过程。经过创立多个阶段,用最少的代码量,将数据模型搬迁到最新版别。

一般来说,阶段性搬迁分为以下几个过程:

描绘数据模型版别的许诺。

经过为分阶段搬迁声明多个 NSManagedObjectModelReference 类,描绘特定版别的 NSManagedObjectModel 许诺。在搬迁期间,Core Data 将完成此许诺。

let v1ModelChecksum = "kk8XL4OkE7gYLFHTrH6W+EhTw8w14uq1klkVRPiuiAk="
let v1ModelReference = NSManagedObjectModelReference(
    modelName: "modelV1"
    in: NSBundle.mainBundle
    versionChecksum: v1ModelChecksum
)
let v2ModelChecksum = "PA0Gbxs46liWKg7/aZMCBtu9vVIF6MlskbhhjrCd7ms="
let v2ModelReference = NSManagedObjectModelReference(
    modelName: "modelV2"                          
    in: NSBundle.mainBundle                                                 
    versionChecksum: v2ModelChecksum
)
let v3ModelChecksum = "iWKg7bxs46g7liWkk8XL4OkE7gYL/FHTrH6WF23Jhhs="
let v3ModelReference = NSManagedObjectModelReference(
    modelName: "modelV3"
    in: NSBundle.mainBundle
    versionChecksum: v3ModelChecksum
)

上述代码为三个不同版别的数据模型创立了许诺。Core Data 会经过检查 versionChecksum 来保证数据版别的正确性。

因为 SwiftData 并不依赖数模模型文件,因而在 SwiftData 中,许诺的办法略有不同(经过代码将每个版别的模型表述出来)。

enum SampleTripsSchemaV1: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }
    @Model
    final class Trip {
        var name: String
        var destination: String
        var start_date: Date
        var end_date: Date
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }
    // Define the other models in this version...
}
enum SampleTripsSchemaV2: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }
    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        var start_date: Date
        var end_date: Date
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }
    // Define the other models in this version...
}
enum SampleTripsSchemaV3: VersionedSchema {
    static var models: [any PersistentModel.Type] {
        [Trip.self, BucketListItem.self, LivingAccommodation.self]
    }
    @Model
    final class Trip {
        @Attribute(.unique) var name: String
        var destination: String
        @Attribute(originalName: "start_date") var startDate: Date
        @Attribute(originalName: "end_date") var endDate: Date
        var bucketList: [BucketListItem]? = []
        var livingAccommodation: LivingAccommodation?
    }
    // Define the other models in this version...
}

描绘所需的搬迁阶段

在上文中,咱们创立了用于 Core Data 阶段式搬迁的三个版别许诺:V1、V2、V3。因而,咱们需求描绘两个搬迁阶段:V1 → V2,V2 → V3。

let lightweightStage = NSLightweightMigrationStage([v1ModelChecksum])
lightweightStage.label = "V1 to V2: Add flightData attribute"
let customStage = NSCustomMigrationStage(
    migratingFrom: v2ModelReference,
    to: v3ModelReference
)
customStage.label = "V2 to V3: Denormalize model with FlightData entity"

在从 V1 搬迁到 V2 时,咱们认为主动搬迁现已能够满意需求,因而不需求供给其他辅助代码。但是在从 V2 搬迁到 V3 时,咱们考虑到主动搬迁无法满意要求,因而需求经过自界说代码来进行搬迁。

customStage.willMigrateHandler = { migrationManager, currentStage in
    guard let container = migrationManager.container else {
        return
    }
    let context = container.newBackgroundContext()
    try context.performAndWait {
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Aircraft")
        fetchRequest.predicate = NSPredicate(format: "flightData != nil")
        do {
           var fetchedResults: [NSManagedObject]
           fetchedResults = try viewContext.fetch(fetchRequest)
           for airplane in fetchedResults {
                let fdEntity = NSEntityDescription.insertNewObject(
                    forEntityName: "FlightData,
                    into: context
                )
                let flightData = airplane.value(forKey: "flightData")
                fdEntity.setValue(flightData, forKey: “data”)
                fdEntity.setValue(airplane, forKey: "aircraft")
                airplane.setValue(nil, forKey: "flightData")
            }
            try context.save()
        } catch {
            // Handle any errors that may occur
        }
    }
}

在上述代码中,经过调用开发者供给的 willMigrateHandler 闭包,在履行从 V2 版别搬迁到 V3 版别的操作前读取了原有数据的 airplane 特点(该特点为 Transformable 类型)。咱们运用 airplane 中的数据创立了新的 FlightData 实体(该实体与 Aircraft 是1对1的联系)。

SwiftData 中也有相似的对应操作:

enum SampleTripsMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
    }
    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }
    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: SampleTripsSchemaV1.self,
        toVersion: SampleTripsSchemaV2.self,
        willMigrate: { context in
            let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())
            // De-duplicate Trip instances here...
            try? context.save() 
        }, didMigrate: nil
    )
    static let migrateV2toV3 = MigrationStage.lightweight(
        fromVersion: SampleTripsSchemaV2.self,
        toVersion: SampleTripsSchemaV3.self
    )
}

启用阶段式搬迁操作

创立一个带有轻量级搬迁阶段和自界说搬迁阶段的 NSStagedMigrationManager,并将其增加到 NSPersistentStoreDescription 选项中,从而让 Core Data 敞开阶段式搬迁操作。

let migrationStages = [lightweightStage, customStage]
let migrationManager = NSStagedMigrationManager(migrationStages)
let persistentContainer = NSPersistentContainer(
    path: "/path/to/store.sqlite",
    managedObjectModel: myModel
)
var storeDescription = persistentContainer?.persistentStoreDescriptions.first
storeDescription?.setOption(
    migrationManager,
    forKey: NSPersistentStoreStagedMigrationManagerOptionKey
)
persistentContainer?.loadPersistentStores { storeDescription, error in
    if let error = error {
        // Handle any errors that may occur
    }
}

在 SwiftData 中设置阶段式搬迁的代码:

struct TripsApp: App {
    let container = ModelContainer(
        for: Trip.self, 
        migrationPlan: SampleTripsMigrationPlan.self
    )
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

相较于之前的搬迁办法,阶段式搬迁具有更清晰的条理;完成自界说搬迁操作的代码量更少,难度也更低。但是,从另一个方面来说,这也需求开发者对搬迁过程有更多的了解,并根据需求及时创立数据模型(将非轻量级搬迁的搬迁使命及时分化成一系列轻量级搬迁过程)。

终究

作为 SwiftData 的基石,苹果在未来的几年中不论是有意仍是无意,仍会持续为 Core Data 增加一些新的 API。考虑到 SwiftData 还需求几年的老练时刻,因而在未来的日子里,许多开发者都需求在一个项目中一起运用 SwiftData 和 Core Data。因而,及时了解 Core Data 的新功用和新动向仍有相当的价值。

欢迎你经过 Twitter、 Discord 频道 或博客的留言板与我进行沟通

订阅下方的 邮件列表,能够及时取得每周最新文章。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的大众号:【肘子的Swift记事本】