BMO

Platforms SPM compatible License happn

BMO 是一个概念。任何数据库对象都可以是一个后备管理对象 (Backed Managed Object)。

它是什么?

BMO 是一组协议的集合,可以轻松地将任何本地数据库 (CoreData、Realm 等) 链接到任何 API (REST 或 SOAP API、SMB 共享或任何其他内容)。目前,BMO 只有一个具体的实现,将 CoreData 链接到 REST API。

以下图表显示了通过 BMO 的请求生命周期:BMO Diagram

  1. 一个 CoreData 请求被发送到 BMO。
  2. BMO 同步返回匹配的对象……
  3. ……同时启动远程更新过程。首先它会通过你的桥接器 (稍后我们将了解它的工作原理);
  4. 你的桥接器将返回一个标准的 Operation 子类,它将负责联系你的 API;
  5. 当 Operation 结束时,BMO 将再次通过你的桥接器,获取 Operation 的结果,从而获得所谓的 MixedRepresentation
  6. BMO 将 MixedRepresentation 导入到 CoreData 数据库中,负责对象的唯一性和合并;
  7. 最后,你会得到导入的结果。所有错误都会被报告,你可以选择获取与原始请求匹配的新的 CoreData 对象。

BMO 组件

为了清晰地分离角色,这个仓库有许多目标

安装和依赖

BMO 与 Carthage 和 SPM 兼容。

BMO 严重依赖于 Operation。创建一个网络 Operation 并不是很难,但我们建议使用 URLRequestOperation,它除了提供基于 Operation 的网络请求之外,还处理了大量的事情(例如,基于网络可用性自动重试幂等请求)。

这是一个你可以用于基于 BMO 的项目的基本 Cartfile。

# Cartfile
github "happn-app/BMO" ~> 0.1
github "happn-app/URLRequestOperation" ~> 1.1.5

依赖

BMO 具有以下依赖项

URLRequestOperation 具有以下依赖项

要求

开始使用

本 Readme 将侧重于使用 CoreData+REST 实现的 BMO。后面的高级用法将展示如何为其他数据库或 API 创建新的 BMO 具体实现。

此处的 Readme 将提供在应用程序中实现 BMO 的一般步骤。如果你想要更详细和全面的指南,请参阅我们的 示例项目

核心数据栈

你的 Core Data 模型只有一个要求:所有映射的实体都必须具有“唯一属性”。这将是 BMO 将读取和写入的属性,以确保你的堆栈中不会有重复的实例。实际上,如果你正在获取本地数据库中已有的对象,则本地对象和获取的对象将合并在一起。数据库中已有的对象将被更新。该属性可以随意命名,但必须在所有实体中具有相同的名称。

具有唯一属性名称 bmoID 的简单模型示例

CoreData Model

BMO 桥接器

桥接器是一个实体(类、结构体,任何东西),它实现了 BridgeProtocol 协议。它是你的本地 Core Data 数据库和你的 API 之间的接口。这是你必须提供给 BMO 的最重要的事情。

桥接器的职责

RestMapper

对于标准的“REST 桥接器”,你可能想要使用 RESTUtils 模块(它是 BMO 的一部分),特别是 RESTMapper 类。该模块将为你提供便利,可以将获取或保存请求转换为你可以返回给 BMO 的 URL Operation,以及将解析的 JSON 转换为 MixedRepresentation(不用担心,我们肯定会解释什么是 MixedRepresentation)。

一个例子胜过千言万语。假设我们在 Core Data 模型中有一个 User 实体,具有以下属性

我们的 API 为 User 返回的 JSON 如下所示

{
	"id": "abc",
	"user_name": "bob.kelso",
	"first_name": "Bob",
	"age": 42
}

在我们的桥接器中,我们将保留一个 REST Mapper,它看起来像这样

/* MyBridge.swift */

private lazy var restMapper: RESTMapper<NSEntityDescription, NSPropertyDescription> = {
   let userMapping: [_RESTConvenienceMappingForEntity] = [
      .restPath("/users(/|username|)"),
      .uniquingPropertyName("bmoID"),
      .propertiesMapping([
         "bmoID":     [.restName("id")],
         "username":  [.restName("user_name")],
         "firstname": [.restName("first_name")],
         "age":       [.restName("age"),       .restToLocalTransformer(RESTIntTransformer())]
      ])
   ]
       
   return RESTMapper(
      model: dbModel,
      defaultPaginator: RESTOffsetLimitPaginator(),
      convenienceMapping: [
         "User": userMapping
      ]
   )
}()

为请求提供 Operation

再一次,我们将信任 RESTUtils 来完成这项工作的繁重部分。

TODO:将连接的 http operation 实用程序从 happn 迁移到 RESTUtils…

从已完成的 Operation 中提取对象的远程表示

我们必须简单地提取远程表示(基本上是从 API 解析的 JSON)并返回它。BMO 无法猜测如何从已完成的 Operation 中检索数据,因为它没有任何关于它的信息。

实现示例

/* MyBridge.swift */

func remoteObjectRepresentations(fromFinishedOperation operation: BackOperation, userInfo: UserInfo) throws -> [RemoteObjectRepresentation]? {
   /* In our case, the operation has a results property containing either the
    * parsed JSON from the API or an error. */
   switch operation.results {
   /* We access the "items" elements because our API returns the objects in this key. 
    * The behaviour may be different with another API. */
   case .success(let success): return success["items"] as? [MyBridge.RemoteObjectRepresentation]
   case .error(let e):         throw Err.operationError(e)
   }
}

MixedRepresentation

正如承诺的那样,我们在这里解释什么是 MixedRepresentation

MixedRepresentation 是一个表示要导入到你的本地数据库中的对象的结构。MixedRepresentation 中的属性保存为 Dictionary,其键是属性名称,值是实际的属性值。要导入的对象的关联保存为 Dictionary,其键是关联名称,但值是远程(又名 API)表示的数组!

这种奇怪的结构之所以存在,是因为它实际上简化了 API 结果在你的本地数据库中的导入和转换。通常,使用 RestMapper 可以很容易地从远程表示创建 MixedRepresentation

这是桥接器这部分的一个实现示例

/* MyBridge.swift */

func mixedRepresentation(fromRemoteObjectRepresentation remoteRepresentation: RemoteObjectRepresentation, expectedEntity: Db.EntityDescription, userInfo: UserInfo) -> MixedRepresentation<Db.EntityDescription, RemoteRelationshipAndMetadataRepresentation, UserInfo>? {
   /* First let’s get which entity the remote representation represents.
    * The REST mapper will do this job for us. */
   guard let entity = restMapper.actualLocalEntity(forRESTRepresentation: remoteRepresentation, expectedEntity: expectedEntity) else {return nil}

   /* The REST mapper does not know about the MixedRepresentation
    * structure, but can convert a remote representation into a Dictionary
    * that we will use to build the MixedRepresentation instance we want. */
   let mixedRepresentationDictionary = restMapper.mixedRepresentation(ofEntity: entity, fromRESTRepresentation: remoteRepresentation, userInfo: userInfo)

   /* We need to use the REST mapper once again to retrieve the uniquing ID from the Dictionary we created above. */
   let uniquingID = restMapper.uniquingID(forLocalRepresentation: mixedRepresentationDictionary, ofEntity: entity)
   /* Finally, with everything we have retrieved above, we can create the
    * MixedRepresentation instance that we return to the caller. */
   return MixedRepresentation(entity: entity, uniquingID: uniquingID, mixedRepresentationDictionary: mixedRepresentationDictionary, userInfo: userInfo)
}

桥接器完成后的:使用 BMO!

创建请求管理器

请求管理器是你用来向 BMO 发送请求的实例。例如,你可以将它保存在你的 app delegate 中。

/* AppDelegate.swift */

import BMO

class AppDelegate : NSObject, UIApplicationDelegate {

   private(set) var requestManager: RequestManager!

   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      /* Setup BMO request manager */
      requestManager = RequestManager(bridges: [YourBridge(dbModel: container.managedObjectModel)], resultsImporterFactory: BMOBackResultsImporterForCoreDataWithFastImportRepresentationFactory())
   }

   /* A struct to help BMO. We are actually working on several solutions
    * to avoid the use of this one. */
   private struct BMOBackResultsImporterForCoreDataWithFastImportRepresentationFactory : AnyBackResultsImporterFactory {

      func createResultsImporter<Bridge : BridgeProtocol>() -> AnyBackResultsImporter<Bridge>? {
         return (AnyBackResultsImporter(importer: BackResultsImporterForCoreDataWithFastImportRepresentation<YourBridge>(uniquingPropertyName: "bmoID")) as! AnyBackResultsImporter<Bridge>)
      }

   }

}

获取数据

完成所有设置后,你可以使用请求管理器来获取一些对象。

/* ViewController.swift */

private func refreshUser(username: String) {
   let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
   fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(User.username), username)

   let context = AppDelegate.shared.context!
   _ = AppDelegate.shared.requestManager!.fetchObject(
      fromFetchRequest: fetchRequest as! NSFetchRequest<NSFetchRequestResult>,
      fetchType: .always, onContext: context, handler: { (user: User?, fullResponse: Result<BridgeBackRequestResult<YourBridge>, Error>) -> Void in
         /* Use the fetched user here. */
      }
   )
}

NSFetchedResultsController

使用 NSFetchedResultsController 是一种响应 CoreData 数据库中发生的更改的好方法。使用此技术,你可以要求 BMO 获取或更新本地模型,而无需设置处理程序,然后自动响应更改。

请参阅 Apple 文档以实现和使用 NSFetchedResultsController (https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller)。

happn 提供了一个 helper,以便将 NSFetchedResultsController 与 UITableView 或 UICollectionView 结合使用 (https://github.com/happn-app/CollectionAndTableViewUpdateConveniences)。

高级用法

该桥接器支持用户信息和元数据。

用户信息用于桥接器内部,并具有你定义的类型。它们在一次请求的生命周期中传递,从 CoreData 请求转换为 Operation,到将 Operation 的结果转换为 MixedRepresentation 等。你可以使用这些用户信息来帮助你完成桥接器中需要的不同任务。

元数据是在请求从 BMO 返回时返回的附加信息。

可能的演变

鸣谢

这个项目最初由 François Lamboleyhappn 工作时创建。

非常感谢 happn 的 iOS 开发人员,没有他们,这个项目的开源是不可能的