KISS

Keep It Simple Stupid

iOS Core Data: custom migration policy to split tags

| comments

Core Data is a great and powerful ORM framework for OS X and iOS with a lot of features. Basically, you as a developer work with your entities in a certain context, and the underlying framework sorts out the data to/from the database.

To start with Core Data, it’s recommended to work through the Core Data Tutorial for iOS doc, where you build a simple app (called Locations) persisting events with creation date and geo coordinate. As an assignment at the end, you’re offered to extend the app, e.g., to add tags or comments.

I decided to add the ability to edit tags for each event. Here’s how Event entity looked in my version 1.1 (it’s version 1.0 plus the tags property):

Then to avoid tags duplication and more complex manipulations I extracted the Tag entity and made many-to-many connection between Event and Tag entities in v1.2:

In this case, the automated persistent store migration doesn’t work properly as it doesn’t save tags when upgrading the database. Our own custom migration process should be involved to split the tags string, create the necessary entities, and associate them with events back.

First, I created the inferred mapping model from v1.1 to v1.2 in Xcode. The default model looks like this:

After trials and errors the right mapping model was simplified to this:

I removed the EventToEvent mapping here, the reason described below. Note the Custom Policy field on the right, where I specify the custom migration policy class:

(TagsMigrationPolicy.h) download
1
2
3
4
5
6
7
8
9
10
11
12
13
//
//  TagsMigrationPolicy.h
//  Locations
//
//  Created by Eugene on 12/25/12.
//  Copyright (c) 2012 Eugene. All rights reserved.
//

#import <CoreData/CoreData.h>

@interface TagsMigrationPolicy : NSEntityMigrationPolicy

@end
(TagsMigrationPolicy.m) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//
//  TagsMigrationPolicy.m
//  Locations
//
//  Created by Eugene on 12/25/12.
//  Copyright (c) 2012 Eugene. All rights reserved.
//

#import "TagsMigrationPolicy.h"
#import "Tag.h"

@implementation TagsMigrationPolicy

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError *__autoreleasing *)error {
    // make sure we're working with correct class
    if ([sInstance.entity.name isEqualToString:@"Event"]) {
        NSManagedObjectContext *destMOC = manager.destinationContext;

        // create the Tag objects from the tags string
        NSString *tagsString = [sInstance valueForKey:@"tags"];
        NSMutableArray *tagsForEntity = [NSMutableArray array];
        if (tagsString.length > 0) {
            NSArray *tags = [tagsString componentsSeparatedByString:@","];
            for (NSString *tag in tags) {
                NSString *trimmedTag = [tag stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
                if (trimmedTag.length > 0) {
                    // don't duplicate tags!
                    NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:@"Tag"];
                    request.predicate = [NSPredicate predicateWithFormat:@"name LIKE %@", trimmedTag];
                    NSError *err = nil;
                    NSArray *retTags = [destMOC executeFetchRequest:request error:&err];
                    if (retTags.count == 0) {
                        NSManagedObject *newTag = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:destMOC];
                        [newTag setValue:trimmedTag forKey:@"name"];
                        [tagsForEntity addObject:newTag];
                    } else {
                        if (retTags.count > 1) {
                            NSLog(@"Warning. Found %d tags with name %@", retTags.count, trimmedTag);
                        }
                        [tagsForEntity addObjectsFromArray:retTags];
                    }
                }
            }
        }

        // create the destination Event object
        NSManagedObject *dst = [NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:destMOC];
        NSArray *keys = sInstance.entity.attributesByName.allKeys;
        NSMutableDictionary *values = [[sInstance dictionaryWithValuesForKeys:keys] mutableCopy];
        // resave tags as objects
        [values setValue:nil forKey:@"tags"];
        [tagsForEntity enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            Tag *tag = obj;
            [tag addEventsObject:(Event *)dst];
        }];
        [dst setValuesForKeysWithDictionary:[values copy]];
        [manager associateSourceInstance:sInstance withDestinationInstance:dst forEntityMapping:mapping];
        return YES;
    } else {
        NSLog(@"Unexpected source class: %@", sInstance.entity.name);
        return [super createDestinationInstancesForSourceInstance:sInstance entityMapping:mapping manager:manager error:error];
    }
}

@end

The class uses the createDestinationInstancesForSourceInstance:entityMapping:manager:error: method to parse the tags string, create Tag objects (w/o duplication), and then recreate the current Event object, plus associate the created/fetched tags with this new object. The catch there was that the default created EventToEvent mapping would produce the same number of Tag objects with empty names. That’s why I have to recreate events manually. Here’s a piece of code that assigns tags to the current event:

1
2
3
4
5
6
7
8
    NSMutableDictionary *values = [[sInstance dictionaryWithValuesForKeys:keys] mutableCopy];
    // resave tags as objects
    [values setValue:nil forKey:@"tags"];
    [tagsForEntity enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        Tag *tag = obj;
        [tag addEventsObject:(Event *)dst];
    }];
    [dst setValuesForKeysWithDictionary:[values copy]];

We get the values from the source instance, set the tags property to nil (remember, in v1.1 it was a string, and now it is a set), then add the event to each tag (which automatically populates the event’s tags property), and set the values to the new instance.

Hint: to simplify debugging the migration policy you can implement the endEntityMapping:manager:error: method, call the super’s implementation, and put a breakpoint there. This gives you the ability to check the state of the migration and stop the app w/o saving any changes.

If you have any questions or suggestions about the post, please leave a comment.

dev, iOS

Don't hesitate to leave a comment below. NB! If you don't see a comment form under the post, it's most likely that an extension (such as Ghostery, NoScript, or AdBlock) of your browser blocks the scripts from disqus.com, and you can unblock that.

« "Should I store CocoaPods' products in git?" My git aliases »

Comments