-->

Core Data change property from Integer 16 to Integ

2019-02-05 09:17发布

问题:

I'm having a very serious problem. The application is live, but unfortunately it's fails on iOS 5, and I need to post an update.

The thing is the ID column of few entities is in Integer 16, but I need to be changed to Integer 32.

It was clearly my mistake, the model was created very long time ago, and it was only being reused. To my surprise (now) on iOS 4, Integer 16 in Core Data could easily keep number as big as 500 000 (bug?), but it doesn't work like that now - it gives me invalid numbers.

Application is live, has it success and Core Data is also used to keep the users scores, achievements and so on, what I don't want to remove, forcing them to reinstall the application. What is the best approach to simply change about ten of properties in different entities from Integer 16 to Integer 32?

Of course I know the names and entities for those properties.

If I just change the Type column for those properties in the xcdatamodeld file it will work, for new user, but what about existing users, that already have sqlite file in their Documents folder. I believe I need to change the persistent store coordinator somehow.

And also what do you thing about the performance, there are about 10 properties that news to be changed from 16 to 32, but Core Data have in usual cases more than 100 000 objects inside.

Regards

回答1:

Turn on NSMigratePersistentStoresAutomaticallyOption' and 'NSInferMappingModelAutomaticallyOption in your NSPersistentStore and then create a second version of your model with the changes. Only do the integer changes to keep the migration simple. That will allow users who install your upgrade to migrate from the broken model to the corrected model.

NOTE: This must be an automatic migration; a manual migration with a mapping model will not work.



回答2:

Background
Previous version of app set attribute as 16 bit in Core Data. This was too small to hold large values greater than approx 32768.
int 16 uses 1 bit to represent sign, so maximum value = 2^15 = 32768
In iOS 5, these values overflowed into negative numbers.

34318 became -31218
36745 became -28791

To repair these negative values, add 2^16 = 65536
Note this solution works only if the original value was less than 65536.

Add a new model
In file navigator, select MyApp.xcdatamodeld
Choose menu Editor/Add Model Version
Version name: proposes "MyApp 2" but you can change e.g. to MyAppVersion2
Based on model: MyApp

In new MyAppVersion2.xcdatamodel change attribute type from integer 16 to integer 64.

In file navigator, select directory MyApp.xcdatamodeld

Open right pane inspector, Versioned Core Data Model Current change from MyApp to MyAppVersion2. In left pane file navigator, green check mark moves from MyApp.xcdatamodel to MyAppVersion2.xcdatamodel.

In MyAppAppDelegate managedObjectModel do not change resource name from @"MyApp"

In Xcode select folder ModelClasses.
File/Add Core Data Mapping Model.

Choose source data model MyApp.xcdatamodel
Choose target data model MyAppVersion2.xcdatamodel
Save As MyAppToMyAppVersion2.xcmappingmodel

Add to target MyApp.

In MyAppAppDelegate persistentStoreCoordinator turn on CoreData manual migration

// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created
// and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

    if (persistentStoreCoordinator_ != nil) {
        return persistentStoreCoordinator_;
    }

    NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] 
                                               stringByAppendingPathComponent: @"MyApp.sqlite"]];

    NSError *error = nil;
    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] 
                                   initWithManagedObjectModel:[self managedObjectModel]];

    // Set Core Data migration options
    // For automatic lightweight migration set NSInferMappingModelAutomaticallyOption to YES
    // For manual migration using a mapping model set NSInferMappingModelAutomaticallyOption to NO
    NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], 
                                       NSMigratePersistentStoresAutomaticallyOption, 
                                       [NSNumber numberWithBool:NO], 
                                       NSInferMappingModelAutomaticallyOption, 
                                       nil];

    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType 
                                                   configuration:nil 
                                                             URL:storeURL 
                                                         options:optionsDictionary 
                                                           error:&error])
    {
        // handle the error
        NSString *message = [[NSString alloc]
                             initWithFormat:@"%@, %@", error, [error userInfo]];

        UIAlertViewAutoDismiss *alertView = [[UIAlertViewAutoDismiss alloc]     
                                             initWithTitle:NSLocalizedString(@"Sorry, Persistent Store Error.  Please Quit.", @"")
                                             message:message
                                             delegate: nil
                                             cancelButtonTitle:NSLocalizedString(@"OK", @"") 
                                             otherButtonTitles:nil]; 
        [message release];
        [alertView show];
        [alertView release];
    }    

    return persistentStoreCoordinator_;
}

Add a migration policy
MyAppToMyAppVersion2MigrationPolicy
The following example converts one entity "Environment" with an integer attribute "FeedID" and a string attribute "title".

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)aSource
                                  entityMapping:(NSEntityMapping *)mapping
                                        manager:(NSMigrationManager *)migrationManager
                                          error:(NSError **)error {
    NSEntityDescription *aSourceEntityDescription = [aSource entity];
    NSString *aSourceName = [aSourceEntityDescription valueForKey:@"name"];

    NSManagedObjectContext *destinationMOC = [migrationManager destinationContext];
    NSManagedObject *destEnvironment;
    NSString *destEntityName = [mapping destinationEntityName];

    if ([aSourceName isEqualToString:kEnvironment])
    {
        destEnvironment = [NSEntityDescription
                           insertNewObjectForEntityForName:destEntityName
                           inManagedObjectContext:destinationMOC];

        // attribute feedID
        NSNumber *sourceFeedID = [aSource valueForKey:kFeedID];
        if (!sourceFeedID)
        {
            // Defensive programming.
            // In the source model version, feedID was required to have a value
            // so excecution should never get here.
            [destEnvironment setValue:[NSNumber numberWithInteger:0] forKey:kFeedID];
        } 
        else
        {
            NSInteger sourceFeedIDInteger = [sourceFeedID intValue];        
            if (sourceFeedIDInteger < 0)
            {            
                // To correct previous negative feedIDs, add 2^16 = 65536            
                NSInteger kInt16RolloverOffset = 65536;            
                NSInteger destFeedIDInteger = (sourceFeedIDInteger + kInt16RolloverOffset);
                NSNumber *destFeedID = [NSNumber numberWithInteger:destFeedIDInteger]; 
                [destEnvironment setValue:destFeedID forKey:kFeedID];

            } else
            {
                // attribute feedID previous value is not negative so use it as is
                [destEnvironment setValue:sourceFeedID forKey:kFeedID];
            }
        }

        // attribute title (don't change this attribute)
        NSString *sourceTitle = [aSource valueForKey:kTitle];
        if (!sourceTitle)
        {
            // no previous value, set blank
            [destEnvironment setValue:@"" forKey:kTitle];
        } else
        {
            [destEnvironment setValue:sourceTitle forKey:kTitle];
        }

        [migrationManager associateSourceInstance:aSource
                          withDestinationInstance:destEnvironment
                                 forEntityMapping:mapping];

        return YES;
    } else
    {
        // don't remap any other entities
        return NO;
    }
}

In file navigator select MyAppToMyAppVersion2.xcmappingmodel
In window, show right side utilities pane.
In window, select Entity Mappings EnvironmentToEnvironment
In right side Entity Mapping, choose Custom Policy enter MyAppToMyAppVersion2MigrationPolicy.
Save file.

References:

Zarra, Core Data Chapter 5 p 87 http://pragprog.com/book/mzcd/core-data

http://www.informit.com/articles/article.aspx?p=1178181&seqNum=7

http://www.timisted.net/blog/archive/core-data-migration/

http://www.cocoabuilder.com/archive/cocoa/286529-core-data-versioning-non-trivial-value-expressions.html

http://www.seattle-ipa.org/2011/09/11/coredata-and-integer-width-in-ios-5/

Privat, Pro Core Data for iOS Ch 8 p273



回答3:

Just wants to confirm Marcus S. Zarra's answer with a small addition. It works good for us to some extent. We made the exact same error in our model. But it has a problem. Values over what seems to be 2^24 is converted to 16-bit values during the automatic migration, but saved as 32-bit but with wrong value.

For example: 17 479 261 becomes 18 851

(17 479 261 mod (2^16)) - (2^16) = -18 851

We downloaded the DB from the phone and looked in the database, and the number is changed in the DB.

We have not yet solved this problem.