saoudrizwan / Disk

Delightful framework for iOS to easily persist structs, images, and data
MIT License
3.08k stars 170 forks source link

Feature idea: Data migrations / support for schema change #54

Closed lacyrhoades closed 5 years ago

lacyrhoades commented 5 years ago

Say we start with some model user:

class User {
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }
    var name: String
}

And later we add a property called like birthdate: Date

In Disk, we are faced with two choices... We can omit birthdate from the CodingKeys enum forever and provide a default value for all user's birthdates (not ideal), or we can add birthdate to the keys and lose all previously stored User files (also not ideal).

Is there some way to get the NSKeyArchiver to fall back and not abort altogether? Like, if in the event that the model DOES provide a default value for a property, it could maybe not absolutely require all of the CodingKeys.

Use case: I use Disk with this protocol to quickly store singleton objects with settings/preferences on them. Currently, if I "add" a preference property to one of these singletons, all of the previous settings get wiped out. Just curious if this is a reasonable feature in the scope of Disk.

public protocol Persistable: Codable {
    init()
    static func load() -> Self
    static func save(_ obj: Self)
    static var persistenceFileName: String { get }
}

extension Persistable {
    public static func load() -> Self {
        if let manager = try? Disk.retrieve(Self.persistencePath, from: .documents, as: Self.self) {
            return manager
        } else {
            return Self()
        }
    }

    public static func save(_ obj: Self) {
        try? Disk.save(obj, to: .documents, as: Self.persistencePath)
    }

    public static var persistencePath: String {
        return self.persistenceFileName.appending(".json")
    }
}
lacyrhoades commented 5 years ago

Ideally not only would the developer be able to "grow" the schema to allow for new properties, but also if an old property were to be removed, there could be some opportunity to fetch that old data from the file storage (just that one last time) and use the value to inform the model with the new schema.

In the event a user is upgrading directly from the first version of the schema directly to the 10th iteration, maybe it behaves opportunistically. If it can make the model object whole considering default values, it makes it, but then also it just fails pretty easily in the event something doesn't make sense.

lacyrhoades commented 5 years ago

Just saw #6 – Thanks!

lacyrhoades commented 5 years ago

Finally was forced to cross this bridge. Still using it for storing singletons. Ended up editing Persistable protocol to have a method init?(_: [String: Any]) as in https://gist.github.com/lacyrhoades/0794ac254620f6133f18649fce42cd02 so that, if a model needs to support upgrading/downgrading and the data doesn't match 100% it can implement that fail-able initializer.

lacyrhoades commented 5 years ago

OK scratch that, I was able to do what I wanted by just overriding public init(from decoder: Decoder) throws { } and it's much shorter.

If a type has migrations, override that and handle custom decoding. If not, don't.