mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-01-12 17:43:34 +08:00
391 lines
19 KiB
Objective-C
391 lines
19 KiB
Objective-C
//
|
|
// RKManagedObjectStore.m
|
|
// RestKit
|
|
//
|
|
// Created by Blake Watters on 9/22/09.
|
|
// Copyright (c) 2009-2012 RestKit. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
#import "RKManagedObjectStore.h"
|
|
#import "RKLog.h"
|
|
#import "RKPropertyInspector.h"
|
|
#import "RKPropertyInspector+CoreData.h"
|
|
#import "RKPathUtilities.h"
|
|
#import "RKInMemoryManagedObjectCache.h"
|
|
#import "RKFetchRequestManagedObjectCache.h"
|
|
#import "NSManagedObjectContext+RKAdditions.h"
|
|
|
|
// Set Logging Component
|
|
#undef RKLogComponent
|
|
#define RKLogComponent RKlcl_cRestKitCoreData
|
|
|
|
extern NSString * const RKErrorDomain;
|
|
|
|
NSString * const RKSQLitePersistentStoreSeedDatabasePathOption = @"RKSQLitePersistentStoreSeedDatabasePathOption";
|
|
NSString * const RKManagedObjectStoreDidFailSaveNotification = @"RKManagedObjectStoreDidFailSaveNotification";
|
|
|
|
static RKManagedObjectStore *defaultStore = nil;
|
|
|
|
@interface RKManagedObjectStore ()
|
|
@property (nonatomic, strong, readwrite) NSManagedObjectModel *managedObjectModel;
|
|
@property (nonatomic, strong, readwrite) NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
|
@property (nonatomic, strong, readwrite) NSManagedObjectContext *persistentStoreManagedObjectContext;
|
|
@property (nonatomic, strong, readwrite) NSManagedObjectContext *mainQueueManagedObjectContext;
|
|
@end
|
|
|
|
@implementation RKManagedObjectStore
|
|
|
|
+ (instancetype)defaultStore
|
|
{
|
|
return defaultStore;
|
|
}
|
|
|
|
+ (void)setDefaultStore:(RKManagedObjectStore *)managedObjectStore
|
|
{
|
|
@synchronized(defaultStore) {
|
|
defaultStore = managedObjectStore;
|
|
}
|
|
}
|
|
|
|
- (id)initWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel
|
|
{
|
|
self = [super init];
|
|
if (self) {
|
|
self.managedObjectModel = managedObjectModel;
|
|
self.managedObjectCache = [RKFetchRequestManagedObjectCache new];
|
|
|
|
// Hydrate the defaultStore
|
|
if (! defaultStore) {
|
|
[RKManagedObjectStore setDefaultStore:self];
|
|
}
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator
|
|
{
|
|
self = [self initWithManagedObjectModel:persistentStoreCoordinator.managedObjectModel];
|
|
if (self) {
|
|
self.persistentStoreCoordinator = persistentStoreCoordinator;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
NSManagedObjectModel *managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
|
|
return [self initWithManagedObjectModel:managedObjectModel];
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)createPersistentStoreCoordinator
|
|
{
|
|
self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
|
|
}
|
|
|
|
- (NSPersistentStore *)addInMemoryPersistentStore:(NSError **)error
|
|
{
|
|
if (! self.persistentStoreCoordinator) [self createPersistentStoreCoordinator];
|
|
|
|
return [self.persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:error];
|
|
}
|
|
|
|
- (NSPersistentStore *)addSQLitePersistentStoreAtPath:(NSString *)storePath
|
|
fromSeedDatabaseAtPath:(NSString *)seedPath
|
|
withConfiguration:(NSString *)nilOrConfigurationName
|
|
options:(NSDictionary *)nilOrOptions
|
|
error:(NSError **)error
|
|
{
|
|
if (! self.persistentStoreCoordinator) [self createPersistentStoreCoordinator];
|
|
|
|
NSURL *storeURL = [NSURL fileURLWithPath:storePath];
|
|
|
|
if (seedPath) {
|
|
BOOL success = [self copySeedDatabaseIfNecessaryFromPath:seedPath toPath:storePath error:error];
|
|
if (! success) return nil;
|
|
}
|
|
|
|
NSDictionary *options = nil;
|
|
if (nilOrOptions) {
|
|
NSMutableDictionary *mutableOptions = [nilOrOptions mutableCopy];
|
|
[mutableOptions setObject:(seedPath ?: [NSNull null]) forKey:RKSQLitePersistentStoreSeedDatabasePathOption];
|
|
options = mutableOptions;
|
|
} else {
|
|
options = @{ RKSQLitePersistentStoreSeedDatabasePathOption: (seedPath ?: [NSNull null]),
|
|
NSMigratePersistentStoresAutomaticallyOption: @(YES),
|
|
NSInferMappingModelAutomaticallyOption: @(YES) };
|
|
}
|
|
|
|
/**
|
|
There seems to be trouble with combining configurations and migration. So do this in two steps: first, attach the store with NO configuration, but WITH migration options; then remove it and reattach WITH configuration, but NOT migration options.
|
|
|
|
http://blog.atwam.com/blog/2012/05/11/multiple-persistent-stores-and-seed-data-with-core-data/
|
|
http://stackoverflow.com/questions/1774359/core-data-migration-error-message-model-does-not-contain-configuration-xyz
|
|
*/
|
|
NSPersistentStore *persistentStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:error];
|
|
if (! persistentStore) return nil;
|
|
if (! [self.persistentStoreCoordinator removePersistentStore:persistentStore error:error]) return nil;
|
|
|
|
NSDictionary *seedOptions = @{ RKSQLitePersistentStoreSeedDatabasePathOption: (seedPath ?: [NSNull null]) };
|
|
persistentStore = [self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nilOrConfigurationName URL:storeURL options:seedOptions error:error];
|
|
if (! persistentStore) return nil;
|
|
|
|
// Exclude the SQLite database from iCloud Backups to conform to the iCloud Data Storage Guidelines
|
|
RKSetExcludeFromBackupAttributeForItemAtPath(storePath);
|
|
|
|
return persistentStore;
|
|
}
|
|
|
|
- (BOOL)copySeedDatabaseIfNecessaryFromPath:(NSString *)seedPath toPath:(NSString *)storePath error:(NSError **)error
|
|
{
|
|
if (NO == [[NSFileManager defaultManager] fileExistsAtPath:storePath]) {
|
|
NSError *localError;
|
|
if (![[NSFileManager defaultManager] copyItemAtPath:seedPath toPath:storePath error:&localError]) {
|
|
RKLogError(@"Failed to copy seed database from path '%@' to path '%@': %@", seedPath, storePath, [localError localizedDescription]);
|
|
if (error) *error = localError;
|
|
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (NSManagedObjectContext *)newChildManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType
|
|
{
|
|
NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType];
|
|
[managedObjectContext performBlockAndWait:^{
|
|
managedObjectContext.parentContext = self.persistentStoreManagedObjectContext;
|
|
managedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
|
|
}];
|
|
|
|
return managedObjectContext;
|
|
}
|
|
|
|
- (void)createManagedObjectContexts
|
|
{
|
|
NSAssert(!self.persistentStoreManagedObjectContext, @"Unable to create managed object contexts: A primary managed object context already exists.");
|
|
NSAssert(!self.mainQueueManagedObjectContext, @"Unable to create managed object contexts: A main queue managed object context already exists.");
|
|
|
|
// Our primary MOC is a private queue concurrency type
|
|
self.persistentStoreManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
|
|
self.persistentStoreManagedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
|
|
self.persistentStoreManagedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
|
|
|
|
// Create an MOC for use on the main queue
|
|
self.mainQueueManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
|
self.mainQueueManagedObjectContext.parentContext = self.persistentStoreManagedObjectContext;
|
|
self.mainQueueManagedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
|
|
|
|
// Merge changes from a primary MOC back into the main queue when complete
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(handlePersistentStoreManagedObjectContextDidSaveNotification:)
|
|
name:NSManagedObjectContextDidSaveNotification
|
|
object:self.persistentStoreManagedObjectContext];
|
|
}
|
|
|
|
- (void)recreateManagedObjectContexts
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.persistentStoreManagedObjectContext];
|
|
|
|
self.persistentStoreManagedObjectContext = nil;
|
|
self.mainQueueManagedObjectContext = nil;
|
|
[self createManagedObjectContexts];
|
|
}
|
|
|
|
- (BOOL)resetPersistentStores:(NSError **)error
|
|
{
|
|
[self.mainQueueManagedObjectContext reset];
|
|
[self.persistentStoreManagedObjectContext reset];
|
|
|
|
NSError *localError;
|
|
for (NSPersistentStore *persistentStore in self.persistentStoreCoordinator.persistentStores) {
|
|
NSURL *URL = [self.persistentStoreCoordinator URLForPersistentStore:persistentStore];
|
|
BOOL success = [self.persistentStoreCoordinator removePersistentStore:persistentStore error:&localError];
|
|
if (success) {
|
|
if ([URL isFileURL]) {
|
|
if (! [[NSFileManager defaultManager] removeItemAtURL:URL error:&localError]) {
|
|
RKLogError(@"Failed to remove persistent store at URL %@: %@", URL, localError);
|
|
if (error) *error = localError;
|
|
return NO;
|
|
}
|
|
|
|
// Check for and remove an external storage directory
|
|
NSString *supportDirectoryName = [NSString stringWithFormat:@".%@_SUPPORT", [[URL lastPathComponent] stringByDeletingPathExtension]];
|
|
NSURL *supportDirectoryFileURL = [NSURL URLWithString:supportDirectoryName relativeToURL:[URL URLByDeletingLastPathComponent]];
|
|
BOOL isDirectory = NO;
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:[supportDirectoryFileURL path] isDirectory:&isDirectory]) {
|
|
if (isDirectory) {
|
|
if (! [[NSFileManager defaultManager] removeItemAtURL:supportDirectoryFileURL error:&localError]) {
|
|
RKLogError(@"Failed to remove persistent store Support directory at URL %@: %@", supportDirectoryFileURL, localError);
|
|
if (error) *error = localError;
|
|
return NO;
|
|
}
|
|
} else {
|
|
RKLogWarning(@"Found external support item for store at path that is not a directory: %@", [supportDirectoryFileURL path]);
|
|
}
|
|
}
|
|
} else {
|
|
RKLogDebug(@"Skipped removal of persistent store file: URL for persistent store is not a file URL. (%@)", URL);
|
|
}
|
|
|
|
// Reclone the persistent store from the seed path if necessary
|
|
if ([persistentStore.type isEqualToString:NSSQLiteStoreType]) {
|
|
NSString *seedPath = [persistentStore.options valueForKey:RKSQLitePersistentStoreSeedDatabasePathOption];
|
|
if (seedPath && ![seedPath isEqual:[NSNull null]]) {
|
|
success = [self copySeedDatabaseIfNecessaryFromPath:seedPath toPath:[persistentStore.URL path] error:&localError];
|
|
if (! success) {
|
|
RKLogError(@"Failed reset of SQLite persistent store: Failed to copy seed database.");
|
|
if (error) *error = localError;
|
|
return NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add a new store with the same options
|
|
NSPersistentStore *newStore = [self.persistentStoreCoordinator addPersistentStoreWithType:persistentStore.type
|
|
configuration:persistentStore.configurationName
|
|
URL:persistentStore.URL
|
|
options:persistentStore.options error:&localError];
|
|
if (! newStore) {
|
|
if (error) *error = localError;
|
|
return NO;
|
|
}
|
|
} else {
|
|
RKLogError(@"Failed reset of persistent store %@: Failed to remove persistent store with error: %@", persistentStore, localError);
|
|
if (error) *error = localError;
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
[self recreateManagedObjectContexts];
|
|
return YES;
|
|
}
|
|
|
|
- (void)handlePersistentStoreManagedObjectContextDidSaveNotification:(NSNotification *)notification
|
|
{
|
|
RKLogDebug(@"persistentStoreManagedObjectContext was saved: merging changes to mainQueueManagedObjectContext");
|
|
RKLogTrace(@"Merging changes detailed in userInfo dictionary: %@", [notification userInfo]);
|
|
NSAssert([notification object] == self.persistentStoreManagedObjectContext, @"Received Managed Object Context Did Save Notification for Unexpected Context: %@", [notification object]);
|
|
[self.mainQueueManagedObjectContext performBlock:^{
|
|
[self.mainQueueManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
|
|
}];
|
|
}
|
|
|
|
+ (BOOL)migratePersistentStoreOfType:(NSString *)storeType
|
|
atURL:(NSURL *)storeURL
|
|
toModelAtURL:(NSURL *)destinationModelURL
|
|
error:(NSError **)error
|
|
configuringModelsWithBlock:(void (^)(NSManagedObjectModel *, NSURL *))block
|
|
{
|
|
BOOL isMomd = [[destinationModelURL pathExtension] isEqualToString:@"momd"]; // Momd contains a directory of versioned models
|
|
NSManagedObjectModel *destinationModel = [[[NSManagedObjectModel alloc] initWithContentsOfURL:destinationModelURL] mutableCopy];
|
|
|
|
// Yield the destination model for configuration (i.e. search indexing)
|
|
if (block) block(destinationModel, destinationModelURL);
|
|
|
|
// Check if the store is compatible with our model
|
|
NSDictionary *storeMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
|
|
URL:storeURL
|
|
error:error];
|
|
if (! storeMetadata) return NO;
|
|
if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:storeMetadata]) {
|
|
// Our store is compatible with the current model, no migration is necessary
|
|
return YES;
|
|
}
|
|
|
|
RKLogInfo(@"Determined that store at URL %@ has incompatible metadata for managed object model: performing migration...", storeURL);
|
|
|
|
NSURL *momdURL = isMomd ? destinationModelURL : [destinationModelURL URLByDeletingLastPathComponent];
|
|
|
|
// We can only do migrations within a versioned momd
|
|
if (![[momdURL pathExtension] isEqualToString:@"momd"]) {
|
|
NSString *errorDescription = [NSString stringWithFormat:@"Migration failed: Migrations can only be performed to versioned destination models contained in a .momd package. Incompatible destination model given at path '%@'", [momdURL path]];
|
|
*error = [NSError errorWithDomain:RKErrorDomain code:NSMigrationError userInfo:@{ NSLocalizedDescriptionKey: errorDescription }];
|
|
return NO;
|
|
}
|
|
|
|
NSArray *versionedModelURLs = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:momdURL
|
|
includingPropertiesForKeys:@[] // We only want the URLs
|
|
options:NSDirectoryEnumerationSkipsPackageDescendants|NSDirectoryEnumerationSkipsHiddenFiles
|
|
error:error];
|
|
if (! versionedModelURLs) {
|
|
return NO;
|
|
}
|
|
|
|
// Iterate across each model version and try to find a compatible store
|
|
NSManagedObjectModel *sourceModel = nil;
|
|
for (NSURL *versionedModelURL in versionedModelURLs) {
|
|
if (! [@[@"mom", @"momd"] containsObject:[versionedModelURL pathExtension]]) continue;
|
|
NSManagedObjectModel *model = [[[NSManagedObjectModel alloc] initWithContentsOfURL:versionedModelURL] mutableCopy];
|
|
if (! model) continue;
|
|
if (block) block(model, versionedModelURL);
|
|
|
|
if ([model isConfiguration:nil compatibleWithStoreMetadata:storeMetadata]) {
|
|
sourceModel = model;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Cannot complete the migration as we can't find a source model
|
|
if (! sourceModel) {
|
|
NSString *errorDescription = [NSString stringWithFormat:@"Migration failed: Unable to find the source managed object model used to create the %@ store at path '%@'", storeType, [storeURL path]];
|
|
*error = [NSError errorWithDomain:RKErrorDomain code:NSMigrationMissingSourceModelError userInfo:@{ NSLocalizedDescriptionKey: errorDescription }];
|
|
return NO;
|
|
}
|
|
|
|
// Infer a mapping model and complete the migration
|
|
NSMappingModel *mappingModel = [NSMappingModel inferredMappingModelForSourceModel:sourceModel
|
|
destinationModel:destinationModel
|
|
error:error];
|
|
if (!mappingModel) {
|
|
RKLogError(@"Failed to obtain inferred mapping model for source and destination models: aborting migration...");
|
|
RKLogError(@"%@", *error);
|
|
return NO;
|
|
}
|
|
|
|
NSString *UUID = (__bridge_transfer NSString*)CFUUIDCreateString(kCFAllocatorDefault, CFUUIDCreate(NULL));
|
|
NSString *migrationPath = [NSTemporaryDirectory() stringByAppendingFormat:@"Migration-%@.sqlite", UUID];
|
|
NSURL *migrationURL = [NSURL fileURLWithPath:migrationPath];
|
|
|
|
// Create a migration manager to perform the migration.
|
|
NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:destinationModel];
|
|
BOOL success = [manager migrateStoreFromURL:storeURL type:NSSQLiteStoreType
|
|
options:nil withMappingModel:mappingModel toDestinationURL:migrationURL
|
|
destinationType:NSSQLiteStoreType destinationOptions:nil error:error];
|
|
|
|
if (success) {
|
|
success = [[NSFileManager defaultManager] removeItemAtURL:storeURL error:error];
|
|
if (success) {
|
|
success = [[NSFileManager defaultManager] moveItemAtURL:migrationURL toURL:storeURL error:error];
|
|
if (success) RKLogInfo(@"Successfully migrated existing store to managed object model at path '%@'...", [destinationModelURL path]);
|
|
} else {
|
|
RKLogError(@"Failed to remove existing store at path '%@': unable to complete migration...", [storeURL path]);
|
|
RKLogError(@"%@", *error);
|
|
}
|
|
} else {
|
|
RKLogError(@"Failed migration with error: %@", *error);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
@end
|