随着移动互联网的快速发展,移动客户端对数据的访问量呈暴发性增长。传统的关系型数据库已经无法高效处理和满足移动客户端日益增长的数据访问量、访问速度和多样性的需求。NoSQL 数据库则是一个比较理想的替代方案。
NoSQL(全称 Not Only SQL)数据库提供了一种对非关系型数据库数据进行存储和检索机制。非关系型数据库不同于关系型数据库采用表关系建模,而使用键 - 值、图和文档等数据结构。这种机制设计简单,具有水平缩放性,对可用性提供了更有效的控制。
IBM MobileFirst Platform 在 Bluemix 上提供的 Cloudant NoSQL DB 服务就是这样一个 NoSQL database as a service (DBaaS)。本文介绍了如何在 iOS 移动应用中使用 IBM MobileFirst Platform 的 Cloudant NoSQL DB 服务(主要针对 Object-C 语言),以及如何使用 Bluemix Dashboard 对移动数据的操作进行监控,使得用户可以快速的构建起 iOS 移动应用。
回页首
为了支持 Mobile 应用,数据库必须满足以下需求:
使用传统的关系型数据库,在保持性能的前提下,移动应用开发人员很难满足用户、数据和参与度日益增长的需要。而使用 NoSQL 数据库,可以不必使用传统关系型数据库的表格关系对数据建模,其提供的无模式数据结构和水平缩放功能,对于移动应用中经常使用的大数据量和多种数据格式是非常理想的。
IBM MobileFirst Platform 在 Bluemix 上的 Cloudant NoSQL DB 服务可以让开发人员快速的开发移动应用,并提供数据的管理、安全性和扩展性方面的功能,使得开发人员重点关注于移动应用的业务方面的功能开发,降低开发成本。
使用 IBM MobileFirst Platform 在 Bluemix 上的 Cloudant NoSQL DB 服务有以下优点:
开发人员可以使用本地的移动应用开发接口实现数据的增删改查、离线同步、安全集成和数据分析功能。
可以将数据直接存储在用户的移动设备中供离线时使用。当设备上线时,用户本地的数据可以跟远程云端的数据进行同步。
可以使用访问控制来创建共有和私有的数据库,可以限制数据库只有某个用户可以访问,也可以设置为共享。
Cloudant NoSQL DB 可以扩展以提供数据的高可用性,数据被自动的分区、集群化和复制。
回页首
IMF Data SDK API 为开 iOS 移动应用提供了以下功能:
选择 Boilerplates 中的 MobileFirst Services Starter 作为模板,创建一个 Bluemix 移动后台应用。
图 1. 创建 Bluemix 移动后台应用
注册一个新的客户端,注意注册的 Bundle ID 和 Version 要和客户端移动应用保持一致。然后为该客户端移动应用选择一个安全认证方式,可选 Facebook、Google 或 Custom 方式。
图 2. 选择安全认证方式
移动应用需要安装 IMFData SDK 以使用本地开发接口来访问 Bluemix 上的 Cloudant NoSQL DB 服务。IMFData SDK 依赖于其他的工具库(CloudantToolkit、CDTDatastore,、CocoaLumberjack 和 FMDB),所以最简单的安装方法是通过 Cocoapods,它会帮助安装所有的依赖库,用户只需在 Podfile 中加入如下代码。
清单 1. Podfile 代码
source 'https://github.com/CocoaPods/Specs.git' pod 'IMFData'
然后在包含 Podfile 的工程目录中执行 pod install 命令即可。
移动应用要使用 SDK API 来访问 Bluemix 上的 Cloudant NoSQL DB 服务,需要先进行初始化。首先,import 调用 IMFData SDK 所需要的头文件。
清单 2. 引用头文件
#import <CloudantToolkit/CloudantToolkit.h> #import <CloudantSync.h> #import <IMFData/IMFData.h>
然后使用 Bluemix application ID 和 Route 来初始化 IMFCore SDK,并生成一个 IMFDataManager 实例。
清单 3. 初始化 IMFCore SDK
NSString *appId = @"12345678-1234-1234-1234-123456789012"; NSString *appRoute = @"https://myTestApp.mybluemix.net"; // 用 IBM Bluemix application ID 和 route 初始化 IMFCore SDK IMFClient *imfClient = [IMFClient sharedInstance]; [imfClient initializeWithBackendRoute:appRoute backendGUID:appId]; // 生成一个 IMFDataManager 实例 IMFDataManager *manager = [IMFDataManager sharedInstance];
本地数据仓库存储在用户移动设备中,远程数据仓库存储在云端 Bluemix 的 Cloudant 数据库。用户可以先将数据存储在本地,这样访问速度比较快,移动设备也可以不必上线。然后本地数据可以在移动设备上线时,阶段性的与云端 Cloudant 数据库进行同步。
清单 4. 创建本地数据仓库
// 获取一个 IMFDataManager 的引用 IMFDataManager *manager = [IMFDataManager sharedInstance]; NSString *name = @"myLocalDB"; NSError *error = nil; // 创建本地数据存储 CDTStore *store = [manager localStore:name error:&error];
注意数据库的名字字母需要小写。
清单 5. 创建远程数据仓库
// 获取一个 IMFDataManager 的引用 IMFDataManager *manager = [IMFDataManager sharedInstance]; NSString *name = @"myRemoteDB"; // 创建远程数据仓库 [manager remoteStore:name completionHandler:^(CDTStore *createdStore, NSError *error) { if(error){ // 错误处理 }else{ CDTStore *store = createdStore; NSLog(@"Successfully created store: %@", store.name); } }];
可以对远程数据仓库设置用户权限。在使用这个功能之前,需要先对 Bluemix 后台应用做安全设置,使得访问者可以通过 Bluemix IMFCore OAuth 进行安全认证,这是对远程数据仓库进行权限控制的前提。
这里的权限使用的是 CouchDB 的安全模型,该模型将用户与 admins 或者 members 权限组关联起来,授予用户 admins 或者 members 的权限。
使用下面的 code 对远程数据仓库设置用户权限。
清单 6. 对远程数据仓库设置用户权限
// 获取 data manager 的引用 IMFDataManager *manager = [IMFDataManager sharedInstance]; // 对当前用户在数据仓库上设置权限 [manager setCurrentUserPermissions: DB_ACCESS_GROUP_MEMBERS forStoreName: @"automobiledb" completionHander:^(BOOL success, NSError *error) { if(error){ // 错误处理 }else{ // 权限设置成功 } }];
Cloudant 以 JSON 文档形式存储数据。为了在应用中以对象的形式访问数据,需要使用数据对象映射器 Class 将本地的对象映射成 JSON 文档格式。
数据对象映射器对本地对象的顶级属性,包括数字、字符串、日期以及原始数据类型进行序列化。如果要对不支持的数据类型进行序列化,可以创建自定义的属性序列化器。
在 iOS 应用中,CDTDataObjectMapper 类将本地对象映射成 JSON 文档格式。当使用 IMFDataManager API 创建数据仓库(CDTStore)时,一个 CDTDataObjectMapper 对象会被自动创建,并置于这个 CDTStore 对象上。
CDTDataObjectMapper 支持对具有 NSNumber、NSString、NSDate 已经所有原始数据类型的顶级属性的映射。
要想被 CDTDataObjectMapper 映射,本地对象类需要满足以下需要:
下面这个 Automobile 类满足上面所有的需求,可以作为参考示例。
清单 7. Automobile 类示例
@interface Automobile : NSObject<CDTDataObject> @property (strong, nonatomic, readwrite) CDTDataObjectMetadata *metadata; @property NSInteger year;@property NSString *model;@property NSString *make;@property Person *owner; -(instancetype) initWithMake: (NSString*) make model: (NSString*) model year: (NSInteger) year; @end ```
然后需要将 Automobile class 和 data type 注册到 CDTDataObjectMapper 上。
清单 8. 注册 data type
// 使用现有的数据仓库 CDTStore *store = existingStore; [store.mapper setDataType:@"Automobile" forClassName:NSStringFromClass([Automobile class])];
CDTDataObjectMapper 仅对对象的顶级属性(包括 NSNumber、NSString、NSDate 或者原始数据类型)进行序列化。如果需要对不支持的数据类型进行序列化,则需要创建自定义的属性序列化器。
自定义的属性序列化器需要符合 CDTPropertySerializer protocol。CDTPropertySerializer protocol 提供了将 JSON 值转换成本地属性值的方法。按照 Object-C 语法规定,propertyValueToJSONValue 方法的返回值必须是一个合法的 JSON 对象。
Automobile 类有一个 owner 属性是 Person 类型。Person 类不是 CDTDataObjectMapper 支持的数据类型,CDTDataObjectMapper 会跳过 owner 属性,不会对其做映射。
清单 9. Person 类的接口定义
@interface Person : NSObject @property NSString *firstName;@property NSString *lastName; @end
下面的代码片段实现了一个针对 Person 对象的自定义序列化器 PersonSerializer,它符合 CDTPropertySerializer protocol。
清单 10. 针对 Person 对象的自定义序列化器 PersonSerializer
@interface PersonSerializer : NSObject<CDTPropertySerializer> @end @implementation PersonSerializer -(id) propertyValueToJSONValue: (id) propertyValue error: (NSError**) error { if(propertyValue && [propertyValue isKindOfClass:[Person class]]){ Person *person = (Person*)propertyValue; NSMutableDictionary *personMap = [NSMutableDictionary dictionary]; [personMap setObject:person.firstName forKey:@"firstName"]; [personMap setObject:person.lastName forKey:@"lastName"]; return personMap; }else{ return nil; } } -(id) jsonValueToPropertyValue: (id) jsonValue error: (NSError**) error { if(jsonValue && [jsonValue isKindOfClass:[NSDictionary class]]){ NSDictionary *personMap = (NSDictionary*)jsonValue; Person *person = [[Person alloc]init]; person.firstName = [personMap objectForKey:@"firstName"]; person.lastName = [personMap objectForKey:@"lastName"]; return person; }else{ return nil; } } @end
为了序列化 Automobile 类中的 owner 属性,需要使用 setPropertySerializer 方法将 PersonSerializer 类设置为 CDTDataObjectMapper 对象的属性序列化器。
清单 11. 设置属性序列化器
// 使用现有的数据仓库 CDTStore *store = existingStore; // ObjectMapper 必须是 DataObjectMapper 的实例或子类 CDTDataObjectMapper *mapper = (CDTDataObjectMapper*) store.mapper; [mapper setPropertySerializer: [[PersonSerializer alloc] init] forClassName:NSStringFromClass([Person class]) withDataType:@"Person"];
用户可以对数据仓库中的内容做更改。
使用 CDTStore 的 save 方法创建一个新的数据对象。
清单 12. 创建数据对象
// 使用现有的数据仓库 CDTStore *store = existingStore; // 创建要保存的 automobile 对象 Automobile *automobile = [[Automobile alloc] initWithMake:@"Toyota" model:@"Corolla" year: 2006]; [store save:automobile completionHandler:^(id savedObject, NSError *error) { if (error) { // 错误处理 } else { // save 成功 Automobile *savedAutomobile = savedObject; NSLog(@"saved revision: %@", savedAutomobile); } }];
可以通过数据的 ID 来获取对象。
清单 13. 通过 ID 获取对象
CDTStore *store = existingStore; NSString *automobileId = existingAutomobileId; // 从数据仓库中获取 automobile 对象 [store fetchById:automobileId completionHandler:^(id object, NSError *error) { if (error) { // 错误处理 } else { // 操作成功 Automobile *savedAutomobile = object; NSLog(@"fetched automobile: %@", savedAutomobile); } }];
使用与数据创建相同的 CDTStore 的 save 方法来保存一个已有的对象。由于对象已存在,所以进行的是更新操作。
清单 14. 更新数据对象
// 使用现有的数据仓库和 automobile 对象 CDTStore *store = existingStore; Automobile *automobile = existingAutomobile; // 更新 automobile 的一些属性值 automobile.year = 2015; // 将 automobile 对象保存到数据仓库中 [store save:automobile completionHandler:^(id savedObject, NSError *error) { if (error) { // 错误处理 } else { // 操作成功 Automobile *savedAutomobile = savedObject; NSLog(@"saved automobile: %@", savedAutomobile); } }];
将需要删除的数据对象传给 CDTStore 的 delete 方法。
清单 15. 删除数据对象
// 使用现有的数据仓库和 automobile 对象 CDTStore *store = existingStore; Automobile *automobile = existingAutomobile; // 将 automobile 对象从数据仓库中删除 [store delete:automobile completionHandler:^(NSString *deletedObjectId, NSString *deletedRevisionId, NSError *error) { if (error) { // 删除操作失败,错误处理 } else { // 删除成功 NSLog(@"deleted Automobile doc-%@-rev-%@", deletedObjectId, deletedRevisionId); } }];
要在数据仓库中进行查询操作,需要首先创建索引。
对本地和远程数据仓库创建索引使用的是相同的方法。
当数据仓库上设置了对象映射器时,这种索引非常有用。
清单 16. 以数据类型创建索引
// 使用现有的数据仓库 CDTStore *store = existingStore; // 声明 Automobile 类使用的数据类型 NSString *dataType = [store.mapper dataTypeForClassName:NSStringFromClass([Automobile class])]; // 创建索引 [store createIndexWithDataType:dataType fields:@[@"year", @"make"] completionHandler:^(NSError *error) { if(error){ // 错误处理 }else{ // 创建成功,继续业务逻辑 } }];
清单 17. 删除数据类型索引
// 使用现有的数据仓库 CDTStore *store = existingStore; // 声明 Automobile 类使用的数据类型 NSString *dataType = [store.mapper dataTypeForClassName:NSStringFromClass([Automobile class])]; // 删除索引 [store deleteIndexWithDataType:dataType completionHandler:^(NSError *error) { if(error){ // 错误处理 }else{ // 创建成功,继续业务处理 } }];
数据查询 API 提供了基于 NSPredicate 的查询和基于数据类型的查询。
首先创建一个 NSPredicate ,然后使用该 NSPredicate 在 CDTStore 对象上执行查询。
清单 18. 基于 NSPredicate 的查询
// 使用现有的数控仓库 CDTStore *store = existingStore; NSPredicate *queryPredicate = [NSPredicate predicateWithFormat:@"(year = 2006)"]; CDTCloudantQuery *query = [[CDTCloudantQuery alloc] initDataType:[store.mapper dataTypeForClassName:NSStringFromClass([Automobile class])] withPredicate:queryPredicate]; [store performQuery:query completionHandler:^(NSArray *results, NSError *error) { if(error){ // 错误处理 }else{ // 查询成功,使用查询结果 } }];
清单 19. 基于数据类型的查询
CDTQuery* query = [[CDTCloudantQuery alloc] initDataType:@"TodoItem"]; [self.datastore performQuery: query completionHandler:^(NSArray *results, NSError *error) { if(error) { [self.logger logErrorWithMessages:@"listItems failed with error: %@", error]; } else { self.itemList = [results mutableCopy]; } if (cb) { cb(); } [testExpectation fulfill]; }];
用户可以将移动设备上的数据与远程数据库进行同步复制。既可以将远程数据库的数据下载(pull)到移动设备的本地数据库,也可以将本地数据库中的数据上传(push)到远程数据库中。
注意,做数据同步的前提是本地数据库和远程数据库的名字要保持一致。
使用 data manager API,用户可以生成 replication 对象来控制本地和远程数据仓库的同步。当运行 pull replication 是,移动设备上本地数据仓库中的数据会被远程数据仓库中的数据刷新。当运行 push replication 时,本地数据仓库中的数据会被发送到远程数据仓库,并对远程数据仓库进行刷新。
清单 20. 运行 pull replication
// store 是一个现有的使用 IMFDataManager 创建的远程数据仓库 CDTStore 对象 __block NSError *replicationError; CDTPullReplication *pull = [manager pullReplicationForStore: store.name]; CDTReplicator *replicator = [manager.replicatorFactory oneWay:pull error:&replicationError];if(replicationError){ // 错误处理 }else{ // replicator 创建成功 } [replicator startWithError:&replicationError]; if(replicationError){ // 错误处理 }else{ // replicator 启动成功 } // ( 可选 ) 通过轮询来监控 replication 的进行状态 while (replicator.isActive) { [NSThread sleepForTimeInterval:1.0f]; NSLog(@"replicator state : %@", [CDTReplicator stringForReplicatorState:replicator.state]); }
清单 21. 运行 push replication
// store 是一个现有的使用 IMFDataManager 创建的本地数据仓库 CDTStore 对象 __block NSError *replicationError; CDTPushReplication *push = [manager pushReplicationForStore: store.name]; CDTReplicator *replicator = [manager.replicatorFactory oneWay:push error:&replicationError]; if(replicationError){ // 错误处理 }else{ // replicator 创建成功 } [replicator startWithError:&replicationError];if(replicationError){ // 错误处理 }else{ // replicator 启动成功 } // ( 可选 ) 通过轮询来监控 replication 的进行状态 while (replicator.isActive) { [NSThread sleepForTimeInterval:1.0f]; NSLog(@"replicator state : %@", [CDTReplicator stringForReplicatorState:replicator.state]); }
回页首
Cloudant NoSQL DB 的 Monitoring dashboard 提供了对数据库和对象的操作的监控信息,包括数据库的增删改查操作的数量,以及数据库的使用量等。
图 3. 数据监控
回页首
本文介绍了如何在 iOS 移动应用中使用 IBM MobileFirst Platform 的 Cloudant NoSQL DB 服务,以及如何使用 Bluemix Dashboard 对移动数据的操作进行监控。希望对读者使用 IBM MobileFirst Platform 的 Cloudant NoSQL DB 服务有所帮助。