-
Notifications
You must be signed in to change notification settings - Fork 28
1.存储 1 AVObject
数据存储(LeanStorage)是 LeanCloud 提供的核心功能之一,它的使用方法与传统的关系型数据库有诸多不同。下面我们将其与传统数据库的使用方法进行对比,让大家有一个初步了解。 下面这条 SQL 语句在绝大数的关系型数据库都可以执行,其结果是在 Todo 表里增加一条新数据:
INSERT INTO Todo (title, content) VALUES ('工程师周会', '每周工程师会议,周一下午 2 点')
使用传统的关系型数据库作为应用的数据源几乎无法避免以下步骤:
- 插入数据之前一定要先创建一个表结构,并且随着之后需求的变化,开发者需要不停地修改数据库的表结构,维护表数据。
- 每次插入数据的时候,客户端都需要连接数据库来执行数据的增删改查(CRUD)操作。
使用 LeanStorage,实现代码如下:
AVObject todo = new AVObject("Todo");
todo.put("title", "工程师周会");
todo.put("content", "每周工程师会议,周一下午2点");
todo.saveInBackground().subscribe(new Observer<AVObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVObject avObject) {
System.out.println("succeed to save Object.");
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
使用 LeanStorage 的特点在于:
- 不需要单独维护表结构。例如,为上面的 Todo 表新增一个 location 字段,用来表示日程安排的地点,那么刚才的代码只需做如下变动:
AVObject todo = new AVObject("Todo");
todo.put("title", "工程师周会");
todo.put("content", "每周工程师会议,周一下午2点");
todo.put("location", "会议室");// 只要添加这一行代码,服务端就会自动添加这个字段
todo.saveInBackground().subscribe(new Observer<AVObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVObject avObject) {
System.out.println("succeed to save Object.");
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
- 数据可以随用随加,这是一种无模式化(Schema Free)的存储方式。
- 所有对数据的操作请求都通过 HTTPS 访问标准的 REST API 来实现。
- 我们为各个平台或者语言开发的 SDK 在底层都是调用统一的 REST API,并提供完整的接口对数据进行增删改查。
LeanStorage 在结构化数据存储方面,与 DB 的区别在于:
- Schema Free/Not free 的差异;
- 数据接口上,LeanStorage 是面向对象的(数据操作接口都是基于 Object 的),开放的(所有移动端都可以直接访问),DB 是面向结构的,封闭的(一般在 Server 内部访问);
- 数据之间关联的方式,DB 是主键外键模型,LeanStorage 则有自己的关系模型(Pointer、Relation 等);
LeanStorage 支持两种存储类型:对象(AVObject)和文件(AVFile),下面我们来逐一具体说明一下他们的用法。
AVObject
是 LeanStorage 对复杂对象的封装,每个 AVObject 包含若干属性值对,也称键值对(key-value)。属性的值是与 JSON 格式兼容的数据。通过 REST API 保存对象需要将对象的数据通过 JSON 来编码。这个数据是无模式化的(Schema Free),这意味着你不需要提前标注每个对象上有哪些 key,你只需要随意设置 key-value 对就可以,云端会保存它。
AVObject
支持以下数据类型:
- 字符(Char)
- 字符串(String)
- 布尔值(Boolean)
- 数字(Integer,Double,Long,Float)
- 日期(Date)。注意,时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会做转化成本地时间。
- 二进制数据(byte[])。注意:我们不推荐在
AVObject
中使用byte[]
来储存大块的二进制数据,比如图片或整个文件。每个AVObject
的大小都不应超过 128 KB。如果需要储存更多的数据,建议使用 AVFile。
- Object,可以把任意的 Java Object 当成属性值进行存储。
- AVGeoPoint,地理位置信息。
- AVObject。可以把一个 AVObject 实例当成另一个 AVObject 的属性值进行存储。
- ArrayList。可以把对象的数组当成属性值进行存储。
- HashMap。可以把一个属性值对的 Map 当成属性值进行存储。
注意:HashMap 和 ArrayList 支持嵌套,这样在一个 AVObject 中就可以使用它们来储存更多的结构化数据。
通过下面的示例,我们可以更好地了解数据类型:
boolean bool = true;
int number = 2015;
String string = number + " 年度音乐排行";
Date date = new Date();
byte[] data = "短篇小说".getBytes();
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(number);
arrayList.add(string);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("数字", number);
hashMap.put("字符串", string);
AVObject object = new AVObject("DataTypes");
object.put("testBoolean", bool);
object.put("testInteger", number);
object.put("testDate", date);
object.put("testData", data);
object.put("testArrayList", arrayList);
object.put("testHashMap", hashMap);
object.save();
构建一个 AVObject
可以使用如下方式:
// 构造方法传入的参数,对应的就是控制台中的 Class Name
AVObject todo = new AVObject("Todo");
每个 AVObject 必须有一个 Class 类名称,这样云端才知道它的数据归属于哪张数据表。
Class 类名称(ClassName)必须以字母开头,只能包含字母、数字和下划线。
生成了 AVObject 实例之后,通过调用 save(同步)
或者 saveInBackground(异步)
函数可以保存这个对象。
假如我们要保存一个 TodoFolder
,它可以包含多个 Todo
,类似于给行程按文件夹的方式分组。我们并不需要提前去后台创建这个名为 TodoFolder 的 Class 类,而仅需要执行如下代码,云端就会自动创建这个类:
AVObject todoFolder = new AVObject("TodoFolder");// 构建对象
todoFolder.put("name", "工作");// 设置名称
todoFolder.put("priority", 1);// 设置优先级
todoFolder.save();// 保存到服务端
创建完成后,打开 控制台 > 存储,点开 TodoFolder 类,就可以看到刚才添加的数据。除了 name、priority(优先级)之外,其他字段都是数据表的内置属性。
内置属性 | 类型 | 描述 |
---|---|---|
objectId |
"String" | 该对象唯一的 Id 标识 |
ACL |
"ACL" | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 |
createdAt |
Date | 该对象被创建的 UTC 时间 |
updatedAt |
Date | 该对象最后一次被修改的时间 |
- 属性名
- 也叫键或 key,必须是由字母、数字或下划线组成的字符串。
自定义的属性名,**不能以双下划线 `__` 开头,也不能与以下系统保留字段和内置属性重名(不区分大小写)**。ACL、className、createdAt、objectId、updatedAt - 属性值
- 可以是字符串、数字、布尔值、数组或字典。
为提高代码的可读性和可维护性,建议使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如 CustomData
。属性,采用小驼峰法,如 imageUrl
。
AVObject
对象在保存时可以设置选项来快捷完成关联操作,可用的选项属性有:
选项 | 类型 | 适用操作 | 说明 |
---|---|---|---|
AVSaveOption.fetchWhenSave | Boolean | create update |
对象成功保存后,自动返回其在云端的最新值。create 操作返回该对象的所有属性,update 操作只返回被更新了的属性的最新值。用法请参考 更新计数器。 |
AVSaveOption.matchQuery | AVQuery | update | 当 query 中的条件满足后,对象才能被更新,否则系统会放弃更新,并返回错误码 305。 通过 query 指定的条件仅对已存在的对象生效,如果用于保存新对象,则不生效。 开发者原本可以通过 AVQuery 和 AVObject 分两步来实现这样的逻辑,但如此一来无法保证操作的原子性从而导致并发问题。该选项可以用来判断多用户更新同一对象数据时可能引发的冲突。用法请参考 按条件更新对象。 |
每个被成功保存在云端的对象会有一个唯一的 Id 标识 objectId,因此获取对象的最基本的方法就是根据 objectId 来查询:
AVQuery<AVObject> avQuery = new AVQuery<>("Todo");
avQuery.getInBackground("558e20cbe4b060308e3eb36c").subscribe(new Observer<AVObject>() {
public void onSubscribe(Disposable disposable) {
}
public void onNext(AVObject o) {
System.out.println(o.toString());
}
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
public void onComplete() {
}
});
除了使用 AVQuery,还可以采用在本地构建一个 AVObject
的方式,通过接口和 objectId 把数据从云端拉取到本地:
// 第一参数是 className,第二个参数是 objectId
AVObject todo = AVObject.createWithoutData("Todo", "558e20cbe4b060308e3eb36c");
todo.fetchInBackground().subscribe(new Observer<AVObject>() {
public void onSubscribe(Disposable disposable) {
}
public void onNext(AVObject o) {
System.out.println(o.toString());
}
public void onError(Throwable throwable) {
throwable.printStackTrace();
}
public void onComplete() {
}
});
每一次对象存储成功之后,云端都会返回 objectId
,它是一个全局唯一的属性。
final AVObject todo = new AVObject("Todo");
todo.put("title", "工程师周会");
todo.put("content", "每周工程师会议,周一下午2点");
todo.put("location", "会议室");// 只要添加这一行代码,服务端就会自动添加这个字段
todo.saveInBackground().subscribe(new Observer<AVObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVObject avObject) {
System.out.println("succeed to save Object. objectId:" + avObject.getObjectId());
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
访问 Todo 的属性的方式为:
AVQuery<AVObject> avQuery = new AVQuery<>("Todo");
avQuery.getInBackground("558e20cbe4b060308e3eb36c").subscribe(new Observer<AVObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVObject avObject) {
int priority = avObject.getInt("priority");
String location = avObject.getString("location");
String title = avObject.getString("title");
String content = avObject.getString("content");
// 获取三个特殊属性
String objectId = avObject.getObjectId();
Date updatedAt = avObject.getUpdatedAt();
Date createdAt = avObject.getCreatedAt();
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
默认属性是所有对象都会拥有的属性,它包括 objectId
、createdAt
、updatedAt
。
- createdAt,对象第一次保存到云端的时间戳。该时间一旦被云端创建,在之后的操作中就不会被修改。
- updatedAt,对象最后一次被修改(或最近一次被更新)的时间。
注:应用控制台对 createdAt 和 updatedAt 做了在展示优化,它们会依据用户操作系统时区而显示为本地时间;客户端 SDK 获取到这些时间后也会将其转换为本地时间;而通过 REST API 获取到的则是原始的 UTC 时间,开发者可能需要根据情况做相应的时区转换。
多终端共享一个数据时,为了确保当前客户端拿到的对象数据是最新的,可以调用刷新接口来确保本地数据与云端的同步:
// 假如已知了 objectId 可以用如下的方式构建一个 AVObject
AVObject anotherTodo = AVObject.createWithoutData("Todo", "5656e37660b2febec4b35ed7");
// 然后调用刷新的方法,将数据从服务端拉到本地
anotherTodo.fetchInBackground().subscribe(new Observable<AVObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVObject avObject) {
// 调用 fetchInBackground 和 refreshInBackground 效果是一样的。
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
在更新对象操作后,对象本地的 updatedAt
字段(最后更新时间)会被刷新,直到下一次 save 操作,updatedAt
的最新值才会被同步到云端,这样做是为了减少网络流量传输。
目前 Todo 这个类已有四个自定义属性:priority
、content
、location
和 title
。为了节省流量,现在只想刷新 priority
和 location
可以使用如下方式:
// 假如已知了 objectId 可以用如下的方式构建一个 AVObject
AVObject anotherTodo = AVObject.createWithoutData("Todo", "5656e37660b2febec4b35ed7");
String keys = "priority,location";// 指定刷新的 key 字符串
anotherTodo.fetchInBackground(keys).subscribe(new Observable<AVObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVObject avObject) {
// 调用 fetchInBackground 和 refreshInBackground 效果是一样的。
}
public void onError(Throwable throwable) {}
public void onComplete() {}
});
刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,刷新操作会丢弃这些修改。
LeanCloud 上的更新对象都是针对单个对象,云端会根据有没有 objectId
来决定是新增还是更新一个对象。
假如 objectId
已知,则可以通过如下接口从本地构建一个 AVObject
来更新这个对象:
// 第一参数是 className,第二个参数是 objectId
AVObject todo = AVObject.createWithoutData("Todo", "558e20cbe4b060308e3eb36c");
// 修改 content
todo.put("content","每周工程师会议,本周改为周三下午3点半。");
// 保存到云端
todo.saveInBackground().blockingSubscribe();
通过使用 保存选项 query
可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新。
例如:用户的账务账户表 Account
有一个余额字段 balance
,同时有多个请求要修改该字段值,为避免余额出现负值,只有满足 balance >= 当前请求的数值
这个条件才允许修改,否则提示「余额不足,操作失败!」。
final int amount = -100;
AVQuery<AVObject> query = new AVQuery<>("Account");
AVObject account = query.getFirst();
account.increment("balance", amount);
AVSaveOption option = new AVSaveOption();
option.query(new AVQuery<>("Account").whereGreaterThanOrEqualTo("balance", -amount));
option.setFetchWhenSave(true);
account.saveInBackground(option).subscribe(new Observer<AVObject>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVObject avObject) {
System.out.println("当前余额为:" + avObject.get("balance"));
}
public void onError(Throwable throwable) {
System.out.println("余额不足,操作失败!");
}
public void onComplete() {}
});
LeanCloud 提供了类似 SQL 语法中的 Update 方式更新一个对象,例如更新一个 TodoFolder 对象可以使用下面的代码:
AVQuery.doCloudQueryInBackground("update TodoFolder set name='家庭' where objectId='558e20cbe4b060308e3eb36c'").subscribe(new Observer<AVCloudQueryResult>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVCloudQueryResult queryResult) {
}
public void onError(Throwable throwable) {
}
});
假如某一个 Todo 完成了,用户想要删除这个 Todo 对象,可以如下操作:
todo.deleteInBackground().blockingSubscribe();
删除对象是一个较为敏感的操作。在控制台创建对象的时候,默认开启了权限保护,关于这部分的内容请阅读: 角色与 ACL 权限管理。
LeanCloud 提供了类似 SQL 语法中的 Delete 方式删除一个对象,例如删除一个 Todo 对象可以使用下面的代码:
// 执行 CQL 语句实现删除一个 Todo 对象
AVQuery.doCloudQueryInBackground("delete from Todo where objectId='558e20cbe4b060308e3eb36c'")
.subscribe(new Observer<AVCloudQueryResult>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVCloudQueryResult queryResult) {
}
public void onError(Throwable throwable) {
}
});
为了减少网络交互的次数太多带来的时间浪费,你可以在一个请求中对多个对象进行创建、更新、删除、获取。接口都在 AVObject 这个类下面:
// 批量创建、更新
saveAll()
saveAllInBackground()
// 批量删除
deleteAll()
deleteAllInBackground()
// 批量获取
fetchall()
fetchAllInBackground()
批量设置 Todo 已经完成:
AVQuery<AVObject> query = new AVQuery<>("Todo");
query.findInBackground().subscribe(new Observer<List<AVObject>>() {
public void onSubscribe(Disposable disposable) {}
public void onNext(List<AVObject> list) {
for (AVObject todo : list) {
todo.put("status", 1);
}
AVObject.saveAllInBackground(list).subscribe(new Observer<AVNull> () {
public void onSubscribe(Disposable disposable) {}
public void onNext(AVNull avNull) {
}
public void onError(Throwable throwable) {
}
public void onComplete() {}
});
}
public void onError(Throwable throwable) {
System.out.println("余额不足,操作失败!");
}
public void onComplete() {}
});
大多数保存功能可以立刻执行,并通知应用「保存完毕」。不过若不需要知道保存完成的时间,则可使用 saveEventually
来代替。
它的优点在于:如果用户目前尚未接入网络,saveEventually
会缓存设备中的数据,并在网络连接恢复后上传。如果应用在网络恢复之前就被关闭了,那么当它下一次打开时,LeanCloud 会再次尝试保存操作。
所有 saveEventually
(或 deleteEventually
)的相关调用,将按照调用的顺序依次执行。因此,多次对某一对象使用 saveEventually
是安全的。
对象可以与其他对象相联系。如前面所述,我们可以把一个 AVObject 的实例 A,当成另一个 AVObject 实例 B 的属性值保存起来。这可以解决数据之间一对一或者一对多的关系映射,就像关系型数据库中的主外键关系一样。
Pointer 只是个描述并没有具象的类与之对应,它与 AVRelation 不一样的地方在于:AVRelation 是在一对多的「一」这一方(上述代码中的一指 TodoFolder)保存一个 AVRelation 属性,这个属性实际上保存的是对被关联数据多的这一方(上述代码中这个多指 Todo)的一个 Pointer 的集合。而反过来,LeanCloud 也支持在「多」的这一方保存一个指向「一」的这一方的 Pointer,这样也可以实现一对多的关系。
简单的说, Pointer 就是一个外键的指针,只是在 LeanCloud 控制台做了显示优化。
现在有一个新的需求:用户可以分享自己的 TodoFolder 到广场上,而其他用户看见可以给与评论,比如某玩家分享了自己想买的游戏列表(TodoFolder 包含多个游戏名字),而我们用 Comment 对象来保存其他用户的评论以及是否点赞等相关信息,代码如下:
AVObject comment = new AVObject("Comment");// 构建 Comment 对象
comment.put("likes", 1);// 如果点了赞就是 1,而点了不喜欢则为 -1,没有做任何操作就是默认的 0
comment.put("content", "这个太赞了!楼主,我也要这些游戏,咱们团购么?");// 留言的内容
// 假设已知了被分享的该 TodoFolder 的 objectId 是 5590cdfde4b00f7adb5860c8
comment.put("targetTodoFolder", AVObject.createWithoutData("TodoFolder", "5590cdfde4b00f7adb5860c8"));
// 以上代码就是的执行结果就会在 comment 对象上有一个名为 targetTodoFolder 属性,它是一个 Pointer 类型,指向 objectId 为 5590cdfde4b00f7adb5860c8 的 TodoFolder
当 Todo 拥有一个字段叫做 TodoFolder 的 Pointer 类型的属性,在获取 Todo 的对象的同时,想一并把被关联的 TodoFolder 也拉取到本地。更多内容可参考关联数据查询。
地理位置是一个特殊的数据类型,LeanCloud 封装了 AVGeoPoint
来实现存储以及相关的查询。
首先要创建一个 AVGeoPoint
对象。例如,创建一个北纬 39.9 度、东经 116.4 度的 AVGeoPoint
对象(LeanCloud 北京办公室所在地):
AVGeoPoint point = new AVGeoPoint(39.9, 116.4);
假如,添加一条 Todo 的时候为该 Todo 添加一个地理位置信息,以表示创建时所在的位置:
todo.put("whereCreated", point);
有时我们需要知道更多关系的附加信息,比如在一个学生选课系统中,我们要了解学生打算选修的这门课的课时有多长,或者学生选修是通过手机选修还是通过网站操作的,此时我们可以使用传统的数据模型设计方法「中间表」。为此,我们创建一个独立的表 StudentCourseMap
来保存 Student
和 Course
的关系:
字段 | 类型 | 说明 |
---|---|---|
course | Pointer | Course 指针实例 |
student | Pointer | Student 指针实例 |
duration | Array | 所选课程的开始和结束时间点,如 ["2016-02-19","2016-04-21"]。 |
platform | String | 操作时使用的设备,如 iOS。 |
如此,实现选修功能的代码如下: |
AVObject studentTom = new AVObject("Student");// 学生 Tom
studentTom.put("name", "Tom");
AVObject courseLinearAlgebra = new AVObject("Course");
courseLinearAlgebra.put("name", "线性代数");
AVObject studentCourseMapTom = new AVObject("StudentCourseMap");// 选课表对象
// 设置关联
studentCourseMapTom.put("student", studentTom);
studentCourseMapTom.put("course", courseLinearAlgebra);
// 设置学习周期
studentCourseMapTom.put("duration", Arrays.asList("2016-02-19", "2016-04-21"));
// 获取操作平台
studentCourseMapTom.put("platform", "iOS");
// 保存选课表对象
studentCourseMapTom.saveInBackground().blockingSubscribe();
查询选修了某一课程的所有学生:
// 微积分课程
AVObject courseCalculus = AVObject.createWithoutData("Course", "562da3fdddb2084a8a576d49");
// 构建 StudentCourseMap 的查询
AVQuery<AVObject> query = new AVQuery<>("StudentCourseMap");
// 查询所有选择了线性代数的学生
query.whereEqualTo("course", courseCalculus);
// 执行查询
query.findInBackground().subscribe(new Observer<List<AVObject>>() {
public void onSubscribe(Disposable disposable) {
}
public void onNext(List<AVObject> list) {
// list 是所有 course 等于线性代数的选课对象
// 然后遍历过程中可以访问每一个选课对象的 student,course,duration,platform 等属性
for (AVObject studentCourseMap : list) {
AVObject student = studentCourseMap.getAVObject("student");
AVObject course = studentCourseMap.getAVObject("course");
ArrayList duration = (ArrayList) studentCourseMap.getList("duration");
String platform = studentCourseMap.getString("platform");
}
}
public void onError(Throwable throwable) {
fail();
}
public void onComplete() {
}
});
同样我们也可以很简单地查询某一个学生选修的所有课程,只需将上述代码变换查询条件即可:
AVQuery<AVObject> query = new AVQuery<>("StudentCourseMap");
AVObject studentTom = AVObject.createWithoutData("Student", "562da3fc00b0bf37b117c250");
query.whereEqualTo("student", studentTom);
AVObject 及其子类如果要想在 Android 的 Activity 之间传递的话,需要使用 AVParcelableObject
来进行封装,其使用方法如下:
- 将 AVObject(或其子类)放入 intent
AVObject student = new AVObject("Student");
student.put("age", 12);
student.put("name", "Mike");
AVParcelableObject parcelableObject = new AVParcelableObject(student);
Intent intent = new Intent(this, ObjectTransferTargetActivity.class);
intent.putExtra("attached", parcelableObject);
this.startActivity(intent);
- 在目标 activity 中获取 AVObject(或其子类)
AVParcelableObject parcelableObject = getIntent().getParcelableExtra("attached");
if (null != parcelableObject) {
attached = parcelableObject.object();
System.out.println(attached);
} else {
System.out.println("parcelableObject is null.");
}
很多开发者在使用 LeanCloud 初期都会产生疑惑:客户端的数据类型是如何被云端识别的? 因此,我们有必要重点介绍一下 LeanStorage 的数据协议。
先从一个简单的日期类型入手,比如在 Android 中,默认的日期类型是 Date,下面会详细讲解一个 Date 是如何被云端正确的按照日期格式存储的。 为一个普通的 AVObject 的设置一个 Date 的属性,然后调用保存的接口。Java SDK 在真正调用保存接口之前,会自动的调用一次序列化的方法,将 Date 类型的数据,转化为如下格式的数据:
{
"__type": "Date",
"iso": "2015-11-21T18:02:52.249Z"
}
然后发送给云端,云端会自动进行反序列化,这样自然就知道这个数据类型是日期,然后按照传过来的有效值进行存储。因此,开发者在进阶开发的阶段,最好是能掌握 LeanStorage 的数据协议。如下表介绍的就是一些默认的数据类型被序列化之后的格式:
类型 | 序列化之后的格式 |
---|---|
{{dateType}} |
{"__type": "Date","iso": "2015-11-21T18:02:52.249Z"} |
{{byteType}} |
{"__type": "Bytes","base64":"utf-8-encoded-string}" |
Pointer |
{"__type":"Pointer","className":"Todo","objectId":"55a39634e4b0ed48f0c1845c"} |
{{relationObjectName}} |
{"__type": "Relation","className": "Todo"} |