通过JSON改进将服务器数据库镜像到客户端数据库的过程?

Sta*_*ich 16 json core-data ios

我有一个适用于iPad的现成企业(非AppStore)遗留iOS应用程序,我需要重构(它是由另一个开发人员,我的前任在我目前的工作中编写的).

此应用程序通过JSON从具有MSSQL数据库的服务器获取其数据.数据库模式有大约30个表,最广泛的是:客户端,城市,代理每个都有大约10,000个记录,并且预计将来会进一步增长.收到JSON后(每个表的一个JSON请求和响应对) - 它被映射到CoreData - 该过程还包括将相应的CoreData实体(客户端,城市,代理和其他)相互粘合在一起,即在CoreData层上设置这些实体之间的关系.

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


问题是准备CoreData层的过程,即服务器到客户端的同步过程:它需要花费太多时间.考虑30个网络请求,产生30个JSON包("pack"我的意思是"一个表 - 一个JSON"),然后映射到30个CoreData实体,然后将它们粘合在一起(在它们之间设置适当的CoreData关系).当我第一次看到在这个项目中完成所有这些工作(太慢)时,我想到的第一个想法是:

"第一次执行完全同步(应用程序的第一次启动时间) - 在一个存档文件(例如数据库转储)中执行整个数据库数据的获取,然后以某种方式将其作为整体导入到核心数据土地".

但后来我意识到,即使这样的单文件转储的传输是可能的,CoreData仍然需要我执行相应的CoreData实体的粘合来设置它们之间的适当关系,以便很难想象我能够如果我依赖这个计划,我会在表现上受益.

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


所以这里有一些问题(受访者,请不要在回答时混淆这些要点 - 我对所有问题都有太多困惑):

  1. 是否有人以上述方式对"首次大量导入整个数据库"方法有这样的经验?如果他们利用JSON < - > CoreData对,我会非常感谢知道任何解决方案.

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

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

  4. 迁移到SQLite:我应该考虑这样的迁移吗?我会从中受益吗?整个复制过程 - >传输 - >客户端准备工作如何?

  5. CoreData和SQLite的其他替代品 - 它们可能是什么样子?

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


更新1

虽然Mundi写的答案很好(一个大的JSON,使用SQLite的"No"),但我仍然对如何对我所描述的问题有任何其他见解感兴趣.


更新2

我确实尝试用我的俄语英语以最好的方式描述我的情况,希望我的问题可以变得非常清楚,每个人都会阅读它.通过第二次更新,我将尝试为其提供更多指南,以使我的问题更加清晰.

请考虑两个二分法:

  1. 我可以/应该在iOS客户端上使用什么作为数据层 - CoreData vs SQLite?
  2. 我可以/应该使用什么作为传输层 - JSON(答案中建议单一JSON一次,甚至可以压缩)或者一些DB-dump-dumps(如果有可能的话,当然 - 请注意我是在我的问题中也问这个问题).

我认为通过这两个二分法的交集形成的"扇区"非常明显,从第一个选择CoreData而第二个选择JSON是iOS开发世界中最广泛的默认值,并且我的应用程序使用它从这个问题.

话虽如此,我声称我会感谢看到有关CoreData-JSON对的任何答案以及考虑使用任何其他"扇区"的答案(如何选择SQLite和某种转储方法,为什么不呢?)

另外,需要注意的是,我不想只删除其他选项的当前选项,我只想让解决方案在其使用的同步和UI阶段快速运行.所以欢迎关于改进现有方案的答案以及建议其他方案的答案!

现在,请参阅以下更新#3,其中提供了有关当前CoreData-JSON情况的更多详细信息:


更新3

正如我所说,目前我的应用程序收到30包JSON - 整个表的一个包.让我们以宽大的表格为例:客户,代理商,城市.

它是Core Data,所以如果一个client记录有非空agency_id字段,我需要创建类的新Core Data实体Agency (NSManagedObject subclass)并用这个记录的JSON数据填充它,这就是为什么我需要为这个类的代理已经拥有相应的Core Data实体Agency (NSManagedObject's subclass),最后我需要做一些事情client.agency = agency;,然后打电话[currentManagedObjectContext save:&error].以这种方式完成,之后我可以请求获取此客户端并要求其.agency属性查找相应的实体.我希望当我这样做时,我完全理智.

现在假设这种模式适用于以下情况:

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

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

在项目中当前实现这一点非常难看:

  1. 由于依赖顺序如下:城市 - >代理 - >客户,即城市需要先被烘焙,应用程序开始为城市创建实体并将其持久保存到核心数据.

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

  3. 然后,最后它处理客户端的JSON:就像在前两个阶段一样,它遍历整个10000元素的JSON数组,逐个执行相应代理和ZOMG城市的获取,这会影响整体性能像前一阶段2那样的方式.

这一切都很糟糕.

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


更新4

给未来受访者的信息:我已经尽力使这个答案非常详细和完善,我真的希望你回答详细的答案.如果你的答案能够真正解决这里讨论的问题的复杂性并且补充我为使我的问题尽可能清晰和普遍而做出的努力,那将是很好的.谢谢.

更新5

相关主题:客户端(iOS)上的核心数据,用于缓存来自服务器的数据策略,尝试使用RestKit发出POST请求并将响应映射到Core Data.

更新6

即使在没有可能打开新的赏金并且有接受的答案之后,我仍然会很高兴看到任何其他答案包含有关此主题所解决的问题的其他信息.提前致谢.

Mun*_*ndi 10

我有一个非常相似的项目经验.核心数据插入需要一些时间,因此我们要求用户这需要一段时间,但这只是第一次.最好的性能调整当然是在保存之间获得批量大小,但我相信你已经意识到这一点.

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

相反,我发现一次性下载所有JSON 要快得多.我不知道你有多少数据,但我测试了> 100.000条记录和40MB + JSON字符串,这种方法非常快,所以瓶颈只是核心数据插入.有了@autorelease游泳池,这甚至可以在第一代iPad上表现出色.

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


Tom*_*ton 6

首先,你做了很多工作,无论你如何分割它都需要一些时间,但有很多方法可以改进.

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

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

  2. 获取批次中City每个的ID Agency.根据你的JSON的结构,这可能是一个像这样的单线程(因为valueForKey在数组上工作):

    NSArray *cityIDs = [myAgencyBatch valueForKey:@"city_id"];
    
    Run Code Online (Sandbox Code Playgroud)
  3. City使用您在上一步中找到的ID,在一次获取中获取当前传递的所有实例.按结果排序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];
    
    Run Code Online (Sandbox Code Playgroud)

现在,你有一个数组Agency和另一个数组City,两者都排序city_id.将它们匹配以建立关系(检查city_id事情是否匹配).保存更改,然后继续下一批.

这将大大减少您需要执行的提取次数,这可以加快速度.有关此技术的更多信息,请参阅Apple的文档中有效实现查找或创建.

另一件可能有用的事情是在开始获取之前用你需要的对象"预热"Core Data的内部缓存.这将节省时间,因为获取属性值不需要访问数据存储.为此,你会做类似的事情:

NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"City"];
// no predicate, get everything
[request setResultType:NSManagedObjectIDResultType];
NSArray *notUsed = [context executeFetchRequest:request error:nil];
Run Code Online (Sandbox Code Playgroud)

..然后忘记结果.这在表面上是无用的,但会改变内部Core Data状态,以便City以后更快地访问实例.

至于你的其他问题,

  • 直接使用SQLite而不是Core Data可能不是您情况的可怕选择.好处是你不需要设置关系,因为你可以使用像city_id外键这样的使用字段.所以,快速导入.当然,缺点是您必须自己完成将模型对象转换为SQL记录或从SQL记录转换的工作,并且可能会重写相当多的现有代码,这些代码假定为Core Data(例如,每次您关注的是您现在的关系)需要通过该外键查找记录).此更改可能会修复导入性能问题,但副作用可能很大.

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


Sed*_*ien 5

你有控制权的服务器吗?我问,因为这听起来像你从以下段落做的:

"第一次执行完全同步(应用程序的第一次启动时间) - 在一个存档文件(例如数据库转储)中执行整个数据库数据的获取,然后以某种方式将其作为整体导入CoreData域".

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

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


Sta*_*ich 2

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


一、交通

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

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


二. 设置核心数据关系

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

让我们想象一下,在我想象的应用程序中,有 15 个容量大的表,其中“容量大”的意思是“不能一次保存在内存中,应该使用批量导入”和 15 个非容量大的表,每个表都有 <500 条记录,例如:

宽敞:

  • 城市(15,000+)
  • 客户(30k+)
  • 用户(15,000+)
  • 活动(5000+)
  • 动作 (2k+) ...

小的:

  • 客户类型 (20-)
  • 访问类型 (10-)
  • 职位 (10-) ...

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

1. 首先使用小桌子

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

这是伪代码:

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];
}
Run Code Online (Sandbox Code Playgroud)

结果是我们有一堆小表字典,每个字典都有相应的对象集及其 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
Run Code Online (Sandbox Code Playgroud)

3. 处理大表——批量

让我们考虑一个clientsJSON拥有超过 30k 客户的大型企业。我们不会迭代整个整体clientsJSON,而是将其分成适当大小的块(500 条记录),因此[managedObjectContext save:...]称为每 500 条记录。此外,将每个 500 条记录批次的操作包装到一个文件中也很重要@autoreleasepool block- 请参阅核心数据性能指南中的减少内存开销

请注意 - 步骤 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
Run Code Online (Sandbox Code Playgroud)

让我们考虑包含我们需要保存的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];
}
Run Code Online (Sandbox Code Playgroud)

我在这里使用的大部分内容都在这些 Apple 指南中进行了描述:核心数据性能高效导入数据。因此,步骤 1-4 的摘要如下:

  1. 当对象被持久化时,它们就会变成错误,因此随着导入操作的进一步进行,它们的属性值变得不必要。

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

  3. 迭代大量记录时使用@autoreleasepool。

  4. dictionaryOfExistingObjectsByIds使用与 Tom 在其答案中引用的方法类似或相似的方法,即高效导入数据- 一种背后有 SQL 谓词的方法IN,可显着减少提取次数。阅读 Tom 的回答并参考 Apple 的相应指南,以更好地理解这项技术。


关于这个主题的好读物

objc.io 问题 #4:导入大型数据集


归档时间:

查看次数:

1582 次

最近记录:

11 年,10 月 前