// 装备背包
var equipBag = new AVObject("GameEquipBag");
equipBag["scale"] = 20;
equipBag["name"] = "装备背包";
// 道具
var equip = new AVObject("GameEquip");
equip["name"] = "短剑";
equip["attackValue"] = 5;
// 设置该道具在装备背包中
equip["gameEquipBag"] = equipBag;
await equip.SaveAsync();
获取 Pointer 对象
假如已知一个道具背包,要找出背包中所有的道具,可以这样做:
var gameEquipBag = AVObject.CreateWithoutData("GameEquipBag", "5c41937c44d904006a538a2b");
var query = new AVQuery<AVObject>("GameEquip");
query = query.WhereEqualTo("gameEquipBag", gameEquipBag);
var equipments = (await query.FindAsync()).ToList();
equipments.ForEach((equip) =>
{
var name = equip.Get<string>("name");
Debug.Log(name);
});
[AVClassName("GameEquip")]
public class GameEquip : AVObject
{
[AVFieldName("name")]
public string Name
{
get { return GetProperty<string>("Name"); }
set { SetProperty<string>(value, "Name"); }
}
[AVFieldName("attackValue")]
public int AttackValue
{
get { return GetProperty<int>("AttackValue"); }
set { SetProperty<int>(value, "AttackValue"); }
}
}
[AVFieldName("name")] 中的 name 为存储后台中对应的「列名」;public string Name 中的 Name 为自定义属性名。
然后在系统启动时,注册子类:
AVObject.RegisterSubclass<GameEquip>();
使用子类
新增和修改
var knife = new GameEquip();
var className = knife.ClassName;
Debug.Log(className);
knife.Name = "小刀";
knife.AttackValue = 1;
await knife.SaveAsync();
查询
var query = new AVQuery<GameEquip>();
await query.FindAsync();
删除
await knife.DeleteAsync();
文件
文件存储也是数据存储的一种方式,图像、音频、视频、通用文件等等都是数据的载体。很多开发者也习惯把复杂对象序列化之后保存成文件,比如 JSON 或 XML 文件。文件存储在 LeanStorage 中被单独封装成一个 AVFile 来实现文件的上传、下载等操作。
上传文件
文件上传是指开发者调用接口将文件存储在云端,并且返回文件最终的 URL 的操作。
文件上传成功后会在系统表 _File 中生成一条记录,此后该记录无法被再次修改,包括 metaData 字段 中的数据。所以如需更新该文件的记录内容,只能重新上传文件,得到新的 id 和 URL。
var gameEquipBag = AVObject.CreateWithoutData("GameEquipBag", "5c41937c44d904006a538a2b");
var query = new AVQuery<AVObject>("GameEquip").WhereEqualTo("gameEquipBag", gameEquipBag).Include("gameEquipBag");
var equipments = (await query.FindAsync()).ToList();
equipments.ForEach((equip) =>
{
var equipName = equip.Get<string>("name");
Debug.Log(equipName);
});
var gameEquipBag = AVObject.CreateWithoutData("GameEquipBag", "56545c5b00b09f857a603632");
// 关键代码,用 include 告知服务端需要返回的关联属性对应的对象的详细信息,而不仅仅是 objectId。这里会返回 gameEquipBag 和 user 的详细信息。
var query = new AVQuery<AVObject>("GameEquip").WhereEqualTo("gameEquipBag", gameEquipBag).Include("gameEquipBag").Include("gameEquipBag.user");
var innerQuery = new AVQuery<AVObject>("GameEquipBag").WhereEqualTo("scale", 20);
var query = new AVQuery<AVObject>("GameEquip").WhereMatchesQuery("gameEquipBag", innerQuery);
var attackQuery = new AVQuery<AVObject>("GameEquip").WhereEqualTo("attackValue", 5);
var levelQuery = new AVQuery<AVObject>("GameEquip").WhereEqualTo("level", 1);
var query = attackQuery.Or(levelQuery);
查询结果数量和排序
获取第一条结果
在很多应用场景下,只要获取满足条件的一个结果即可,例如获取满足条件的第一条 GameEquip:
var query = new AVQuery<AVObject>("GameEquip");
var equipment = await query.FirstAsync();
var query = new AVQuery<AVObject>("GameEquip").Select("name");
// 如果有多个 select 字段时,可以这样写:
var query = new AVQuery<AVObject>("GameEquip").Select("name").Select("attackValue")
var query = new AVQuery<AVObject>("GameEquip").WhereEqualTo("attackValue", 5);
var count = await query.CountAsync();
排序
对于数字、字符串、日期类型的数据,可对其进行升序或降序排列。
// 根据 attackValue 升序排列
var query = new AVQuery<AVObject>("GameEquip").OrderBy("attackValue");
// 根据 attackValue 降序排列
var query = new AVQuery<AVObject>("GameEquip").OrderByDescending("attackValue");
一个查询可以附加多个排序条件,如按 attackValue 升序、createdAt 降序排列:
var query = new AVQuery<AVObject>("GameEquip").OrderBy("attackValue").OrderByDescending("createdAt");
数据存储开发指南 · Unity
数据存储(LeanStorage)是 LeanCloud 提供的核心功能之一。下面我们用一个简单的示例来说明它的基本用法。
下面这段代码在创建了一个
GameEquip类型的对象,并将它保存到云端:如果你熟悉关系型数据库的话,需要注意 LeanStorage 的不同点。在 LeanStorage 里不需要事先建立表结构(schema),并且可以随时增加新的属性。通常这被称为无模式(schema-free)。例如,为上面的
GameEquip类型新增一个表示等级的level属性,只需做如下变动:LeanStorage 在结构化数据存储方面,与 MySQL、Postgres、MongoDB 等数据库的区别在于:
SDK 安装
请阅读 Unity 安装指南。
对象
AVObject是 LeanStorage 对复杂对象的封装,每个 AVObject 包含若干属性值对,也称键值对(key-value)。属性的值是与 JSON 格式兼容的数据。通过 REST API 保存对象需要将对象的数据通过 JSON 来编码。这个数据是无模式化的(Schema Free),这意味着你不需要提前标注每个对象上有哪些 key,你只需要随意设置 key-value 对就可以,云端会保存它。数据类型
AVObject支持以下数据类型:其中
int、float、double类型的数据,服务端统一为Number类型来做处理,SDK 会在开发者获取相关值时自动做类型转换。保存对象
现在我们保存一个道具背包
GameEquipBag,背包中可以有多个道具GameEquip。我们并不需要提前去后台创建这个名为GameEquipBag的 Class 类,而仅需要执行如下代码,云端就会自动创建这个类:运行以上代码后,要想确认保存动作是否已经生效,可以到 LeanCloud 应用管理平台的数据管理页面来查看数据的存储情况。
除了 scale、name 之外,其他字段都是数据表的内置属性。
objectIdACLcreatedAtupdatedAt自定义的属性名,不能以双下划线
__开头,也不能与以下系统保留字段和内置属性重名(不区分大小写)。为提高代码的可读性和可维护性,建议使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如
CustomData。属性,采用小驼峰法,如imageUrl。获取对象
每个被成功保存在云端的对象会有一个唯一的 Id 标识
objectId,因此获取对象的最基本的方法就是根据objectId来查询:获取 objectId
每一次对象存储成功之后,云端都会返回 objectId,它是一个全局唯一的属性。
访问对象的属性
objectId、createdAt、updatedAt 三个特殊属性可以直接获取,其他的自定义属性可以使用相应数据类型的
Get<T>泛型方法:如果访问的属性不存在,SDK 会抛出异常,如果您不确认某个属性是否有值,可以这样获取属性:
默认属性
默认属性是所有对象都会拥有的属性,它包括
objectId、createdAt、updatedAt。createdAt:对象第一次保存到云端的时间戳。该时间一旦被云端创建,在之后的操作中就不会被修改。 updatedAt:对象最后一次被修改(或最近一次被更新)的时间。
注:应用控制台对
createdAt和updatedAt做了在展示优化,它们会依据用户操作系统时区而显示为本地时间;客户端 SDK 获取到这些时间后也会将其转换为本地时间;而通过 REST API 获取到的则是原始的 UTC 时间,开发者可能需要根据情况做相应的时区转换。更新对象
LeanStorage 上的更新对象都是针对单个对象,云端会根据有没有 objectId 来决定是新增还是更新一个对象。
假如 objectId 已知,则可以通过如下接口从本地构建一个 AVObject 来更新这个对象:
更新操作是覆盖式的,云端会根据最后一次提交到服务器的有效请求来更新数据。更新是字段级别的操作,未更新的字段不会产生变动,这一点请不用担心。
更新数组
使用以下方法可以方便地维护数组类型的数据:
将指定对象附加到数组末尾:
AddToListAddRangeToList如果数组中不包含指定对象,将该对象加入数组,对象的插入位置是随机的:
AddUniqueToListAddRangeUniqueToList从数组字段中删除指定的对象:
RemoveAllFromList例如
GameEquip有一个字段repairTime是数组类型,记录着装备的维修时间,可以这样存储数据。删除对象
要删除某个对象,使用
AVObject的DeleteAsync方法。删除某一个属性
如果仅仅想删除对象的某一个属性,使用
Remove方法。批量操作
为了减少网络交互的次数太多带来的时间浪费,你可以在一个请求中对多个对象进行创建、更新、删除、获取。接口都在
AVObject这个类下面:不同类型的批量操作所引发不同数量的 API 调用,具体请参考 API 调用次数的计算。
关联数据
Pointer
一个道具背包中会有许多种道具,这是一种典型的一对多关系。下面我们使用 Pointers 来存储这种一对多的关系。
获取 Pointer 对象
假如已知一个道具背包,要找出背包中所有的道具,可以这样做:
更多内容可参考 关联数据查询。
子类化
LeanCloud 希望设计成能让人尽快上手并使用。你可以通过
avobject.get<T>方法访问所有的数据。但是在很多现有成熟的代码中,子类化能带来更多优点,诸如简洁、可扩展性以及 IDE 提供的代码自动完成的支持等等。子类化不是必须的,你可以将下列代码转化:可以写成:
子类化 AVObject
要实现子类化,需要下面几个步骤:
class添加[AVClassName("xxx")]。它的值必须是一个字符串,也就是你过去传入 AVObject 构造函数的类名。这样一来,后续就不需要再在代码中出现这个字符串类名;get及set方法;AVObject.RegisterSubclass<yourClassName>();。下面是实现
GameEquip子类化的例子:[AVFieldName("name")]中的name为存储后台中对应的「列名」;public string Name中的Name为自定义属性名。然后在系统启动时,注册子类:
使用子类
新增和修改
查询
删除
文件
文件存储也是数据存储的一种方式,图像、音频、视频、通用文件等等都是数据的载体。很多开发者也习惯把复杂对象序列化之后保存成文件,比如 JSON 或 XML 文件。文件存储在 LeanStorage 中被单独封装成一个
AVFile来实现文件的上传、下载等操作。上传文件
文件上传是指开发者调用接口将文件存储在云端,并且返回文件最终的 URL 的操作。
文件上传成功后会在系统表 _File 中生成一条记录,此后该记录无法被再次修改,包括 metaData 字段 中的数据。所以如需更新该文件的记录内容,只能重新上传文件,得到新的 id 和 URL。
如果
_File表打开了 删除权限,该记录才可以被删除。从数据流构建文件
AVFile支持图片、视频、音乐等常见的文件类型,以及其他任何二进制数据,在构建的时候,传入对应的数据流即可:AVFile构造函数的第一个参数指定文件名称,第二个构造函数接收一个byte数组,也就是将要上传文件的二进制,第三个参数是自定义元数据的字典,比如你可以把文件的作者的名字当做元数据存入这个字典,LeanCloud 的服务端会把它保留起来,这样在以后获取的时候,这种类似的自定义元数据都会被获取。
上例将文件命名为
resume.txt,这里需要注意两点:从本地路径构建文件
在 Unity 中,如果很清楚地知道某一个文件所存在的路径,比如在游戏中上传一张游戏截图,可以通过SDK直接获取指定的文件,上传到LeanCloud 中。
从网络路径构建文件
从一个已知的 URL 构建文件也是很多应用的需求。例如,从网页上拷贝了一个图像的链接,代码如下:
从 本地路径构建文件 会产生实际上传的流量,并且文件最后是存在云端,而本处从网络路径构建的文件实体并不存储在云端,只是会把文件的物理地址作为一个字符串保存在云端。
上传进度监听
一般来说,上传文件都会有一个上传进度条显示用以提高用户体验:
文件元数据
AVFile 的 metaData 属性,可以用来保存和获取该文件对象的元数据信息。metaData 一旦保存到云端就无法再次修改。
关联文件
使用
Pointer字段类型将AVFile关联到AVObject对象的一个字段上:查询的时候需要额外的
include一下:文件下载
因为多平台适配会造成困扰,因此 Unity SDK 不提供直接下载文件的方式。我们推荐拿到
avFile.Url这个属性后,用 Unity 自带的 WWW 类或者 UnityWebRequest 类实现文件下载。文件删除
删除文件就意味着,执行之后在数据库中立刻删除记录,并且原始文件也会从存储仓库中删除(所有涉及到物理级别删除的操作请谨慎使用)
启用 HTTPS 域名
如果希望使用 HTTPS 域名来访问文件,需要进入 控制台 > 数据存储 > 文件 > 设置,为需要启用 HTTPS 的域名勾选 启用 SSL。HTTPS 文件流量无免费的使用额度,收费标准将在该选项开启时显示。
「启用 https 域名」会影响到 API 返回的文件地址是 HTTPS 还是 HTTP 类型的 URL。需要注意的是,即使没有启用这一选项,终端仍然可以选择使用 HTTPS URL 来访问文件,但由此会产生 HTTPS 流量扣费。
在启用文件 HTTPS 域名之后,之前已保存在
_File表中的文件的 URL 会自动被转换为以 HTTPS 开头。如果取消 HTTPS 域名,已经改为 HTTPS 域名的文件不会变回到 HTTP。LeanCloud 即时通讯组件也使用
AVFile来保存消息的图片、音频等文件,并且把文件的地址写入到了消息内容中。当文件 HTTPS 域名被开启后,之前历史消息中的文件地址不会像_File表那样被自动转换,而依然保持 HTTP。设置自定义文件域名
LeanCloud 提供公用的二级域名来让开发者及其用户能够便捷地访问到存储在云端的文件。但由于受网络法规的管控与限制,我们无法 100% 保证该公用域名随时可用。因此,强烈建议开发者使用自定义域名来访问自己的文件,以避免公用域名不可用之时应用的体验会受到影响。请前往 数据存储 > 文件 > 设置 设置自定义域名。
查询
LeanCloud Unity SDK 提供了许多查询方法来简化操作。
首先需要明确最核心的一点,在我们的 SDK 中,
AVQuery对象的所有以Where开头的方法,以及限定查询范围类的方法(Skip、Limit、ThenBy、Include等)都会返回一个全新的对象,它并不是在原始的AVQuery对象上修改内部属性。比如:以上代码是用户经常会犯的错误案例,请勿拷贝到项目中使用!
上面那段代码会返回
GameEquip中所有的数据,而不是所设想的只有name等于短剑的数据。正确的写法是:以此类推,
AVQuery<T>的所有复合查询条件都应该使用.这个符号来创建链式表达式。例如,查找所有name等于短剑,且attackValue大于5的GameEquip:基本查询
最基础的用法是根据 objectId 来查询对象:
比较查询
WhereEqualToWhereNotEqualToWhereGreaterThanWhereGreaterThanOrEqualToWhereLessThanWhereLessThanOrEqualTo利用上述表格介绍的逻辑操作的接口,我们可以很快地构建条件查询。
例如,查询攻击力大于 4 的所有装备 :
查询攻击力大于等于 4 的 GameEquip:
查询备选范围内满足条件的值
当我们要查询的属性值,存在一个可选集合的时候,可以使用
containedIn来进行查询。例如我们要查出来名字为
短剑或长刀的所有装备:如果想查询排除
短剑或长刀的所有装备,可以使用WhereNotContainedIn方法来实现。多个查询条件
当多个查询条件并存时,它们之间默认为 AND 关系,即查询只返回满足了全部条件的结果。建立 OR 关系则需要使用组合查询。
在简单查询中,如果对一个对象的同一属性设置多个条件,那么先前的条件会被覆盖,查询只返回满足最后一个条件的结果。例如要找出攻击力为 5 和 6 的所有装备,错误写法是:
正确作法是使用 组合查询 · OR 关系 来构建这种条件。
字符串查询
前缀查询类似于 SQL 的 LIKE 'keyword%' 条件。因为支持索引,所以该操作对于大数据集也很高效。
包含查询类似于 SQL 的 LIKE '%keyword%' 条件,比如查询标题包含「剑」的
GameEquip:数组查询
如果一个 Key 对应的值是一个数组,你可以查询 key 的数组包含了数字 2 的所有对象:
同样,你可以查询出 Key 的数组同时包含了 2,3 和 4 的所有对象:
查询「全不包含」的情况:
查询「部分包含(指查询的数组属性中,包含有目标集合的部分元素)」的情况:
空值查询
假设用户可以有选择地为背包自己命名,要想找出那些已经有自定义命名的背包:
找出所有没有命名的背包:
关系查询
Pointer 查询
基于在 Pointer 小节介绍的存储方式:一个道具背包
GameEquipBag中会有许多种道具GameEquip,这是一种典型的一对多关系。现在已知一个GameEquipBag,想查询所有的GameEquip对象,可以使用如下代码:关联属性查询
正如在 Pointer 中保存
GameEquip的GameEquipBag属性一样,假如查询到了一些GameEquip对象,想要一并查询出每一个GameEquip对应的GameEquipBag对象的时候,可以加上 include 关键字查询条件。同理,假如GameEquipBag表里还有 pointer 型字段user时,再加上一个递进的查询条件,形如 include(b.c),即可一并查询出每一条GameEquipBag对应的 AVUser 对象。代码如下:此外需要格外注意的是,假设对象有一个 Array 类型的字段
arrayKey内部是 Pointer 类型:可以用 include 方法获取数组中的 pointer 数据,例如:
但是 Array 类型的 include 操作只支持到第一层,不支持 include(b.c) 这种递进关联查询。
内嵌查询
道具表
GameEquip中有一个所属背包字段gameEquipBag指向GameEquipBag表。查询容量为 20 的背包GameEquipBag中的所有道具GameEquip(注意查询针对的是GameEquip),使用内嵌查询接口就可以通过一次查询来达到目的与普通查询一样,内嵌查询默认也最多返回 100 条记录,想修改这一默认请参考 限定结果返回数量。
如果所有返回的记录没有匹配到外层的查询条件,那么整个查询也查不到结果。
LeanCloud 云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,limit 默认为 100,最大 1000),然后将子查询的结果填入主查询的对应位置,再执行主查询。
如果子查询匹配到了 100 条以上的记录(性别等区分度低的字段重复值往往较多),且主查询有其他查询条件(region = 'cn'),那么可能会出现没有结果或结果不全的情况,其本质上是子查询查出的 100 条记录没有满足主查询的其他条件。
我们建议采用以下方案进行改进:
组合查询
组合查询就是把诸多查询条件合并成一个查询,再交给 SDK 去云端查询。方式有两种:OR 和 AND。
OR 查询
OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询攻击力是 5 ,或等级为 1 的道具:
查询结果数量和排序
获取第一条结果
在很多应用场景下,只要获取满足条件的一个结果即可,例如获取满足条件的第一条
GameEquip:限定返回数量
为了防止查询出来的结果过大,云端默认针对查询结果有一个数量限制,即 limit,它的默认值是 100。比如一个查询会得到 10000 个对象,那么一次查询只会返回符合条件的 100 个结果。limit 允许取值范围是 1 ~ 1000。例如设置返回 10 条结果:
跳过数量
设置 skip 这个参数可以告知云端本次查询要跳过多少个结果。将 skip 与 limit 搭配使用可以实现翻页效果。例如,在翻页中每页显示数量为 10,要获取第 3 页的对象:
上述方法的执行效率比较低,因此不建议广泛使用。建议选用 createdAt 或者 updatedAt 这类的时间戳进行 分段查询。
返回指定属性/字段
通常列表展现的时候并不是需要展现某一个对象的所有属性,这样既满足需求又节省流量,还可以提高一部分的性能。例如只返回道具
GameEquip的名字:所指定的属性或字段也支持 Pointer 类型。例如,获取
GameEquip这个对象的所属背包(gameEquipBag属性,Pointer 类型),仅展示这个背包的容量:统计总数量
通常用户在执行完搜索后,结果页面总会显示出诸如「搜索到符合条件的结果有 1020 条」这样的信息。例如,查询一下攻击力为 5 的道具有多少个:
排序
对于数字、字符串、日期类型的数据,可对其进行升序或降序排列。
一个查询可以附加多个排序条件,如按 attackValue 升序、createdAt 降序排列:
查询性能优化
影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。
LiveQuery
LiveQuery 衍生于 AVQuery,它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。
设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用
AVQuery并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的AVQuery。订阅成功后,一旦有符合AVQuery的AVObject发生变化,云端就会主动、实时地将信息通知到客户端。LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。
启用 LiveQuery
请参考 SDK 安装文档。
效果示例
下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果:
构建订阅
LiveQuery 的核心用法是定义一个查询,然后订阅符合这个查询条件的对象的变化。例如有一个
Todo表记录着所有待办任务,我们订阅这个表中的数据变化。create
create指的是所有新增的数据事件,例如订阅所有新增的Todo。例如在其他客户端新增一个Todo后,自动在当前客户端做出更新:在上方代码中,首先构建了一个 Query,然后订阅这个 Query 并接收事件。当表中新增的数据符合这个 Query 时,
creates 事件会被触发。例如其他客户端运行下面的代码会触发create事件:update
update 事件的触发时机为:表中有符合条件的数据被更新,更新后的数据依然符合匹配的 Query 条件。比如我们订阅
Todo表中处在doing状态的数据的update事件:在上方代码中,首先构建了一个 Query,然后订阅这个 Query 并接收事件。其他客户端更新一条处在
doing状态的todo数据的title时,会触发这个update事件:注意,在更新数据的代码中,我们只修改了
title字段,没有修改state字段。如果state字段的值被修改了,update事件不会被触发,会触发leave事件。enter
enter 事件的触发时机为:表中已有的一条数据更新之前不符合条件,在更新数据后,符合当前订阅指定的 Query 条件。例如我们监听
Todo表中state状态从doing变为done的事件:在上方代码中,首先构建了一个 Query,然后订阅这个 Query 并接收事件。其他客户端将一条
todo的state从doing修改为done时,会触发当前enter事件:这条 todo 的数据更新后,符合了订阅时的查询条件,enter 事件被触发。
请注意明确区分
create和enter的不同行为:create:对象从无到创建,并且符合查询条件。enter:对象原来就存在,但是修改之前不符合查询条件,修改之后符合了查询条件。leave
与
enter相反,当对象从符合条件变为不符合条件的时候,之前的查询条件订阅会触发leave事件。例如,我们现在订阅处在doing状态的leave事件:在上方代码中,首先构建了一个 Query,然后订阅这个 Query 并接收事件。其他客户端将一条
todo的state从doing修改为done时,会触发当前leave事件:这条
todo的数据更新后,不再匹配原有的state为doing的查询条件,此时原有的查询条件会触发leave事件。delete
delete 事件的触发时机为:符合当前订阅条件的数据被删除。例如,我们现在订阅
todo这个表的delete事件:在上方代码中,首先构建了一个 Query,然后订阅这个 Query 并接收事件。其他客户端删掉一条
todo时,会触发当前的delete事件:取消订阅
取消订阅指针对某一个或者某一些 LiveQuery,不再希望云端将数据变更推送到客户端。
连接被断开
可能存在 LiveQuery 连接被断开的情况:
如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。
而另外一种极端情况——当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。
注意事项
为了避免内存泄漏,SDK 不会缓存 AVLiveQuery 对象,所以请在开发时注意 AVLiveQuery 对象的生命周期。
LiveQuery 使用误区
因为 LiveQuery 的实时性,很多用户会试着用 LiveQuery 来实现一个简单的聊天功能。我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,并且后期维护的难度非常大(聊天记录,对话维护之类的代码会很混乱)。如果您需要实现聊天功能,请使用即时通讯服务。
用户
用户系统几乎是每款应用都要加入的功能。除了基本的注册、登录和密码重置,移动端开发还会使用手机号一键登录、短信验证码登录等功能。LeanStorage 提供了一系列接口来帮助开发者快速实现各种场景下的需求。
AVUser是用来描述一个用户的特殊对象,与之相关的数据都保存在_User数据表中。用户的属性
默认属性
用户名、密码、邮箱是默认提供的三个属性,访问方式如下:
请注意代码中,密码是仅仅是在注册的时候可以设置的属性(这部分代码可参照用户名和密码注册),它在注册完成之后并不会保存在本地(SDK 不会以明文保存密码这种敏感数据),所以在登录之后,再访问密码这个字段是为空的。
自定义属性
用户对象和普通对象一样也支持添加自定义属性。例如,为当前用户添加年龄属性 age:
修改属性
很多开发者会有这样的疑问:「为什么我不能修改任意一个用户的属性?」
例如,先为当前用户增加一个 age 属性,登录后再更改它的值:
AVUser的自定义属性在使用上与AVObject没有本质区别。注册
手机号注册或登录
很多网站为了简化注册及登录流程,都使用了「手机号 + 验证码」的方式来登录,这种方式这种方式会为没有注册过的用户自动创建账号。
首先调用发送验证码的接口:
然后在 UI 上给与用户输入验证码的输入框,用户点击登录的时候调用如下接口:
用户名和密码注册
采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码,并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调在客户端,应用切勿再次对密码加密,这会导致重置密码等功能失效。
例如,注册一个用户的示例代码如下(用户名
Tom密码cat!@#123):如果注册不成功,请检查一下返回的错误对象。最有可能的情况是用户名已经被另一个用户注册,错误代码 202,即
_User表中的username字段已存在相同的值,此时需要提示用户尝试不同的用户名来注册。同样,邮件email和手机号码mobilePhoneNumber字段也要求在各自的列中不能有重复值出现,否则会出现 203、214 错误。开发者也可以要求用户使用 Email 做为用户名注册,即在用户提交信息后将
_User表中的username和email字段都设为相同的值,这样做的好处是用户在忘记密码的情况下可以直接使用「邮箱重置密码」功能,无需再额外绑定电子邮件。设置手机号码
如果一开始没有选择用手机号码注册,之后要求用户绑定并验证手机号,可以调用「延迟验证」的接口。首先更新用户的手机号:
如果在 「控制台 > 内建账户 > 设置」 中勾选了 用户注册或更新手机号时,向注册手机号码发送验证短信,更新用户手机号成功后会自动发送一条验证短信到用户的手机中,此时可以调用以下接口验证手机号。
如果用户没有收到验证短信,可以调用以下接口重发短信:
验证邮箱
如果在 「控制台 > 内建账户 > 设置」 中勾选了 用户注册时,发送验证邮件,那么当一个
AVUser在注册时设置了邮箱,云端就会向该邮箱自动发送一封包含了激活链接的验证邮件,用户打开该邮件并点击激活链接后便视为通过了验证。有些用户可能在注册之后并没有点击激活链接,而在未来某一个时间又有验证邮箱的需求,这时需要调用如下接口让云端重新发送验证邮件:
当用户通过更新用户属性的方式更新新邮箱并成功 save 后,云端会自动向新邮箱发一封验证邮件,此时开发者不需要再单独调用
requestEmailVerify接口来发送验证邮件。登录
我们提供了多种登录方式,以满足不同场景的应用。
用户名和密码登录
邮箱和密码登录
手机号和密码登录
如果该用户的
mobilePhoneNumber字段设置了手机号,可以使用「手机号 + 密码」的方式登录:以上的手机号码即使没有经过验证,只要密码正确也可以成功登录。如果希望阻止未验证的手机号码用于登录,则需要在 「控制台 > 内建账户 > 设置」 中勾选 未验证手机号码的用户,禁止登录。这种方式也提高了用户账号的合法性与安全性。
手机号和验证码登录
详见手机号注册或登录
测试用的手机号和固定验证码
对于使用「手机号 + 验证码」登录的应用来说,在上架前提交至 Apple Store 进行审核的过程中,可能会面临 Apple 人员因没有有效的手机号码而无法登录来进行评估审核,或者开发者也无法提供固定手机号和验证码的尴尬情况。
另外,开发者在开发测试过程中也会面临在短时间内需要多次登录或注销的操作,由于验证码有时间间隔与总次数限制,这样就会带来种种不便。
为解决这些问题,我们允许为每个应用设置一个用于测试目的的手机号码,LeanCloud 平台会为它生成一个固定的验证码,每次使用这一对号码组合进行验证都会得到成功的结果。请进入 应用控制台 > 短信 > 设置 > 测试手机号 来设置测试手机号。
注意:测试手机号同样无法突破运营商的短信限制,所以测试手机号的使用方式是:不发送短信,直接使用固定的手机号和验证码测试注册或登录。
游客登录
有时你不希望强制用户在一开始就进行注册,例如先使用游客身份玩游戏,可以用以下接口匿名创建一个用户并登录:
但以这种方式登录的用户,一旦登出就无法再次以该用户身份登录,与该用户关联的数据也将无法访问,此时可以通过以下方式转化为普通用户。
以设置用户名、密码后注册为例:
判断一个用户是否是匿名用户:
当前用户
常见的应用不会每次都要求用户都登录,这是因为它将用户数据缓存在了客户端。 同样,只要是调用了登录相关的接口,LeanCloud SDK 都会自动缓存登录用户的数据。 例如,判断当前用户是否为空,为空就跳转到登录页面让用户登录,如果不为空就跳转到首页:
SessionToken
所有登录接口调用成功之后,云端会返回一个 SessionToken 给客户端,客户端在发送 HTTP 请求的时候,Unity SDK 会在 HTTP 请求的 Header 里面自动添加上当前用户的 SessionToken 作为这次请求发起者
AVUser的身份认证信息。如果在 「控制台 > 内建账户 > 设置」 中勾选了 密码修改后,强制客户端重新登录,那么当用户密码再次被修改后,已登录的用户对象就会失效,开发者需要使用更改后的密码重新调用登录接口,使 SessionToken 得到更新,否则后续操作会遇到 403 (Forbidden) 的错误。
验证 SessionToken 是否在有效期内
使用 SessionToken 登录
在没有用户名密码的情况下,客户端可以使用 SessionToken 来登录。常见的使用场景有:
登录后可以调用
user.SessionToken方法得到当前登录用户的 sessionToken。使用 sessionToken 登录:
请避免在外部浏览器使用 URL 来传递 SessionToken,以防范信息泄露风险。
账户锁定
输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码
{"code":1,"error":"登录失败次数超过限制,请稍候再试,或者通过忘记密码重设密码。"},开发者可在客户端进行必要提示。锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。
重置密码
邮箱重置密码
如果用户忘记了密码,可以调用以下接口发送重置密码的邮件:
密码重置流程如下:
关于自定义邮件模板和验证链接,请参考自定义邮件验证和重设密码页面。
手机号码重置密码
用户需要先绑定手机号码才能使用这个功能。首先获取短信验证码:
然后使用短信验证码来重置密码:
登出
用户登出系统时,SDK 会自动清理当前用户的缓存。
用户的查询
为了安全起见,新创建的应用的
_User表默认关闭了 find 权限,这样每位用户登录后只能查询到自己在_User表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的 find 查询权限。设置数据表权限的方法,请参考 数据与安全 Class 级别的权限。我们推荐开发者在 云引擎 中封装用户查询,只查询特定条件的用户,避免开放
_User表的全部查询权限。查询用户代码如下:
第三方账户登录
第三方登录是应用常见的功能。它直接使用第三方平台(如微信、QQ)已有的账户信息来完成新用户注册,这样不但简化了用户注册流程的操作,还提升了用户体验。
开发此功能的主要步骤有:
常规开发流程介绍
配置平台账号
在 LeanCloud 应用控制台 > 内建账户 > 设置 > 第三方集成,配置相应平台的 应用 ID 和 应用 Secret Key 。点击保存,自动生成 回调 URL 和 登录 URL。
以微博开放平台举例,它需要单独配置 回调 URL。 在微博开放平台的 应用信息 > 高级信息 > OAuth2.0 授权设置 里的「授权回调页」中绑定生成的 回调 URL。测试阶段,在微博开放平台的 应用信息 > 测试信息 添加微博账号,在腾讯开放平台的 QQ 登录 > 应用调试者 里添加 QQ 账号即可。在应用通过审核后,可以获取公开的第三方登录能力。
配置平台账号的目的在于创建 AVUser 时,LeanCloud 云端会使用相关信息去校验 authData 的合法性,确保 AVUser 实际对应着一个合法真实的用户,确保平台安全性。如果想关闭自动校验 authData 的功能,需要在应用控制台「内建账户 > 设置」中取消勾选「第三方登录时,验证用户 AccessToken 合法性」。
获取 authData 并创建 AVUser
LeanCloud 暂不提供 获取第三方 authData 的 SDK。开发者需要调用微信、QQ 等官方的 SDK,并根据其文档进行获取,也可以使用其他服务商提供的社交登录组件。
开发者在获取了第三方的完整 authData 后,就可以使用我们提供的 AVUser 类的
LoginWithAuthDataAsync()或AssociateAuthDataAsync()两个接口,传入 authData,进行用户数据的绑定了。在操作成功之后,这部分第三方账户数据会存入_User表的authData字段里。LeanCloud 后端要求 authData 至少含有
openid 或 uid、access_token和expires_in三个字段。微信和 QQ 使用openid,其他平台使用uid。如果是新用户,则生成一个新的 AVUser 并登录。示例代码如下:
成功后,在你的控制台的 _User 表里会生成一条新的 AVUser,它的数据格式如下:
如果是已有用户,则返回对应 authData 的 AVUser 实例并登录。
用户已经有了 AVUser 并登录成功后,可以用这个接口绑定新的第三方账号信息。绑定成功后,新的第三方账户信息会被添加到 AVUser 的 authData 字段里。示例代码如下:
_User表的对应 AVUser 数据的 authData 字段会新增一个 facebook 的数据,如下:以上就是一个第三方登录开发的基本流程。
扩展需求
接入 UnionId 体系
方平台的账户体系变得日渐复杂,它们的 authData 出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。
当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开发平台下的移动应用和小程序之间互通。
微信官方为了解决这个问题,引入 UnionID 的体系,即:同一微信号,对同一个微信开放平台账号下的不同应用,不管是移动 App、网站应用还是小程序,UnionID 都是相同的。也就是说,UnionID 可以作为用户的唯一标识。
其他平台,如 QQ 的 UnionID 体系,和微信的设计保持一致。
LeanCloud 支持 UnionID 体系。你只需要给
LogInWithAuthDataAndUnionIdAsync和AssociateAuthDataAndUnionIdAsync接口传入更多的参数,即可完成新 UnionID 体系的集成。要使用到的关键参数列表:
platformweixinapp、wxminiprogram、qqapp1等。unionIdPlatformweixin、weibo和qq。unionIdasMainAccount、unionIdPlatform一起使用。asMainAccountunionId、unionIdPlatform一起使用。接入新 UnionID 系统时,每次传入的 authData 必须包含成对的平台
uid 或 openid和平台unionid。示例代码如下:然后让我们来看看生成的 authData 的数据格式:
当你想加入该 UnionID 下的一个新平台,比如 miniprogram1 时,再次登录后生成的数据为:
可以看到,最终该 authData 实际包含了来自
weixin这个unionId体系内的两个不同平台,weixinapp1代表来自移动应用,miniprogram1来自小程序。_weixin_unionid这个字段的值就是用户在weixin这个unionId平台的唯一标识 UnionID 值。当一个用户以来自
weixinapp1的 OpenIDoTY851axxxgujsEl0f36Huxk和 UnionIDox7NLs06ZGfdxxxxxe0F1po78qE一起传入生成新的 AVUser 后,接下来这个用户以来自 miniprogram 不同的 OpenIDohxoK3ldpsGDGGSaniEEexxx和同样的 UnionIDox7NLs06ZGfdxxxxxe0F1po78qE一起传入时,LeanCloud 判定是同样的 UnionID,就直接把来自miniprogram的新用户数据加入到已有 authData 里了,不会再创建新的用户。这样一来,LeanCloud 后台通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个 AVUser 上,实现互通。
已有 authData 应用接入 UnionID
先梳理一遍业务,看看是否在过去开发过程集成了移动应用程序、小程序等多个平台的 authData,导致同一个用户的数据已经被分别保存为不同的 AVUser:
如果没有的话,直接按前面 接入 UnionID 体系 小节的代码集成即可。
如果有的话,需要确认自身的业务需要,确定要以哪个已有平台的账号为主。比如决定使用某个移动应用上生成的账号,则在该移动应用程序更新版本时,使用
asMainAccount参数。这个移动应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意,在第二种情况下
_User表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。Unity SDK 注意事项
Optimization中的Stripping Level设置为Disabled。Unity 自从升级到 5.0 之后就会出现一个 iOS 上访问 HTTPS 请求时的 SSL 证书访问错误:NSURLErrorDomain error -1012。解决方案是:在 Unity 构建完成 iOS 项目之后,使用 XCode 打开项目,找到
Classes/Unity/WWWConnection.mm文件,找到这个方法:把该方法按照如下代码修改即可:
目前 Unity 官方还在修复此问题,截止到 V5.0.1f1 该问题一直存在,因此所有升级到 Unity 5.0 的开发者都需要如此修改,才能确保 iOS 正确运行。