通过JSON改进将服务器数据库镜像到客户端数据库的过程?
我有一个现成的企业(非AppStore)传统的iPad应用程序,我需要重构(这是另一个开发人员,我的前任在我目前的工作)写的。
此应用程序通过JSON从具有MSSQL数据库的服务器获取其数据。 数据库模式有大约30个表格,最广泛的是:客户端,城市,机构各有大约10000个logging,并且未来有望进一步增长。 在接收到JSON(每个表的一个JSON请求和响应对)后,它被映射到CoreData–这个过程还包括将相应的CoreData实体(Client,City,Agency等)相互粘合在一起在CoreData层上设置这些实体之间的关系。
本身的项目CoreData fetch-part(或者read-part)被大量优化 – 我猜,它使用了CoreData几乎所有可能的性能和内存调整,这就是为什么UI层的应用程序非常快速且响应速度很快认为它的工作是完全令人满意和充分的。
问题是CoreData层的准备过程,即服务器到客户端的同步过程:花费太多时间。 考虑30个networking请求产生30个JSON包(“包”我的意思是“一个表 – 一个JSON”),然后将其映射到30个CoreData实体,然后粘贴在一起(适当的CoreData关系设置在它们之间)。 当我第一次看到这个项目是如何完成的时候(太慢),首先想到的是:
“首次执行完整的同步(应用程序的首次启动时间) – 在一个归档文件(类似于数据库转储)中执行整个数据库数据的获取,然后以某种方式将其作为整体导入到核心数据土地”。
但后来我意识到,即使这样的一个文件转储的传输是可能的,CoreData仍然会要求我执行相应的CoreData实体的粘合,以在它们之间设置适当的关系,这样很难想象我可以如果我依靠这个计划的话,就会在performance上受益。
另外,我的同事build议我考虑将SQLite作为Core Data的完整替代品,但不幸的是我没有使用它的经验,这就是为什么我完全盲目地预见到这样严重的devise决定的所有后果(即使有同步过程很慢,我的应用程序确实工作 ,尤其是它的UI性能现在非常好)。 我只能想象SQLite和Core Data不同,它不会促使我在客户端粘贴一些额外的关系,因为SQLite具有良好的旧外键系统,不是吗?
因此,这里是一些问题(受访者,当你回答时请不要混合这些问题 – 我对所有这些问题都有太多的疑惑):
-
有没有人有这样的经验,就像我上面所说的那样,采取“首次大量导入整个数据库”的方法? 我会非常感谢知道任何解决scheme,如果他们利用JSON < – > CoreData对或不。
-
核心数据是否具有一些全局的导入机制,可以批量创build相应的30表格模式(可能使用上述“30包JSON”之外的某些特定源),而不需要为30个实体build立对应关系?
-
如果2)是不可能的,是否有加快同步过程的可能性? 在这里我的意思是我的应用程序使用的当前JSON < – > CoreDatascheme的改进。
-
迁移到SQLite:我应该考虑这种迁移? 我会从中受益吗? 复制 – >传输 – >客户端准备过程的整个过程如何?
-
CoreData和SQLite的其他替代品 – 它们可能是什么样子?
-
你可能对我所描述的情况有任何其他想法或异象?
更新1
虽然Mundi写的答案很好(一个大的JSON,使用SQLite的“No”),但是如果对于我所描述的问题有其他的了解,我仍然很感兴趣。
更新2
我曾尝试用我的俄语英语来描述我的情况,希望我的问题能够让所有读者都清楚的了解。 通过第二次更新,我将尝试提供一些更多的指导,使我的问题更加清晰。
请考虑两个二分法:
- 我可以/应该在iOS客户端上使用什么作为数据层 – CoreData和SQLite?
- 我可以/应该使用什么作为传输层 – JSON(在回答中提到的单JSON-at-once,甚至可能是压缩)或某些DB本身转储(当然,如果它是可能的话) – 注意我是也在我的问题中问这个)。
我认为这是非常明显的,这两个二分法的交集形成的“扇区”,从第一个selectCoreData和从第二个selectJSON是iOS开发世界中最广泛的默认设置,也被我的应用程序使用从这个问题。
话虽如此,我声明我会很高兴看到有关CoreData-JSON对的任何答案,以及考虑使用其他任何“扇区”的答案(selectSQLite和某种types的转储方法,为什么不呢?)
另外,重要的是要注意,我不想只是放弃其他一些替代scheme的当前选项,我只是想让解决scheme在其使用的同步和UI阶段快速工作。 所以有关改进现行scheme的答案以及提出其他scheme的答案是值得欢迎的!
现在,请参阅以下更新#3,其中提供了有关我当前的CoreData-JSON情况的更多详细信息:
更新3
正如我所说,目前我的应用程序收到30包JSON – 整个表一包。 以宽敞的桌子为例:客户,代理,城市。
它是核心数据,所以如果client
logging有非空的agency_id
字段,我需要创build类Agency (NSManagedObject subclass)
新的核心数据实体,并填写此logging的JSON数据,这就是为什么我需要已经有相应的核心这个代理类Agency (NSManagedObject's subclass)
数据实体,最后我需要做一些像client.agency = agency;
然后调用[currentManagedObjectContext save:&error]
。 这样做后,我可以再请求这个客户被提取,并要求它的财产寻找相应的实体。 当我这样做时,我希望自己完全健康。
现在想象这个模式适用于以下情况:
我刚刚收到了以下三个JSON包:10000个客户,4000个城市和6000个代理(客户有一个城市,城市有很多客户;客户有代理,代理有很多客户,代理有一个城市,城市有代理)。
现在我想在核心数据层次上设置以下关系:我希望我的客户端实体client
连接到相应的城市和相应的代理机构。
目前在项目中执行这个工作确实很丑陋:
-
由于依赖顺序如下:城市 – >代理 – >客户(即城市需要先烘烤),应用程序开始为城市创build实体,并坚持到核心数据。
-
然后处理机构的JSON:它遍历每个JSONlogging – 对于每个机构,它创build一个新的实体
agency
并通过其city_id
获取相应的实体city
,并使用agency.city = city
将其连接。 迭代遍历整个机构的JSON数组完成后,当前的托pipe对象上下文被保存(实际上 – [managedObjectContext保存:]几次,每个处理500个logging后)。 在这一步很明显,6000个机构中的每一个机构为每个客户提供4000个城市中的一个对整个同步过程具有巨大的性能影响。 -
然后,最后处理客户的JSON:就像在前两个阶段一样,它遍历整个10000个元素的JSON数组,并逐个执行相应的代理和ZOMG城市的提取,这同样影响了整体性能像以前的阶段2的方式呢。
这是非常糟糕的。
我在这里可以看到的唯一性能优化是,第一阶段可能会留下一个带有城市id的大字典(我的意思是NSNumber的真实id),并将城市实体作为值进行故障),因此可以防止下面的丑陋find过程第二阶段,然后在第三阶段使用类似的caching技巧做同样的事情,但问题是,刚刚描述的所有30个表格之间存在更多的关系[客户 – 城市,客户机构,代理 – 城市]涉及caching所有实体的最终程序将最有可能触及iPad设备为我的应用程序保留的资源。
更新4
给未来的回应者留言:我已经尽了最大的努力使这个答案详细,结构良好,我真的希望你用详细的答案来回答。 如果你的回答真的能够解决这里讨论的问题的复杂性,并且补充了我为尽可能清晰和概括我的问题所做的努力,这将是非常好的。 谢谢。
更新5
相关主题: 客户端(iOS)上的核心数据caching服务器中的数据策略 , 尝试使用RestKit创buildPOST请求并将响应映射到核心数据 。
更新6
即使没有可能开放新的奖励,并且有可接受的答案,我仍然很高兴看到任何其他答案包含关于本主题所解决的问题的更多信息。 提前致谢。
我有一个非常类似的项目的经验。 核心数据插入需要一些时间,所以我们调整用户,这将需要一段时间,但只有第一次 。 最好的性能调整当然是保存批量大小,但我相信你知道这一点。
一个性能build议:我已经尝试了一些东西,发现创build多个下载线程可能会对性能产生影响,我想是因为每个请求都有一些来自服务器的延迟。
相反,我发现一次下载所有的JSON要快得多。 我不知道你有多less数据,但是我用> 100.000条logging和一个40MB + JSONstring进行了testing,结果非常快,所以瓶颈就是核心数据插入。 有了@autorelease
池,这甚至在第一代iPad上performance得可以接受。
远离SQLite API – 它会让你多于一年(提供高生产力)来复制Core Data提供的性能优化。
首先,你做了很多工作,无论你如何分割,都需要一些时间,但是有一些方法可以改进。
我build议分批进行抓取,批量大小与批量大小匹配,以便处理新对象。 例如,在创build新的Agency
logging时,请执行以下操作:
-
确保当前
Agency
批次按city_id
sorting。 (我会解释为什么以后)。 -
获取批次中每个
Agency
的City
ID。 根据你的JSON的结构,这可能是这样的一个valueForKey
(因为valueForKey
在数组上):NSArray *cityIDs = [myAgencyBatch valueForKey:@"city_id"];
-
通过使用您在上一步中find的ID,获取当前传递的所有
City
实例。 按city_id
sorting结果。 就像是: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
sorting。 匹配他们build立关系(检查city_id
万一不匹配)。 保存更改,然后继续下一批。
这将大大减less你需要做的取数,这应该加快速度。 有关此技术的更多信息,请参阅在Apple文档中实现“高效查找或创build” 。
另一个可能的帮助就是在开始提取内容之前,用核心数据的内部caching“热身”所需的对象。 这将节省以后的时间,因为获取属性值不需要访问数据存储。 为此,你可以这样做:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"City"]; // no predicate, get everything [request setResultType:NSManagedObjectIDResultType]; NSArray *notUsed = [context executeFetchRequest:request error:nil];
然后只是忘了结果。 这表面上没有用处,但会改变内部核心数据状态,以便稍后更快地访问City
实例。
至于你的其他问题,
-
直接使用SQLite而不是Core Data可能不是您的情况的一个可怕的select。 好处是你不需要build立关系,因为你可以使用像
city_id
这样的使用字段作为外键。 所以,快速导入。 不利的一面是,你必须做你自己的工作,将你的模型对象转换为SQLlogging,或者重写相当多的现有的代码,假设核心数据(例如,每次你现在跟随一个关系需要通过该外键查找logging)。 这种变化可能会解决您的导入性能问题,但副作用可能是显着的。 -
如果您以文本forms传输数据,JSON通常是非常好的格式。 如果您可以在服务器上准备一个Core Data存储,并且如果您将原样使用该文件,而不是将其合并到现有的数据存储中,那么这肯定会加快速度。 您的导入过程将在服务器上运行一次,然后再也不会。 但那些大“如果”,特别是第二个。 如果您到了需要将新的服务器数据存储与现有数据合并的位置,那么您马上就会回到现在的位置。
你有控制服务器? 我问,因为这听起来像你从以下段落:
“首次执行完整的同步(应用程序的首次启动时间) – 执行整个数据库数据(例如,一个存档文件(类似于数据库转储)的提取),然后以某种方式将其作为整体导入到CoreData land ”。
如果发送转储是可能的,为什么不发送核心数据文件本身? 核心数据(默认情况下)由SQLite数据库支持 – 为什么不在服务器上生成该数据库,将其压缩并通过线路发送?
这意味着你可以消除所有的JSONparsing,networking请求等,并用一个简单的文件下载和归档提取取代它。 我们在一个项目上做了这个,并且它的性能改善了不可估量的。
- 对于表中的每一行,必须有一个时间戳列。 如果没有,你应该添加它。
- 第一次和每次你获取数据库转储你存储最后更新的date和时间。
- 下次每次指示数据库仅返回自上次下载操作以来更改或更新的logging。 还应该有一个“删除”的标志,为您删除消失的logging。
- 那么你只需要更新某些匹配logging就可以在各个方面节省时间。
为了加速首次同步,您还可以使用应用程序发布种子数据库,以便在不进行任何networking操作的情况下立即导入。
- 手动下载JSON文件。
- 把它们放到你的项目中。
- 在项目configuration或头文件中的某处记下下载date和时间。
- 在第一次运行时,find并加载所述文件,然后像更新它们一样继续。
- 如有疑问, 请参阅手册。
例:
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];
在这里你有我的build议:
- 使用魔法logging 。 这是一个CoreData包装,为您节省了大量的样板代码,并附带了非常有趣的function。
- 像其他人所build议的那样,在一个请求中下载所有的JSON。 如果您可以将第一个JSON文档embedded到应用程序中,则可以保存下载时间,并在首次打开应用程序时开始填充数据库。 另外,使用magicalrecord很容易在单独的线程中执行此保存操作,然后自动同步所有上下文。 这可以提高您的应用程序的响应速度。
- 一旦你解决了第一个导入问题,似乎你应该重构那个丑陋的方法 。 再次,我会build议使用魔法logging来轻松创build这些实体。
我们最近把一个相当大的项目从Core Data移到了SQLite,其中一个主要原因是批量插入性能。 我们在过渡时遇到了很多不同的function,如果可以避免的话,我不build议你做这个开关。 在过渡到SQLite之后,我们确实遇到了性能问题,除了Core Data为我们透明处理的大容量插入以外,即使我们解决了这些新问题,也需要一些时间来恢复和运行。 虽然我们花了一些时间和精力从Core Data转换到SQLite,但我不能说有任何遗憾。
清理完成之后,我build议您在确定批量插入性能之前先进行基线测量。
- 测量在当前状态下插入这些logging需要多长时间。
- 跳过设置这些对象之间的关系,然后测量插入性能。
- 创build一个简单的SQLite数据库,并测量插入性能。 这应该给你一个非常好的基准估计,它需要多长时间来执行实际的SQL插入,并且还会给你一个核心数据开销的好主意。
几件事情,你可以尝试蝙蝠加快插入:
- 确保在执行批量插入时没有活动的提取结果控制器。 通过主动,我的意思是获取结果控制器有一个非零委托。 根据我的经验,Core Data的更改跟踪是尝试批量插入时最昂贵的操作。
- 在单个上下文中执行所有更改,并停止合并来自不同上下文的更改,直到完成此批量插入为止。
要深入了解实际情况,请启用Core Data SQLdebugging,并查看正在执行的SQL查询。 理想情况下,你会想看到很多INSERT和几个UPDATE。 但是,如果您遇到太多的SELECT和/或UPDATE,那么这就表示您正在阅读或更新对象太多。
使用Core-Data Profiler工具可以更好地了解Core Data发生的情况。
我决定写我自己的答案,总结我发现对我的情况有用的技术和build议。 感谢所有贴出答案的人。
一,运输
-
“一个JSON”。 这是我想尝试的想法。 谢谢@mundi 。
-
在将JSON发送给客户端之前将它归档的想法,无论是一个JSON包还是一个30个独立的“一个表一个包”。
II。 build立核心数据关系
我将描述一个使用虚构的大型导入操作导入JSON-> CoreData导入的过程,就好像它是在一个方法中执行的一样(我不确定是否会这样做 – 也许我把它分成一个逻辑块)。
让我们想象一下,在我的虚拟应用程序中,有15个宽敞的表格,其中“宽敞”意味着“不能同时存储在内存中,应该使用批量导入”和15个不具有<500个logging的非宽敞表格,例如:
宽敞的:
- 城市(15k +)
- 客户(30k +)
- 用户(15k +)
- 事件(5k +)
- 行动(2K +)…
小:
- client_types(20-)
- visit_types(10-)
- 职位(10-)…
让我们想象一下,我已经下载了JSON包并将其parsing为复合NSArray / NSDictionaryvariables:我有citiesJSON,clientsJSON,usersJSON,…
1.先用小桌子工作
我的伪方法首先从小表导入。 我们来看看client_types表:我遍历clientTypesJSON
并创buildclientTypesJSON
对象(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中获取的小表中的对象的caching字典与它们build立关系
让我们考虑复杂的表clients
:我们有clients
clientsJSON
,我们需要为每个客户端loggingbuild立一个客户端clientType
关系,这很容易,因为我们有一个客户端clientTypes
和他们的ID的caching:
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.处理大型表格 – 批次
让我们考虑一个拥有30k +客户端的大客户clientsJSON
。 我们不遍历整个clientsJSON
而是将其分割成适当大小的块(500条logging),以便每500条logging调用[managedObjectContext save:...]
。 另外,将每个500条logging批处理的操作封装到@autoreleasepool block
是非常重要的 – 请参阅减less内核数据性能指南中的内存开销
注意 – 步骤4描述了应用于一批500条logging的操作,而不是整个客户clientsJSON
!
4.处理大型表格 – build立与大型表格的关系
考虑下面的方法,我们稍后会用到:
@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
Client
clientsJSON
包,其中包含我们需要保存的一批(500) Client
logging。 我们还需要在这些客户和他们的代理之间build立一种关系( 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]; }
我在这里使用的大多数是在这些苹果指南中描述的: 核心数据性能 , 高效导入数据 。 所以步骤1-4的总结如下:
-
当对象持久存在时,将对象转换成错误,因此当导入操作进一步完成时,它们的属性值变得不必要。
-
构build字典作为值的对象和它们的
ids
作为关键字,所以这些字典可以作为查询表时,构build这些对象和其他对象之间的关系。 -
迭代大量logging时使用@autoreleasepool。
-
使用一种类似于
dictionaryOfExistingObjectsByIds
的方法,或者在Tom的回答中引用一个方法,从有效地导入数据 – 一个有SQLIN
谓词的方法来显着减less取数。 阅读汤姆的回答,并引用苹果的相应指南,以更好地理解这种技术。
很好的阅读这个话题
objc.io问题#4:导入大型数据集