如何在Swift中迁移可编码数据模式

6tdlim6h  于 2023-01-16  发布在  Swift
关注(0)|答案(1)|浏览(164)

我很好奇,是否有一些最佳实践被公认为是在Swift中跨模式更改迁移可编码数据的好方法?
例如,我可能:

struct RecordV1: Codable {
    var name: String
}

struct RecordV2: Codable {
    var firstName: String   // Renamed old name field
    var lastName: String    // Added this field
}

我希望能够将保存为RecordV 1的内容加载到RecordV 2中。
我希望以这样一种方式实现我的数据结构,即所存储的内容具有嵌入其中的版本号,以便在将来,当加载数据时,当较新版本的代码与较新版本的数据一起工作时,某些机制将有机会将旧数据迁移到最近的模式中。我希望解决方案是相当优雅的,不涉及大量的重新输入样板代码。越快越好!

kmpatx3s

kmpatx3s1#

我已经搜索了很多地方,但是我还没有找到任何关于这个问题的解决方案。下面是我能想到的最好的解决方案。如果人们能提出替代方案(特别是使用合并,或者以更好的方式使用协议和泛型),我会很高兴。
原谅这篇文章的长度。我会把它分解成几个部分,但是把它们都粘贴到一个Swift Playground中,它应该会工作。
第一部分是定义可迁移结构的协议,定义标识MigratableData版本的方法,定义从以前版本保存的结构导入数据的初始化器,还有一个init(from: withVersion: using:),它爬上迁移链,解码正确版本的数据,然后将结构向前迁移到当前版本。
我实现了init(from: withVersion: using:)方法作为默认协议实现。我特别关注this article,它暗示这是一个坏主意。我很想知道如何避免这个问题。

import Foundation

protocol MigratableData: Codable {
    associatedtype PrevMD: MigratableData               // so we can refer to the previous MigratableData type being migrated from
    associatedtype CodingKeyEnum: CodingKey             // needed only for MDWrapper below

    static func getDataVersion() -> Int                 // basically an associated constant
    init(from: PrevMD)
    init(from: Data, withVersion dataVersion: Int, using: JSONDecoder) throws  // JSONDecode instead of decoder because .decode is not part of Decoder
}

extension MigratableData {
    // default implementation of init(from: withVersion: using:)
    init(from data: Data, withVersion dataVersion: Int, using decoder: JSONDecoder) throws {
        if Self.getDataVersion() == dataVersion {
            self = try decoder.decode(Self.self, from: data)
        } else if Self.getDataVersion() > dataVersion {
            self.init(from: try PrevMD(from: data, withVersion: dataVersion, using: decoder))
        } else {
            fatalError("Data is too new!")
        }
    }
}

一旦我们有了这个协议,就只需要定义一些结构并在它们之间建立连接--隐式地使用版本号和init(from:)

struct RecordV1: Codable {
    enum CodingKeys: CodingKey { case name }  // needed only for MDWrapper below

    var name: String
}

struct RecordV2: Codable {
    enum CodingKeys: CodingKey { case firstName, lastName }   // needed only for MDWrapper below

    var firstName: String   // Renamed old name field
    var lastName: String    // Added this field
}

extension RecordV1: MigratableData {
    typealias CodingKeyEnum = CodingKeys
    static func getDataVersion() -> Int { 1 }

    // We set up an "upgrade circularity" but it's safe and never gets invoked.
    init(from oldRecord: RecordV1) {
        fatalError("This should not be called")
    }
}

extension RecordV2: MigratableData {
    typealias CodingKeyEnum = CodingKeys
    static func getDataVersion() -> Int { 2 }
    
    init(from oldRecord: RecordV1) {
        self.firstName = oldRecord.name     // We do a key migration here
        self.lastName = "?"    // Since we have no way of generating anything
    }
}

为了使用它,并证明它是有效的,我们可以做以下几点:

let encoder = JSONEncoder()
let decoder = JSONDecoder()

// creating test data
let recordV1 = RecordV1(name: "John")
let recordV2 = RecordV2(firstName: "Johnny", lastName: "AppleSeed")

// converting it to Data that would be stored somewhere
let dataV1 = try encoder.encode(recordV1)
let dataV2 = try encoder.encode(recordV2)

// you can view as strings while debugging
let stringV1 = String(data: dataV1, encoding: .utf8)
let stringV2 = String(data: dataV2, encoding: .utf8)

// loading back from the data stores, migrating if needed.
let recordV1FromV1 = try RecordV1(from: dataV1, withVersion: 1, using: decoder)
let recordV2FromV1 = try RecordV2(from: dataV1, withVersion: 1, using: decoder)

let recordV2FromV2 = try RecordV2(from: dataV2, withVersion: 2, using: decoder)

所以撇开所有缺点不谈,上面的工作方式是我想要的。我只是不喜欢在加载时跟踪要迁移的版本(withVersion:参数)。理想情况下,版本号应该是保存的数据的一部分,这样它就可以被自动读取和应用。
到目前为止,我最好的解决方案是将数据结构 Package 在一个名为MDWrapper的泛型中:

// MDWrapper is short for MigratableDataWrapper
//
// It encapsulates the data and the version into one object that can be
// easily encoded/decoded with automatic migration of data schema from
// older versions, using the init(from: oldVersionData) initializers
// that are defined for each distinct MigratedData type.
struct MDWrapper<D: MigratableData> {
    var dataVersion: Int
    var data: D
    var stringData = ""     // Not ever set, read, or saved, but used as a placeholder for encode/decode
    
    init(data: D) {
        self.data = data
        self.dataVersion = D.getDataVersion()
    }
}

extension MDWrapper : Codable {
    
    enum CodingKeys: CodingKey { case stringData, dataVersion }
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(dataVersion, forKey: .dataVersion)
        
        // Here we encode the wrapped data into JSON Data, then hide it's
        // structure by encoding that data into a base64 string that's
        // fed into our wrapped data representation, and spit it out
        // in the stringData coding key. We never actually write out
        // a data field
        let jsonEncoder = JSONEncoder()
        let jsonData = try jsonEncoder.encode(data)
        let base64data = jsonData.base64EncodedString()
        
        try container.encode(base64data, forKey: .stringData)
    }

    init(from decoder: Decoder) throws {
        dataVersion = D.getDataVersion()

        let container = try decoder.container(keyedBy: CodingKeys.self)
        let version = try container.decode(Int.self, forKey: .dataVersion)
        let base64data = try container.decode(Data.self, forKey: .stringData)

        let jsonDecoder = JSONDecoder()
        data = try D.init(from: base64data, withVersion: version, using: jsonDecoder)
    }
}

我不得不做各种各样的跳跃通过箍工作。同样,建设性的批评将是最受欢迎的。这里有一些更多的测试来证明这一工作,正如你可以看到手册版本号已经消失:

// created wrapped versions of the test data above
let recordV1Wrapper = MDWrapper<RecordV1>(data: recordV1)
let recordV2Wrapper = MDWrapper<RecordV2>(data: recordV2)

// creating Data that can be stored from the wrapped versions
let wrappedDataV1 = try encoder.encode(recordV1Wrapper)
let wrappedDataV2 = try encoder.encode(recordV2Wrapper)

// string for debug viewing
let wrappedStringV1 = String(data: wrappedDataV1, encoding: .utf8)
let wrappedStringV2 = String(data: wrappedDataV2, encoding: .utf8)

// loading back from the data stores, migrating if needed.
let rebuiltV1WrapperFromV1Data = try decoder.decode(MDWrapper<RecordV1>.self, from: wrappedDataV1)
let rebuiltV2WrapperFromV1Data = try decoder.decode(MDWrapper<RecordV2>.self, from: wrappedDataV1)

let rebuiltV2WrapperFromV2Data = try decoder.decode(MDWrapper<RecordV2>.self, from: wrappedDataV2)

// isolating the parts we actually need so we can discard the wrappers
let rebuiltV1FromV1 = rebuiltV1WrapperFromV1Data.data
let rebuiltV2FromV1 = rebuiltV2WrapperFromV1Data.data

let rebuiltV2FromV2 = rebuiltV2WrapperFromV2Data.data

相关问题