Dave DeLong 几乎是所有方面的专家,所以我觉得我在告诉耶稣如何在水上行走。当然,他的职位是从 2009 年开始的,那是很久以前的事了。
但是,Bot 发布的链接中的方法不一定是处理大量删除的最佳方法。
基本上,该帖子建议获取对象 ID,然后遍历它们,对每个对象调用 delete。
问题是,当您删除单个对象时,它还必须处理所有关联的关系,这可能会导致进一步的获取。
因此,如果您必须像这样进行大规模删除,我建议调整您的整体数据库,以便您可以隔离特定核心数据存储中的表。这样您就可以删除整个商店,并可能重建您想要保留的小部分。这可能是最快的方法。
但是,如果要删除对象本身,则应遵循此模式...
在自动释放池中批量删除,并确保预取任何级联关系。所有这些加在一起,将最大限度地减少您实际访问数据库的次数,从而减少执行删除所需的时间。
在建议的方法中,归结为...
- 获取所有要删除的对象的 ObjectIds
- 遍历列表,并删除每个对象
如果你有级联关系,你会遇到很多额外的数据库行程,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。