Automatic Lightweight Migration without explicitly create model versions? #401

Open
opened 2025-12-29 15:30:59 +01:00 by adam · 5 comments
Owner

Originally created by @dwirandytlvk on GitHub (Jan 5, 2023).

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 🙏

Originally created by @dwirandytlvk on GitHub (Jan 5, 2023). 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 🙏
adam added the question label 2025-12-29 15:30:59 +01:00
Author
Owner

@JohnEstropia commented on GitHub (Jan 5, 2023):

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

@JohnEstropia commented on GitHub (Jan 5, 2023): 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
Author
Owner

@dwirandytlvk commented on GitHub (Jan 5, 2023):

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?

@dwirandytlvk commented on GitHub (Jan 5, 2023): 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`?
Author
Owner

@JohnEstropia commented on GitHub (Jan 5, 2023):

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.

@JohnEstropia commented on GitHub (Jan 5, 2023): > 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 `typealias`es for your model classes, as shown in some examples in the README: <img width="854" alt="Screen Shot 2023-01-05 at 18 56 05" src="https://user-images.githubusercontent.com/3029684/210752553-5187f624-469b-4f45-8959-351199d640dc.png"> 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.
Author
Owner

@dwirandytlvk commented on GitHub (Jan 10, 2023):

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

@dwirandytlvk commented on GitHub (Jan 10, 2023): 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
Author
Owner

@JohnEstropia commented on GitHub (Jan 12, 2023):

@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
@JohnEstropia commented on GitHub (Jan 12, 2023): @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`: <img width="784" alt="Screen Shot 2023-01-12 at 11 24 36" src="https://user-images.githubusercontent.com/3029684/211961072-5378c696-ead1-4736-aefc-ff4483195377.png">
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/CoreStore#401