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. …同时启动远程更新过程。 首先,它会通过您的桥接 (bridge)(稍后我们将看到它是如何工作的);
  4. 您的桥接将返回一个标准的 Operation 子类,它将负责联系您的 API;
  5. 当操作结束时,BMO 将再次通过您的桥接,并将操作的结果返回给您,以获取所谓的 MixedRepresentation;
  6. BMO 将 MixedRepresentation 导入到 CoreData 数据库中,负责对象的唯一化和合并;
  7. 最后,您将获得导入的结果。 所有错误都会被报告,您可以选择性地获得与原始请求匹配的新的 CoreData 对象。

BMO 组件

为了清楚地分离角色,此存储库包含许多目标 (targets)

安装和依赖

BMO 与 Carthage 和 SPM 兼容。

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

这是您可以用于基于 BMO 的项目的基本 Cartfile。

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

依赖

BMO 具有以下依赖项

URLRequestOperation 具有以下依赖项

需求

开始使用

此自述文件将重点介绍使用 BMO 的 CoreData+REST 实现。高级用法稍后将展示如何为其他数据库或 API 创建新的 BMO 具体实现。

此处的自述文件将提供在应用程序中实现 BMO 的一般步骤。如果您想要更详细和彻底的指南,请参阅我们的 示例项目

核心数据栈 (Core Data Stack)

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

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

CoreData Model

BMO 桥接 (Bridge)

桥接 (bridge) 是实现 Bridge 协议的实体(类、结构体等)。 它是您的本地 Core Data 数据库和您的 API 之间的接口。 这是您必须提供给 BMO 的最重要的事情。

桥接的职责

RestMapper

对于标准的“REST 桥接”,您可能需要使用 RESTUtils 模块(它是 BMO 的一部分),尤其是 RESTMapper 类。 该模块将为您提供便利,将获取或保存请求转换为 URL Operation,您可以将其返回给 BMO,以及将解析的 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 操作实用程序从 happn 迁移到 RESTUtils…

从完成的操作中提取对象的远程表示

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

实现示例

/* MyBridge.swift */

func remoteObjectRepresentations(fromFinishedOperation operation: BackOperationType, userInfo: UserInfoType) throws -> [RemoteObjectRepresentationType]? {
   /* 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.RemoteObjectRepresentationType]
   case .error(let e):         throw Err.operationError(e)
   }
}

MixedRepresentation

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

MixedRepresentation 是一种结构,表示要导入到本地数据库中的对象。 MixedRepresentation 中的属性以 Dictionary 的形式保存,其键是属性名称,值是实际属性值。 要导入的对象的relationships关系以 Dictionary 的形式保存,其键是关系名称,但值是远程(即 API)表示的数组!

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

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

/* MyBridge.swift */

func mixedRepresentation(fromRemoteObjectRepresentation remoteRepresentation: RemoteObjectRepresentationType, expectedEntity: DbType.EntityDescriptionType, userInfo: UserInfoType) -> MixedRepresentation<DbType.EntityDescriptionType, RemoteRelationshipAndMetadataRepresentationType, UserInfoType>? {
   /* 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!

创建请求管理器 (Request Manager)

请求管理器是您用来向 BMO 发送请求的实例。 例如,您可以将其保存在您的应用程序委托中。

/* 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<BridgeType : Bridge>() -> AnyBackResultsImporter<BridgeType>? {
         return (AnyBackResultsImporter(importer: BackResultsImporterForCoreDataWithFastImportRepresentation<YourBridge>(uniquingPropertyName: "bmoId")) as! AnyBackResultsImporter<BridgeType>)
      }

   }

}

获取数据

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

/* 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: AsyncOperationResult<BridgeBackRequestResult<YourBridge>>) -> Void in
         /* Use the fetched user here. */
      }
   )
}

NSFetchedResultsController

使用 NSFetchedResultsController 是对 CoreData 数据库中发生的更改做出反应的好方法。 使用此技术,您可以要求 BMO 获取或更新本地模型,甚至不需要设置处理程序,然后自动对更改做出反应。

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

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

高级用法

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

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

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

可能的演变

鸣谢

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

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