核心数据 NSPersistentStoreCoordinator 的 +metadataForPersistentStoreOfType:URL:error: 有时返回 nil

Posted

技术标签:

【中文标题】核心数据 NSPersistentStoreCoordinator 的 +metadataForPersistentStoreOfType:URL:error: 有时返回 nil【英文标题】:Core Data NSPersistentStoreCoordinator's +metadataForPersistentStoreOfType:URL:error: sometimes returns nil 【发布时间】:2014-04-02 16:48:22 【问题描述】:

我有一种方法可以通过多个NSManagedObjectModel 版本逐步迁移core data sqlite 存储,直到存储处于当前版本。该方法的灵感来自 Marcus Zarra 的核心数据书中的代码。

该应用程序正在生产中,但我的方法在大约 0.5% 的情况下失败。当它失败时,它会返回 NO 并使用 Crashlytics 记录错误:

NSSQLiteErrorDomain = 14; NSUnderlyingException = "I/O error for database at <store url>. SQLite error code:14, 'unable to open database file'"

sqlite 存储使用预写日志记录 (WAL),如果我在第一次手动删除 sqlite-WAL 文件后调用 +metadataForPersistentStoreOfType:URL:error:,有时会遇到相同的错误。

渐进式迁移代码:

- (BOOL)progressivelyMigrateURL:(NSURL*)sourceStoreURL 
                        toModel:(NSManagedObjectModel*)finalModel 
                          error:(NSError**)error

    NSURL *storeDirectoryURL = [sourceStoreURL URLByDeletingLastPathComponent];
    NSString *storeExtension = [sourceStoreURL pathExtension];
    
    NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator 
        metadataForPersistentStoreOfType:NSSQLiteStoreType 
                                     URL:sourceStoreURL 
                                   error:error];
    if (!sourceMetadata) return NO;
    
    while (![finalModel isConfiguration:nil 
            compatibleWithStoreMetadata:sourceMetadata]) 
        
        NSManagedObjectModel *sourceModel = 
            [self managedObjectModelForMetadata:sourceMetadata];
        if (!sourceModel) return NO;
        
        NSString *modelName = nil;
        NSManagedObjectModel *targetModel = 
            [self suitableTargetModelForMigrationFromSourceModel:sourceModel 
                                                       modelName:&modelName];
        if (!targetModel) return NO;
        
        NSMigrationManager *manager = 
            [[NSMigrationManager alloc] initWithSourceModel:sourceModel 
                                           destinationModel:targetModel];
        NSMappingModel *mappingModel = 
            [NSMappingModel mappingModelFromBundles:nil 
                                     forSourceModel:sourceModel 
                                   destinationModel:targetModel];
        NSURL *destinationStoreURL = 
            [[storeDirectoryURL URLByAppendingPathComponent:modelName] 
                URLByAppendingPathExtension:storeExtension];
        
        BOOL migrated = [manager migrateStoreFromURL:sourceStoreURL 
                                                type:NSSQLiteStoreType 
                                             options:nil 
                                    withMappingModel:mappingModel 
                                    toDestinationURL:destinationStoreURL 
                                     destinationType:NSSQLiteStoreType 
                                  destinationOptions:nil 
                                               error:error];
        if (!migrated) return NO;
        
        NSString *sourceModelName = 
            [self versionStringForManagedObjectModel:sourceModel];
        NSURL *backUpURL = [self backupURLWithDirectoryURL:storeDirectoryURL 
                                             pathExtension:storeExtension 
                                                 modelName:sourceModelName];
        BOOL replaced = [self replaceStoreAtURL:sourceStoreURL 
                                 withStoreAtURL:destinationStoreURL 
                                      backupURL:backUpURL 
                                          error:error];
        if (replaced == NO) return NO;
        
        sourceMetadata = [NSPersistentStoreCoordinator 
            metadataForPersistentStoreOfType:NSSQLiteStoreType 
                                         URL:sourceStoreURL 
                                       error:error];
        if (!sourceMetadata) return NO;
    
    
    return YES;


- (NSManagedObjectModel *)managedObjectModelForMetadata:(NSDictionary *)metadata

    for (NSURL *URL in [self modelURLs]) 
        NSManagedObjectModel *model = 
            [[NSManagedObjectModel alloc] initWithContentsOfURL:URL];
        if ([model isConfiguration:nil compatibleWithStoreMetadata:metadata]) 
            return model;
        
    
    return nil;


- (NSManagedObjectModel *)suitableTargetModelForMigrationFromSourceModel:(NSManagedObjectModel *)sourceModel 
                                                               modelName:(NSString **)modelName

    for (NSURL *modelURL in [self modelURLs]) 
        NSManagedObjectModel *targetModel = 
            [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
        NSMappingModel *mappingModel = 
            [NSMappingModel mappingModelFromBundles:nil 
                                     forSourceModel:sourceModel 
                                   destinationModel:targetModel];
        
        if (mappingModel) 
            *modelName = [[modelURL lastPathComponent] stringByDeletingPathExtension];
            return targetModel;
        
    
    
    return nil;


- (NSArray *)modelURLs

    NSMutableArray *modelURLs = 
        [[[NSBundle mainBundle] URLsForResourcesWithExtension:@"mom" 
                                                 subdirectory:nil] mutableCopy];
    
    NSArray *momdURLs = 
        [[[NSBundle mainBundle] URLsForResourcesWithExtension:@"momd" 
                                                 subdirectory:nil] mutableCopy];
    for (NSURL *momdURL in momdURLs) 
        NSString *directory = [momdURL lastPathComponent];
        NSArray *array = 
            [[NSBundle mainBundle] URLsForResourcesWithExtension:@"mom" 
                                                    subdirectory:directory];
        [modelURLs addObjectsFromArray:array];
    
    
    return [modelURLs copy];


- (NSURL *)backupURLWithDirectoryURL:(NSURL *)URL 
                       pathExtension:(NSString *)extension 
                           modelName:(NSString *)name

    NSString *GUID = [[NSProcessInfo processInfo] globallyUniqueString];
    NSString *pathComponant = [NSString stringWithFormat:@"%@-%@", GUID, name];
    
    return [[URL URLByAppendingPathComponent:pathComponant] 
        URLByAppendingPathExtension:extension];


- (BOOL)replaceStoreAtURL:(NSURL *)originalStoreURL 
           withStoreAtURL:(NSURL *)newStoreURL 
                backupURL:(NSURL *)backupURL 
                    error:(NSError **)error

    BOOL storeMoved = [self moveStoreAtURL:originalStoreURL 
                                     toURL:backupURL 
                                     error:error];
    if (!storeMoved) return NO;
    
    storeMoved = [self moveStoreAtURL:newStoreURL 
                                toURL:originalStoreURL 
                                error:error];
    if (!storeMoved) return NO;
    
    return YES;


- (BOOL)moveStoreAtURL:(NSURL *)sourceURL 
                 toURL:(NSURL *)targetURL 
                 error:(NSError **)error

    NSMutableArray *sourceURLs = [@[sourceURL] mutableCopy];
    NSMutableArray *targetURLs = [@[targetURL] mutableCopy];
    
    NSString *walExtension = @"sqlite-wal";
    if ([self storeAtURL:sourceURL hasAccessoryFileWithExtension:walExtension]) 
        [sourceURLs addObject:[self URLByReplacingExtensionOfURL:sourceURL 
                                                   withExtension:walExtension]];
        [targetURLs addObject:[self URLByReplacingExtensionOfURL:targetURL 
                                                   withExtension:walExtension]];
    
    
    NSString *shmExtension = @"sqlite-shm";
    if ([self storeAtURL:sourceURL hasAccessoryFileWithExtension:shmExtension]) 
        [sourceURLs addObject:[self URLByReplacingExtensionOfURL:sourceURL 
                                                   withExtension:shmExtension]];
        [targetURLs addObject:[self URLByReplacingExtensionOfURL:targetURL 
                                                   withExtension:shmExtension]];
    
    
    NSFileManager *fileManager = [NSFileManager defaultManager];
    for (int i = 0; i < [sourceURLs count]; i++) 
        BOOL fileMoved = [fileManager moveItemAtURL:sourceURLs[i] 
                                              toURL:targetURLs[i] 
                                              error:error];
        if (!fileMoved) return NO;
    
    
    return YES;


- (BOOL)storeAtURL:(NSURL *)URL hasAccessoryFileWithExtension:(NSString *)extension

    NSURL *accessoryURL = [self URLByReplacingExtensionOfURL:URL 
                                               withExtension:extension];
    return [[NSFileManager defaultManager] fileExistsAtPath:[accessoryURL path]];


- (NSURL *)URLByReplacingExtensionOfURL:(NSURL *)URL withExtension:(NSString *)extension

    return [[URL URLByDeletingPathExtension] URLByAppendingPathExtension:extension];


- (NSString *)versionStringForManagedObjectModel:(NSManagedObjectModel *)model

    NSString *string = @"";
    for (NSString *identifier in model.versionIdentifiers) 
        string = [string stringByAppendingString:identifier];
    
    return string;

抱歉,代码太长了。

【问题讨论】:

【参考方案1】:

可能的原因是您的moveStoreAtURL:toURL:error: 方法。您收到的错误是mentioned in Apple's docs,这是由于无法复制所有持久存储文件的结果。看起来您正试图击中所有这些,但要么 (a) 复制代码中存在我现在找不到的错误,要么 (b) 商店在您的应用程序中“活动”,正在使用持久存储协调器,因此您无法从副本中获得一致的状态。

您可以通过一些调试来修复它,如果您确保商店没有在使用中。更改日志模式会更好,并且可能更可靠,这样您就没有 walshm 文件(该链接也描述了这些文件)。更好的是,如果您的存储文件不是太大,请使用 migratePersistentStore:toURL:options:withType:error 让 Core Data 进行复制。虽然在某些情况下它可能会占用过多的内存,但这应该可以保证正常工作。

【讨论】:

谢谢汤姆。我不认为存储是“活动的”,因为迁移发生在实例化持久存储协调器之前。持久存储协调器本身是延迟创建的,但我很确定它不会被任何其他线程访问,直到迁移发生很长时间后。不过,我将添加日志记录来检查这一点。不过我会看看你建议的方法。 嗨,汤姆。我将根据您的建议使用 migratePersistentStore:toURL:options:withType:error 来移动商店。我不是 100% 确定这会解决我的问题,但似乎是一种更安全的做事方式,所以这是有道理的。感谢您的帮助!【参考方案2】:

我正在使用NSMigrationManager,所以我不能使用NSPersistentStoreCoordinator's - migratePersistentStore...,所以我的解决方案是强制检查点操作:

- (void)performCheckpointStoreWithSourceModel:(NSManagedObjectModel *)sourceModel sourceStoreURL:(NSURL *)sourceStoreURL 
    NSPersistentStoreCoordinator *tempPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:sourceModel];
    [tempPSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:sourceStoreURL options:@NSSQLitePragmasOption: @@"journal_mode": @"DELETE" error:nil];
    [tempPSC removePersistentStore:[tempPSC persistentStoreForURL:sourceStoreURL] error:nil];

...在使用 NSMigrationManager 执行迁移之前:

if (![manager migrateStoreFromURL:sourceStoreURL
                             type:type
                          options:nil
                 withMappingModel:mappingModel
                 toDestinationURL:destinationStoreURL
                  destinationType:type
               destinationOptions:nil
                            error:error]) 
    return NO;

【讨论】:

以上是关于核心数据 NSPersistentStoreCoordinator 的 +metadataForPersistentStoreOfType:URL:error: 有时返回 nil的主要内容,如果未能解决你的问题,请参考以下文章

哪里不使用核心数据?是不是可以将核心数据用作 MySql 之类的数据库?

使用 MKMapView、核心位置和核心数据

大数据三大核心技术:拿数据、算数据、卖数据!

核心数据道模式

通过迁移将核心数据实体及其数据移动到新的核心数据模型文件中

核心数据与用于核心数据的单个 MOC 和主线程合并冲突