10

以前有人问过这个问题,但是没有描述的解决方案足够快,可以满足我的应用程序需求。

在我们设置的通信协议中,每次执行同步时,服务器都会发送一组新的所有客户。早些时候,我们一直以 plist 的形式存储。现在想用Core Data。

可能有数千个条目。单独删除每个需要很长时间。有没有办法删除 Core Data 中特定表中的所有行?

delete from customer

sqlite 中的这个调用会立即发生。在 iPad1 上,在 Core Data 中单独浏览每一个可能需要 30 秒。

关闭 Core Data 是否合理,即删除持久性存储和所有托管对象上下文,然后放入 sqlite 并对表执行删除命令?在此过程中没有其他活动正在进行,因此我不需要访问数据库的其他部分。

4

4 回答 4

26

Dave DeLong 几乎是所有方面的专家,所以我觉得我在告诉耶稣如何在水上行走。当然,他的职位是从 2009 年开始的,那是很久以前的事了。

但是,Bot 发布的链接中的方法不一定是处理大量删除的最佳方法。

基本上,该帖子建议获取对象 ID,然后遍历它们,对每个对象调用 delete。

问题是,当您删除单个对象时,它还必须处理所有关联的关系,这可能会导致进一步的获取。

因此,如果您必须像这样进行大规模删除,我建议调整您的整体数据库,以便您可以隔离特定核心数据存储中的表。这样您就可以删除整个商店,并可能重建您想要保留的小部分。这可能是最快的方法。

但是,如果要删除对象本身,则应遵循此模式...

在自动释放池中批量删除,并确保预取任何级联关系。所有这些加在一起,将最大限度地减少您实际访问数据库的次数,从而减少执行删除所需的时间。

在建议的方法中,归结为...

  1. 获取所有要删除的对象的 ObjectIds
  2. 遍历列表,并删除每个对象

如果你有级联关系,你会遇到很多额外的数据库行程,IO真的很慢。您希望尽量减少必须访问数据库的次数。

虽然最初听起来可能违反直觉,但您希望获取的数据比您认为要删除的数据多。原因是所有这些数据都可以通过几个 IO 操作从数据库中获取。

因此,在您的获​​取请求中,您要设置...

[fetchRequest setRelationshipKeyPathsForPrefetching:@[@"relationship1", @"relationship2", .... , @"relationship3"]];

其中这些关系代表所有可能具有级联删除规则的关系。

现在,当您的提取完成时,您将拥有所有将被删除的对象,以及由于这些对象被删除而将被删除的对象。

如果您有一个复杂的层次结构,您希望尽可能提前预取。否则,当您删除一个对象时,Core Data 将不得不为每个对象单独获取每个关系,以便它可以管理级联删除。

这将浪费大量时间,因为您将执行更多的 IO 操作。

现在,在您的 fetch 完成后,您将遍历对象并删除它们。对于大型删除,您可以看到一个数量级的加速。

此外,如果您有很多对象,请将其分成多个批次,并在自动释放池中进行。

最后,在单独的后台线程中执行此操作,这样您的 UI 就不会挂起。您可以使用单独的 MOC,连接到持久存储协调器,并让主 MOC 处理 DidSave 通知以从其上下文中删除对象。

虽然这看起来像代码,但将其视为伪代码......

NSManagedObjectContext *deleteContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateConcurrencyType];
// Get a new PSC for the same store
deleteContext.persistentStoreCoordinator = getInstanceOfPersistentStoreCoordinator();

// Each call to performBlock executes in its own autoreleasepool, so we don't
// need to explicitly use one if each chunk is done in a separate performBlock
__block void (^block)(void) = ^{
    NSFetchRequest *fetchRequest = //
    // Only fetch the number of objects to delete this iteration
    fetchRequest.fetchLimit = NUM_ENTITIES_TO_DELETE_AT_ONCE;
    // Prefetch all the relationships
    fetchRequest.relationshipKeyPathsForPrefetching = prefetchRelationships;
    // Don't need all the properties
    fetchRequest.includesPropertyValues = NO;
    NSArray *results = [deleteContext executeFetchRequest:fetchRequest error:&error];
    if (results.count == 0) {
        // Didn't get any objects for this fetch
        if (nil == results) {
            // Handle error
        }
        return;
    }
    for (MyEntity *entity in results) {
        [deleteContext deleteObject:entity];
    }
    [deleteContext save:&error];
    [deleteContext reset];

    // Keep deleting objects until they are all gone
    [deleteContext performBlock:block];
};

[deleteContext preformBlock:block];

当然,您需要进行适当的错误处理,但这是基本思想。

如果要删除的数据太多以至于会破坏内存,请分批获取。不要获取所有属性。预取关系以最小化 IO 操作。使用 autoreleasepool 来防止内存增长。修剪上下文。在后台线程上执行任务。

如果您有一个非常复杂的图,请确保为整个对象图中的所有实体预取所有级联关系。

请注意,您的主要上下文必须处理 DidSave 通知,以使其上下文与删除保持同步。

编辑

谢谢。很多好点。除了为什么要创建单独的 MOC 之外,所有的解释都很好?关于不删除整个数据库,而是使用 sqlite 从特定表中删除所有行的任何想法?- 大卫

您使用单独的 MOC,因此在执行长删除操作时不会阻止 UI。请注意,当实际提交到数据库时,只有一个线程可以访问数据库,因此任何其他访问(如获取)都会阻止任何更新。这是将大型删除操作分成块的另一个原因。小部分的工作将为其他 MOC 提供一些访问商店的机会,而无需等待整个操作完成。

如果这会导致问题,您还可以实现优先级队列(通过dispatch_set_target_queue),但这超出了本问题的范围。

至于在 Core Data 数据库上使用 sqlite 命令,Apple 多次表示这是一个坏主意,您不应该在 Core Data 数据库文件上运行直接 SQL 命令。


最后,让我注意这一点。根据我的经验,我发现当我遇到严重的性能问题时,通常是由于设计不佳或实施不当造成的。重新审视你的问题,看看你是否可以重新设计你的系统以更好地适应这个用例。

如果您必须发送所有数据,也许可以在后台线程中查询数据库并过滤新数据,以便将数据分成三组:需要修改的对象、需要删除的对象和需要插入的对象。

这样,您只需更改需要更改的数据库。

如果数据几乎每次都是全新的,请考虑在这些实体拥有自己的数据库的地方重构您的数据库(我假设您的数据库已经包含多个实体)。这样您就可以删除该文件,然后从一个新的数据库重新开始。这很快。现在,重新插入数千个对象不会很快。

您必须跨商店手动管理任何关系。这并不难,但它不像同一家商店内的关系那样自动。

如果我这样做,我将首先创建新数据库,然后拆除现有数据库,用新数据库替换它,然后删除旧数据库。

如果你只是通过这种批处理机制来操作你的数据库,并且你不需要对象图管理,那么也许你想考虑使用 sqlite 而不是 Core Data。

于 2012-08-24T20:45:16.963 回答
6

iOS 9 及更高版本

使用NSBatchDeleteRequest. 我在模拟器中对一个拥有超过 400,000 个实例的 Core Data 实体进行了测试,删除几乎是瞬间完成的。

// fetch all items in entity and request to delete them
let fetchRequest = NSFetchRequest(entityName: "MyEntity")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)

// delegate objects
let myManagedObjectContext = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let myPersistentStoreCoordinator = (UIApplication.sharedApplication().delegate as! AppDelegate).persistentStoreCoordinator

// perform the delete
do {
    try myPersistentStoreCoordinator.executeRequest(deleteRequest, withContext: myManagedObjectContext)
} catch let error as NSError {
    print(error)
}

请注意, @Bot 链接到的答案和@JodyHagins 提到的答案也已更新为此方法。

于 2015-08-16T03:59:58.937 回答
1

实际上,您唯一的选择是单独删除它们。我使用大量对象执行此方法,并且速度非常快。这是某人通过仅加载托管对象 ID 来执行此操作的一种方式,因此它可以防止任何不必要的开销并使其更快。

核心数据:删除实体所有实例的最快方法

于 2012-08-24T17:42:48.687 回答
-1

是的,删除持久存储并从头开始是合理的。这发生得相当快。您可以做的是从持久存储协调器中删除持久存储(使用持久存储 URL),然后使用持久存储的 url 从目录文件夹中删除数据库文件。我是使用 NSFileManager 的 removeItemAtURL 完成的。

编辑:要考虑的一件事:确保禁用/释放当前的 NSManagedObjectContext 实例,并停止任何其他可能正在使用相同持久存储的 NSManagedObjectContext 做某事的线程。如果上下文尝试访问持久存储,您的应用程序将崩溃。

于 2012-08-24T17:28:10.677 回答