Converge 1.1.0

Converge 1.1.0

测试已测试
Lang语言 Obj-CObjective C
许可证 MIT
发布最新发布2016年6月

David DellerDavid Deller维护。



 
依赖项
AFNetworking~> 2.5.0
TransformerKit~> 0.5.0
InflectorKit~> 0.0.1
ISO8601DateFormatter~> 0.6
TCKUtilities~> 1.0.0
 

Converge 1.1.0

  • David Deller

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 级别的错误修正,以确保安全。要获取 yx 级别的更改,请编辑你的 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的子类。给它以下属性

  • id — 64位整型
  • title — 字符串
  • bodyText — 字符串

您会注意到我们稍微调整了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

URL 路径

默认情况下,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",
    };
}

请注意,您只需显式定义无法自动确定的属性,而不是全部属性。如果您还有模型上的其他属性,如 idname 等,这两处都相同,就没有必要将其放入此字典中。

您可以处理嵌套在多个级别的服务器数据,如果可能的话,你不想将其作为一个 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。