Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic Lightweight Migration without explicitly create model versions? #466

Open
dwirandytlvk opened this issue Jan 5, 2023 · 5 comments
Labels

Comments

@dwirandytlvk
Copy link

HI @JohnEstropia
i would like to ask about lightweight migration in CoreStore
i watch at WWDC video https://developer.apple.com/videos/play/wwdc2022/10120/ that we can do lightweight migration without creating new model version.

Does CoreStore has support to do that?
Since i have many model/table that will be a tedious process to duplicate previous model into new version and make adjustment in new version model

thank you in advance 🙏

@JohnEstropia
Copy link
Owner

I think you misunderstood. Lightweight migrations allow for migrations between two model versions, without creating a Mapping Model. You still need to keep the models from your past versions.

With that in mind, CoreStore does support lightweight migrations. See https://github.com/JohnEstropia/CoreStore#migrations

@dwirandytlvk
Copy link
Author

Perhaps i will explain my case study

I have three table

enum V1 {
    class User: CoreStoreObject {
        @Field.Relationship("profile", inverse: \.$user, deleteRule: .cascade)
        var profile: UserProfile?
        @Field.Relationship("authentication", inverse: \.$user, deleteRule: .cascade)
        var authentication: UserAuthentication?
    }
    
    class UserAuthentication: CoreStoreObject {
        @Field.Stored("_loginMethod")
        var _loginMethod: String? = nil
        @Field.Stored("loginId")
        var loginId: String? = nil
        @Field.Stored("profileId")
        var profileId: String? = nil
        @Field.Stored("lastLoginTimetamp")
        var lastLoginTimestamp: Int = 0
        @Field.Stored("createdTimestamp")
        var createdTimestamp: Int = 0
        
        // Foreign Key Relationship
        @Field.Relationship("user")
        var user: User?
    }
    
    class UserProfile: CoreStoreObject {
        @Field.Stored("name")
        var name: String? = nil
        @Field.Coded("phoneNumbers", coder: FieldCoders.Json.self)
        var phoneNumbers: [String] = []
        @Field.Relationship("emails", inverse: \.$userProfile, deleteRule: .cascade)
        var emails: [UserEmail]
        @Field.Stored("")
        var photoUrl: String? = nil
        
        // Foreign Key Relationship
        @Field.Relationship("user")
        var user: User?
    }
    
    class UserEmail: CoreStoreObject {
        @Field.Stored("email")
        var email: String = ""
        
        // Foreign Key Relationship
        @Field.Relationship("userProfile")
        var userProfile: UserProfile?
    }
}

and after i publish my app, i want to update my published app and add 2 column in UserAuthentication which isCorporateUser and isVerified, so i have to copy my previous CoreStoreObject from v1, into enum v2 and add 2 column into UserAuthentication

enum V2 {
    class User: CoreStoreObject, ImportableObject {
        typealias ImportSource = [String: Any]
        
        @Field.Relationship("profile", inverse: \.$user, deleteRule: .cascade)
        var profile: UserProfile?
        @Field.Relationship("authentication", inverse: \.$user, deleteRule: .cascade)
        var authentication: UserAuthentication?
        
        func didInsert(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws {
            let profileJson = source["profile"] as? UserProfile.ImportSource ?? [:]
            profile = try transaction.importObject(Into<UserProfile>(), source: profileJson)
            
            let authenticationJson = source["authentication"] as? UserProfile.ImportSource ?? [:]
            authentication = try transaction.importObject(Into<UserAuthentication>(), source: authenticationJson)
        }
    }
    
    class UserAuthentication: CoreStoreObject, ImportableObject {
        typealias ImportSource = [String: Any]
        
        @Field.Stored("_loginMethod")
        var _loginMethod: String? = nil
        @Field.Stored("loginId")
        var loginId: String? = nil
        @Field.Stored("profileId")
        var profileId: String? = nil
        @Field.Stored("lastLoginTimetamp")
        var lastLoginTimestamp: Int = 0
        @Field.Stored("createdTimestamp")
        var createdTimestamp: Int = 0
        
        // Foreign Key Relationship
        @Field.Relationship("user")
        var user: User?
        
        
        // Additional Field
        @Field.Stored("isCorporateUser")
        var isCorporateUser: Bool = false
        @Field.Stored("isVerified")
        var isVerified: Bool = false
        
        func didInsert(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws {
            _loginMethod = source["_loginMethod"] as? String ?? ""
            loginId = source["loginId"] as? String ?? ""
            profileId = source["profileId"] as? String ?? ""
            lastLoginTimestamp = source["lastLoginTimestamp"] as? Int ?? 0
            createdTimestamp = source["createdTimestamp"] as? Int ?? 0
        }
    }
    
    class UserProfile: CoreStoreObject, ImportableObject {
        typealias ImportSource = [String: Any]
        
        @Field.Stored("name")
        var name: String? = nil
        @Field.Coded("phoneNumbers", coder: FieldCoders.Json.self)
        var phoneNumbers: [String] = []
        @Field.Relationship("emails", inverse: \.$userProfile, deleteRule: .cascade)
        var emails: [UserEmail]
        @Field.Stored("photoUrl")
        var photoUrl: String? = nil
        
        // Foreign Key Relationship
        @Field.Relationship("user")
        var user: User?
        
        func didInsert(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws {
            name = source["name"] as? String ?? ""
            phoneNumbers = source["phoneNumbers"] as? [String] ?? []
            photoUrl = source["photoUrl"] as? String ?? ""
            
            let emailJson: [UserEmail.ImportSource] = source["emails"] as? [UserEmail.ImportSource] ?? []
            emails = try transaction.importObjects(Into<UserEmail>(), sourceArray: emailJson)
        }
    }
    
    class UserEmail: CoreStoreObject, ImportableObject {
        typealias ImportSource = [String: Any]
        
        @Field.Stored("email")
        var email: String = ""
        
        @Field.Stored("domain")
        var domain: String = ""
        
        // Foreign Key Relationship
        @Field.Relationship("userProfile")
        var userProfile: UserProfile?
        
        func didInsert(from source: [String : Any], in transaction: CoreStore.BaseDataTransaction) throws {
            email = source["email"] as? String ?? ""
        }
    }
}

So my DataStack will be like this

let currentStack = DataStack(
            CoreStoreSchema(
                modelVersion: "v1",
                entities: [
                    Entity<V1.User>("User"),
                    Entity<V1.UserProfile>("UserProfile"),
                    Entity<V1.UserAuthentication>("UserAuthentication"),
                    Entity<V1.UserEmail>("UserEmail")
                ],
                versionLock: [
                   .....
                ]
            ),
            CoreStoreSchema(
                modelVersion: "v2",
                entities: [
                    Entity<V2.User>("User"),
                    Entity<V2.UserProfile>("UserProfile"),
                    Entity<V2.UserAuthentication>("UserAuthentication"),
                    Entity<V2.UserEmail>("UserEmail")
                ]
            ),
            migrationChain: ["v1", "v2"]
        )

i'm wondering is it possible, to not create V2 and just add new column into UserAuthentication in V1?

@JohnEstropia
Copy link
Owner

i'm wondering is it possible

No, it won't be. Core Data needs to know the old model for it to determine if any migrations, including lightweight ones, are needed in the first place.

To save you some maintenance work when adding new versions, I recommend using typealiases for your model classes, as shown in some examples in the README:
Screen Shot 2023-01-05 at 18 56 05

This way, you'd only need to refer to V1 or V2 in your migration setup code, and your app can use the aliased names forever.

@dwirandytlvk
Copy link
Author

ah i see okay, thanks john @JohnEstropia
May i know what is your suggestion if i have more than 50 CoreStoreObject that separated in several modules how to manage each object based on version?

For example i have 10 CoreStoreObject in Booking Module, 20 CoreStoreObject in Flight Module and 30 CoreStoreObject in Hotel Module each CoreStoreObject has relationship each other, so i can not be split into multiple database

@JohnEstropia
Copy link
Owner

@dwirandytlvk You're more familiar with your object relationships so this would be up to you, but you have many options:

  • let the module that sets up your DataStack depend on all modules that contain your objects
  • separate all ORM-related code to its own module
  • inject the CoreStoreObject subclasses dynamically during DataStack setup. CoreStoreSchema doesn't need static typing, just the Entity<T> instance passed as DynamicEntity: Screen Shot 2023-01-12 at 11 24 36

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants