【问题标题】:Improve process of mirroring server database to a client database via JSON?改进通过 JSON 将服务器数据库镜像到客户端数据库的过程?
【发布时间】:2013-07-21 16:06:19
【问题描述】:

我有一个现成的适用于 iPad 的企业(非 AppStore)旧版 iOS 应用程序,我需要对其进行重构(它是由另一位开发人员编写的,我目前工作的前任)。

此应用程序通过 JSON 从具有 MSSQL 数据库的服务器获取其数据。数据库模式有大约 30 个表,最大容量是:Client、City、Agency,每个表都有大约 10.000 条记录,预计未来会进一步增长。收到 JSON 后(每个表有一个 JSON 请求和响应对) - 它被映射到 CoreData - 该过程还包括将相应的 CoreData 实体(客户、城市、代理机构等)彼此粘合在一起,即在 CoreData 层上设置这些实体之间的关系。

项目的CoreData fetch-part(或read-part)本身已经过高度优化 - 我猜它使用了CoreData几乎所有可能的性能和内存调整,这就是为什么应用程序的UI层非常快速且响应迅速,所以我认为它的工作是完全令人满意和充分的。


问题是CoreData层的准备过程,即服务器到客户端的同步过程:耗时太长。考虑 30 个网络请求产生 30 个 JSON 包(“包”我的意思是“一个表 - 一个 JSON”),然后映射到 30 个 CoreData 实体,然后将它们粘合在一起(在它们之间设置适当的 CoreData 关系)。当我第一次看到这一切是如何在这个项目中完成的(太慢了)时,我脑海中浮现的第一个想法是:

“第一次执行完整的同步(应用程序的第一次启动时间) - 在一个存档文件(类似于数据库转储)中执行 整个数据库 数据的获取,然后以某种方式将其作为一个整体导入核心数据领域”。

但后来我意识到,即使这种单一文件转储的传输是可能的,CoreData 仍然需要我对相应的 CoreData 实体进行粘合以设置它们之间的适当关系,所以很难想象如果我依赖这个方案,我可以在性能上受益。

另外,我的同事建议我将 SQLite 视为 Core Data 的完全替代品,但不幸的是我没有使用它的经验,这就是为什么我完全无法预见如此严肃的设计决策的所有后果(即使同步过程非常缓慢,我的应用确实可以工作,尤其是它的 UI 性能现在非常好)。关于 SQLite,我唯一能想象到的是,与 Core Data 相比,它不会促使我在客户端粘合一些额外的关系,因为 SQLite 有其良好的旧外键系统,不是吗?


以下是问题(受访者,请不要在回答时混淆这些观点 - 我对所有这些观点都感到困惑):

  1. 有没有人有像我上面描述的那样采用“首次大量导入整个数据库”方法的经验?如果他们是否利用 JSONCoreData 对,我将非常感谢了解任何解决方案。

  2. Core Data 是否有一些全局导入机制,可以允许大量创建相应的 30 表模式(可能使用上述“30 包 JSON”以外的某些特定源)而无需设置30个实体的对应关系?

  3. 如果 2) 是不可能的,是否有可能加快同步过程?这里我指的是我的应用使用的当前 JSONCoreData 方案的改进。

  4. 迁移到 SQLite:我应该考虑这种迁移吗?我会从中得到什么好处?那么复制->传输->客户端准备的整个过程会是怎样的呢?

  5. CoreData 和 SQLite 的其他替代品 - 它们会是什么或看起来像什么?

  6. 您对我所描述的情况还有其他想法或看法吗?


更新 1

虽然 Mundi 写的答案很好(一个大的 JSON,对于使用 SQLite “否”),我仍然对我所描述的问题有任何其他见解感兴趣。


更新 2

我确实尝试使用我的俄语英语以最好的方式来描述我的情况,希望我的问题对所有阅读它的人来说都非常清楚。通过第二次更新,我将尝试为其提供更多指南,以使我的问题更加清晰。

请考虑两种二分法:

  1. 我可以/应该使用什么作为 iOS 客户端上的数据层 - CoreData 与 SQLite?
  2. 我可以/应该使用什么作为传输层 - JSON(答案中建议的一次性 JSON,甚至可能压缩)或一些 DB 本身转储(当然,如果可能的话 -请注意,我也在我的问题中提出这个问题)。

我认为由这两个二分法相交形成的“扇区”很明显,从第一个中选择 CoreData,从第二个中选择 JSON,是 iOS 开发世界中最广泛使用的默认设置,它也被使用来自这个问题的我的应用程序。

话虽如此,我声称我会很高兴看到有关 CoreData-JSON 对的任何答案以及考虑使用任何其他“部门”的答案(选择 SQLite 及其某种转储方法怎么样,为什么不?)

另外,需要注意的重要一点是,我不想仅仅放弃当前选项而使用其他一些替代方案,我只想让解决方案在其使用的同步和 UI 阶段都能快速运行。因此,欢迎提供有关改进当前方案的答案以及建议其他方案的答案!

现在,请查看以下更新 #3,它提供了我当前 CoreData-JSON 情况的更多详细信息:


更新 3

正如我所说,目前我的应用程序接收 30 包 JSON - 一个包用于整个表。让我们以大容量表为例:Client、Agency、City。

它是核心数据,所以如果 client 记录有非空的 agency_id 字段,我需要创建新的核心数据实体 Agency (NSManagedObject subclass) 并用该记录的 JSON 数据填充它,这就是我需要的原因已经为Agency (NSManagedObject's subclass) 类的这个机构有相应的核心数据实体,最后我需要做类似client.agency = agency; 的事情,然后调用[currentManagedObjectContext save:&error]。以这种方式完成后,我可以要求获取此客户端并要求其.agency 属性查找相应的实体。我希望当我这样做时我是完全清醒的。

现在想象一下这种模式应用于以下情况:

我刚刚收到以下 3 个单独的 JSON 包:10000 个客户和 4000 个城市和 6000 个代理(客户有一个城市,城市有很多客户;客户有代理,代理有很多客户,代理有一个城市,城市有很多机构)。

现在我想在核心数据级别设置以下关系:我希望我的客户实体client 连接到相应的城市和相应的机构。

目前这个在项目中的实现做了一件很丑陋的事情:

  1. 由于依赖顺序如下:City -> Agency -> Client,即需要先烘焙 City,应用程序开始为 City 创建实体并将它们持久化到 Core Data。

  2. 然后它处理机构的 JSON:它遍历每个 JSON 记录 - 对于每个机构,它创建一个新实体 agency 并通过其 city_id,它获取相应的实体 city 并连接它使用agency.city = city。在完成整个机构 JSON 数组的迭代后,保存当前的托管对象上下文(实际上 -[managedObjectContext save:] 会执行多次,每次处理 500 条记录后)。在这一步,很明显,为 6000 个代理机构中的每一个的每个客户获取 4000 个城市中的一个对整个同步过程有巨大的性能影响。

  3. 然后,最后处理客户端的JSON:和前2阶段一样,遍历整个10000个元素的JSON数组,逐个执行对应机构和ZOMG城市的获取,这会影响整体表现与之前的第 2 阶段相同。

一切都很糟糕。

我在这里看到的唯一性能优化是,第一阶段可以留下一个带有城市 ID(我的意思是 NSNumber 的真实 ID)和错误城市实体作为值的大型字典,因此可以防止丑陋 find 下一个阶段 2 的过程,然后使用类似的缓存技巧在阶段 3 上做同样的事情,但问题是在刚刚描述的所有 30 个表之间有更多的关系 [Client-City , Client-Agency, Agency-City ] 所以涉及缓存所有实体的最终过程最有可能影响 iPad 设备为我的应用程序保留的资源。


更新 4

致未来受访者的信息:我已尽力使此答案详细且格式正确,我真的希望您能以详细的答案来回答。如果您的回答能够真正解决此处讨论的问题的复杂性,并补充我为使我的问题尽可能清晰和笼统而做出的努力,那就太好了。谢谢。

更新 5

相关主题:Core Data on client (iOS) to cache data from a server Strategy、Trying to make a POST request with RestKit and map the response to Core Data。

更新 6

即使不再可能打开新的赏金并且有已接受的答案,我仍然很高兴看到包含有关本主题解决的问题的其他信息的任何其他答案。提前致谢。

【问题讨论】:

  • 那么,您的问题主要是关于如何有效地建立关系?你的代码现在是如何做到的?有一些优化,但您已经在使用哪些优化?
  • 请注意我说的是 fetch-part 的优化——“fetch-part(或 read-part)被高度优化”——实际上这意味着它只是一个常规的 CoreData 获取——如文档中所述,其中描述了此处描述的所有获取调整(预获取,仅获取需要的属性等),即没有什么神奇的,但我称它为“重”,因为如果你曾经见过初始状态我得到了这个我的遗留项目,你会想知道 CoreData 怎么会被滥用这么多!..
  • 话虽如此,我将尝试更新我的问题,为它提供更多关于我试图通过这个问题实现的目标的指南。我希望它也能解决@Alex 的需求。给我一分钟)
  • 请看我刚刚写的更新#3。我希望我已经很好地描述了我的项目的问题。
  • 您的服务器上是否启用了 gzip/deflate 压缩?

标签: ios json core-data


【解决方案1】:

我有一个非常相似的项目的经验。 Core Data 插入需要一些时间,因此我们要求用户这将需要一段时间,但只是第一次。最好的性能调整当然是在保存之间获得正确的批量大小,但我相信你知道这一点。

一个性能建议:我尝试了一些事情,发现创建许多下载线程可能会影响性能,我想是因为对于每个请求,服务器等都有一些延迟。

相反,我发现一次性下载所有 JSON 要快得多。我不知道你有多少数据,但我测试了超过 100.000 条记录和一个 40MB+ JSON 字符串,这真的很快,所以瓶颈只是核心数据插入。使用@autorelease 池,这甚至在第一代 iPad 上的表现也可以接受。

远离 SQLite API - 复制使用 Core Data 开箱即用的性能优化将花费您超过一个人年的时间(提供高生产力)。

【讨论】:

  • 谢谢你的回答,你说的我明白了!
  • @Stanislaw 感谢您的赏金。
  • @Alex 让我重申一下:我发现 JSON 文件大小不是瓶颈。
  • @Mundi,对不起,我累了,写了令人困惑的评论。我们在谈论文件数量吗?一个 JSON 与多个 JSON?
  • @Alex 数量和大小。最好是一个文件,越小越好。但实际尺寸并不那么重要——这就是我的观点。
【解决方案2】:

首先,你做了很多工作,无论你怎么切都需要一些时间,但有一些方法可以改进。

我建议您分批进行提取,批量大小与您的批量大小相匹配,以处理新对象。例如,在创建新的 Agency 记录时,请执行以下操作:

  1. 确保当前Agency 批次按city_id 排序。 (我稍后会解释原因)。

  2. 获取批次中每个Agency 的City ID。根据您的 JSON 的结构,这可能是这样的单行代码(因为 valueForKey 适用于数组):

    NSArray *cityIDs = [myAgencyBatch valueForKey:@"city_id"];
    
  3. 使用您在上一步中找到的 ID 在一次提取中获取当前传递的所有 City 实例。按city_id 对结果进行排序。比如:

    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"City"];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"city_id in %@", cityIDs];
    [request setPredicate:predicate];
    [request setSortDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"city_id" ascending:YES] ]];
    NSArray *cities = [context executeFetchRequest:request error:nil];
    

现在,您有一组Agency 和另一组City,均按city_id 排序。匹配它们以建立关系(检查city_id,以防事情不匹配)。保存更改,然后继续下一批。

这将大大减少您需要执行的提取次数,从而加快处理速度。有关此技术的更多信息,请参阅 Apple 文档中的 Implementing Find-or-Create Efficiently。

另一件可能有帮助的事情是在开始获取之前使用您需要的对象“预热”Core Data 的内部缓存。这将在以后节省时间,因为获取属性值不需要访问数据存储。为此,您可以执行以下操作:

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"City"];
// no predicate, get everything
[request setResultType:NSManagedObjectIDResultType];
NSArray *notUsed = [context executeFetchRequest:request error:nil];

..然后忘记结果。这表面上没有用,但会改变内部核心数据状态,以便稍后更快地访问City 实例。

现在至于你的其他问题,

  • 直接使用 SQLite 代替 Core Data 对您的情况来说可能不是一个糟糕的选择。好处是您无需设置关系,因为您可以使用 city_id 之类的字段作为外键。所以,快速导入。当然,缺点是您必须自己完成将模型对象转换为 SQL 记录/从 SQL 记录转换的工作,并且可能会重写很多假设 Core Data 的现有代码(例如,每次您遵循关系时,您现在需要通过该外键查找记录)。此更改可能会解决您的导入性能问题,但副作用可能很大。

  • 如果您以文本形式传输数据,JSON 通常是一种非常好的格式。 如果您可以在服务器上准备一个 Core Data 存储,如果您可以按原样使用该文件而不是试图将其合并到现有的数据存储中,那几乎肯定会加快速度。您的导入过程将在服务器上运行一次,然后再也不会运行。但这些都是很大的“如果”,尤其是第二个。如果您到达需要将新服务器数据存储与现有数据合并的位置,您将立即回到现在的位置。

【讨论】:

  • 我知道这个“有效地实现查找或创建”,实际上这正是我在 UPDATE 3 中描述的作为我将要进行的下一个优化 - 我的字典变体是只是核心数据编程指南这一部分中描述的通用版本 - 而不是有两个排序数组 - ids 数组和它们相应对象的数组 - 我只有一个字典。更通用的我的变体使我有可能将我的客户与多个实体粘合在一起,但当我迭代当前的客户批次时,与两个或更多实体粘合在一起。
  • 感谢您的回答,尤其是“热身”技巧-我不知道。可以获取整批(fx 10000)实体吗? (我知道他们有问题,但无论如何......)
  • 应该没问题,它们是错误的,Core Data 应该管理缓存。
  • 还有一个问题,当我有你在线时) - 这真的意味着我最终会发现自己有一个大型复杂方法,只涉及处理客户端及其所有依赖项,即粘合所有这些核心数据实体客户,代理商,......彼此 - 我的意思是“每个具有 2> 依赖关系的实体的一个大方法”???我知道这个感叹听起来是修辞,但我想这个方法包含所有这些粘合材料的最终实现,尺寸非常大。在具有复杂数据库结构和大容量的实际项目中是否如此?!提前致谢。
  • 感谢您的回答。在首先编写涉及所有可能优化的代码之前,您刚刚证实了我对编写通用代码的怀疑(也许它会有大量的行并且不够通用 - 但至少那时它可能会很好指向开始任何抽象和概括)。
【解决方案3】:
  1. 对于表格中的每一行,都必须有一个 timestamp 列。如果没有,您应该添加它。
  2. 第一次和每次获取数据库转储时都会存储上次更新日期和时间。
  3. 在每次您指示数据库仅返回自上次下载操作以来更改或更新的记录时。还应该有一个“已删除”标志,以便您删除消失的记录。
  4. 那么您只需要update certain matching records 即可在各个方面节省时间。

为了加快首次同步,您还可以在应用中附带种子数据库,以便无需任何网络操作即可立即导入。

  1. 手动下载 JSON 文件。
  2. Put them into your project.
  3. 在项目配置或头文件的某处记录下载日期和时间。
  4. 在第一次运行时,找到并加载所述文件,然后像更新它们一样继续操作。
  5. 如有疑问,refer to the manual.

例子:

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"cities" 
                                            ofType:@"json"];
NSData *citiesData = [NSData dataWithContentsOfFile:filePath];
// I assume that you're loading an array
NSArray *citiesSeed = [NSJSONSerialization JSONObjectWithData:citiesData 
                       options:NSJSONReadingMutableContainers error:nil];

【讨论】:

  • 感谢您的回答。我的应用程序已经按照您的描述进行。我的问题范围实际上是关于执行同步过程的第一次。
  • @Stanislaw 看到我对答案的补充
  • @Stanislaw 我必须说你的问题太详细了。请参阅我的更新答案。你真的要我讲这些细节吗?
  • 对不起,@sanmai,我犯了一个愚蠢的错误并评论了错误的答案。
  • 现在对您的“查看答案的补充”的真正评论:在我的情况下,“种子”方法不是一个选项,因为在服务器上有大量不断变化的数据。我不喜欢预先打包种子数据库的想法,因为在新版本出现之前,预先打包的信息会变得过于过时。
【解决方案4】:

您可以控制服务器吗?我问,因为这听起来像您从以下段落中所做的:

“第一次执行完整的同步(应用程序的第一次启动时间) - 执行整个数据库数据的获取,比如说,一个存档文件(类似于数据库转储),然后以某种方式将其作为一个整体导入到CoreData 领域”。

如果可以发送转储,为什么不发送核心数据文件本身? Core Data(默认情况下)由 SQLite 数据库支持——为什么不在服务器上生成该数据库,将其压缩并通过网络发送呢?

这意味着您可以消除所有 JSON 解析、网络请求等,并将其替换为简单的文件下载和存档提取。我们在一个项目中做到了这一点,它极大地提高了性能。

【讨论】:

  • [重新编辑评论] 感谢您的回答。从服务器发送数据库转储是我正在考虑的选项之一,但现在我不确定我可以要求服务器人员离开当前的工作方案有多少步骤 - 我们的沟通并不那么容易(他们有点像来自“不同的部门”)除了发送压缩的数据库转储外,我还考虑传输整个 JSON 文件的压缩存档 - 它看起来与您的变体非常相似,使用额外的 JSON->CoreData 粘合压缩核心数据本身在我的肩膀上工作。
  • 顺便说一句,“核心数据文件本身”是什么意思 - 这是 SQLite 转储还是什么?您能否更新您的答案并提供更多详细信息?
  • 这是个好主意——但问题是要在服务器上创建一个 Core Data 托管的 SQLite 数据库。该过程需要自动运行。这个问题的解决方案应该有自己的关于 SO 的问题。 ;)
【解决方案5】:

这里有我的建议:

  • 使用magicalrecord。它是一个 CoreData 包装器,可以为您节省大量样板代码,而且它具有非常有趣的特性。
  • 按照其他人的建议,在一个请求中下载所有 JSON。如果您可以将第一个 JSON 文档嵌入到应用程序中,则可以节省下载时间并在您第一次打开应用程序时立即开始填充数据库。此外,使用magicrecord 很容易在单独的线程中执行此保存操作,然后自动同步所有上下文。这可以提高您应用的响应速度。
  • 似乎你应该在解决第一个导入问题后重构那个丑陋的方法。同样,我建议使用 magicrecord 轻松创建这些实体。

【讨论】:

    【解决方案6】:

    我们最近将一个相当大的项目从 Core Data 迁移到 SQLite,其中一个主要原因是批量插入性能。我们在过渡过程中丢失了很多功能,如果可以避免的话,我不建议您进行切换。在转换到 SQLite 之后,我们实际上在 Core Data 透明地为我们处理的批量插入以外的领域遇到了性能问题,即使我们修复了这些新问题,也需要一些时间才能恢复运行。虽然我们在从 Core Data 过渡到 SQLite 的过程中花费了一些时间和精力,但我不能说有任何遗憾。

    了解了这一点后,我建议您在着手修复批量插入性能之前先进行一些基线测量。

    1. 测量在当前状态下插入这些记录需要多长时间。
    2. 完全跳过设置这些对象之间的关系,然后测量插入性能。
    3. 创建一个简单的 SQLite 数据库,并用它来衡量插入性能。这应该可以很好地估计执行实际 SQL 插入所需的时间,并且还可以让您很好地了解 Core Data 开销。

    您可以立即尝试一些方法来加快插入速度:

    1. 确保在执行批量插入时没有活动的提取结果控制器。通过活动,我的意思是获取具有非零委托的结果控制器。根据我的经验,Core Data 的更改跟踪是尝试进行批量插入时最昂贵的操作。
    2. 在单个上下文中执行所有更改,并停止合并来自不同上下文的更改,直到完成此批量插入。

    要更深入地了解幕后的实际情况,请启用Core Data SQL debugging 并查看正在执行的 SQL 查询。理想情况下,您会希望看到很多 INSERT 和一些 UPDATE。但是,如果您遇到过多的 SELECT 和/或 UPDATE,则表明您正在阅读或更新对象过多。

    使用 Core-Data 分析器工具更好地了解 Core Data 正在发生的事情。

    【讨论】:

    • 感谢您的回答,我将使用您的建议作为我未来重构工作的指南!
    【解决方案7】:

    我决定编写自己的答案,总结我发现对我的情况有用的技巧和建议。感谢所有发布答案的人。


    我。交通

    1. “一个 JSON”。这是我想尝试的想法。谢谢@mundi。

    2. 在将 JSON 发送到客户端之前对其进行存档的想法,无论是一个 JSON 包还是 30 个单独的“一个表 - 一个包”。


    二。设置核心数据关系

    我将描述一个使用虚构的大型导入操作导入 JSON->CoreData 导入的过程,就好像它是在一种方法中执行的一样(我不确定它是否会这样 - 也许我将它分成逻辑块)。

    假设在我的虚拟应用程序中有 15 个大容量表,其中“大容量”表示“不能一次保存在内存中,应该使用批量导入”和 15 个非大容量表,每个表都有

    宽敞:

    • 城市 (15k+)
    • 客户 (30k+)
    • 用户 (15k+)
    • 事件 (5k+)
    • 动作 (2k+) ...

    小:

    • client_types (20-)
    • visit_types (10-)
    • 职位 (10-) ...

    让我们想象一下,我已经下载了 JSON 包并将其解析为复合 NSArray/NSDictionary 变量:我有 cityJSON、clientsJSON、usersJSON、...

    1.首先使用小表

    我的伪方法首先导入小表。让我们以 client_types 表为例:我遍历 clientTypesJSON 并创建 ClientType 对象(NSManagedObject 的子类)。不仅如此,我将结果对象收集在字典中,这些对象作为其值,这些对象的“id”(外键)作为键。

    这是伪代码:

    NSMutableDictionary *clientTypesIdsAndClientTypes = [NSMutableDictionary dictionary];
    for (NSDictionary *clientTypeJSON in clientsJSON) {
        ClientType *clientType = [NSEntityDescription insertNewObjectForEntityForName:@"ClientType" inManagedObjectContext:managedObjectContext];
    
        // fill the properties of clientType from clientTypeJSON
    
        // Write prepared clientType to a cache
        [clientTypesIdsAndClientTypes setValue:clientType forKey:clientType.id];
    }
    
    // Persist all clientTypes to a store.
    NSArray *clientTypes = [clientTypesIdsAndClientTypes allValues];
    [managedObjectContext obtainPermanentIDsForObjects:clientTypes error:...];
    
    // Un-fault (unload from RAM) all the records in the cache - because we don't need them in memory anymore.
    for (ClientType *clientType in clientTypes) {
        [managedObjectContext refreshObject:clientType mergeChanges:NO];
    }
    

    结果是我们有一堆小表的字典,每个都有对应的对象集和它们的 id。我们稍后将使用它们而无需重新获取,因为它们很小并且它们的值(NSManagedObjects)现在是错误的。

    2。使用第 1 步中获得的小表中对象的缓存字典来建立与它们的关系

    让我们考虑复杂的表clients:我们有clientsJSON,我们需要为每个客户记录设置clientType关系,这很容易,因为我们确实有一个带有clientTypes及其ID的缓存:

    for (NSDictionary *clientJSON in clientsJSON) {
        Client *client = [NSEntityDescription insertNewObjectForEntityForName:@"Client" inManagedObjectContext:managedObjectContext];
    
        // Setting up SQLite field 
        client.client_type_id = clientJSON[@"client_type_id"];
    
        // Setting up Core Data relationship beetween client and clientType
        client.clientType = clientTypesIdsAndClientTypes[client.client_type_id];
    }
    
    // Save and persist
    

    3.处理大表 - 批量

    让我们考虑一个大型clientsJSON,其中有 30k+ 个客户。我们不会遍历整个clientsJSON,而是将其拆分为适当大小的块(500 条记录),以便每 500 条记录调用一次[managedObjectContext save:...]。此外,将每个 500 条记录批次的操作包装到 @autoreleasepool block 中也很重要 - 请参阅 Reducing memory overhead in Core Data Performance guide

    小心 - 步骤 4 描述了应用于一批 500 条记录的操作,而不是整个clientsJSON!

    4.处理大表 - 与大表建立关系

    考虑下面的方法,一会儿我们会用到:

    @implementation NSManagedObject (Extensions)
    + (NSDictionary *)dictionaryOfExistingObjectsByIds:(NSArray *)objectIds inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext {
        NSDictionary *dictionaryOfObjects;
    
        NSArray *sortedObjectIds = [objectIds sortedArrayUsingSelector:@selector(compare:)];
    
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass(self)];
    
        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(id IN %@)", sortedObjectIds];
        fetchRequest.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey: @"id" ascending:YES]];
    
        fetchRequest.includesPropertyValues = NO;
        fetchRequest.returnsObjectsAsFaults = YES;
    
        NSError *error;
        NSArray *fetchResult = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    
        dictionaryOfObjects = [NSMutableDictionary dictionaryWithObjects:fetchResult forKeys:sortedObjectIds];
    
        return dictionaryOfObjects;
    }
    @end
    

    让我们考虑 clientsJSON 包,其中包含我们需要保存的一批 (500) Client 记录。我们还需要在这些客户和他们的代理之间建立关系(Agency,外键是agency_id)。

    NSMutableArray *agenciesIds = [NSMutableArray array];
    NSMutableArray *clients = [NSMutableArray array];
    
    for (NSDictionary *clientJSON in clientsJSON) {
        Client *client = [NSEntityDescription insertNewObjectForEntityForName:@"Client" inManagedObjectContext:managedObjectContext];
    
        // fill client fields...
    
        // Also collect agencies ids
        if ([agenciesIds containsObject:client.agency_id] == NO) {
            [agenciesIds addObject:client.agency_id];
        }        
    
        [clients addObject:client];
    }
    
    NSDictionary *agenciesIdsAndAgenciesObjects = [Agency dictionaryOfExistingObjectsByIds:agenciesIds];
    
    // Setting up Core Data relationship beetween Client and Agency
    for (Client *client in clients) {
        client.agency = agenciesIdsAndAgenciesObjects[client.agency_id];
    }
    
    // Persist all Clients to a store.
    [managedObjectContext obtainPermanentIDsForObjects:clients error:...];
    
    // Un-fault all the records in the cache - because we don't need them in memory anymore.
    for (Client *client in clients) {
        [managedObjectContext refreshObject:client mergeChanges:NO];
    }
    

    我在这里使用的大部分内容都在以下 Apple 指南中进行了描述:Core Data performance、Efficiently importing data。所以步骤 1-4 的总结如下:

    1. 当对象被持久化时将它们变成故障,因此随着导入操作的深入,它们的属性值变得不必要。

    2. 以对象作为值,将它们的ids 作为键来构造字典,因此在构建这些对象和其他对象之间的关系时,这些字典可以用作查找表。

    3. 在遍历大量记录时使用@autoreleasepool。

    4. 使用类似于dictionaryOfExistingObjectsByIds 的方法或Tom 在他的回答中引用的方法,来自Efficiently importing data - 一种在其后面具有SQL IN 谓词的方法,以显着减少提取次数。阅读 Tom 的回答并参考 Apple 的相应指南,以更好地理解这项技术。


    关于这个主题的好读物

    objc.io issue #4: Importing Large Data Sets

    【讨论】:

    • 很棒的总结!谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-06
    • 2012-11-08
    • 2021-11-27
    • 1970-01-01
    • 2010-11-04
    相关资源
    最近更新 更多