前语

在这之前,我发布了一篇文章,在其中解说了怎么运用映射模型和自界说搬迁策略履行复杂的 Core Data 搬迁。尽管这种办法性能杰出且运行杰出,但很难保护,不适用于应用程序扩展,并且存在高度的过错危险。

例如,对于每个需要自界说搬迁的新模型,你需要界说一个映射模型,以界说怎么将每个模型的现有版别搬迁到新版别。与你可能认为的相反(以及我所认为的),Core Data 在跨多个版别进行搬迁时并不会按次序迭代映射模型,相反,它需要从当时版别到新版别的准确模型。

除此之外,你需要运用 Xcode 的 UI 和映射模型来界说一切这些内容,这使得 PR 难以审查,过错难以发现。出于这些原因,我最近从头设计了咱们的搬迁流程,改用分阶段搬迁,对开发者体会产生了巨大的影响!

什么是分阶段搬迁?

正如在 WWDC23 中宣告的那样,与在 Swift 数据模型之间履行搬迁的办法十分类似,你现在能够运用 NSStagedMigrationManager 实例以编程办法界说 Core Data 搬迁。

该办法经过界说一系列搬迁过程(称为阶段),描绘了怎么在模型的不同版别之间进行搬迁。

例如,假定你的应用程序当时正在运用数据模型的第 1 版,你想要搬迁到第 3 版。搬迁管理器将次序应用一切必要的阶段,以从第 1 版搬迁到第 2 版,然后从第 2 版搬迁到第 3 版。

提供一些布景信息

为了演示 Core Data 分阶段搬迁的作业原理,我将运用我之前在有关运用映射模型进行自界说 Core Data 搬迁的文章中运用的相同示例。

与之前的文章一样,咱们想要将 Track 模型中的 json 特点转化为一个单独的实体,该实体将为每个曲目保存一切相关的艺术家信息。将此特点转化也将使模型更灵敏、更易于保护,因为咱们将能够删去 json 特点本身和 artistName,而运用新的联系。

让咱们比较一下咱们的 Track 模型之前和之后的情况,CoreData.swift 文件代码如下:

Copy code
CoreData.swift
// Before
import Foundation
import CoreData
@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }
    @NSManaged public var imageURL: String?
    @NSManaged public var json: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artistName: String?
}
// After
@objc(Track)
public class Track: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Track> {
        return NSFetchRequest<Track>(entityName: "Track")
    }
    @NSManaged public var imageURL: String?
    @NSManaged public var lastPlayedAt: Date?
    @NSManaged public var title: String?
    @NSManaged public var artists: NSSet?
    @objc(addArtistsObject:)
    @NSManaged public func addToArtists(_ value: Artist)
    @objc(removeArtistsObject:)
    @NSManaged public func removeFromArtists(_ value: Artist)
    @objc(addArtists:)
    @NSManaged public func addToArtists(_ values: NSSet)
    @objc(removeArtists:)
    @NSManaged public func removeFromArtists(_ values: NSSet)
}
@objc(Artist)
public class Artist: NSManagedObject, Identifiable {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Artist> {
        return NSFetchRequest<Artist>(entityName: "Artist")
    }
    @NSManaged public var name: String?
    @NSManaged public var id: String?
    @NSManaged public var imageURL: String?
    @NSManaged public var tracks: NSSet?
    @objc(addTracksObject:)
    @NSManaged public func addToTracks(_ value: Track)
    @objc(removeTracksObject:)
    @NSManaged public func removeFromTracks(_ value: Track)
    @objc(addTracks:)
    @NSManaged public func addToTracks(_ values: NSSet)
    @objc(removeTracks:)
    @NSManaged public func removeFromTracks(_ values: NSSet)
}

从上面的代码中能够看出,搬迁并不是微乎其微的,而且,对咱们来说,Core Data 不能自动揣度它。让咱们看看怎么运用分阶段搬迁以代码方式界说搬迁过程。

创立搬迁管理器

要界说咱们的阶段,咱们需要将咱们的模型拆分为三个不同的模型版别和搬迁:

  1. 保持原始模型版别不变。
  2. 第二个模型版别包含一切特点,并添加 Artist 实体和联系。这将是一个自界说阶段。
  3. 第三个模型版别删去了 jsonartistName 特点。这将是一个轻量级的阶段。

咱们需要将搬迁分化为三个阶段的原因是,就现在而言,咱们不能在同一个阶段中运用并删去特点。

让咱们从创立一个担任创立 NSStagedMigrationManager 实例并界说一切阶段的工厂类开始。StagedMigrationFactory.swift 文件代码如下:

import Foundation
import CoreData
import OSLog
// 1
extension Logger {
    private static var subsystem = "dev.polpiella.CustomMigration"
    static let storage = Logger(subsystem: subsystem, category: "Storage")
}
// 2
extension NSManagedObjectModelReference {
    convenience init(in database: URL, modelName: String) {
        let modelURL = database.appending(component: "(modelName).mom")
        guard let model = NSManagedObjectModel(contentsOf: modelURL) else { fatalError() }
        self.init(model: model, versionChecksum: model.versionChecksum)
    }
}
// 3
final class StagedMigrationFactory {
    private let databaseURL: URL
    private let jsonDecoder: JSONDecoder
    private let logger: Logger
    init?(
        bundle: Bundle = .main,
        jsonDecoder: JSONDecoder = JSONDecoder(),
        logger: Logger = .storage
    ) {
        // 4
        guard let databaseURL = bundle.url(forResource: "CustomMigration", withExtension: "momd") else { return nil }
        self.databaseURL = databaseURL
        self.jsonDecoder = jsonDecoder
        self.logger = logger
    }
    // 5
    func create() -> NSStagedMigrationManager {
        let allStages = [
            v1toV2(),
            v2toV3()
        ]
        return NSStagedMigrationManager(allStages)
    }
    // 6
    private func v1toV2() -> NSCustomMigrationStage {
        struct Song: Decodable {
            let artists: [Artist]
            struct Artist: Decodable {
                let id: String
                let name: String
                let imageURL: String
            }
        }
        // 7
        let customMigrationStage = NSCustomMigrationStage(
            migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration"),
            to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2")
        )
        // 8
        customMigrationStage.didMigrateHandler = { migrationManager, currentStage in
            guard let container = migrationManager.container else {
                return
            }
            // 9
            let context = container.newBackgroundContext()
            context.performAndWait {
                let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Track")
                fetchRequest.predicate = NSPredicate(format: "json != nil")
                do {
                    let allTracks = try context.fetch(fetchRequest)
                    let addedArtists = [String: NSManagedObject]()
                    for track in allTracks {
                        if let jsonString = track.value(forKey: "json") as? String {
                            let jsonData = Data(jsonString.utf8)
                            let object = try? self.jsonDecoder.decode(Song.self, from: jsonData)
                            let artists: [NSManagedObject] = object?.artists.map { jsonArtist in
                                if let matchedArtist = addedArtists[jsonArtist.id] {
                                    return matchedArtist
                                }
                                let artist = NSEntityDescription
                                    .insertNewObject(
                                        forEntityName: "Artist",
                                        into: context
                                    )
                                artist.setValue(jsonArtist.name, forKey: "name")
                                artist.setValue(jsonArtist.imageURL, forKey: "imageURL")
                                artist.setValue(jsonArtist.id, forKey: "id")
                                return artist
                            } ?? []
                            track.setValue(Set<NSManagedObject>(artists), forKey: "artists")
                        }
                    }
                    try context.save()
                } catch {
                    logger.error("(error.localizedDescription)")
                }
            }
        }
        return customMigrationStage
    }
    // 10
    private func v2toV3() -> NSCustomMigrationStage {
        NSCustomMigrationStage(
            migratingFrom: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 2"),
            to: NSManagedObjectModelReference(in: databaseURL, modelName: "CustomMigration 3")
        )
    }
}

回到上面的代码,让咱们逐步分化:

  1. 咱们界说了一个自界说记录器,以将搬迁过程中产生的任何过错报告到控制台。
  2. 咱们扩展了 NSManagedObjectModelReference,创立了一个方便的初始化办法,它承受数据库 URL 和模型称号,并回来一个新的 NSManagedObjectModelReference 实例。
  3. 咱们界说了一个工厂类,担任创立 NSStagedMigrationManager 实例并界说一切阶段。
  4. 咱们运用 bundle 初始化工厂,并检索数据库的 URL、JSON 解码器和记录器。
  5. 咱们创立了 NSStagedMigrationManager 实例,并界说了一切阶段。
  6. 咱们界说了一个办法,该办法将回来从咱们模型的第 1 版搬迁到第 2 版的搬迁阶段。
  7. 咱们创立了一个 NSCustomMigrationStage 实例,并传递咱们要从何处搬迁和搬迁到的目标模型引用。文件名需要与包中的 .mom 文件的称号匹配。
  8. 咱们界说了 didMigrateHandler 闭包,在模型搬迁后调用。此刻,新的模型版别可在上下文中运用,你能够填充其特点。你有必要知道,还有一个在从前模型版别上履行的单独处理程序,称为 willMigrateHandler,但咱们在这种情况下不会运用它。
  9. 咱们创立了一个新的后台上下文,并获取一切具有 json 特点的曲目。然后,咱们将 JSON 字符串解码为 Song 目标,并为 JSON 中的每个艺术家创立一个新的 Artist 实体。然后,咱们将 Track 实体的 artists 联系设置为新的 Artist 实体。
  10. 咱们界说了一个办法,该办法将回来从咱们模型的第 2 版搬迁到第 3 版的搬迁阶段。这个搬迁十分简略,事实上,它应该是一个轻量级的搬迁。然而,我找不到一个能够在一切情况下运用的 NSLightweightMigrationStage 实例的办法。如果你知道怎么做,请告诉我!

设置运用 Core Data 栈。

设置运用分阶段搬迁的 Core Data 栈。

现在咱们有了创立 NSStagedMigrationManager 实例的办法,咱们需要设置咱们的 Core Data 栈以运用它。PersistenceController.swift 文件代码如下:

PersistenceController.swift
import CoreData
struct PersistenceController {
    static let shared = PersistenceController()
    let container: NSPersistentContainer
    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "CustomMigration")
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
        if let description = container.persistentStoreDescriptions.first {
            if let migrationFactory = StagedMigrationFactory() {
                description.setOption(migrationFactory.create(), forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
            }
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error (error), (error.userInfo)")
            }
        })
    }
}

这部分十分简略,你只需要将 NSStagedMigrationManager 实例设置为持久化存储描绘的选项。

总结

这篇文章介绍了运用分阶段搬迁来改进 Core Data 搬迁流程的重要性和办法。传统的搬迁办法运用映射模型,但这种办法不易保护,扩展性差且简略出错。分阶段搬迁经过界说一系列搬迁过程,使得在不同模型版别之间进行搬迁变得愈加简略和可控。文章以一个示例来阐明分阶段搬迁的作业原理,以及怎么以代码方式界说搬迁过程。最终,文章展现了怎么设置运用分阶段搬迁的 Core Data 栈。经过运用分阶段搬迁,能够明显提高开发者体会,简化搬迁流程,并下降过错危险。