我很好奇,在Swift中是否有被普遍认为是跨模式迁移可编码数据的最佳实践?
例如,我可能有:
struct RecordV1: Codable {
var name: String
}
struct RecordV2: Codable {
var firstName: String // Renamed old name field
var lastName: String // Added this field
}
我希望能够加载的东西,保存为RecordV1到RecordV2。
我想实现我的数据结构以这样一种方式,存储有一个版本号嵌入在它,以便在将来,当加载数据时,在未来的某个日期,当新版本的代码与最新版本的数据一起工作时,一些机制将有机会将旧数据迁移到最新的模式。我希望解决方案是相当优雅的,不涉及大量的重复输入的样板代码。越快越好!
我已经搜索了高和低,但我还没有找到任何解决这个问题的任何地方讨论。下面是我能想到的最好的解决方案。我希望人们能提出替代方案(特别是使用组合,或者以更好的方式使用协议和泛型)。
请原谅这篇文章的长度。我将把它分成几个部分,但是把它们都粘贴到Swift Playground中,它应该可以工作了。
第一部分是为可迁移结构定义一个协议,定义一个方法来标识MigratableData的版本,并定义一个初始化器来从以前版本保存的结构中导入数据。还有一个init(from: withVersion: using:)
,它沿着迁移链爬上并解码正确版本的数据,然后将结构向前迁移到当前版本。
我将init(from: withVersion: using:)
方法实现为默认协议实现。我特别担心这篇文章暗示这是一个坏主意。我很想听听如何避免这个问题。
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:
参数)迁移的版本。理想情况下,版本号应该是保存数据的一部分,这样它就可以被自动读取和应用。
// 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