BMO 是一个概念。任何数据库对象都可以是一个后端管理对象 (Backed Managed Object)。
BMO 是一系列协议的集合,可以轻松地将任何本地数据库(CoreData、Realm 等)链接到任何 API(REST 或 SOAP API、SMB 共享或任何其他内容)。 目前,BMO 有一个具体的实现,将 CoreData 链接到 REST API。
Operation
子类,它将负责联系您的 API;MixedRepresentation
;MixedRepresentation
导入到 CoreData 数据库中,负责对象的唯一化和合并;为了清楚地分离角色,此存储库包含许多目标 (targets)
MixedRepresentation
s 导入到您使用的任何数据库中;BMO 与 Carthage 和 SPM 兼容。
BMO 严重依赖于 Operation
。 创建网络操作并非难事,但我们建议使用 URLRequestOperation
,除了提供基于 Operation
的网络请求之外,它还处理了大量事情(例如,基于网络可用性自动重试幂等请求)。
这是您可以用于基于 BMO 的项目的基本 Cartfile。
# Cartfile
github "happn-app/BMO" ~> 0.1
github "happn-app/URLRequestOperation" ~> 1.1.5
BMO 具有以下依赖项
Result
类型。(BMO 是用 Swift 4.2 创建的; 此依赖项将被删除。)URLRequestOperation 具有以下依赖项
Result
类型。(URLRequestOperation 是用 Swift 4.2 创建的; 此依赖项将被删除。)Operation
的实现,为轻松运行和重试基本操作提供便利。此自述文件将重点介绍使用 BMO 的 CoreData+REST 实现。高级用法稍后将展示如何为其他数据库或 API 创建新的 BMO 具体实现。
此处的自述文件将提供在应用程序中实现 BMO 的一般步骤。如果您想要更详细和彻底的指南,请参阅我们的 示例项目。
您的 Core Data 模型只有一个要求:所有映射的实体都必须具有“唯一属性”。 这将是 BMO 读取和写入的属性,以确保您的堆栈中不会有重复的实例。 实际上,如果您正在获取已在本地数据库中的对象,则本地对象和获取的对象将合并在一起。 数据库中已有的对象将被更新。 该属性可以随意命名,但必须在所有实体中具有相同的名称。
具有唯一属性名称 bmoId
的简单模型示例
桥接 (bridge) 是实现 Bridge 协议的实体(类、结构体等)。 它是您的本地 Core Data 数据库和您的 API 之间的接口。 这是您必须提供给 BMO 的最重要的事情。
桥接的职责
Operation
,以在您的 API 上执行给定的请求。RestMapper
在这里可以帮助您。[[String: Any?]]
);MixedRepresentation
。 稍后我们将看到这是什么。 对于此任务,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
]
)
}()
再一次,我们将信任 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
中的属性以 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 发送请求的实例。 例如,您可以将其保存在您的应用程序委托中。
/* 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 是对 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 Lamboley 在 happn 工作时创建。
非常感谢 happn 的 iOS 开发人员,没有他们,这个项目不可能开源