初步理解AV
AV.Object
AV.Object
是云服务对复杂对象的封装,每个AV.Object
包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个AV.Object
上有哪些 key,你只需要随意设置键值对就可以,云端会保存它
比如说,一个保存着单个 Todo 的
AV.Object
可能包含如下数据:
title: "给小林发邮件确认会议时间",
isComplete: false,
priority: 2,
tags: ["工作", "销售"]
数据类型
AV.Object
支持的数据类型包括String
、Number
、Boolean
、Object
、Array
、Date
等等。你可以通过嵌套的方式在Object
或Array
里面存储更加结构化的数据
AV.Object
还支持两种特殊的数据类型Pointer
和File
,可以分别用来存储指向其他AV.Object
的指针以及二进制数据
// 基本类型
const bool = true;
const number = 2023;
const string = `${number} 流行音乐榜单`;
const date = new Date();
const array = [string, number];
const object = {
number: number,
string: string,
};
// 构建对象
const TestObject = AV.Object.extend("TestObject");//新建一个class
const testObject = new TestObject();//创建一个实例对象
testObject.set("testNumber", number);//设置对应字段和值,以下同理
testObject.set("testString", string);
testObject.set("testDate", date);
testObject.set("testArray", array);
testObject.set("testObject", object);
testObject.save();//保存到云端
我们不推荐在
AV.Object
里面存储图片、文档等大型二进制数据。每个AV.Object
的大小不应超过 128 KB。如需存储大型文件,可创建AV.File
实例并将其关联到AV.Object
的某个属性上。具体文章下面会提到
注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。
云服务控制台 > 数据存储 > 结构化数据 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。
构建对象
方式一:
在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将云服务里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线,class类名的命名最好以大驼峰式命名例如:OfferJob
// 为 AV.Object 创建子类,类名为Todo
const Todo = AV.Object.extend("Todo");
// 为该类创建一个新实例
const todo = new Todo();
// 你还可以直接使用 AV.Object 的构造器
const todo = new AV.Object("Todo");
方式二:
如果你使用的是 ES6,还可以通过
extends
关键字来创建AV.Object
的子类,然而 SDK 无法自动识别你创建的子类。你需要通过这种方式手动注册一下:
class Todo extends AV.Object {
// 自定义属性和方法
}
// 注册子类
AV.Object.register(Todo);
保存对象
className.save()
// 声明 class
const TableHandler = AV.Object.extend("ZtlOfferJob");
// 构建对象
const new_item = new TableHandler();
// 为属性赋值
new_item .set("job_name", job_name);
new_item .set("job_sex", 2job_sex;
// 将对象保存到云端
new_item.save().then(
(it) => {
// 成功保存之后,执行其他逻辑
console.log(`保存成功。objectId:${it.id}`);
},
(error) => {
// 异常处理
}
);
如果class不存在,运行代码则自动创建
以下是对象的内置属性,将在对象保存时自动创建
内置属性 | 类型 | 描述 |
---|---|---|
objectId | String | 该对象唯一的 ID 标识。 |
ACL | AV.ACL | 该对象的权限控制,实际上是一个 JSON 对象,控制台做了展现优化。 |
createdAt | Date | 该对象被创建的时间。 |
updatedAt | Date | 该对象最后一次被修改的时间。 |
对于已经存在在云端的AV.Object,可以通过它的ObjectId来获取
//查询类名为OfferJob的数据表
const query = new AV.Query("OfferJob");
//通过一个ObjectId来查询该条数据
query.get("582570f38ac247004f39c24b").then((it) => {
// it就是 objectId 为 582570f38ac247004f39c24b 的 OfferJob实例
const title = it.get("title");
const priority = it.get("priority");
// 获取内置属性
const objectId = it.id;
const updatedAt = it.updatedAt;
const createdAt = it.createdAt;
});
如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回
undefined
。所以要注意属性值要设置,我一般是给它置空或者先定义
如果需要一次性获取返回对象的所有属性(比如进行数据绑定)而非显式地调用
get
,可以利用AV.Object
实例的toJSON
方法:
const query = new AV.Query("Todo");
query.get("582570f38ac247004f39c24b").then((todo) => {
console.log(todo.toJSON());
});
同步对象
当云端数据发生更改时,你可以调用
fetch
方法来刷新对象,使之与云端数据同步:
const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b");
todo.fetch().then((todo) => {
// todo 已刷新
});
刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,
fetch
操作会丢弃这些修改。为避免这种情况,你可以在fetch括号里面加keys,里面再加你想让他刷新的属性,这样只有指定的属性会被刷新(包括内置属性objectId
、createdAt
和updatedAt
),其他属性不受影响
const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b");
todo
.fetch({
keys: "priority, location",
})
.then((todo) => {
// 只有 priority 和 location 会被获取和刷新
});
更新对象
要更新一个对象,只需指定需要更新的属性名和属性值,然后调用
save
方法。例如:
const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b");
todo.set("content", "这周周会改到周三下午三点。");
todo.save();
如果想要查看哪些属性有尚未保存的修改,可以调用
dirtyKeys
方法
todo.dirtyKeys(); // ['content']
如果希望撤销尚未保存的修改,可以调用
revert
方法。直接调用revert()
将撤销所有尚未保存的修改。还可以额外传入keys
数组作为参数,指定需要撤销的属性
todo.revert(["content"]);
删除对象
从云端删除一个
Todo
对象,分为软删除和硬删除
软删除:className.set('enable',false),数据还在,只是让它不显示而已
data_handler: async (params) => {
const { object_id, AV } = params;
const query = new AV.Query('ZtlOfferJob');
const this_obj = await query.get(object_id);
if (!this_obj) {
throw new Error('没有检索到业务对象');
}
this_obj.set('enable', false);
return await this_obj.save();
}
硬删除:object.destroy(),直接删除数据
const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b");
todo.destroy();
如果只需删除对象的一个属性,可以用
unset
:
const todo = AV.Object.createWithoutData("Todo", "582570f38ac247004f39c24b");
// priority 属性会被删除
todo.unset("priority");
// 保存对象
todo.save();
批量操作
1.saveAll
2.destroyAll
3.fetchAll
示例:下面的代码将所有
Todo
的isComplete
设为true
:
const query = new AV.Query("Todo");
query.find().then((todos) => {
// 获取需要更新的 todo
todos.forEach((todo) => {
// 更新属性值
todo.set("isComplete", true);
});
// 批量更新
AV.Object.saveAll(todos);
});
序列化和反序列化
在实际的开发中,把
AV.Object
当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此AV.Object
也提供了序列化和反序列化的方法
序列化:
const todo = new AV.Object("Todo"); // 构建对象
todo.set("title", "马拉松报名"); // 设置名称
todo.set("priority", 2); // 设置优先级
todo.set("owner", AV.User.current()); // 这里就是一个 Pointer 类型,指向当前登录的用户
// 将 AV.Object 对象序列化成 JSON 对象
const json = todo.toFullJSON();
// 将 JSON 对象序列化为字符串
const serializedString = JSON.stringify(json);
AV.Object
还提供了另一个方法toJSON()
。它们的区别是toJSON()
得到的对象仅包含对象的 payload,一般用于展示,而toFullJSON()
得到的对象包含了元数据,一般用于传输。在使用时请注意区分。
反序列化:
// 将字符串反序列化为 JSON 对象
const json = JSON.parse(serializedString);
// 将 JSON 对象反序列化成 AV.Object 对象
const todo = AV.parseJSON(json);
查询
基础查询
执行一次基础查询通常包括这些步骤:
构建
AV.Query
向其添加查询条件
执行查询并获取包含满足条件的对象的数组
示例:
//查询所有lastname为“Smith”的对象组成的数组
const query = new AV.Query("Student");
query.equalTo("lastName", "Smith");
query.find().then((students) => {
// students 是包含满足条件的 Student 对象的数组
});
查询条件
可以给
AV.Object
添加不同的条件来改变获取到的结果
//查询所有firstName不为Jack的对象
query.notEqualTo("firstName", "Jack");
对于能够排序的属性(比如数字、字符串),可以进行比较查询
lessThan
lessThanOrEqualTo
greaterThan
greaterThanOrEqualTo
// 限制 age < 18
query.lessThan("age", 18);
// 限制 age <= 18
query.lessThanOrEqualTo("age", 18);
// 限制 age > 18
query.greaterThan("age", 18);
// 限制 age >= 18
query.greaterThanOrEqualTo("age", 18);
可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果,都满足条件了才能通过筛选
可以通过指定
limit
限制返回结果的数量(默认为100
):
// 最多获取 10 条结果
query.limit(10);
由于性能原因,
limit
最大只能设为1000
。即使将其设为大于1000
的数,云端也只会返回 1,000 条结果。
如果只需要一条结果,可以直接用
first
//查询Todo这个类,取所有priority为2的数据
const query = new AV.Query("Todo");
query.equalTo("priority", 2);
//结果只能取第一个
query.first().then((todo) => {
// todo 是第一个满足条件的 Todo 对象
});
可以通过设置
skip
来跳过一定数量的结果:
// 跳过前 20 条结果
query.skip(20);
把
skip
和limit
结合起来,就能实现翻页功能:
const query = new AV.Query("Todo");
query.equalTo("priority", 2);
query.limit(10);
query.skip(20);
需要注意的是,
skip
的值越高,查询所需的时间就越长。作为替代方案,可以通过设置createdAt
或updatedAt
的范围来实现更高效的翻页,因为它们都自带索引。
对于能够排序的属性,可以指定结果的排序规则:
// 按 createdAt 升序排列
query.ascending("createdAt");
// 按 createdAt 降序排列
query.descending("createdAt");
还可以为同一个查询添加多个排序规则:
query.addAscending("priority");
query.addDescending("createdAt");
下面的代码可用于查找包含或不包含某一属性的对象:
// 查找包含 'images' 的对象
query.exists("images");
// 查找不包含 'images' 的对象
query.doesNotExist("images");
字符串查询
可以用
startsWith
来查找某一属性值以特定字符串开头的对象
const query = new AV.Query("Todo");
// 查找以lunch字符串开头的title
query.startsWith("title", "lunch");
可以用
contains
来查找某一属性值包含特定字符串的对象
const query = new AV.Query("Todo");
//这个挺重要,常用于模糊查询,查找含有lunch字符串的title,我一般不查字符串
//查由字符串组成的变量,因为用户输入的内容是不固定的
query.contains("title", "lunch");
注意
startsWith
和contains
都是 区分大小写 的,所以上述查询会忽略Lunch
、LUNCH
等字符串。
如果想查找某一属性值不包含特定字符串的对象,可以使用
matches
进行基于正则表达式的查询(不推荐,影响性能):
const query = new AV.Query("Todo");
// 'title' 不包含 'ticket'(不区分大小写)
const regExp = new RegExp("^((?!ticket).)*$", "i");
query.matches("title", regExp);
下面的代码查找所有数组属性
tags
包含工作
的对象:
//过滤出所有tags的属性值为数组并且里面包含‘工作’的数据
query.equalTo("tags", "工作");
下面的代码查询数组属性长度为 3(正好包含 3 个标签)的对象:
query.sizeEqualTo("tags", 3);
下面的代码查找所有数组属性
tags
同时包含工作
、销售
和会议
的对象:
query.containsAll("tags", ["工作", "销售", "会议"]);
如需获取某一属性值包含一列值中任意一个值的对象,可以直接用
containedIn
而无需执行多次查询
const priorityOneOrTwo = new AV.Query("Todo");
//查找所有 priority 为 1 或 2 的 todo 对象
priorityOneOrTwo.containedIn("priority", [1, 2]);
反过来,还可以用
notContainedIn
来获取某一属性值不包含一列值中任何一个的对象。
const priorityOneOrTwo = new AV.Query("Todo");
priorityOneOrTwo.notContainedIn("priority", [1, 2]);
统计总数量
如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用
count
来代替find
const query = new AV.Query("Todo");
query.equalTo("isComplete", true);
//会输出满足条件的数据的条数
query.count().then((count) => {
console.log(`${count} 个 todo 已完成。`);
});
组合查询
组合查询就是把诸多查询条件用一定逻辑合并到一起(
OR
或AND
)再交给云端去查询。组合查询不支持在子查询中包含
GeoPoint
或其他非过滤性的限制(例如near
、withinGeoBox
、limit
、skip
、ascending
、descending
、include
)。
OR 查询
OR 操作表示多个查询条件符合其中任意一个即可
const priorityQuery = new AV.Query("Todo");
priorityQuery.greaterThanOrEqualTo("priority", 3);
const isCompleteQuery = new AV.Query("Todo");
isCompleteQuery.equalTo("isComplete", true);
//两个查询条件满足一个即可
const query = AV.Query.or(priorityQuery, isCompleteQuery);
AND 查询
使用 AND 查询的效果等同于往
AV.Query
添加多个条件
const startDateQuery = new AV.Query("Todo");
startDateQuery.greaterThanOrEqualTo(
"createdAt",
new Date("2023-05-20 00:00:00")
);
const endDateQuery = new AV.Query("Todo");
endDateQuery.lessThan("createdAt", new Date("2023-11-02 00:00:00"));
//两个条件都要满足才行
const query = AV.Query.and(startDateQuery, endDateQuery);
单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当查询条件中包含不止一个 OR 查询时,就必须使用 AND 查询
const createdAtQuery = new AV.Query("Todo");
createdAtQuery.greaterThanOrEqualTo("createdAt", new Date("2018-04-30"));
createdAtQuery.lessThan("createdAt", new Date("2018-05-01"));
const locationQuery = new AV.Query("Todo");
locationQuery.doesNotExist("location");
const priority2Query = new AV.Query("Todo");
priority2Query.equalTo("priority", 2);
const priority3Query = new AV.Query("Todo");
priority3Query.equalTo("priority", 3);
//当不止一个or的时候,必须用and,保证两边都为真才行
const priorityQuery = AV.Query.or(priority2Query, priority3Query);
const timeLocationQuery = AV.Query.or(locationQuery, createdAtQuery);
const query = AV.Query.and(priorityQuery, timeLocationQuery);
查询性能优化
- 不等于和不包含查询(无法使用索引)
- 通配符在前面的字符串查询(无法使用索引)
- 有条件的
count
(需要扫描所有数据)skip
跳过较多的行数(相当于需要先查出被跳过的那些行)- 无索引的排序(另外除非复合索引同时覆盖了查询和排序,否则只有其中一个能使用索引)
- 无索引的查询(另外除非复合索引同时覆盖了所有条件,否则未覆盖到的条件无法使用索引,如果未覆盖的条件区分度较低将会扫描较多的数据)
其它的太难了,之后再写吧 !