Workaround for Core Data store migration in applications built on 10.6 but that must also run on 10.5

Contents:

Problem Description

A change to the auto-generated value expression in property mappings for relationships with a destination that has subentities was made on 10.6. The new value expression assumes the existence of a method (destinationInstancesForSourceRelationshipNamed:sourceInstances:) that does not exist on NSMigrationManager in Mac OS X v10.5. Building a mapping model that auto-generates such a value expression on Mac OS X v10.6; performing Core Data store migration with that compiled mapping model on Mac OS X v10.5 results in an unexpected termination with a message logged to the console similar to the following:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:
'*** -[NSMigrationManager destinationInstancesForSourceRelationshipNamed:sourceInstances:]:
unrecognized selector sent to instance 0xNNNNNNN'

Workaround

The original value expression auto-generated by building on Mac OS X v10.5 silently failed to preserve the destination instances from the relationship, so the preferred workaround is to add the new NSMigrationManager method if it isn't already present in the runtime (that is, when running on Mac OS X v10.5). Add the NSMigrationManager category by copying the source code in Listing 1-1 into your project, and invoke 

[NSMigrationManager addRelationshipMigrationMethodIfMissing]

in a method that is guaranteed to be invoked before migration could be attempted—for example, if you have an application delegate, this could be in its init method; if you’re writing a framework, this could be the initializer of an object guaranteed to be created before migration could be attempted.

Listing 1-1Category of NSMigrationManager to work around the problem
 
#import <objc/runtime.h>
#import <CoreData/CoreData.h>
 
@implementation NSMigrationManager (Workaround)
 
+ (void)addRelationshipMigrationMethodIfMissing {
    SEL correctMethodSignature = @selector(destinationInstancesForSourceRelationshipNamed:sourceInstances:);
    Class migrationManagerClass = [NSMigrationManager class];
 
    if (NULL == class_getInstanceMethod(migrationManagerClass, correctMethodSignature)) {
        Method m = class_getInstanceMethod(migrationManagerClass, @selector(workaround_destinationInstancesForSourceRelationshipNamed:sourceInstances:));
        class_addMethod(migrationManagerClass, correctMethodSignature,
                       method_getImplementation(m), method_getTypeEncoding(m));
    }
}
 
 
- (NSArray *)workaround_destinationInstancesForSourceRelationshipNamed:(NSString *)srcRelationshipName sourceInstances:(id)source {
 
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSEntityMapping *eMapping = [self currentEntityMapping];
    NSEntityDescription* srcEntityInSrcModel = [self sourceEntityForEntityMapping:eMapping];
    NSEntityDescription* srcEntityInDstModel = [self destinationEntityForEntityMapping:eMapping];
 
    /* Source will be an NSManagedObject for a to-one relationship */
    NSArray *sourceInstances = ([source isKindOfClass:
                    [NSManagedObject class]]) ? [NSArray arrayWithObject: source] : source;
 
    /* Validate source relationship name */
    NSRelationshipDescription *relationshipInSrcModel = nil;
    if (nil != srcRelationshipName) {
        relationshipInSrcModel = [[srcEntityInSrcModel relationshipsByName] objectForKey:srcRelationshipName];
    } else {
        NSString *reason = [[NSString alloc] initWithFormat:
            @"Property mapping in %@ missing required source relationship name argument to destinationInstancesForSourceRelationshipNamed:sourceInstances:",
           [eMapping name]];
        [pool drain];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                            reason:[reason autorelease] userInfo: nil];
    }
    if (!relationshipInSrcModel) {
        NSString *reason = [[NSString alloc] initWithFormat:
             @"Can't find relationship for name (%@) for entity (%@) in source model.",
             srcRelationshipName, [srcEntityInSrcModel name]];
        [pool drain];
        @throw [NSException exceptionWithName:NSInvalidArgumentException
                            reason:[reason autorelease] userInfo: nil];
    }
 
    /* Try to determine the destination relationship name by looking up the property mapping */
    NSString *dstRelationshipName = nil;
    for (NSPropertyMapping *pMapping in [eMapping relationshipMappings]) {
        NSExpression *expression = [pMapping valueExpression];
        if (([expression expressionType]==NSFunctionExpressionType) &&
             [[expression function] isEqualToString:@"destinationInstancesForSourceRelationshipNamed:sourceInstances:"]) {
            NSArray *arguments = [expression arguments];
            if ([arguments count] == 2) {
                id arg = [arguments objectAtIndex:0];
                if ([arg isKindOfClass:[NSExpression class]] &&
                    ([arg expressionType] == NSConstantValueExpressionType) &&
                    [[(NSExpression *)arg constantValue] isEqual:srcRelationshipName]) {
                    if (dstRelationshipName) {
                        NSString *reason = [[NSString alloc] initWithFormat:
                            @"More than one property mapping (%@, %@) in %@ calls destinationInstancesForSourceRelationshipNamed:sourceInstances: with the same source relationship name %@, can't determine the correct destination relationship",
                           dstRelationshipName, [pMapping name], [eMapping name], srcRelationshipName];
                        [pool drain];
                        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[reason autorelease] userInfo: nil];
                    }
                    dstRelationshipName = [pMapping name];
                }
            }
        }
    }
 
    /* Lookup the destination relationship */
    NSRelationshipDescription *relationshipInDstModel = nil;
    if (nil != dstRelationshipName) {
        relationshipInDstModel = [[srcEntityInDstModel relationshipsByName] objectForKey:dstRelationshipName];
    } else {
        [pool drain];
        return nil;
    }
    if (!relationshipInDstModel) {
        NSString *reason = [[NSString alloc] initWithFormat: @"Can't find relationship for name (%@) for entity (%@) in destination model.", dstRelationshipName, [srcEntityInDstModel name]];
        [pool drain];
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[reason autorelease] userInfo: nil];
    }
 
    NSEntityDescription* dstEntityInSrcModel = [relationshipInSrcModel destinationEntity];
    NSEntityDescription* dstEntityInDstModel = [relationshipInDstModel destinationEntity];
 
    /* Lookup entity mappings for relationship destination subentities */
    NSMutableArray* mappings = [NSMutableArray array];
    for (NSEntityMapping* m in [[self mappingModel] entityMappings]) {
        if (([[self sourceEntityForEntityMapping:m] isKindOfEntity:dstEntityInSrcModel]) &&
            ([[self destinationEntityForEntityMapping:m] isKindOfEntity:dstEntityInDstModel])) {
            [mappings addObject:m];
        }
    }
 
    /* Find the destination instance for each source instance */
    NSMutableArray* results = [[NSMutableArray alloc] initWithCapacity:[sourceInstances count]];
    for (NSManagedObject* mo in sourceInstances) {
        NSManagedObject* peer = nil;
        for (NSEntityMapping* m in mappings) {
            // for each dest find peer in destination context by iterating through all relevant entity mappings
            NSArray* si = [[NSArray alloc] initWithObjects:mo,nil];
            NSArray* dests = [self destinationInstancesForEntityMappingNamed:[m name] sourceInstances:si];
            [si release];
 
            if (([dests count] > 1) || (([dests count] == 1) && (nil != peer))) {
                NSString *reason = [[NSString alloc] initWithFormat:
                    @"More than one destination instance found for source instance of type (%@) for relationship mapping (%@).",
                    [[mo entity] name], dstRelationshipName];
                [results release];
                [pool drain];
                @throw [NSException exceptionWithName:NSInvalidArgumentException reason:[reason autorelease] userInfo: nil];
            }
            if ([dests count] == 1) {
                peer = [dests objectAtIndex:0];
                [results addObject:peer];
            }
        }
    }
 
    [pool drain];
    return [results autorelease];
}
 
@end