我必须创建一个功能类似于联系人应用程序的应用程序。您可以在客户端的iPhone上添加联系人,并将其上传到客户端的iPad上。如果客户在iPad上更新联系人,则应在iPhone上更新。
大部分内容相当直接。我使用Parse.com
作为后端,并使用Core Data
在本地保存联系人。我遇到的唯一问题是在用户离线时管理联系人。
假设我有iPhone和iPad。它们目前都具有相同版本的在线数据库。我的iPhone现在处于离线状态。现在是早上9点。
上午10点,我更新了iPad上的联系人的电话号码。它在本地和在线保存更改。上午11点我更新了iPhone上同一个联系人的电子邮件地址,但我仍然处于离线状态。
中午,我的iPhone连接到互联网并检查服务器是否有变化。它看到它的更改比最新更新(检查updatedAt
时间戳属性)更新,因此它不会下载联系人的新电话号码(“已过时”),而是覆盖电话号码以及电子邮件地址(将新电话号码更新为旧版本,因为它在上午10点电话号码更新期间处于离线状态,并且其更改可能更新近。)
我应该如何管理上面遇到的在线/离线问题?我能想到的解决方案是在联系人的每个属性上保留更新的时间戳,而不仅仅是整个联系人的一般updatedAt
属性,例如:何时更新名字,何时更新姓氏,然后手动检查离线设备是否对每个属性进行了更新的更改,而不是覆盖整个对象,但这似乎很草率。
我还在考虑在每个updatedLocally
对象上都有updatedOnline
和Core Data
时间戳属性。这样如果两者不匹配,我可以进行差异检查,并使用最新的冲突,但这似乎仍然不是最干净的解决方案。有没有其他人遇到类似的东西?如果是这样,你是如何解决的?
Pseudocode /摘要我的想法?涵盖了每个测试用例,但仍然不是很优雅/完整:
Parse.com上的2个实体:联系和联系历史
联系人有第一个,最后一个,电话,电子邮件,在线更新
联系历史记录包含要引用的联系人的主键以及具有历史记录的相同属性。例如first: [{value:"josue",onlineUpdate:"9AM"},{value:"j",onlineUpdate:"10AM"},{value:"JOSUEESP",onlineUpdate:"11AM"}]
1核心数据实体,联系方式:
联系人有第一个,最后一个电话,电子邮件,onlineUpdate和offlineUpdate(重要提示:这仅适用于核心数据,而非Parse)
for every contact in parse database as onlineContact {
if onlineContact does not exist in core data {
create contact in core data
}
else {
// found matching local object to online object, check for changes
var localContact = core data contact with same UID as onlineContact
if localContact.offlineUpdate more recent than onlineContact.onlineUpdate {
for every attribute in localContact as attribute {
var lastOnlineValueReceived = Parse database Contact History at the time localContact.onlineUpdate for attribute
if lastOnlineValueReceived == localContact.attribute {
// this attribute did not change in the offline update. use latest available online value
localContact.attribute = onlineContact.attribute
}
else{
// this attribute changed during the more recent offline update, update it online
onlineContact.attribute = localContact.attribute
}
}
}
else if onlineContact.onlineUpdate more recent than localContact.offlineUpdate {
// another device updated the contact. use the online contact.
localContact = offlineContact
}
else{
// when a device is connected to the internet, and it saves a contact
// the offline/online update times are the same
// therefore contacts should be equivalent in this else statement
// do nothing
}
}
TL; DR:您如何构建一种版本控制系统进行在线/离线更新而不会意外覆盖?我想将带宽使用限制在最低限度。
答案 0 :(得分:3)
我建议使用基于密钥的更新而不是基于联系人的更新。
您不应该将整个联系人发送到服务器,在大多数情况下,用户只需更改一些属性(“姓氏”通常不会经常更改)。这也减少了带宽的使用
随着您的离线联系人的应用更改,您发送
本地联系人与服务器的旧版本号/上次更新时间戳。服务器现在可以
只需查看旧版本号即可确定您的本地数据是否是最新的。
如果旧版本号与服务器的当前版本号匹配,则客户端无需更新任何其他信息。如果不是这种情况,服务器应该向您发送新联系人(在应用您请求的更新后)。
您还可以保存这些提交,这将导致联系历史记录
每次更换钥匙时都不会存储整个联系人,而只会更改自己的更改。
伪代码中的简单实现可能如下所示:
for( each currentContact in offlineContacts ) do
{
if( localChanges.length > 0){ // updates to be made
commitAllChanges();
answer = getServerAnswer();
if(answer.containsContact() == true){
// server sent us a contact as answer so
// we should overwrite the contact
currentContact = answer.contact;
} else {
// the server does not want us to overwrite the contact, so we are up to date!
}
// ...
}
} // end of iterating over contacts
服务器端看起来很简单:
for (currentContactToUpdate in contactsToUpdate) do
{
sendBackContact = false; // only send back the updated contact if the client missed updates
for( each currentUpdate in incomingUpdates ) do {
oldClientVersion = currentUpdate.oldversion;
oldServerVersion = currentContact.getVersion();
if( oldClientVersion != oldServerVersion ){
sendBackContact = true;
// the client missed some updates from other devices
// because he tries to update an old version
}
currentContactToUpdate.apply(currentUpdate);
}
if(sendBackContact == true){
sendBack(currentUpdate);
}
}
为了更好地理解工作流程,我将提供一个示例:
上午8点客户端和服务器都是最新的,每台设备都在线
每个设备都有一个条目(在这种情况下是一行),用于具有主键ID的联系人'Foo Bar'。 每个条目的版本都是相同的,因此所有条目都是最新的。
_ Server iPhone iPad
ID 42 42 42
Ver 1 1 1
First Foo Foo Foo
Last Bar Bar Bar
Mail f@b f@b f@b
(原谅这种可怕的格式,很遗憾不支持任何类型的表......)
上午9点,您的iPhone处于离线状态。您注意到Foo Bar的电子邮件已更改为“foo @ b”。
您可以像这样更改手机上的联系信息:
UPDATE 42 FROM 1 TO 2 Mail=foo@b
// ^ID ^old version ^new version ^changed attribute(s)
所以现在手机中的联系人会是这样的:
_ iPhone
ID 42
Ver 2
First Foo
Last Bar
Mail foo@b
上午10点,您的iPad处于离线状态。你注意到'Foo Bar'实际上写成'Voo Bar'!您可以立即在iPad上应用更改。
UPDATE 42 FROM 1 TO 2 First=Voo
请注意,iPad仍然认为当前版本的联系人42是1.服务器和iPad都没有注意到您如何更改邮件地址和增加版本号,因为没有设备连接到网络。这些更改仅在本地存储并在iPad上可见。
上午11点,您将iPad连接到网络。 iPad会发送最近的更新
到服务器。
之前:
_ Server iPad
ID 42 42
Ver 1 2
First Foo Voo
Last Bar Bar
Mail f@b f@b
iPad - >服务器:
UPDATE 42 FROM 1 TO 2 First=Voo
服务器现在可以看到您正在更新联系人42的版本1.由于版本1是当前版本,因此您的客户端是最新的(在您离线时平均没有更改)。
服务器 - > ipad公司
UPDATED 42 FROM 1 TO 2 - OK
后:
_ Server iPad
ID 42 42
Ver 2 2
First Voo Voo
Last Bar Bar
Mail f@b f@b
12 AM您将iPad与网络断开连接并连接iPhone。
iPhone尝试提交最近的更改。
之前:
_ Server iPhone
ID 42 42
Ver 2 2
First Voo Voo
Last Bar Bar
Mail f@b foo@b
iPhone - >服务器
UPDATE 42 FROM 1 TO 2 Mail=foo@b
服务器会注意到您如何尝试更新同一联系人的旧版本。
他将应用您的更新,因为它比iPad的更新更新,但是
将发送新的联系人数据,以确保您获得更新的名字。
之后:
_ Server iPhone
ID 42 42
Ver 2 2
First Voo Voo
Last Bar Bar
Mail foo@b foo@b
服务器 - > ipad公司
UPDATED 42 FROM 1 TO 3 - Ver=2;First=Voo;.... // send the whole contact
/* Note how the version number was changed to 3, and not to 2, as requested.
* If the new version number was (still) 2 the iPad would miss the update
*/
下次您的iPad连接到网络并且没有提交更改时,应该只发送当前版本的联系人,看看它是否仍然是最新的。
现在您已经提交了两个离线更改,而没有相互覆盖。
您可以轻松扩展此方法并进行一些优化。
例如:
我希望这有帮助。
答案 1 :(得分:1)
我对iOs,核心数据和parse.com一无所知,所以我只建议一个通用的算法解决方案。我认为你可以采用类似于版本控制系统的方法。
最简单的方法是将所有历史记录保存在服务器上:保留联系人列表的所有修订版。现在,在同步期间,电话会发送有关其所见的最后一次服务器修订的信息,此修订将成为当前电话修订和当前服务器修订的“共同父级”。
现在您可以看到自该版本以来服务器和手机上发生的变化,并应用通常的3路比较:如果某个字段仅在服务器上发生了变化,则将新字段发送到手机;如果某个字段仅在手机上发生了变化,那么也可以在服务器上进行更改,如果某些字段在手机和服务器上都已更改且更改不同,那么您就会发生冲突并且不得不询问用户。
此方法的一种变体可能是使用更改,而不是修订。服务器和客户端的主要数据不是联系人列表,而是其更改的历史记录。 (如果需要,还可以保留当前联系人列表以及一组“关键帧”;它不会用于冲突解决算法,但可以使用它以便可以快速显示和使用。)
然后,当用户同步数据时,您只下载/上传更改。如果有任何冲突更改,您只需要向用户询问,否则您只需合并它们。如何定义更改以及哪些更改被视为冲突,取决于您。一种简单的方法可以将变更定义为一对(字段,新值),如果它们具有相同的字段,则两个变更是冲突的。您还可以使用更高级的冲突解决逻辑,例如,如果一个更改仅更改电子邮件的前半部分,另一个更改,则可以合并它们。
答案 2 :(得分:1)
执行此操作的正确方法是保留事务日志。每当您保存Core Data时,都会在事务日志中创建一个日志条目。当您下次在线时,您将对服务器回放事务日志。
这就是iCloud和其他同步服务的工作方式。
答案 3 :(得分:0)
不是为每个核心数据对象分别设置标志,而是可以使用单独的表来存储所有更新的联系人的ID(数据库表中存储联系信息的主键)。
稍后当用户上线时,您只需从实际的联系人详细信息表中提取这些联系人,然后将其上传到您的服务器上。