前语

跟着应用程序和用户群的增加,你需求增加新功用,删去其他功用,并改动应用程序的工作方法。这是软件开发生命周期的天然结果,咱们应该接受。

跟着应用程序的开展,你的数据模型也会发生变化。你需求更改数据结构的方法,以适应新功用,一起确保用户不会在不同版别之间丢掉任何数据。假如你运用 Core Data 在应用程序中持久化信息,那么 Core Data 搬迁就会发挥作用。

什么是 Core Data 搬迁?

Core Data 搬迁是将数据模型从一个版别更新到另一个版别的进程,由于数据的形状发生了变化(例如,增加或删去新特点)。

在大多数情况下,Core Data 将主动处理搬迁进程。可是,有些情况下,你需求经过供给一个映射模型来自界说搬迁进程,告知 Core Data 究竟怎么从源模型搬迁到方针模型中的每个特点和实体。

甚至有些情况下,映射模型是不够的,你需求编写自界说搬迁战略来处理特定情况。这是本文要要点评论的情况。

示例

让咱们考虑一个应用程序,在 Core Data 栈中存储表明音乐曲目的方针。模型非常简略,只包含一个实体:Track,Track.swift 代码如下:

Copy code
Track.swift
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?
}

上面的 Track 实体有五个特点:

  • imageURL:表明曲目封面图画的 URL 的字符串。
  • json:表明来自服务器的原始 JSON 数据呼应的字符串。
  • lastPlayedAt:表明上次播映曲目的日期。
  • title:表明曲目的标题的字符串。
  • artistName:表明艺术家的名称的字符串。

Core Data 栈不会与 iCloud 同步,并具有以下设置,CoreDataStack.swift 文件代码如下:

Copy code
CoreDataStack.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 {
            description.shouldMigrateStoreAutomatically = true
            description.shouldInferMappingModelAutomatically = false
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error (error), (error.userInfo)")
            }
        })
    }
}

假如你仔细观察上面的示例,你会注意到咱们告知 Core Data 主动搬迁存储,由于咱们不想做渐进式搬迁,这种搬迁速度慢得多且更复杂,而且咱们还告知 Core Data 不要主动揣度映射模型,这意味着咱们将不得不为每个搬迁供给一个映射模型文件,而且能够答应咱们自界说这个进程。

持久化了一首歌曲后,运用 Core Data Lab 查看数据库,咱们能够看到特点被相应保存:

Swift 定制 Core Data 搬迁

更新模型

当时版别的模型存在一些可扩展性问题:

  1. 模型仅答应每个曲目有一个艺术家,而实际上,一个曲目能够有多个艺术家。
  2. 模型存储一个表明曲目数据的原始 JSON 字符串,这不太高效,当应用程序需求解析 JSON 字符串以显现曲目数据以获取艺术家列表时,可能会导致性能问题。

为了处理这些问题,让咱们删去 artistNamejson 特点,采用一个新的 Artist 实体,该实体将与 Track 实体建立一对多的联系。

Artist 实体将具有一个表明艺术家名称的 name 特点,以及 idimageURL 特点,咱们将从原始 JSON 字符串中获取它们。

创立一个新的模型版别

首要,让咱们经过挑选 .xcdatamodeld 文件,然后从菜单栏中挑选 Editor > Add Model Version... 来创立一个新的模型版别。

Swift 定制 Core Data 搬迁

给它起一个名称,并以第一个模型版别为基础:

Swift 定制 Core Data 搬迁

现在,让咱们创立 Artist 实体并增加一切字段:

Swift 定制 Core Data 搬迁

也让咱们为新的 Artist 实体创立 NSManagedObject 子类,Artist.swift 代码如下:

Copy code
import Foundation
import CoreData
@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)
}

正如你在上面的示例中看到的那样,咱们将向 Track 实体增加一个对多的 artists 联系,还将向 Artist 实体增加一个对多的 tracks 联系。

现在,让咱们为 Track 实体增加缺失的联系,并删去 artistNamejson 特点:

Swift 定制 Core Data 搬迁

并更新 NSManagedObject 子类以反映更改,Track.swift 文件代码如下:

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 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)
}

最终但并非最不重要的,让咱们将新的模型设置为 .xcdatamodeld 文件的当时模型:

Swift 定制 Core Data 搬迁

创立映射模型

由于咱们告知 Core Data 不要主动揣度映射模型,所以咱们将不得不创立一个映射模型文件来在两个版别之间建立桥梁。

从菜单栏中挑选 File > New > File...,然后挑选 Mapping Model

Swift 定制 Core Data 搬迁

然后,挑选源模型:

Swift 定制 Core Data 搬迁

最终,挑选方针模型:

Swift 定制 Core Data 搬迁

编写自界说搬迁战略

默认情况下,Core Data 将尽力映射特点,而且大部分工作都将由它主动完结(包括已删去的特点)。

但是,由于咱们创立了一个新的实体,而且咱们希望保留现有数据,因而咱们需求告知 Core Data 怎么搬迁。

咱们将创立一个新的类,该类继承自 NSEntityMigrationPolicy,并在旧的 Track 实体上创立并链接一个新的联系到 Artist 实体,V2MigrationPolicy.swift 文件代码如下:

Copy code
import CoreData
struct Song: Decodable {
    let artists: [Artist]
    struct Artist: Decodable {
        let id: String
        let name: String
        let imageURL: String
    }
}
class V2MigrationPolicy: NSEntityMigrationPolicy {
    private let decoder = JSONDecoder()
    override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws {
        // 1
        let sourceKeys = sInstance.entity.attributesByName.keys
        let sourceValues = sInstance.dictionaryWithValues(forKeys: sourceKeys.map { $0 as String })
        // 2
        let destinationInstance = NSEntityDescription.insertNewObject(forEntityName: mapping.destinationEntityName!, into: manager.destinationContext)
        let destinationKeys = destinationInstance.entity.attributesByName.keys.map { $0 as String }
        // 3
        for key in destinationKeys {
            if let value = sourceValues[key] {
                destinationInstance.setValue(value, forKey: key)
            }
        }
        if let jsonString = sInstance.value(forKey: "json") as? String {
            // 3
            let jsonData = Data(jsonString.utf8)
            let object = try? decoder.decode(Song.self, from: jsonData)
            // 4
            let artists: [NSManagedObject] = object?.artists.map { jsonArtist in
                // 5
                let request = Artist.fetchRequest()
                request.fetchLimit = 1
                request.predicate = NSPredicate(format: "name == %@", jsonArtist.name)
                // Do not add duplicates to the list...
                if let matchedArtists = try? manager.destinationContext.fetch(request), let matchedArtist = matchedArtists.first {
                    return matchedArtist
                }
                // 6
                let artist = NSEntityDescription.insertNewObject(forEntityName: "Artist", into: manager.destinationContext)
                artist.setValue(jsonArtist.name, forKey: "name")
                artist.setValue(jsonArtist.imageURL, forKey: "imageURL")
                artist.setValue(jsonArtist.id, forKey: "id")
                return artist
            } ?? []
            // 7
            destinationInstance.setValue(Set<NSManagedObject>(artists), forKey: "artists")
        }
        // 8
        manager.associate(sourceInstance: sInstance, withDestinationInstance: destinationInstance, for: mapping)
    }
}

让咱们逐步解释上面的代码:

  1. 获取源实体的特点名称和值。
  2. 创立与源实体相同类型的全新方针实体。
  3. 将源实体的特点值复制到方针实体。
  4. 假如源实体具有 json 特点,则将其解析为 Song 方针。
  5. 为避免重复项,请查看艺术家是否现已存在于方针上下文中。
  6. 假如艺术家不存在,则创立一个新的 Artist 实体,将其插入到上下文中,并设置其特点。
  7. 设置方针实体上的新艺术家联系。
  8. 将源和方针实例相关起来。

最终,让咱们将此自界说战略增加到映射模型中:

Swift 定制 Core Data 搬迁

现在,假如咱们再次运转应用程序并运用 Core Data Lab 查看数据库,咱们能够看到一个新的实体现已填充了正确的数据。

总结

文章介绍了在应用程序开展进程中,数据模型可能需求进行更改的情况下,怎么运用 Core Data 搬迁来保持数据的一致性和完整性。首要,它解释了什么是 Core Data 搬迁,以及为什么需求进行搬迁。接着,经过一个示例应用程序,具体介绍了怎么更新数据模型,增加新实体和联系,以处理现有模型的可扩展性问题。然后,文章介绍了怎么创立映射模型来界说不同模型版别之间的映射联系,并演示了怎么编写自界说搬迁战略来处理特定情况,例如将旧模型数据搬迁到新模型的新联系中。最终,经过将自界说搬迁战略增加到映射模型中,完结了整个搬迁进程。