核心数据设计 - 如何同时拥有应用数据和用户数据?

Dav*_*nte 2 database-design core-data ios7

我有一种情况,我有我的应用程序附带的基本信息集.用户可以添加或编辑此信息.但有时,我需要能够更新基础数据.我不想触及用户更改/添加.我看过这个问题,但所用的类比有点陌生.

这必须是一个常见问题,我希望听到这种混合数据集的实践和经验.

当用户修改了一个对象时,我想为每条记录设置一个标志.然而,这带来了他们可能不小心这样做的问题.

我也可以创建两个数据集,但这会打开重复问题.

因此,我们非常欢迎您.

que*_*ish 5

所以我可以告诉你有4个要求(实际上,这可能是4个不同的问题)

  1. 应用程序数据和用户数据应分开存放.
  2. 应用程序附带"基础"数据集
  3. 应用程序"基础"数据将由用户编辑.
  4. 应用程序"基础"数据需要定期更新,但不能与用户更改冲突.

应用程序数据和用户数据应分开存放.

第一个要求,即应用程序和用户数据的分离,与iOS数据存储指南相关.未正确遵循数据存储准则可导致应用商店拒绝(2.23:应用必须遵循iOS数据存储指南,否则将被拒绝),并通过一个神秘的回复指向您进行技术问答1719.数据存储指南存在的原因多种 - 控制备份的大小,在设备空间不足时自动清除文件的策略,以及在何处(开发人员错误)的一般指导原则.

用户创建或编辑的数据属于<Application Sandbox>/Documents.这对应于NSDocumentDirectory.

可以根据需要重新生成的数据<Application Sanbox>/Library/Caches.这通常是下载的数据或缓存文件.iOS负责自动清除此目录下的任何内容.这对应于NSCachesDirectory.数据也可以存储<Application Sandbox>/tmp,但在这种情况下,建议您的应用程序定期清除这些文件.这对应于NSTemporaryDirectory().

应用程序数据 - 应用程序需要的数据,可能(或可能不)能够轻松地重新创建 - 属于,<Application Sandbox>/Library/Application Support并且还应标记为"不备份"(NSURLIsExcludedFromBackupKey)属性.当文件标记有该属性时,将不会备份该文件,系统将不会自动清除数据.因此,建议您的应用程序至少尝试控制这些文件的增长并在不需要时清除它们.此目录对应于NSApplicationSupportDirectory,并且约定是使用您的包标识符创建子目录,以及其下的其他子目录.

这对核心数据应用程序意味着什么?

用户数据和应用程序数据应在不同位置使用不同的存储文件.您仍将使用单个NSPersistentStoreCoordinator,但为其添加两个不同的持久性存储.您的托管对象模型将需要两种不同的配置 - 每个商店都有自己的配置,每个实体都附加到其中一个配置.

组态:

配置编辑器

这将推动您的数据模型的设计 - 您不应该在两个不同的商店中存在单个实体类型,这意味着您将无法在用户存储中存在"用户编辑的Foo实体",而"应用程序提供了Foo"实体"存在于应用程序商店中 - 除非Foo是抽象的,并且每个商店都有自己的具体实体(这只是一种可能的解决方案).跨存储关系可以实现为获取的属性(稍微更多).

由于Core Data SQLite持久性存储不是单个文件而是文件集合,因此建议每个商店都有自己的存储文件目录 - 这使得保存,删除,备份,应用更新等更加高效和可靠.考虑到这一点,您的文件结构应如下所示:

应用程序商店URL: <Application Sandbox>/Library/Application Support/com.yourcompany.YourApp/ApplicationData/Application.sqlite 您必须创建目录com.yourcompany.YourApp/ApplicationData,并且需要在它们上设置NSURLIsExcludedFromBackupKey.

用户存储URL: <Application Sandbox>/Documents/UserData/User.sqlite 您需要创建目录UserData.

这听起来像很多工作,但它不是:

- (NSURL *)supportFilesDirectoryURL {
    NSURL       *result                         = nil;
    NSURL       *applicationSupportDirectoryURL = [[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
    NSString    *bundleName                     = [[NSBundle bundleForClass:[self class]] bundleIdentifier];

    result = [applicationSupportDirectoryURL URLByAppendingPathComponent:bundleName isDirectory:@YES];

    return result;
}

- (NSURL *) applicationStoreURL {
    NSError             *error                  = nil;
    NSFileCoordinator   *coordinator            = nil;
    __block BOOL        didCreateDirectory      = NO;
    NSURL               *supportDirectoryURL    = [self supportFilesDirectoryURL];
    NSURL               *storeDirectoryURL      = [supportDirectoryURL URLByAppendingPathComponent:@"ApplicationData" isDirectory:YES];

    coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
    [coordinator coordinateWritingItemAtURL:storeDirectoryURL options:NSFileCoordinatorWritingForDeleting error:&error byAccessor:^(NSURL *writingURL){
        NSFileManager   *fileManager    = [[NSFileManager alloc] init];
        NSError         *fileError      = nil;
        if (![fileManager createDirectoryAtURL:writingURL withIntermediateDirectories:YES attributes:nil error:&fileError]){
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                // Handle the error
            }];
        } else {
            // Setting NSURLIsExcludedFromBackupKey on the directory will exclude all items in this directory
            // from backups. It will also prevent them from being purged in low space conditions. Because of this,
            // the files inside this directory should be purged by the application.
            [writingURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&fileError];
            didCreateDirectory = YES;
        }
    }];

    // See NSFileCoordinator.h for an explanation.
    if (didCreateDirectory == NO){
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // Handle the error.
        }];
    }

    return [NSURL URLWithString:@"Application.sqlite" relativeToURL:storeDirectoryURL ];
}
Run Code Online (Sandbox Code Playgroud)

您必须将两个存储添加到持久性存储协调器,每个存储都具有托管对象模型中定义的正确配置名称:

    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:@"Application" URL:[self applicationStoreURL] options:nil error:&error]) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // Handle the error.
        }];
    }

    if (![persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:@"User" URL:[self userStoreURL] options:nil error:&error]) {
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // Handle the error.
        }];
    }
Run Code Online (Sandbox Code Playgroud)

不幸的是,获取的属性在嵌套上下文中存在问题.

如果使用的上下文不是根上下文,则使用fetched属性会崩溃 - 您应该复制此雷达以确保在将来的版本中修复此问题.提取的属性对于符合数据存储准则至关重要.

应用程序附带"基础"数据集

有很多方法可以解决这个问题:

  1. 从JSON/XML /属性列表/ CSV /等创建托管对象.数据并插入到托管对象上下文中并保存.
  2. 除了用户和应用程序商店之外,从商店开始作为只读商店.
  3. 除了用户和应用程序商店之外,从一组预构建的SQLite文件(例如来自命令行工具)开始,作为只读存储.
  4. 从3开始预先构建的商店,但执行迁移以将这些对象移动到另一个商店.

每个都有优点和缺点:

  1. 这可能是缓慢,复杂,难以维护和容易出错的.这是执行此操作的常见方法之一,也是最糟糕的方法之一.认真.但如果这是你最舒服的维护,那么对你来说更有力量.
  2. 还记得我之前说过一个给定的实体不应该存在于多个商店/配置中吗?好吧,我没有告诉你全部真相.给定实体应仅存在于单个可写存储中.对于我们的Foo实体,我们可以有一个可写的商店,但是我们想要的只有多个只读商店.如果使用该选项NSReadOnlyPersistentStoreOption将商店添加到持久性商店coordintor ,则它将是只读的.这意味着您可以使用预构建的只读的NSSQLiteStore并包含"入门"数据,或者您可以更加雄心勃勃并使用可以读取其他数据格式的NSAtomicStore或NSIncrementalStore实现.这是最不易出错的解决方案,也是最容易维护的解决方案之一.
  3. 要为此创建一组SQLite文件,您需要构建一个工具,将大部分Core Data代码和托管对象模型共享为您的应用程序.否则这与3相同.
  4. 您可以在存储(例如选项2或3中使用的存储)之间执行迁移,并使用迁移将数据移动到应用程序或用户存储中.迁移通常比执行选项1更高效,但它们有一些限制:例如,轻量级迁移可以很好地工作,但不会做任何事情来防止重复或冲突.需要进行自定义迁移.这样的迁移将是这样的:sqlStore = [persistentStoreCoordinator migratePersistentStore:store toURL:[self applicationStoreURL] options:nil withType:NSSQLiteStoreType error:&error];其中,store是已经添加到协调器(此操作将其删除,并添加迁移存储)存储来源.这有效地注入的内容storeNSSQLiteStoreType在applicationStoreURL商店.

应用程序"基础"数据将由用户编辑.

这在很大程度上取决于数据模型的设计以及您要完成的任务.我无法对此进行概括.由于应用程序和用户数据存在于不同的存储中,并且由于数据存储准则(及其意图),因此很难完全满足您的要求.但是,精心设计的数据模型应该可以轻松实现.用户有哪些变化?你怎么能让它"属于"他们?这里一个很好的例子是"收藏".用户喜欢的内容 - 产品库存 - 不应更改,"收藏夹"属于用户.他们没有编辑书籍/音乐/小部件.他们在自己和任何标识内容之间创建了一种关系.这些用例是获取属性真正发挥作用的地方.

应用程序"基础"数据需要定期更新,但不能与用户更改冲突.

Annnnd这是一切都崩溃的地方.由于之前的要求,这非常困难,而且我们对数据模型或我们试图用它完成的事情知之甚少.

通常,您不希望更新/导入任何可能与设备上更改的数据冲突的内容.这里的好处是,如上所述,如果用户和应用程序数据之间有清晰的分离,那么更改应用程序数据应该很容易,因为用户不会更改它.您可以丢弃所有应用程序数据并替换它,如果您的数据模型设计得很好,它就可以正常工作.

但是,假设您说的是来自远程源的数据,并通过设备上的更改导入数据.每隔一段时间你就会下载一堆带有产品更新的JSON.解析JSON,创建托管对象,并保存这些对象.你几乎肯定会在某些时候发生冲突.在对象或属性级别对设备进行的某些更改将与导入的新信息冲突.您可以使用an NSMergePolicy来处理冲突,但在此特定方案中,合并策略可能不够.哪组信息"正确"?设备上的信息,还是远程信息?您是否盲目接受新数据,或者您是否必须按财产检查房产?人们这样做.我不是在开玩笑.你认为他们现在已经学会了.

良好的用户和应用程序数据分离有助于解决您将遇到的这个(和其他)问题.