测试已测试 | ✓ |
Lang语言 | Obj-CObjective C |
许可证 | MIT |
发布最新发布 | 2016年6月 |
依赖项 | |
AFNetworking | ~> 2.5.0 |
TransformerKit | ~> 0.5.0 |
InflectorKit | ~> 0.0.1 |
ISO8601DateFormatter | ~> 0.6 |
TCKUtilities | ~> 1.0.0 |
Converge 是一个 Objective-C 库,可以从网络服务器接收数据并将其放入 Core Data。它还可以执行反向操作,将 Core Data 中的数据发送回网络服务器。它使用 AFNetworking 进行 HTTP 请求和 JSON 解析。
Converge 适用于 Ruby on Rails 服务器,并倾向于 约定优于配置 的哲学。如果你创建了一个与你的 ActiveRecord 模型相同的 Core Data 模型,Converge 可以自动确定大部分或所有属性和关系,因此你几乎不需要进行配置。
不使用 Rails?工作稍微偏离常规?没问题;Converge 非常灵活,应该能够在几乎任何类型的服务器和数据结构上使用。如果你使用 Rails 约定,会更容易一些。
Converge 不执行同步。同步是一个著名的难题,通过不尝试解决它,我们能够使 Converge 简单和容易,同时仍然很有用。Converge 仅在你明确请求的情况下执行数据操作;除非你决定,否则永远不会从或发送到网络服务器。Converge 仅在指定的数据集上操作;预计你可能只需要让客户端知道服务器上的部分记录。
Converge 认为 网络服务器的数据是真相。如果在 Core Data 的数据版本和网络服务器的版本之间出现任何差异,则网络服务器总是获胜,Core Data 的版本将被覆盖以匹配服务器。
使用 CocoaPods 安装 Converge,通过将此内容添加到 Podfile,然后运行 pod install
pod 'Converge', '~> 1.0.0'
注意:对于此 pod,请确保指定所有三位小数,而不是 CocoaPods 建议的两位。
版本号将采用 x.y.z
的形式。 y
版本可能包含向后不兼容的 API 更改;z
版本仅包含向后兼容的错误修正。指定版本如上所述意味着,当你运行 pod update
时,它只会将你更新到 z
级别的错误修正,以确保安全。要获取 y
和 x
级别的更改,请编辑你的 Podfile。
假设您有一个名为Article的模型。在您的服务器上,您可以通过如下URL访问文章集合
GET /articles
服务器的响应,以JSON格式,看起来如下
[
{
"id": 1,
"title": "Lorem ipsum",
"body_text": "Dolor sit amet"
},
{
"id": 2,
"title": "Consectetur adipiscing elit",
"body_text": "Aliquam ac mi ac leo"
}
]
在Core Data中,您应该尽可能配置您的模型以匹配服务器。创建一个模型,命名为Article,并使其成为ConvergeRecord
的子类。给它以下属性
您会注意到我们稍微调整了body_text
的名称,使其根据Cocoa规范采用驼峰式命名。这是可以的;Converge会自动处理驼峰式和回转。如果服务器也使用了驼峰式命名,那也是可以的。或者,如果出于某种原因您决定在Core Data中使用下划线,那也可以。在这些情况下,您不需要进行任何配置。然而,重要的是名称本质上是相同的。如果我们将其称为bodyStuff
,则无法自动工作。(如果名称需要不同,请参见下面的映射部分。)
现在,要检索一些数据。在您的Objective-C应用程序中
ConvergeClient *client = [ConvergeClient.alloc initWithBaseURL:@"http://example.com" context:self.managedObjectContext];
AFHTTPRequestOperation *operation = [client fetchRecordsOfClass:Article.class parameters:nil success:^(AFHTTPRequestOperation *operation_, NSArray *records)
{
// We got some records!
}
failure:^(NSError *error)
{
// Handle the error
}];
Converge会自动执行上述GET请求。
如果您数据库中已经存在具有相同ID的任何文章,Converge会用服务器的新的版本覆盖它们。默认情况下,Converge期望ID属性的名称为id
。您可以通过覆盖模型类的"+ IDAttributeName
来更改此设置。
您可以使用这些类似的方法来检索单个记录
- (AFHTTPRequestOperation *)fetchRecord:(ConvergeRecord *)record parameters:(id)parameters success:(ConvergeSuccessBlock)success failure:(ConvergeFailureBlock)failure;
- (AFHTTPRequestOperation *)fetchRecordOfClass:(Class)recordClass withID:(id)recordID parameters:(id)parameters success:(ConvergeSuccessBlock)success failure:(ConvergeFailureBlock)failure;
或者这些,用于将新记录发送到服务器
- (AFHTTPRequestOperation *)sendNewRecord:(ConvergeRecord *)record parameters:(NSDictionary *)parameters success:(ConvergeSuccessBlock)success failure:(ConvergeFailureBlock)failure;
以及更新现有记录
- (AFHTTPRequestOperation *)sendUpdatedRecord:(ConvergeRecord *)record parameters:(NSDictionary *)parameters success:(ConvergeSuccessBlock)success failure:(ConvergeFailureBlock)failure;
您的服务器可能由类似的数据库支撑,如同Core Data,允许表达记录之间的关系。有时,以外键形式输出这些很有用;换句话说,相关记录的ID。
以下是从服务器获取的一些ProductReview记录的示例
[
{
"id": 1,
"product_id": 5,
"name": "Alice",
"review_text": "Your products all suck"
},
{
"id": 2,
"product_id: 6",
"name": "Bob",
"review_text": "I'm really satisfied with my vaccum cleaner!"
}
]
将所有产品数据与每个评价一起包含在内是不高效的,因此服务器仅发送ID,并假设我们已经有产品记录。
好消息是Converge会自动处理这种情况。在Core Data中设置Product和ProductReview模型,并在Product和ProductReview之间建立一对一的关系,以匹配服务器数据中隐含的关系。在Product的一侧将关系命名为productReviews
,在ProductReview的一侧命名为product
。当Converge遇到服务器数据中的product_id
属性时,它将搜索名为product
的关系,并进行连接。
唯一的注意事项是:Converge在解析ProductReviews时会根据该ID搜索相关产品。因此,在评估外键之前,相关记录必须已经存在于Core Data中。因此,您应在检索ProductReview记录之前检索任何可能需要的产品记录。
如果是一个多对多关系,这也适用于外键列表
{
"id": 1,
"product_ids": [5, 6]
"name": "Alice",
"review_text": "Your products all suck"
}
在这个例子中,在ProductReview的一侧,关系应该命名为products
。Converge在查找_ids
属性之前将其统一。
有时,在响应中将一个记录嵌入另一个记录中更方便。这可以节省您多次进行HTTP请求的需要,并避免了上面提到的需要先验请求。
{
"id": 1,
"name": "Katamari Damacy T-Shirt",
"image": {
"id": 4,
"url": "http://example.com/katamari.jpg",
"title": "Roll it up!"
}
}
正如前一个部分所述,确保在Core Data中使用相同的名称设置关系。在这种情况下,在Product一侧命名关系为image
。
这同样适用于您有多个嵌入记录的情况。
{
"id": 1,
"name": "Katamari Damacy T-Shirt",
"images": [
{
"id": 4,
"url": "http://example.com/katamari.jpg",
"title": "Roll it up!"
},
{
"id": 5,
"url": "http://example.com/rolling.jpg",
"title": "Rollin'"
}
]
}
在本例中,将关系命名为 images
。
默认情况下,Converge 根据模型名和 Rails 规范确定其应请求的 URL。因此,对于名为 ArticleComment 的模型,Converge 将尝试使用以下 URL:
GET /article_comments
GET /article_comments/1
POST /article_comments
PATCH /article_comments/1
无需配置即可使用此默认行为。
您可能习惯于看到这样的 URL,带有 .json
扩展名。而 Converge 在每个请求中都包含以下标题以代替这种方式。
Accept: application/json
有些服务器,例如 Ruby on Rails,如果提供了像这样的 Accept 标题,则不需要文件名扩展名。
如果您的服务器使用不同的 URL 策略,请在您的模型中覆盖以下方法。
此方法用于 POST 请求,并且在没有 ID 时用于 GET 请求。
+ (NSString *)collectionURLPathForHTTPMethod:(NSString *)HTTPMethod parameters:(NSDictionary *)parameters
此方法用于 PATCH 请求,并且在有 ID 时用于 GET 请求。
+ (NSString *)URLPathForID:(id)recordID HTTPMethod:(NSString *)HTTPMethod parameters:(NSDictionary *)parameters
在某些情况下,您可能需要具有与服务器不同的属性和关系名称。可能是 Converge 的名称变换逻辑错误,或者可能是您正在尝试使用服务器不允许的属性名,例如 description
,或者可能是您的服务器开发者固执己见且命名不佳。无论如何,Converge 允许您覆盖其映射中的任何一个。
对于属性,在你的模型类中覆盖 + attributeMap
。返回一个字典,包含您想要定制的映射属性;使用 Core Data 名称作为键,服务器名称作为值。
+ (NSDictionary *)attributeMap
{
return @{
@"description_": @"description",
};
}
请注意,您只需显式定义无法自动确定的属性,而不是全部属性。如果您还有模型上的其他属性,如 id
、name
等,这两处都相同,就没有必要将其放入此字典中。
您可以处理嵌套在多个级别的服务器数据,如果可能的话,你不想将其作为一个 Core Data 关系(如上所述的嵌入记录部分中解释的)。比如以下是从服务器获得的:
{
"id": 1,
"name": "Katamari Damacy T-Shirt",
"image": {
"id": 4,
"url": "http://example.com/katamari.jpg",
"title": "Roll it up!"
}
}
可能您不希望有一个单独的相关 Image 记录,只想将 url
属性包含在您的 Product 记录中。您可以通过指定服务器的属性名作为数组来实现这一点,表明如何遍历嵌入记录
+ (NSDictionary *)attributeMap
{
return @{
@"imageURL": @[@"image", @"url"],
};
}
如前所述,ID 属性是特殊的,因为 Converge 使用它来匹配服务器数据到现有记录,搜索外键等。如果您在 Core Data 中使用除 id
之外的 ID 属性名称,必须覆盖 + IDAttributeName
。如果这与服务器使用的 ID 属性名称也不同,您还必须在 + attributeMap
中包含它。
对于外键名称,采用同样的格式覆盖 + foreignKeyMap
。
对于嵌入记录名称,覆盖 + relationshipMap
。
有时,从服务器获得的数据的格式可能与您希望在 Core Data 中存储的格式不同。Converge 通常会避免在这种情况下猜测您想要的内容,但它确实提供了一种在解析数据时自动转换数据的方法。
例如,如果服务器以字符串格式提供时间戳,而您想将其存储为 NSDate:
{
"id": 1,
"title": "Graeme Devine is right",
"created_at": "2011-07-10 09:51:03 -0700"
}
在你的模型类中,覆盖方法 + (ConvergeAttributeConversionBlock)conversionForAttribute:(NSString *)ourAttributeName
。例如:
+ (ConvergeAttributeConversionBlock)conversionForAttribute:(NSString *)ourAttributeName
{
if ([ourAttributeName isEqualToString:@"createdAt"])
{
return ^NSDate *(NSString *value)
{
if (value == nil || ![value isKindOfClass:NSString.class]) return nil;
ISO8601DateFormatter *formatter = ISO8601DateFormatter.new;
return [formatter dateFromString:value];
};
}
return [super conversionForAttribute:ourAttributeName];
}
为了您的方便,Converge提供了一些内置的转换。
+ (ConvergeAttributeConversionBlock)stringToIntegerConversion;
+ (ConvergeAttributeConversionBlock)stringToFloatConversion;
+ (ConvergeAttributeConversionBlock)stringToDecimalConversion;
+ (ConvergeAttributeConversionBlock)stringToDateConversion;
+ (ConvergeAttributeConversionBlock)stringToURLConversion;
但是,您仍然需要按照这种方式指定它们:
+ (ConvergeAttributeConversionBlock)conversionForAttribute:(NSString *)ourAttributeName
{
if ([ourAttributeName isEqualToString:@"createdAt"])
{
return self.stringToDateConversion;
}
return [super conversionForAttribute:ourAttributeName];
}
如果您想进行反向操作,在将数据发送回服务器时,以相同的方式覆盖同样在同一个方法中的+ (ConvergeAttributeConversionBlock)reverseConversionForAttribute:(NSString *)ourAttributeName
。请注意,您不能重用相同的转换逻辑,而必须编写一个执行反向操作的转换。
如果您需要重复检查服务器的新数据,那么在从上次请求以来没有任何变化的情况下,服务器仍然将整个数据集传输回您可能会不够高效。Converge提供了一种避免这种情况的方法。
在您的 ConvergeClient
实例上,将属性 trackModifiedTimes
设置为 YES
。下次您进行GET请求时,Converge将记录URL(包括任何GET参数)以及日期和时间。下次您告诉Converge发出相同的请求(即,相同的URL和GET参数)时,Converge会查找上一次请求的时间戳,并在If-Modified-Since
头部发送此信息。其余部分取决于您的服务器;如果服务器发送了304 Not Modified
响应且响应体中没有数据,则服务器可以选择这样做。(Rails可以自动进行此操作。)在这种情况下,Converge将立即调用您的success
回调。如果服务器发送了200
响应,那么Converge的行为与通常一样,并处理响应数据。
注意1:在收到304 Not Modified
的情况下,您的success
回调的第二参数将是null
。如果您需要通常存在的数据,则需要查询Core Data以获取它,因为服务器响应中不可用。
注意2:当启用trackModifiedTimes
时,为了跟踪请求,Converge将在应用程序的文档目录中创建一个文件。您可以通过- requestTimestampsFileURL
获取到该文件的URL。如果您的数据库不再与服务器同步(例如,删除整个数据库或仅删除之前请求的一些记录),则应删除此时间戳文件;否则,Converge将仍然将不存在的记录视为已缓存,它们在后续请求中将不会被检索。
由于上述原因,trackModifiedTimes
默认是关闭的。
启用trackModifiedTimes
时,它仅用于GET请求;不用于其他请求,如POST或PATCH。
可能您的服务器使用的是完全不同寻常的数据结构,即使有上述配置选项,Converge也无法理解它。您可以在Converge解析JSON后、在没有逻辑将其理解之前覆盖Converge的操作。
如果是这种情况,您可能需要查看是否在模型类中覆盖这些方法:
- (BOOL)mergeChangesFromProvider:(NSDictionary *)providerRecord withQuery:(NSDictionary *)query recursive:(BOOL)recursive error:(NSError **)errorRef;
+ (NSArray *)mergeChangesFromProviderCollection:(NSArray *)collection withQuery:(NSDictionary *)query recursive:(BOOL)recursive deleteStale:(BOOL)shouldDeleteStale context:(NSManagedObjectContext *)context skipInvalidRecords:(BOOL)skipInvalid error:(NSError **)errorRef;
这样,您需要自己完成必要的所有操作,以便将数据放入Core Data中。
HTTP DELETE操作尚未实现(因为到目前为止还没有必要)。
有一个基本的测试套件,但需要更好的测试覆盖率。每当发现一个错误时,添加一个新的回归测试。
MIT。请参阅License.txt
。
欢迎提交拉取请求 :)
请尽量匹配现有样式。如果您在考虑进行重大更改,也许首先在问题中发起讨论以提出它。
在做出更改后,请确保测试仍然通过。根据需要添加新的测试。
由David Deller创建 [email protected]。
版权所有 © 2012-2015 TripCraft LLC。