Magical Record import (next step)

I’ve put next step in the title as this is not the same problem as my previous question with almost the exact same title.

I have a Person entity.

  • Using retryWhen to update tokens based on http error code
  • iOS White to Transparent Gradient Layer is Gray
  • Points to note for Future upgrade of App without new install in iPhone?
  • iOS lazy-loading of table images
  • How to Play a sound using AVAudioPlayer when in Silent Mode in iPhone
  • Change colour of dark grey highlight when holding down custom UIButton?
  • Person
    --------
    name      - mappedKeyName: FullName
    email     - mappedKeyName: EmailAddress
    personID  - mappedKeyName: Id
    --------
    photos
    

    And a Photo entity.

    Photo
    --------
    image
    createDate - mappedKeyName: Date
    photoID    - mappedKeyName: Id
    --------
    owner (type Person) - mappedKeyName: UserId - relatedByAttribute: personID
    

    There are other objects that relate to Person too and the JSON for these comes as so…

    {
        ObjectId : blah,
        Owner : {
            Id : 12345asdfg,
            FullName : Oliver,
            EmailAddress : oliver@oliver.com
        }
    }
    

    With this JSON my setup works with the import. Any person records that don’t exist (with the Id) are created. And any that do exist are updated.

    However, the photos JSON object comes like this…

    {
        Id : thisIsThePhotoID,
        Date : today,
        UserId : 12345asdfg
    }
    

    When the objects come down like this the Magical record import stops when it gets to the person import.

    The code crashes at…

    - (id) MR_relatedValueForRelationship:(NSRelationshipDescription *)relationshipInfo
    {
        NSString *lookupKey = [self MR_lookupKeyForRelationship:relationshipInfo];
        return lookupKey ? [self valueForKeyPath:lookupKey] : nil;  // it stops here.
    }
    

    The value of lookupKey is @”personID”.

    Printing out relationshipInfo at the breakpoint gives…

    $6 = 0x1fd695e0 (<NSRelationshipDescription: 0x1fd695e0>),
        name owner,
        isOptional 0,
        isTransient 0,
        entity Photo,
        renamingIdentifier owner,
        validation predicates (),
        warnings (),
        versionHashModifier (null)
        userInfo {
            mappedKeyName = UserId;
            relatedByAttribute = personID;
        },
        destination entity Person,
        inverseRelationship photos,
        minCount 1,
        maxCount 1,
        isOrdered 0,
        deleteRule 1
    

    I really have no idea why this isn’t working. I don’t get any sensible errors to report.

    Solutions Collect From Internet About “Magical Record import (next step)”

    MagicalRecord cannot map the relationship automatically with this JSON format:

    {
        Id : thisIsThePhotoID,
        Date : today,
        UserId : 12345asdfg
    }
    

    In order for MagicalRecord to map the relationship to a Person object, it would have to be an object in the JSON as well, for example:

    {
        Id : thisIsThePhotoID,
        Date : today,
        User : {
            UserId : 12345asdfg
        }
    }
    

    This way MagicalRecord knows it’s an object and it will do the appropriate lookup in your existing database for the Person record with the above ID and map the relationship.

    So there are two issues with this, though. If you cannot change the JSON output you have to create a category class on Photo where you manually map the relationship yourself. I’ll get to that after the second issue.

    The second issue is that the above JSON format assumes you already have parsed the users and stored the records in your database. If you have not MagicalRecord will create a new Person record with the above ID, but since no other attributes exist on that object (notice the UserId key is the only attribute in the dictionary) it will be fairly empty and not include the name and email address. You can always extend your JSON (if you have that possibility) to include those attributes as well in the Person dictionary inside the Photo dictionary:

    {
        Id : thisIsThePhotoID,
        Date : today,
        User : {
            UserId : 12345asdfg,
            FullName : Oliver,
            EmailAddress : oliver@oliver.com
        }
    }
    

    The JSON payload is quite small so it doesn’t hurt to do that if you can. Plus it will only create a new Person record if one doesn’t exist in the database already.

    And then for the manual mapping. If you cannot change the JSON to the above format you have to manually override the relationship mapping as the JSON is not prepared the way MagicalRecord does mapping.

    Create a category class for Photo called Photo+Mapping.h/.m. I like to stick with +Mapping for these. Then the class should be Photo (Mapping) in the header and implementation file and you’re good to go.

    MagicalRecord has a number of instance methods available to override (see the latter part of this article on MagicalRecord importing written by the author of MagicalRecord), among them are import<;attributeName>;: and import<;relationshipName>;:. There are also a willImport:, didImport: and shouldImport: methods on the class itself which allows you to override any mapping.

    For your case you can use import<;relationshipName>;: or shouldImport:. I took these two because one has a bit of a benefit depending on whether you have already mapped all your Person objects and they’re available for relationship mapping on the Photo object.

    Here are the examples of what you can do (you can choose to combine a few of them if you wish, it doesn’t hurt to do so). A note here: ALWAYS use the current NSManagedObjectContext when overriding mapping (easily accessible with MagicalRecord through self.managedObjectContext) otherwise you will end up with context issues.

    Be sure to import Person:

    #import "Photo+Mapping.h"
    #import "Person.h"
    
    // Assuming you only want to import the Photo object if you already have a Person stored this is a great method to tell MagicalRecord whether to continue with importing or not
    -(BOOL)shouldImport:(id)data {
        Person *person = [Person findFirstByAttribute:data[@"UserId"] value:@"personID" inContext:self.managedObjectContext];
        if (!person) {
            // no Person object exists so don't import the Photo object - again this is up to you since you might want to create the record if not
            return NO;
        }
        // you can set the relationship here (you might as well) or use the importPerson: method below (doing a second lookup, which is unnecessary at this point)
        [self setPerson:person];
        return YES;
    }
    
    // If you use this method you're doing the lookup to check whether a record exist when MagicalRecord is trying to map the Person relationship
    -(void)importPerson:(id)data {
        Person *person = [Person findFirstByAttribute:data[@"UserId"] value:@"personID" inContext:self.managedObjectContext];
        if (!person) {
            // if no Person record exists for the associated UserId, you should create one (or not - if you choose not to, it's wise to throw away this Photo object)
            person = [Person createInContext:self.managedObjectContext];
            [person setPersonID:data[@"UserId"]];
        }
        // set the relationship
        [self setPerson:person];
    }
    
    // finally you can also use the following method when MagicalRecord is done mapping and get rid of the Photo object if the Person relationship is nil:
    -(void)didImport:(id)data {
        if (!self.person) {
             [self deleteInContext:self.managedObjectContext];
        }
    }
    

    Hope this helps! Let me know if you have any questions.