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

时间:2014-08-16 05:35:06

标签: core-data ios7 database-design

我的情况是我的应用附带了一组基本信息。用户可以添加或编辑此信息。但有时,我需要能够更新基础数据。我不想触及用户更改/添加内容。我看过this question,但所使用的类比有点陌生。

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

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

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

所以我们非常欢迎这些想法。

1 个答案:

答案 0 :(得分:5)

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

  1. 应将应用程序数据和用户数据分开。
  2. 应用程序附带" base"数据集
  3. 申请"基地"数据将由用户编辑。
  4. 申请"基地"数据需要定期更新,但不能与用户更改发生冲突。
  5. 应将应用程序数据和用户数据分开。

    第一个要求,即应用程序和用户数据的分离,与iOS Data Storage Guidelines有关。未正确遵循数据存储准则可导致应用商店拒绝(2.23:应用必须遵循iOS数据存储指南,否则将被拒绝),并通过一个神秘的回复指向您Technical Q&A 1719several reasons存在数据存储指南 - 控制备份的大小,在设备空间不足时自动清除文件的策略,以及针对何处(开发人员错误)的一般指导原则。

    用户创建或编辑的数据属于<Application Sandbox>/Documents。这相当于NSDocumentDirectory

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

    应用程序数据 - 应用程序需要并且可能(或可能不)能够轻松重新创建的数据属于<Application Sandbox>/Library/Application Support,并且还应标记为&#34;不备份&#34; (NSURLIsExcludedFromBackupKey)属性。当文件标记有该属性时,将不会备份该文件,系统将不会自动清除数据。因此,建议您的应用程序至少尝试控制这些文件的增长并在不需要时清除它们。此目录对应于NSApplicationSupportDirectorythe convention is to create a subdirectory with your bundle identifier以及其下的其他子目录。

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

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

    配置:

    Configuration editor

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

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

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

    用户商店网址: <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 ];
    }
    

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

        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.
            }];
        }
    
      

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

         

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

    应用程序附带&#34; base&#34;数据集

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

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

      1. 这可能是缓慢,复杂,难以维护和容易出错的。这是执行此操作的常见方法之一,也是最糟糕的方法之一。认真。但如果这是你最舒服的维护方式,那就更有力量了。
      2. 还记得我之前说过一个给定的实体不应该存在于多个商店/配置中吗?好吧,我没有告诉你全部真相。给定实体应仅存在于单个可写存储中。对于我们的Foo实体,我们可以有一个可写的商店,但是我们想要的只有多个只读商店。如果使用选项NSReadOnlyPersistentStoreOption将商店添加到持久性商店coordintor,则它将是只读的。这意味着您可以使用预构建的只读的NSSQLiteStore,它包含您的&#34; starter&#34;数据,或者您可以更加雄心勃勃,并使用可以读取其他数据格式的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是已添加到协调器的源存储(此操作将删除它,并添加迁移的存储)。这有效地将store的内容注入applicationStoreURL的NSSQLiteStoreType商店。
      5. 申请&#34;基地&#34;数据将由用户编辑。

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

        申请&#34;基地&#34;数据需要定期更新,但不能与用户更改发生冲突。

        Annnnd这就是它崩溃的地方。由于之前的要求,这非常困难,而且我们对数据模型或我们尝试用它完成的工作知之甚少。

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

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

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