1 基础概念
1.1 NoSQLDatabase 是什么
NoSQLDatabase 是一个支持即时同步功能的 NoSQL 对象存储数据库; 支持存储, 查询, 同步 JSON 数据对象, 以及监听数据对象的变更
NoSQLDatabase 为开发者解决应用数据同步的问题,开发者只需要关注本地数据库读写, NoSQLDatabase 数据库会自动将数据实时同步至云端 / 多端
1.2 数据结构
NoSQLDatabase 支持存储 JSON 格式的数据对象, 它是键值对 (Key-Value) 的集合; 其中每一个键值对 (Key-Value) 都称之为节点; 一个节点包含 key 和 value
一个节点下的所有节点统称为子节点
一个节点在 JSON 对象中的位置可唯一的由根节点到该节点经过的所有节点的集合表示; 路径 (Path) 将这些节点连接起来并以 “/” 分隔
1.3 数据约束
没有任何子节点和属性的节点视为删除, 同时向父节点传播; 如 {a:{b:{c:{}}}} 中, c 没有任何子节点或属性, 应视为不存在 -> 则 b 不存在 -> a 不存在
JSON 对象树的深度不超过 8
key 不能以 ~ _ 开头, 不能包含 \ “ ‘, 单个 key 长度没有限制
path 用于定位 JSON 数据对象中的某一位置, 以根节点到该对象经过的所有 key 组成 path, 以 “/” 分隔, 总层级不超过 8 层, 长度(包括/)不超过 128 个字符
1.4 数据存储类型
存储支持 JSON 原生类型, 但不支持数组; 数组被转化为 string 存储
写入 JSON Object
{
"a": 1,
"b": [1, "2", false, null],
"undefined": "i am a string",
"d": null, // null 和 undefined 保存为 null
"e": true,
"f": "{\"a\":1}",
"g": {"a": 1},
"h": ""
}
读出
{
"a": 1,
"b": "[1,\"2\",false,null]", // 数组被转化成 string
"undefined": "i am a string",
"d": null,
"e": true,
"f": "{\"a\":1}", // JSON string 不变
"g": {"a": 1},
"h": "" // 空 string 保留
}
1.5 同步和事件
NoSQLDatabase 始终保持云端和本地同步, 任何数据副本 (db) 发生的变更都会被应用到其他所有副本
本地 db 可以接收云端数据对象的变更(包括冲突)事件, 开发者可以利用这些事件触发应用的业务功能
一个典型的数据流转过程如下
存储到本地
同步到云端(发送数据变更到其他订阅服务)
同步至相关的其他客户端
2 服务申请和接入
在【Namespace管理】上创建 namespace, 选择 namespace 需要使用的数据分隔和鉴权方式(或选择一个已存在的 namespace)
引入 SDK 依赖, 将安全保镖的安全文件置于工程 res/drawable 下
其他依赖, 使用 gradle 管理或自行下载, 如果已经引入则不再需要依赖
// 三方安全保镖 compile 'com.taobao.android:securityguardaar3:5.3.83@aar' compile 'com.taobao.android:securitybodyaar3:5.3.44@aar' // okhttp compile 'com.squareup.okhttp3:okhttp:3.9.0'
确保 AndroidManifest.xml 中有以下权限
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
在 AndroidManifest.xml 中注册 CMNS 依赖
<receiver android:name="com.aliyun.push.api.PushMessageReceiver"> <intent-filter> <action android:name="com.aliyun.cmns.intent.SEND_NOTIFICATION"/> <action android:name="com.aliyun.cmns.intent.CANCEL_NOTIFICATION"/> <action android:name="com.aliyun.cmns.intent.PASSTHROUGH"/> <action android:name="com.aliyun.cmns.intent.NOTIFICATION_CLICKED"/> <action android:name="com.aliyun.cmns.intent.NOTIFICATION_REMOVE"/> <action android:name="push.nosql.alisync.yunos.com"/> </intent-filter> </receiver>
3 初始化
3.1 数据空间
一个 namespace 按照一定的分隔方式将该 namespace 划分成多个数据空间;每个数据空间之间互相隔离且访问时需要鉴权; 一种数据空间划分的方式唯一对应一种鉴权方式;
注意
一个本地数据库只代表云端一个 namespace 下的一个数据空间本地和云端进行数据同步时只同步当前空间下的数据, 不会同步 namespace 下所有数据
目前支持的数据空间划分和鉴权方式有:
账号维度:使用云ID的账号ID进行空间划分, 使用账号token进行空间鉴权
设备维度:使用云ID的设备ID进行空间划分, 使用设备token进行空间鉴权
示意图:
初始化一个 NoSQLDatabase 实例, 需要指定数据空间划分方式和要访问的数据空间的 ID, 开发者通过该实例对数据库(空间)进行操作:
初始化数据库实例
NoSQLDatabase db = new NoSQLDatabase(
context,
accessKey, // 项目关联的网关应用AppKey
namespace, // 在控制台上创建的 namespace
shardId, // 标示本地不同数据空间的本地数据库, 建议传入数据空间的 id(即账号 id 或设备 id); 如果本地同时存在多个不同数据空间的数据库副本(如切换账号时), 必须重新创建数据库并使用不同的 shardId 加以区分, 否则本地不同数据空间的数据会混乱
"syncstore-nosql.aicc.aliyun.com" // 服务地址, https:// 开头
);
注意
应用中同一配置的数据库应当只被创建一次并保持单例, 严格避免重复创建相同配置的数据库实例不同配置的数据库实例可以共存数据库读写操作, 数据变化事件和数据库事件的回调返回到数据库被创建时的线程上(需要该线程有 Looper)
3.2 访问云端资源
NoSQLDatabase 与云端进行同步, 访问云端资源时需要当前数据空间对应的鉴权体系颁发的 token;
将当前鉴权体系的 token 传递给 NoSQLDatabase 实例, NoSQLDatabase 实例使用该 token(缓存的)进行鉴权与云端进行同步;
token 无效或超过有效期后 NoSQLDatabase 实例会发出事件(之后同步暂停), 此时需要用户重新向鉴权体系申请 token 并传递给 NoSQLDatabase 实例;
NoSQLDatabase db = new NoSQLDatabase(...);
// 可选, 传入安全图片名后缀指定使用特定的安全图片
db.setAuthCode("aicc");
// 监听数据库事件
db.setOnDatabaseEventListener(new OnDatabaseEventListener() {
@Override
public void onSyncStart() {
Log.d(TAG, "sync start");
}
@Override
public void onSyncFinish(int code) {
Log.d(TAG, "sync finish");
// code 为 0 代表成功
}
@Override
public void onSyncError(DatabaseException e) {
Log.d(TAG, "sync error: " + e.toString());
// 返回同步过程中遇到的错误, 用于开发调试
}
@Override
public void onTokenInvalid(int code) {
// token 无效后同步自动停止, code 反映具体哪个 token 无效
// 详见【使用参考】(https://help.aliyun.com/document_detail/73257.html)的错误码参考部分中11XXX 系列错误码
Log.d(TAG, "token invalid: " + Srting.valueOf(code));
// 需要重新获取并传入 auth 信息
db.setXXXToken(XXXToken);
// 重新继续同步
db.goOnline();
}
});
// IAS 和 DIS 需要不同的 auth 信息, 获取并传入相应的 auth 信息, 根据具体 namespace 申请的鉴权方式而定
db.setAccountToken(IASToken); // 或 db.setDeviceToken(DISToken); 或两者同时使用
db.goOnline(); // 调用后数据库进入同步状态
db.close(false); // 用完关闭
4 数据操作
介绍如何对数据库中的数据进行增删改查
对某个节点的数据操作通过一个对该节点的引用完成, 一个引用 (ref) 可从一个 db 的实例中获取;开发者需要通过引用操作数据库中相应位置的数据;
注意
ref 自身不包含任何数据, 只代表数据库中 JSON 结构某一位置的一个引用
获取数据库 a/b 位置的引用, 如数据库中内容为 { a:{b:{c:1}} } 则指向其中 b 的值, 即 {c:1}
NodeReference root = db.getReference("/");
NodeReference refDepth1 = db.getReference("a");
NodeReference refDepth2 = db.getReference("a/b");
4.1 数据写入
NoSQLDatabase 提供三种数据写入方式
方法 | desc |
| 写入并覆盖 ref 下所有数据, 相当于 delete + update |
| 向 ref 写入新数据, 已有数据与新写入的数据合并, 重复的数据被新值覆盖(所有写入的数据, 无论写入的值是否和之前不同, 都会被同步) |
| 向 ref 写入新数据, 功能与 update 相同, 只有新值中与旧值不同(或旧值不存在)的部分会被写入(只有值发生变化的部分数据会被同步), 性能低于 update |
| 在当前 ref 节点下创建一个新节点, key 为生成的随机唯一值, value 为传入的 value(类似于向列表中添加条目) |
| 同前 set, 返回 Future |
| 同前 update, 返回 Future |
| 同前 updateWithDiff, 返回 Future |
| 同前 push, 返回 Future |
在数据库在线模式下, 本地的写操作会立即被同步到云端
// ref 现在的数据是 { c:{d:1} }
JSONObject o1 = new JSONObject("{\"e\":1}");
ref.set(o1, new PutCallback() {
@Override
public void onSuccess() {
// success
// ref 下数据变为 { e:1 }
}
@Override
public void onError(DatabaseException e) {
// error
}
});
// ref 现在的数据是 { c:{d:1} }
JSONObject o2 = new JSONObject("{\"c\":{\"e\":2},\"f\":8}");
ref.update(o2, new PutCallback() {
@Override
public void onSuccess() {
// success
// ref 下数据变为 { c:{d:1, e:2}, f:8 }
}
@Override
public void onError(DatabaseException e) {
// error
}
});
// ref 现在的数据是 { c:{d:1} }
JSONObject o3 = new JSONObject("{\"a\":1}");
ref.push(o3, new PutCallback() {
@Override
public void onSuccess() {
// success
// ref 下数据变为 { c:{d:1}, "Qivd-1mviOEnfd=":{a:1} }
}
@Override
public void onError(DatabaseException e) {
// error
}
});
4.2 数据查询
方法 | desc |
| 查询当前 ref 下的数据(按照当前 ref 指定的数据视图进行查询, 默认无数据视图, 查询全部数据) |
| 查询当前 ref 下所有的 key, 返回 key 的列表 |
| 同前 get, 返回 Future |
| 同前 keys, 返回 Future |
ref.get(new GetCallback() {
@Override
public void onSuccess(JSONObject object) {
// success
}
@Override
public void onError(DatabaseException e) {
// error
}
});
开发者可以在 ref 上构建数据视图进行高级查询;详细的数据视图 API 请参考 API 文档;
4.3 数据删除
方法 | desc |
| 删除当前节点下指定节点 |
| 删除当前节点下全部数据包括本节点 |
| 同前 delete, 返回 Future |
| 同前 remove, 返回 Future |
// ref 现在的数据是 { c:{d:1} }
ref.delete("c/d", new PutCallback() {
@Override
public void onSuccess() {
// success
// 再次 ref.get() 返回 null (由于子节点全为空, 父节点也视为删除)
}
@Override
public void onError(DatabaseException e) {
// error
}
});
// ref 现在的数据是 { c:{d:1} }
ref.remove(new PutCallback() {
@Override
public void onSuccess() {
// success
// 再次 ref.get() 返回 null
}
@Override
public void onError(DatabaseException e) {
// error
}
});
4.4 数据视图
在 ref 上可通过指定一些条件组合对 ref 中的数据进行过滤, 得到 ref 中满足条件的一部分数据; 这些过滤条件的集合被称为数据视图
可指定的条件包括
指定返回结果的数量
指定返回结果按何种方式(按哪个字段)进行过滤
指定过滤条件(>, <, =)
数据库中有以下内容:
{
"notes": {
"Fumcjda-cja8dand": {
"title": "note1",
"content": "some content",
"time": 1497506811474
},
"adfIead-I9ewniwe": {
"title": "note2",
"content": "some content",
"time": 1497506851057
},
...
"Moviene-8dna0dJe": {
"title": "note3",
"content": "some content",
"time": 1497506854890
}
}
}
获取最近的 3 条 notes 的集合, 数据视图匹配当前节点下所有对象节点中的 time 字段, 没有 time 字段的节点或非对象节点不会参与匹配, 也不会出现在结果中
NodeReference ref = db.getReference("notes");
ref.orderByChild("time").limitToLast(3);
注意
排序条件只指定结果包含最近的 3 条数据, 但不保证结果中 3 个节点的排列顺序(因为 Object key 的无序性), 如在结果对象上进行 key 遍历, 无法保证第一条是 time 最小的一条
ref.get(new GetCallback() {
@Override
public void onSuccess(JSONObject object) {
// 返回最近三条结果
// {
// "Fumcjda-cja8dand": {
// "title": "note1",
// "content": "some content",
// "time": 1497506811474
// },
// "adfIead-I9ewniwe": {
// "title": "note2",
// "content": "some content",
// "time": 1497506851057
// },
// "Moviene-8dna0dJe": {
// "title": "note3",
// "content": "some content",
// "time": 1497506854890
// }
// }
}
@Override
public void onError(DatabaseException e) {
// error
}
});
若使用关系型数据库存储以上数据, 每个 note 为一行, 以上查询可类比为:
select * from notes order by time limit 3;
5 事件监听
事件监听是指通过事件触发的方式来获取云端数据变化事件通过监听云端事件,本地可获取并处理数据,以及触发应用业务逻辑
在数据库在线模式下, 云端的数据变化会被准实时地同步到本地, 并发出相应事件(如有监听)
方法 | desc |
| 监听当前 ref 上的云端数据变更事件 |
| 取消节点上的变更事件注册, 调用后不会再收到数据变更 |
监听某个 ref 上的云端数据变化, 本节点的内容发生了变更(包括子节点发生了增删改)后会收到回调;回调时云端的变更已经应用到本地有冲突时本地的数据维持不变, 冲突部分的数据会通过事件的形式通知出来, 开发者可以在事件中对冲突进行处理, 如没有监听, 则冲突事件/数据会被丢弃
注意
事件监听根据监听的范围不同会产生一定的消耗, 监听的范围越大, 产生的消耗也越大监听事件触发时会返回 ref 下所有数据, 请避免在会返回大量数据的 ref 上进行监听, 如只关心变化事件, 可使用 ref.orderByKey().limitToFirst(10) 等数据视图进行过滤以减少数据返回量请保证只对必要的范围进行监听, 并且在不需要时及时取消监听
ref.setOnDataChangeListener(new OnDataChangeListener() {
@Override
public void onChange(JSONObject newObject, JSONObject conflictObject) {
if (conflictObject != null) {
// 更新后有冲突
// newObject 本地更新后的数据(包括成功合并的没有冲突部分的云端数据)
// conflictObject 冲突部分的云端数据
// 开发者可选择应用冲突数据的云端版本
ref.update(conflictObject, null);
} else {
// 更新后没有冲突
}
});
// 不用时移除监听, 否则有内存泄露
ref.removeOnDataChangeListener();
6 Best Practice
严格避免重复创建相同配置的数据库实例, 一个数据库实例应当维持单例; 重复打开同一数据库会造成潜在的性能问题
进行数据结构设计时, 尽量使用固定的数据结构, 避免使用过于动态的数据结构, 如频繁大范围改变数据的结构, 层级和路径
进行数据结构设计时, 避免在一个对象中放置的值节点(k:v 中 v 是 primitive type 的值)的个数过多(保持在 10 以内以获得更优性能, 尽量不超过 20 个), 过多的值节点会影响同步性能; 需要将数据拆分到多个对象节点中, 每个对象节点中放置有限个数且较少的值节点
Example
Bad
{
"item_list": {
"item_1": "this is item 1",
"item_2": "this is item 2",
...
"item_10000": "this is item 10000"
}
}
Good
{
"item_list": {
"item_1": {
"id": "item_1",
"content": "this is item 1",
},
"item_2": {
"id": "item_2",
"content": "this is item 2",
},
...
"item_10000": {
"id": "item_10000",
"content": "this is item 10000",
}
}
}
避免创建多余的 ref, 使用尽量少的 ref 完成数据操作
在 ref 上进行监听需要消耗一定性能, 尽量缩小监听范围(注意无法监听值节点), 并在不需要的时候取消监听
ref 的监听事件触发时会返回 ref 下所有数据, 可能会造潜在的大量性能消耗; 如果不关心具体数据, 只希望获取变更事件, 可使用数据视图对 ref 返回的数据进行过滤
一次接口调用读写的数据量不应太大, 数据库操作耗时与读写数据量不是线性关系, 随着一次操作读写数据量增加, 操作耗时会更快地增加, 因此需要尽量避免一次性读写大量数据, 否则读写性能将会极大地下降; 如:避免一次性写入一个超大的对象, 或数据库中数据较多时直接 get 根节点下的全部数据(同时也需要注意读出的数据也会占用大量内存, 有耗尽内存的风险); 建议将一次写入的节点(所有对象节点 + 值节点)个数控制在 100 个以内, 一次读取的节点控制在 500 个以内, 以获得最佳性能; 对于读操作可以使用数据视图分页读取大量数据(需要相应的数据结构设计)