BMO 是一个概念。任何数据库对象都可以是一个后备管理对象 (Backed Managed Object)。
BMO 是一组协议的集合,可以轻松地将任何本地数据库 (CoreData、Realm 等) 链接到任何 API (REST 或 SOAP API、SMB 共享或任何其他内容)。目前,BMO 只有一个具体的实现,将 CoreData 链接到 REST API。
Operation
子类,它将负责联系你的 API;MixedRepresentation
;MixedRepresentation
导入到 CoreData 数据库中,负责对象的唯一性和合并;为了清晰地分离角色,这个仓库有许多目标
MixedRepresentation
s 导入到你使用的任何数据库中;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 具有以下依赖项
Operation
的实现,为轻松运行和重试基本 Operation 提供了便利。本 Readme 将侧重于使用 CoreData+REST 实现的 BMO。后面的高级用法将展示如何为其他数据库或 API 创建新的 BMO 具体实现。
此处的 Readme 将提供在应用程序中实现 BMO 的一般步骤。如果你想要更详细和全面的指南,请参阅我们的 示例项目。
你的 Core Data 模型只有一个要求:所有映射的实体都必须具有“唯一属性”。这将是 BMO 将读取和写入的属性,以确保你的堆栈中不会有重复的实例。实际上,如果你正在获取本地数据库中已有的对象,则本地对象和获取的对象将合并在一起。数据库中已有的对象将被更新。该属性可以随意命名,但必须在所有实体中具有相同的名称。
具有唯一属性名称 bmoID
的简单模型示例
桥接器是一个实体(类、结构体,任何东西),它实现了 BridgeProtocol
协议。它是你的本地 Core Data 数据库和你的 API 之间的接口。这是你必须提供给 BMO 的最重要的事情。
桥接器的职责
Operation
,它在你的 API 上执行给定的请求。RestMapper
在这里可以帮助你。[[String: Any?]]
);MixedRepresentation
。我们稍后会看到这是什么。对于这项任务,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
]
)
}()
再一次,我们将信任 RESTUtils 来完成这项工作的繁重部分。
TODO:将连接的 http operation 实用程序从 happn 迁移到 RESTUtils…
我们必须简单地提取远程表示(基本上是从 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
中的属性保存为 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 发送请求的实例。例如,你可以将它保存在你的 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 是一种响应 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 Lamboley 在 happn 工作时创建。
非常感谢 happn 的 iOS 开发人员,没有他们,这个项目的开源是不可能的