数据存储开发指南 · Objective-C
数据存储是云服务提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端:
// 构建对象
LCObject *todo = [LCObject objectWithClassName:@"Todo"];
// 为属性赋值
[todo setObject:@"工程师周会" forKey:@"title"];
[todo setObject:@"周二两点,全体成员" forKey:@"content"];
// 将对象保存到云端
[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// 成功保存之后,执行其他逻辑
NSLog(@"保存成功。objectId:%@", todo.objectId);
} else {
// 异常处理
}
}];
我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。
SDK 安装与初始化
请阅读数据存储、即时通讯 Objective C SDK 配置指南。
对象
LCObject
LCObject
是云服务对复杂对象的封装,每个 LCObject
包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个 LCObject
上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。
比如说,一个保存着单个 Todo 的 LCObject
可能包含如下数据:
title: "给小林发邮件确认会议时间",
isComplete: false,
priority: 2,
tags: ["工作", "销售"]
数据类型
LCObject
支持的数据类型包括 String
、Number
、Boolean
、Object
、Array
、Date
等等。你可以通过嵌套的方式在 Object
或 Array
里面存储更加结构化的数据。
LCObject
还支持两种特殊的数据类型 Pointer
和 File
,可以分别用来存储指向其他 LCObject
的指针以及二进制数据。
LCObject
同时支持 GeoPoint
,可以用来存储地理位置信息。参见 GeoPoint。
以下是一些示例:
// 基本类型
NSNumber *boolean = @(YES);
NSNumber *number = [NSNumber numberWithInt:2018];
NSString *string = [NSString stringWithFormat:@"%@ 流行音乐榜单", number];
NSDate *date = [NSDate date];
NSData *data = [@"Hello world!" dataUsingEncoding:NSUTF8StringEncoding];
NSArray *array = [NSArray arrayWithObjects: string, number, nil];
NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: number, @"number", string, @"string", nil];
// 构建对象
LCObject *testObject = [LCObject objectWithClassName:@"TestObject"];
[testObject setObject:boolean forKey:@"testBoolean"];
[testObject setObject:number forKey:@"testInteger"];
[testObject setObject:string forKey:@"testString"];
[testObject setObject:date forKey:@"testDate"];
[testObject setObject:data forKey:@"testData"];
[testObject setObject:array forKey:@"testArray"];
[testObject setObject:dictionary forKey:@"testDictionary"];
[testObject saveInBackground];
我们不推荐通过 NSData
在 LCObject
里面存储图片、文档等大型二进制数据。每个 LCObject
的大小不应超过 128 KB。如需存储大型文件,可创建 LCFile
实例并将其关联到 LCObject
的某个属性上。参见 文件。
注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。
云服务控制台 > 数据存储 > 结构化数据 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。
若想了解云服务是如何保护应用数据的,请阅读数据和安全。
构建对象
下 面的代码构建了一个 class 为 Todo
的 LCObject
:
LCObject *todo = [LCObject objectWithClassName:@"Todo"];
// 等同于
LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"];
在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。
保存对象
下面的代码将一个 class 为 Todo
的对象存入云端:
// 构建对象
LCObject *todo = [LCObject objectWithClassName:@"Todo"];
// 为属性赋值
[todo setObject:@"马拉松报名" forKey:@"title"];
[todo setObject:@2 forKey:@"priority"];
// 将对象保存到云端
[todo saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
// 成功保存之后,执行其他逻 辑
NSLog(@"保存成功。objectId:%@", todo.objectId);
} else {
// 异常处理
}
}];
为了确认对象已经保存成功,我们可以到 云服务控制台 > 数据存储 > 结构化数据 > Todo
里面看一下,应该会有一行新的数据产生。点一下这个数据的 objectId
,应该能看到类似这样的内容:
{
"title": "马拉松报名",
"priority": 2,
"ACL": {
"*": {
"read": true,
"write": true
}
},
"objectId": "582570f38ac247004f39c24b",
"createdAt": "2017-11-11T07:19:15.549Z",
"updatedAt": "2017-11-11T07:19:15.549Z"
}
注意,无需在 云服务控制台 > 数据存储 > 结构化数据 里面创建新的 Todo
class 即可运行前面的代码。如果 class 不存在,它将自动创建。
以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定:
内置属性 | 类型 | 描述 |
---|---|---|
objectId | NSString | 该对象唯一的 ID 标识。 |
ACL | LCACL | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 |
createdAt | NSDate | 该对象被创建的时间。 |
updatedAt | NSDate | 该对象最后一次被修改的时间。 |
这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的 LCObject
不存在这些属性。
属性名(keys)只能包含字母、数字和下划线。自定义属性不得以双下划线(__
)开头或与任何系统保留字段和内置属性(ACL
、className
、createdAt
、objectId
和 updatedAt
)重名,无论大小写。
属性值(values)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 数据类型。
我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 CustomData
。属性,采用小驼峰法,如 imageUrl
。
获取对象
对于已经保存到云端的 LCObject
,可以通过它的 objectId
将其取回:
LCQuery *query = [LCQuery queryWithClassName:@"Todo"];
[query getObjectInBackgroundWithId:@"582570f38ac247004f39c24b" block:^(LCObject *todo, NSError *error) {
// todo 就是 objectId 为 582570f38ac247004f39c24b 的 Todo 实例
NSString *title = todo[@"title"];
int priority = [[todo objectForKey:@"priority"] intValue];
// 获取内置属性
NSString *objectId = todo.objectId;
NSDate *updatedAt = todo.updatedAt;
NSDate *createdAt = todo.createdAt;
}];
对象拿到之后,可以通过 get
方法来获取各个属性的值。注意 objectId
、updatedAt
和 createdAt
这三个内置属性不能通过 get
获取或通过 set
修改,只能由云端自动进行填充。尚未保存的 LCObject
不存在这些属性。
如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 nil
。
同步对象
当云端数据发生更改时,你可以调用 fetchInBackgroundWithBlock
方法来刷新对象,使之与云端数据同步:
LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"];
[todo fetchInBackgroundWithBlock:^(LCObject *todo, NSError *error) {
// todo 已刷新
}];
刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,fetchInBackgroundWithBlock
操作会丢弃这些修改。为避免这种情况,你可以在刷新时指定 需要刷新的属性,这样只有指定的属性会被刷新(包括内置属性 objectId
、createdAt
和 updatedAt
),其他属性不受影响。
LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"];
NSArray *keys = [NSArray arrayWithObjects:@"priority", @"location", nil];
[todo fetchInBackgroundWithKeys:keys block:^(LCObject *todo, NSError *error) {
// 只有 priority 和 location 会被获取和刷新
}];
更新对象
要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 saveInBackground
方法。例如:
要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 saveInBackground
方法。例如:
LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"];
[todo setObject:@"这周周会改到周三下午三点。" forKey:@"content"];
[todo saveInBackground];
云服务会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。
有条件更新对象
通过传入 query
选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回 305
错误。
例如,用户的账户表 Account
有一个余额字段 balance
,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求:
LCObject *account = [LCObject objectWithClassName:@"Account" objectId:@"5745557f71cfe40068c6abe0"];
// 对 balance 原子减少 100
NSInteger amount = -100;
[account incrementKey:@"balance" byAmount:@(amount)];
// 设置条件
LCQuery *query = [[LCQuery alloc] init];
[query whereKey:@"balance" greaterThanOrEqualTo:@(-amount)];
LCSaveOption *option = [[LCSaveOption alloc] init];
option.query = query;
// 操作结束后,返回最新数据。
// 如果是新对象,则所有属性都会被返回,
// 否则只有更新的属性会被返回。
option.fetchWhenSave = YES;
[account saveInBackgroundWithOption:option block:^(BOOL succeeded, NSError * _Nullable error) {
if (succeeded) {
NSLog(@"当前余额为:%@", account[@"balance"]);
} else if (error.code == 305) {
NSLog(@"余额不足,操作失败!");
}
}];
query
选项只对已存在的对象有效,不适用于尚未存入云端的对象。
query
选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过 LCQuery
查询 LCObject
再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。
更新计数器
设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 原子操作 来增加或减少一个属性内保存的数字:
[post incrementKey:@"likes" byAmount:@1];
可以指定需要增加或减少的值。若未指定,则默认使用 1
。
注意,虽然原子增减支持浮点数,但因为底层数据库的浮点数存储格式限制,会有舍入误差。
因此,需要原子增减的字段建议使用整数以避免误差,例如 3.14
可以存储为 314
,然后在客户端进行相应的转换。
否则,以比较大小为条件查询对象的时候,需要特殊处理,
< a
需改查 < a + e
,> a
需改查 > a - e
,== a
需改查 > a - e
且 < a + e
,其中 e
为误差范围,据所需精度取值,比如 0.0001
。
更新数组
更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据:
addObject:forKey:
将指定对象附加到数组末尾。addObjectsFromArray:forKey:
将指定对象数组附加到数组末尾。addUniqueObject:forKey:
将指定对象附加到数组末尾,确保对象唯一。addUniqueObjectsFromArray:forKey:
将指定对象数组附加到数组末尾,确保对象唯一。removeObject:forKey:
从数组字段中删除指定对象的所有实例。removeObjectsInArray:forKey:
从数组字段中删除指定的对象数组。
例如,Todo
用一个 alarms
属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性:
-(NSDate*) getDateWithDateString:(NSString*) dateString{
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSDate *date = [dateFormat dateFromString:dateString];
return date;
}
NSDate *alarm1 = [self getDateWithDateString:@"2018-04-30 07:10:00"];
NSDate *alarm2 = [self getDateWithDateString:@"2018-04-30 07:20:00"];
NSDate *alarm3 = [self getDateWithDateString:@"2018-04-30 07:30:00"];
NSArray *alarms = [NSArray arrayWithObjects:alarm1, alarm2, alarm3, nil];
LCObject *todo = [LCObject objectWithClassName:@"Todo"];
[todo addUniqueObjectsFromArray:alarms forKey:@"alarms"];
[todo saveInBackground];
删除对象
下面的代码从云端删除一个 Todo
对象:
LCObject *todo = [LCObject objectWithClassName:@"Todo" objectId:@"582570f38ac247004f39c24b"];
[todo deleteInBackground];
注意,删除对象是一个较为敏感的操作,我们建议你阅读《ACL 权限管理开发指南》来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。
批量操作
// 批量构建和更新
+ (BOOL)saveAll:(NSArray *)objects error:(NSError **)error;
+ (void)saveAllInBackground:(NSArray *)objects
block:(LCBooleanResultBlock)block;
// 批量删除
+ (BOOL)deleteAll:(NSArray *)objects error:(NSError **)error;
+ (void)deleteAllInBackground:(NSArray *)objects
block:(LCBooleanResultBlock)block;
// 批量同步
+ (BOOL)fetchAll:(NSArray *)objects error:(NSError **)error;
+ (void)fetchAllInBackground:(NSArray *)objects
block:(LCArrayResultBlock)block;
下面的代码将所有 Todo
的 isComplete
设为 true
:
LCQuery *query = [LCQuery queryWithClassName:@"Todo"];
[query findObjectsInBackgroundWithBlock:^(NSArray *todos, NSError *error) {
// 获取需要更新的 todo
for (LCObject *todo in todos) {
// 更新属性值
todo[@"isComplete"] = @(YES);
}
// 批量更新
[LCObject saveAllInBackground:todos];
}];
虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。
后台运行
细心的开发者已经发现,在所有的示例代码中几乎都是用了异步来访问云端,形如 xxxxInBackground
的用法都是提供给开发者在主线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用这种命名方式的函数。
离线存储对象
大 多数保存功能可以立刻执行,并通知应用「保存完毕」。不过若不需要知道保存完成的时间,则可使用 saveEventually
来代替。
它的优点在于:如果用户目前尚未接入网络,saveEventually
会缓存设备中的数据,并在网络连接恢复后上传。如果应用在网络恢复之前就被关闭了,那么当它下一次打开时,SDK 会再次尝试保存操作。
所有 saveEventually
(或 deleteEventually
)的相关调用,将按照调用的顺序依次执行。因此,多次对某一对象使用 saveEventually
是安全的。
数据模型
对象之间可以产生关联。拿一个博客应用来说,一个 Post
对象可以与许多个 Comment
对象产生关联。云服务支持三种关系:一对一、一对多、多对多。
一对一、一对多关系
一对一、一对多关系可以通过将 LCObject
保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个 Comment
指向一个 Post
。
下面的代码会创建一个含有单个 Comment
的 Post
:
// 创建 post
LCObject *post = [[LCObject alloc] initWithClassName:@"Post"];
[post setObject:@"饿了……" forKey:@"title"];
[post setObject:@"中午去哪吃呢?" forKey:@"content"];
// 创建 comment
LCObject *comment = [[LCObject alloc] initWithClassName:@"Comment"];
[comment setObject:@"当然是肯德基啦!" forKey:@"content"];
// 将 post 设为 comment 的一个属性值
[comment setObject:post forKey:@"parent"];
// 保存 comment 会同时保存 post
[comment saveInBackground];
云端存储时,会将被指向的对象用 Pointer
的形式存起来。你也可以用 objectId
来指向一个对象:
LCObject *post = [LCObject objectWithClassName:@"Post" objectId:@"57328ca079bc44005c2472d0"];
[comment setObject:post forKey:@"post"];
请参阅 关系查询 来了解如何获取关联的对象。
多对多关系
想要建立多对多关系,最简单的办法就是使用 数组。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 中间表 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。
我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。
序列化和反序列化
在实际的开发中,把 LCObject
当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此 LCObject
也提供了序列化和反序列化的方法。
序列化:
LCObject *todo = [[LCObject alloc] initWithClassName:@"Todo"]; // 构建对象
[todo setObject:@"马拉松报名" forKey:@"title"]; // 设置名称
[todo setObject:@2 forKey:@"priority"]; // 设置优先级
[todo setObject:[LCUser currentUser] forKey:@"owner"]; // 这里就是一个 Pointer 类型,指向当前登录的用户
NSMutableDictionary *serializedJSONDictionary = [todo dictionaryForObject]; // 获取序列化后的字典
NSError *err;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:serializedJSONDictionary options:0 error:&err]; // 获取 JSON 数据
NSString *serializedString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; // 获取 JSON 字符串
// serializedString 的内容是:{"title":"马拉松报名","className":"Todo","priority":2}
反序列化:
NSMutableDictionary *objectDictionary = [NSMutableDictionary dictionaryWithCapacity:10];// 声明一个 NSMutableDictionary
[objectDictionary setObject:@"马拉松报名" forKey:@"title"];
[objectDictionary setObject:@2 forKey:@"priority"];
[objectDictionary setObject:@"Todo" forKey:@"className"];
LCObject *todo = [LCObject objectWithDictionary:objectDictionary]; // 由 NSMutableDictionary 转化一个 LCObject
[todo saveInBackground]; // 保存到云端
查询
我们已经了解到如何从云端获取单个 LCObject
,但你可能还会有一次性获取多个符合特定条件的 LCObject
的需求,这时候就需要用到 LCQuery
了。
基础查询
执行一次基础查询通常包括这些步骤:
- 构建
LCQuery
; - 向其添加查询条件;
- 执行查询并获取包含满足条件的对象的数组。
下面的代码获取所有 lastName
为 Smith
的 Student
:
LCQuery *query = [LCQuery queryWithClassName:@"Student"];
[query whereKey:@"lastName" equalTo:@"Smith"];
[query findObjectsInBackgroundWithBlock:^(NSArray *students, NSError *error) {
// students 是包含满足条件的 Student 对象的数组
}];