Skip to content

Instantly share code, notes, and snippets.

  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save alemar11/f5644343a773b9955b09b0edfdffbac8 to your computer and use it in GitHub Desktop.
Memory Efficient Migration Manager
@import CoreData;
/**
A subclass of NSMigrationManager that is more memory efficient than
NSMigrationManager itself.
This class maintains its own source and destination instance association
tables, which store only the URI representation of the instances's object IDs,
rather than the objects themselves. This means that objects do not accumulate
in the migration manager, allowing the contexts to flush when saving.
Note: This class will not attempt to use a store specific migration manager.
Trying to set the value of usesStoreSpecificMigrationManager to YES results in
an exception.
*/
@interface JCMMemoryEfficientMigrationManager : NSMigrationManager
/**
Returns the migration batch size of the receiver.
The default value is 0. A batch size of 0 is treated as infinite, which
disables the batch migration behavior.
The migration batch size represents the maximum number of iterations of the
instance creation and relationship creations stages that may be processed,
before the receiver saves the destination context.
*/
@property (nonatomic) NSUInteger migrationBatchSize;
@end
#import "JCMMemoryEfficientMigrationManager.h"
static float const totalMigrationProgressPerPass = 0.33;
@interface NSManagedObjectContext (MemoryEfficientMigrationManager)
- (NSArray *)jcm_objectsWithIDs:(NSArray *)objectIDs;
- (NSPersistentStoreCoordinator *)jcm_persistentStoreCoordinator;
@end
@interface JCMMemoryEfficientMigrationManager ()
@property (nonatomic, strong) NSMutableDictionary *bySourceAssociationTable;
@property (nonatomic, strong) NSMutableDictionary *byDestinationAssociationTable;
@property (nonatomic, strong) NSMappingModel *mappingModel;
@property (nonatomic, strong) NSManagedObjectContext *sourceContext;
@property (nonatomic, strong) NSPersistentStoreCoordinator *sourceCoordinator;
@property (nonatomic, strong) NSManagedObjectContext *destinationContext;
@property (nonatomic, strong) NSPersistentStoreCoordinator *destinationCoordinator;
@property (nonatomic, getter = isMigratingRelationships) BOOL migratingRelationships;
@property (nonatomic) BOOL migrationWasCanceled;
@property (nonatomic) NSError *migrationCancelationError;
// Key-Value Observing
@property (nonatomic) float migrationProgress;
@property (nonatomic, strong) NSEntityMapping *currentEntityMapping;
@end
@implementation JCMMemoryEfficientMigrationManager
@synthesize mappingModel = _mappings; // _mappingModel is taked by the superclass
@synthesize sourceContext = _sourceContext;
@synthesize destinationContext = _destinationContext;
#pragma mark - Initializing
- (instancetype)initWithSourceModel:(NSManagedObjectModel *)sourceModel destinationModel:(NSManagedObjectModel *)destinationModel
{
self = [super initWithSourceModel:sourceModel destinationModel:destinationModel];
if (self) {
_migrationProgress = 1;
}
return self;
}
#pragma mark - Performing migration opetations
- (BOOL)migrateStoreFromURL:(NSURL *)sourceURL type:(NSString *)sStoreType options:(NSDictionary *)sOptions withMappingModel:(NSMappingModel *)mappings toDestinationURL:(NSURL *)dURL destinationType:(NSString *)dStoreType destinationOptions:(NSDictionary *)dOptions error:(NSError *__autoreleasing *)error
{
// Each of the three passes are worth 33% of the total progress, the final save accounts for the remaining 1%.
float progressPerEntityMappingPerPass = 0.33 / self.mappingModel.entityMappings.count;
self.migrationProgress = 0;
self.mappingModel = mappings;
// Set up core data stacks
self.sourceCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.sourceModel];
NSPersistentStore *sourceStore = [self.sourceCoordinator addPersistentStoreWithType:sStoreType configuration:nil URL:sourceURL options:sOptions error:error];
if (!sourceStore) {
[self doCleanupOnFailure:(error ? *error : nil)];
return NO;
}
self.sourceContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
self.sourceContext.persistentStoreCoordinator = self.sourceCoordinator;
self.destinationCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.destinationModel];
NSPersistentStore *destinationStore = [self.destinationCoordinator addPersistentStoreWithType:dStoreType configuration:nil URL:dURL options:dOptions error:error];
if (!destinationStore) {
[self doCleanupOnFailure:(error ? *error : nil)];
return NO;
}
self.destinationContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
self.destinationContext.persistentStoreCoordinator = self.destinationCoordinator;
// First pass
for (NSEntityMapping *entityMapping in self.mappingModel.entityMappings) {
if (![self doFirstPassForMapping:entityMapping error:error]) {
[self doCleanupOnFailure:(error ? *error : nil)];
return NO;
}
self.migrationProgress += progressPerEntityMappingPerPass;
}
// Second pass
for (NSEntityMapping *entityMapping in self.mappingModel.entityMappings) {
if (![self doSecondPassForMapping:entityMapping error:error]) {
[self doCleanupOnFailure:(error ? *error : nil)];
return NO;
}
self.migrationProgress += progressPerEntityMappingPerPass;
}
// Third pass
for (NSEntityMapping *entityMapping in self.mappingModel.entityMappings) {
if (![self doThirdPassForMapping:entityMapping error:error]) {
[self doCleanupOnFailure:(error ? *error : nil)];
return NO;
}
self.migrationProgress += progressPerEntityMappingPerPass;
}
// Final save
if (![[self destinationContext] save:error]) {
[self doCleanupOnFailure:(error ? *error : nil)];
return NO;
}
[self doCleanupOnFailure:nil];
return YES;
}
/**
Performs instance creation for the specified entity mapping
*/
- (BOOL)doFirstPassForMapping:(NSEntityMapping *)entityMapping error:(NSError *__autoreleasing *)error
{
self.currentEntityMapping = entityMapping;
Class policyClass = (entityMapping.entityMigrationPolicyClassName ? NSClassFromString(entityMapping.entityMigrationPolicyClassName) : [NSEntityMigrationPolicy class]);
NSEntityMigrationPolicy *policy = [[policyClass alloc] init];
if ([policy respondsToSelector:@selector(beginEntityMapping:manager:error:)]) {
if (![policy beginEntityMapping:entityMapping manager:self error:error]) {
return NO;
}
if (self.migrationWasCanceled) {
if (error) *error = self.migrationCancelationError;
return NO;
}
}
if ([policy respondsToSelector:@selector(createDestinationInstancesForSourceInstance:entityMapping:manager:error:)]) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:entityMapping.sourceEntityName];
NSArray *sourceInstances = [[self sourceContext] executeFetchRequest:fetchRequest error:error];
if (!sourceInstances) {
return NO;
}
NSUInteger iteration = 0;
for (NSManagedObject *sourceInstance in sourceInstances) {
if (![policy createDestinationInstancesForSourceInstance:sourceInstance entityMapping:entityMapping manager:self error:error]) {
return NO;
}
if (self.migrationWasCanceled) {
if (error) *error = self.migrationCancelationError;
return NO;
}
// Conserve memory
iteration++;
if (self.migrationBatchSize > 0 && iteration >= self.migrationBatchSize) {
if (![self.destinationContext save:error]) {
return NO;
}
[self.destinationContext reset];
}
}
}
if ([policy respondsToSelector:@selector(endInstanceCreationForEntityMapping:manager:error:)]) {
if (![policy endInstanceCreationForEntityMapping:entityMapping manager:self error:error]) {
return NO;
}
if (self.migrationWasCanceled) {
if (error) *error = self.migrationCancelationError;
return NO;
}
}
[self.sourceContext reset];
if (![[self destinationContext] save:error]) {
return NO;
}
return YES;
}
/**
Performs relationship creation for the specified entity mapping
*/
- (BOOL)doSecondPassForMapping:(NSEntityMapping *)entityMapping error:(NSError *__autoreleasing *)error
{
self.currentEntityMapping = entityMapping;
Class policyClass = (entityMapping.entityMigrationPolicyClassName ? NSClassFromString(entityMapping.entityMigrationPolicyClassName) : [NSEntityMigrationPolicy class]);
NSEntityMigrationPolicy *policy = [[policyClass alloc] init];
if ([policy respondsToSelector:@selector(createRelationshipsForDestinationInstance:entityMapping:manager:error:)]) {
NSArray *destinationInstances = [self destinationInstancesForEntityMappingNamed:entityMapping.name sourceInstances:nil];
NSUInteger iteration = 0;
for (NSManagedObject *destinationInstance in destinationInstances) {
self.migratingRelationships = YES;
if (![policy createRelationshipsForDestinationInstance:destinationInstance entityMapping:entityMapping manager:self error:error]) {
return NO;
}
self.migratingRelationships = NO;
if (self.migrationWasCanceled) {
if (error) *error = self.migrationCancelationError;
return NO;
}
// Conserve memory
iteration++;
if (self.migrationBatchSize > 0 && iteration > self.migrationBatchSize) {
[self.sourceContext reset];
if (![self.destinationContext save:error]) {
return NO;
}
}
}
}
if ([policy respondsToSelector:@selector(endRelationshipCreationForEntityMapping:manager:error:)]) {
if (![policy endRelationshipCreationForEntityMapping:entityMapping manager:self error:error]) {
return NO;
}
if (self.migrationWasCanceled) {
if (error) *error = self.migrationCancelationError;
return NO;
}
}
[self.sourceContext reset];
if (![[self destinationContext] save:error]) {
return NO;
}
return YES;
}
/**
Performs custom validation for the specified entity mapping
*/
- (BOOL)doThirdPassForMapping:(NSEntityMapping *)entityMapping error:(NSError *__autoreleasing *)error
{
self.currentEntityMapping = entityMapping;
Class policyClass = (entityMapping.entityMigrationPolicyClassName ? NSClassFromString(entityMapping.entityMigrationPolicyClassName) : [NSEntityMigrationPolicy class]);
NSEntityMigrationPolicy *policy = [[policyClass alloc] init];
if ([policy respondsToSelector:@selector(performCustomValidationForEntityMapping:manager:error:)]) {
if (![policy performCustomValidationForEntityMapping:entityMapping manager:self error:error]) {
return NO;
}
if (self.migrationWasCanceled) {
if (error) *error = self.migrationCancelationError;
return NO;
}
}
if ([policy respondsToSelector:@selector(endEntityMapping:manager:error:)]) {
if (![policy endEntityMapping:entityMapping manager:self error:error]) {
return NO;
}
if (self.migrationWasCanceled) {
if (error) *error = self.migrationCancelationError;
return NO;
}
}
// Conserve memory
if (![self.destinationContext save:error]) {
return NO;
}
[self.destinationContext reset];
return YES;
}
- (void)doCleanupOnFailure:(NSError *)error
{
self.migrationProgress = 1;
self.sourceContext = nil;
self.sourceCoordinator = nil;
self.destinationContext = nil;
self.destinationCoordinator = nil;
self.mappingModel = nil;
self.migrationWasCanceled = NO;
self.migrationCancelationError = nil;
}
- (void)cancelMigrationWithError:(NSError *)error
{
self.migrationWasCanceled = YES;
self.migrationCancelationError = error;
}
#pragma mark - Working with source and destination instances
- (void)associateSourceInstance:(NSManagedObject *)sourceInstance withDestinationInstance:(NSManagedObject *)destinationInstance forEntityMapping:(NSEntityMapping *)entityMapping
{
NSError *error = nil;
BOOL success = [destinationInstance.managedObjectContext obtainPermanentIDsForObjects:@[destinationInstance] error:&error];
if (success) {
[self createAssociationsBySource:sourceInstance withDestination:destinationInstance forEntityMapping:entityMapping];
[self createAssociationsByDestination:destinationInstance fromSource:sourceInstance forEntityMapping:entityMapping];
} else {
[self cancelMigrationWithError:error];
}
}
- (void)createAssociationsBySource:(NSManagedObject *)sourceInstance withDestination:(NSManagedObject *)destinationInstance forEntityMapping:(NSEntityMapping *)entityMapping
{
NSMutableDictionary *table = [self bySourceAssociationTableForEntityMappingNamed:entityMapping.name];
NSMutableArray *associations = table[sourceInstance.objectID.URIRepresentation];
if (!associations) {
associations = [[NSMutableArray alloc] init];
table[sourceInstance.objectID.URIRepresentation] = associations;
}
[associations addObject:destinationInstance.objectID.URIRepresentation];
}
- (void)createAssociationsByDestination:(NSManagedObject *)destinationInstance fromSource:(NSManagedObject *)sourceInstance forEntityMapping:(NSEntityMapping *)entityMapping
{
NSMutableDictionary *table = [self byDestinationAssociationTableForEntityMappingNamed:entityMapping.name];
NSMutableArray *associations = table[destinationInstance.objectID.URIRepresentation];
if (!associations) {
associations = [[NSMutableArray alloc] init];
table[destinationInstance.objectID.URIRepresentation] = associations;
}
[associations addObject:sourceInstance.objectID.URIRepresentation];
}
/*
The behavior of this method must account for situations that differ from what
the public documentation of this method would suggest.
When you create a relationship mapping the the Xcode Mapping Editor the auto-
generated value expression for the destination value is of the form:
FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:" , "<entity mapping name>", $source.<relationship name>)
Thus, if the relationship is a to-one relationship, the argument passed as
sourceInstances will be an instance of NSManagedObject rather than an array.
The expected return value in this situation is also a managed object rather
than an array.
Furthermore, during the relationship creation stage, if the argument passed as
sourceInstances is nil, contrary to what the public documentation for this
method says, the return value in this situation must also be nil.
*/
- (id)destinationInstancesForEntityMappingNamed:(NSString *)mappingName sourceInstances:(id)sourceInstances
{
if (!sourceInstances && self.migratingRelationships) return nil;
BOOL toOne = NO;
if ([sourceInstances isKindOfClass:[NSManagedObject class]]) {
sourceInstances = @[sourceInstances];
toOne = YES;
}
NSDictionary *table = [self bySourceAssociationTableForEntityMappingNamed:mappingName];
NSMutableArray *destinationInstanceIDs;
if (sourceInstances) {
destinationInstanceIDs = [[NSMutableArray alloc] init];
for (NSManagedObject *sourceInstance in sourceInstances) {
NSArray *associations = table[sourceInstance.objectID.URIRepresentation];
if (associations) {
[destinationInstanceIDs addObjectsFromArray:associations];
}
}
} else {
destinationInstanceIDs = [[table allValues] valueForKeyPath:@"@unionOfArrays.self"];
}
NSArray *destinationInstances = [[self destinationContext] jcm_objectsWithIDs:destinationInstanceIDs];
return (toOne ? [destinationInstances firstObject] : destinationInstances);
}
/*
See the discussion for destinationInstancesForEntityMappingNamed:sourceInstances:.
*/
- (id)sourceInstancesForEntityMappingNamed:(NSString *)mappingName destinationInstances:(id)destinationInstances
{
if (!destinationInstances && self.migratingRelationships) return nil;
BOOL toOne = NO;
if ([destinationInstances isKindOfClass:[NSManagedObject class]]) {
destinationInstances = @[destinationInstances];
toOne = YES;
}
NSDictionary *table = [self byDestinationAssociationTableForEntityMappingNamed:mappingName];
NSMutableArray *sourceInstanceIDs;
if (destinationInstances) {
sourceInstanceIDs = [[NSMutableArray alloc] init];
for (NSManagedObject *destinationInstance in destinationInstances) {
NSArray *associations = table[destinationInstance.objectID.URIRepresentation];
if (associations) {
[sourceInstanceIDs addObjectsFromArray:associations];
}
}
} else {
sourceInstanceIDs = [[table allValues] valueForKeyPath:@"@unionOfArrays.self"];
}
NSArray *sourceInstances = [[self sourceContext] jcm_objectsWithIDs:sourceInstanceIDs];
return (toOne ? [sourceInstances firstObject] : sourceInstances);
}
- (NSMutableDictionary *)bySourceAssociationTableForEntityMappingNamed:(NSString *)mappingName
{
if (!_bySourceAssociationTable) {
_bySourceAssociationTable = [[NSMutableDictionary alloc] init];
}
NSMutableDictionary *table = _bySourceAssociationTable[mappingName];
if (!table) {
table = [[NSMutableDictionary alloc] init];
_bySourceAssociationTable[mappingName] = table;
}
return table;
}
- (NSMutableDictionary *)byDestinationAssociationTableForEntityMappingNamed:(NSString *)mappingName
{
if (!_byDestinationAssociationTable) {
_byDestinationAssociationTable = [[NSMutableDictionary alloc] init];
}
NSMutableDictionary *table = _byDestinationAssociationTable[mappingName];
if (!table) {
table = [[NSMutableDictionary alloc] init];
_byDestinationAssociationTable[mappingName] = table;
}
return table;
}
- (void)reset
{
_bySourceAssociationTable = nil;
_byDestinationAssociationTable = nil;
}
#pragma mark - Using store-specific migraiton managers
- (BOOL)usesStoreSpecificMigrationManager
{
return NO;
}
- (void)setUsesStoreSpecificMigrationManager:(BOOL)flag
{
if (flag) {
[NSException raise:NSInvalidArgumentException format:@"%@ does not support using store-specific migration managers.", NSStringFromClass([self class])];
}
}
@end
@implementation NSManagedObjectContext (JCMMemoryEfficientMigrationManager)
- (NSArray *)jcm_objectsWithIDs:(NSArray *)objectIDs
{
if (!objectIDs) return nil;
NSMutableArray *objects = [[NSMutableArray alloc] initWithCapacity:objectIDs.count];
for (NSURL *URIRepresentation in objectIDs) {
NSManagedObjectID *ID = [[self jcm_persistentStoreCoordinator] managedObjectIDForURIRepresentation:URIRepresentation];
[objects addObject:[self objectWithID:ID]];
}
return objects;
}
- (NSPersistentStoreCoordinator *)jcm_persistentStoreCoordinator
{
if (self.persistentStoreCoordinator) return self.persistentStoreCoordinator;
return [self.parentContext jcm_persistentStoreCoordinator];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment