0

I have a Core Data data model with several model versions (kept to allow migrations). My entities have the codegen setting set to Manual / none, and I have corresponding classes in my codebase where I hook into certain methods like validateForUpdate, awakeFromInsert, etc:

@objc(Book)
class Book: NSManagedObject {
    @NSManaged var title: String

    override func validateForUpdate() throws {
        try super.validateForUpdate()

        try customValidation()
    }
}

During migration of an old store, it is necessary for me to load the old store using the old model version and perform some updates to managed objects. However, the validation from the NSManagedObject class still runs, even though this was only designed to run against the latest version. It may refer to attributes that don't exist in the old version, giving me unrecognized selector errors.

An example of the code I'm using to load a store with the old model is:

// book_5 is the name of the older model version
let model = NSManagedObjectModel(contentsOf: Bundle.main.url(forResource: "books_5", withExtension: "momd")!)!

let container = NSPersistentContainer(name: "TestModel", managedObjectModel: model)
let description = NSPersistentStoreDescription(url: storeLocation)
container.persistentStoreDescriptions = [description]

container.loadPersistentStores { _, _ in
    let entity = model.entitiesByName["Book"]!
    let newBook = NSManagedObject(entity: entity, insertInto: container.viewContext)
    newBook.setValue("Test Book", forKey: "title")
    try! container.viewContext.save()
}

When inserting the object into the context, the awakeFromInsert method is fired, triggering an error because it refers to attributes that only exist in the newest model version.

Is there a way to workaround this? I feel like this should be possible because manipulating source entity objects in a custom entity mapping during a heavyweight migration does not hit the same issue.

Andrew Bennet
  • 2,600
  • 1
  • 21
  • 55
  • 1
    Maybe this https://stackoverflow.com/questions/3025742/detecting-a-lightweight-core-data-migration ? Or else, if there are new properties and you just want to avoid `unrecognized selector`, you might be able to check with `yourBook.entity.propertiesByName.keys` and check if the value is there? – Larme Oct 17 '22 at 08:18
  • I'm not sure how that linked question solves this problem. If I add checks like you suggest to my model validation overrides, then I think they may fail to validate the latest model version correctly... – Andrew Bennet Oct 21 '22 at 14:28
  • Excuse naive suggestion (no real experience with this), but seen question and can't help thinking that given how A's older stuff tends to work might not overriding `awakeFromInsert` to set up the missing stuff before calling the super-class instance be the way to go. i,e. something similiar to what Antoine does in his post here https://www.avanderlee.com/swift/nsmanagedobject-awakefrominsert-preparefordeletion/ – shufflingb Oct 22 '22 at 09:38
  • I am already overriding `awakeFromInsert`. I'm probably not explaining the problem very well; here's another try: I have one Core Data model with an entity that has a corresponding class which overrides `awakeFromInsert`. I have a separate Core Data model that has a different entity, but with the same name. Problem: Core Data uses the class lifecycle events for both entities -- I guess core data looks up the class by name at runtime. I have two different entities with the same name is because I'm loading an older model version: it has an entity with same name but different internals. – Andrew Bennet Oct 22 '22 at 11:02

1 Answers1

0

It's hard to know without more context, but from your explanation I understand you want to run the customValidation method only with the new Core Data model version.

Perhaps you can prevent the validation to run depending on what model version you have loaded.

For example, you can create a computed property on NSManagedObjectContext to indicate if the validation should run:

extension NSManagedObjectContext {
    
    var shouldRunCustomValidation: Bool {
        let coordinator = self.persistentStoreCoordinator
        let model = NSManagedObjectModel(contentsOf: Bundle.main.url(forResource: "books_5", withExtension: "momd")!)!
        
        return coordinator?.managedObjectModel != model
    }
}

... and use it in your Book class before running the custom validation:

@objc(Book)
class Book: NSManagedObject {
    @NSManaged var title: String
    
    override func validateForUpdate() throws {
        try super.validateForUpdate()
        
        guard let context = self.managedObjectContext,
              context.shouldRunCustomValidation else { return }
        
        try customValidation()
    }
}
cgontijo
  • 286
  • 2
  • 7