let query = LCQuery(className: "Todo")
let _ = query.get("582570f38ac247004f39c24b") { (result) in
switch result {
case .success(object: let todo):
// todo 就是 objectId 为 582570f38ac247004f39c24b 的 Todo 实例
let title = todo.get("title")
let priority = todo.get("priority")
// 获取内置属性
let objectId = todo.objectId
let updatedAt = todo.updatedAt
let createdAt = todo.createdAt
case .failure(error: let error):
print(error)
}
}
对象拿到之后,可以通过 get 方法来获取各个属性的值。注意 objectId、updatedAt和createdAt 这三个内置属性不能通过 get 获取或通过 set 修改,只能由云端自动进行填充。尚未保存的 LCObject 不存在这些属性。
如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `nil。
同步对象
当云端数据发生更改时,你可以调用 fetch 方法来刷新对象,使之与云端数据同步:
let todo = LCObject(className: "Todo", objectId: "582570f38ac247004f39c24b")
_ = todo.fetch { result in
switch result {
case .success:
// todo 已刷新
break
case .failure(error: let error):
print(error)
}
}
let todo = LCObject(className: "Todo", objectId: "582570f38ac247004f39c24b")
todo.fetch(keys: ["priority", "location"]) { (result) in
switch result {
case .success:
// 只有 priority 和 location 会被获取和刷新
break
case .failure(error: let error):
print(error)
}
}
更新对象
要更新一个对象,只需指定需要更新的属性名和属性值,然后调用 save 方法。例如:
do {
let todo = LCObject(className: "Todo", objectId: "582570f38ac247004f39c24b")
try todo.set("content", value: "这周周会改到周三下午三点。")
todo.save { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
func dateWithString(_ string: String) -> LCDate {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
let date = LCDate(dateFormatter.date(from: string)!)
return date
}
func testSetArray() {
do {
let todo = LCObject(className: "Todo")
let reminder1 = dateWithString("2018-04-30 07:10:00")
let reminder2 = dateWithString("2018-04-30 07:20:00")
try todo.set("reminders", value: [reminder1, reminder2])
let result = todo.save()
assert(result.isSuccess)
let reminder3 = dateWithString("2018-04-30 07:30:00")
try todo.append("reminders", element: reminder3, unique: true)
_ = todo.save { result in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
}
删除对象
下面的代码从云端删除一个 Todo 对象;
let todo = LCObject(className: "Todo", objectId: "582570f38ac247004f39c24b")
_ = todo.delete { result in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
删除对象是一个较为敏感的操作,我们建议你阅读 ACL 权限管理开发指南 来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。
批量操作
可以在一次请求中包含多个构建、保存、删除和同步对象的操作:
// 创建一个保存所有 LCObject 的数组
let objects: [LCObject] = []
// 批量构建和更新
_ = LCObject.save(objects, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
// 批量删除
_ = LCObject.delete(objects, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
// 批量同步
_ = LCObject.fetch(objects, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
下面的代码将所有 Todo 的 isComplete 设为 true:
let query = LCQuery(className: "Todo")
_ = query.find { (result) in
switch result {
case .success(objects: let todos):
// 获取需要更新的 todo
for todo in todos {
do {
// 更新属性值
try todo.set("isComplete", value: true)
} catch {
print(error)
}
}
// 批量更新
let _ = LCObject.save(todos, completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
case .failure(error: let error):
print(error)
}
}
想要建立多对多关系,最简单的办法就是使用 数组。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 中间表 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。
let query = LCQuery(className: "Student")
query.whereKey("lastName", .equalTo("Smith"))
_ = query.find { result in
switch result {
case .success(objects: let students):
// students 是包含满足条件的 Student 对象的数组
break
case .failure(error: let error):
print(error)
}
}
查询条件
可以给 LCObject 添加不同的条件来改变获取到的结果。
下面的代码查询所有 firstName 不为 Jack 的对象:
query.whereKey("firstName", .notEqualTo("Jack"))
对于能够排序的属性(比如数字、字符串),可以进行比较查询:
// 限制 age < 18
query.whereKey("age", .lessThan(18));
// 限制 age <= 18
query.whereKey("age", .lessThanOrEqualTo(18));
// 限制 age > 18
query.whereKey("age", .greaterThan(18));
// 限制 age >= 18
query.whereKey("age", .greaterThanOrEqualTo(18));
可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是 AND 的关系:
let query = LCQuery(className: "Todo")
query.whereKey("priority", .equalTo(2))
_ = query.getFirst { result in
switch result {
case .success(object: let todo):
print(todo)
case .failure(error: let error):
print(error)
}
}
可以通过 .selected 指定需要返回的属性。下面的代码只获取每个对象的 title 和 content(包括内置属性 objectId、createdAt 和 updatedAt):
let query = LCQuery(className: "Todo")
query.whereKey("title", .selected)
query.whereKey("content", .selected)
_ = query.getFirst { result in
switch result {
case .success(object: let todo):
let title = todo.get("title") // √
let content = todo.get("content") // √
let notes = todo.get("notes") // nil
case .failure(error: let error):
print(error)
}
}
如需获取某一属性值包含一列值中任意一个值的对象,可以直接用 .containedIn 而无需执行多次查询。下面的代码构建的查询会查找所有 priority 为 1或2 的 todo 对象:
// 单个查询
let priorityOneOrTwo = LCQuery(className: "Todo")
priorityOneOrTwo.whereKey("priority", .containedIn([1, 2]))
// 这样就可以了 :)
// ---------------
// vs.
// ---------------
// 多个查询
let priorityOne = LCQuery(className: "Todo")
priorityOne.whereKey("priority", .equalTo(1))
let priorityTwo = LCQuery(className: "Todo")
priorityTwo.whereKey("priority", .equalTo(2))
let priorityOneOrTwo = priorityOne.or(priorityTwo)
// 好像有些繁琐 :(
反过来,还可以用 .notContainedIn 来获取某一属性值不包含一列值中任何一个的对象。
关系查询
查询关联数据有很多种方式,常见的一种是查询某一属性值为特定 LCObject 的对象,这时可以像其他查询一样直接用 .equalTo。比如说,如果每一条博客评论 Comment 都有一个 post 属性用来存放原文 Post,则可以用下面的方法获取所有与某一 Post 相关联的评论:
let post = LCObject(className: "Post", objectId: "57328ca079bc44005c2472d0")
let query = LCQuery(className: "Comment")
query.whereKey("post", .equalTo(post))
_ = query.find { result in
switch result {
case .success(objects: let comments):
// comments 包含与 post 相关联的评论
break
case .failure(error: let error):
print(error)
}
}
有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用 .included。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章:
let query = LCQuery(className: "Comment")
// 获取最新发布的
query.whereKey("createdAt", .descending)
// 只获取 10 条
query.limit = 10
// 同时包含博客文章
query.whereKey("post", .included)
_ = query.find { result in
switch result {
case .success(objects: let comments):
// comments 包含最新发布的 10 条评论,包含各自对应的博客文章
for comment in comments {
// 该操作无需网络连接
let post = comment.get("post") as? LCObject
}
case .failure(error: let error):
print(error)
}
}
query.find(cachePolicy: .onlyCache) { (result) in
switch result {
case .success(objects: let objects):
print(objects)
case .failure(error: let error):
print(error)
}
}
do {
let query = LCQuery(className: "Todo")
self.liveQuery = try LiveQuery(query: query, eventHandler: { (liveQuery, event) in })
self.liveQuery.subscribe { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
LiveQuery 不支持内嵌查询,也不支持返回指定属性。
订阅成功后,就可以接收到和 LCObject 相关的更新了。假如在另一个客户端上创建了一个 Todo 对象,对象的 title 设为 更新作品集,那么下面的代码可以获取到这个新的 Todo:
do {
let query = LCQuery(className: "Todo")
self.liveQuery = try LiveQuery(query: query, eventHandler: { (liveQuery, event) in
switch event {
case .create(object: let object):
print(object["title"] as Any) // 更新作品集
default:
break
}
})
} catch {
print(error)
}
此时如果有人把 Todo 的 content 改为 把我最近画的插画放上去,那么下面的代码可以获取到本次更新:
do {
let query = LCQuery(className: "Todo")
self.liveQuery = try LiveQuery(query: query, eventHandler: { (liveQuery, event) in
switch event {
case let .update(object: object, updatedKeys: updatedKeys):
if let key = updatedKeys.first {
print(object[key] as Any) // 把我最近画的插画放上去
}
default:
break
}
})
} catch {
print(error)
}
_ = file.save { result in
switch result {
case .success:
if let value = file.objectId?.value {
print("文件保存完成。objectId: \(value)")
}
case .failure(error: let error):
// 保存失败,可能是文件无法被读取,或者上传过程中出现问题
print(error)
}
}
文件上传后,可以在 _File class 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的 objectId 和 URL。
已经保存到云端的文件可以关联到 LCObject:
do {
if let _ = file.objectId?.value {
let todo = LCObject(className: "Todo")
try todo.set("attachments", value: file)
todo.save { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
}
} catch {
print(error)
}
上传进度监听
上传过程中可以实时向用户展示进度:
_ = file.save(progress: { (progress) in
print(progress)
}) { (result) in
switch result {
case .success:
// 保存后的操作
break
case .failure(error: let error):
print(error)
}
}
let point = LCGeoPoint(latitude: 39.9, longitude: 116.4)
现在可以将这个地理位置存储为一个对象的属性:
try todo.set("location", value: point)
地理位置查询
给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的 LCQuery 添加 .locatedNear 条件。下面的代码查找 location 属性值离某一点最近的 Todo 对象:
let query = LCQuery(className: "Todo")
let point = LCGeoPoint(latitude: 39.9, longitude: 116.4)
query.whereKey("location", .locatedNear(point))
// 限制为 10 条结果
query.limit = 10
_ = query.find { result in
switch result {
case .success(objects: let todos):
// todos 是包含满足条件的 Todo 对象的数组
break
case .failure(error: let error):
print(error)
}
}
_ = LCSMSClient.requestVerificationCode(mobilePhoneNumber: "+8618200008888") { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
用户填入验证码后,用下面的方法完成注册:
_ = LCSMSClient.verifyMobilePhoneNumber("+8618200008888", verificationCode: "123456", completion: { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
})
_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in
switch result {
case .success(object: let user):
print(user)
case .failure(error: let error):
print(error)
}
}
邮箱登录
下面的代码用邮箱和密码登录一个账户:
_ = LCUser.logIn(email: "tom@leancloud.rocks", password: "cat!@#123") { result in
switch result {
case .success(object: let user):
print(user)
case .failure(error: let error):
print(error)
}
}
_ = LCUser.logIn(mobilePhoneNumber: "+8618200008888", password: "cat!@#123") { result in
switch result {
case .success(object: let user):
print(user)
case .failure(error: let error):
print(error)
}
}
_ = LCUser.requestLoginVerificationCode(mobilePhoneNumber: "+8618200008888") { result in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
用户填写收到的验证码后,用下面的方法完成登录:
_ = LCUser.logIn(mobilePhoneNumber: "+8618200008888", verificationCode: "123456") { result in
switch result {
case .success(object: let user):
print(user)
case .failure(error: let error):
print(error)
}
}
输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码 { "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." },开发者可在客户端进行必要提示。
锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。
_ = LCUser.requestVerificationMail(email: "tom@leancloud.rocks") { result in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
_ = LCUser.requestVerificationCode(mobilePhoneNumber: "+8618200008888") { result in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
_ = LCUser.verifyMobilePhoneNumber(mobilePhoneNumber: "+8618200008888", verificationCode: "123456") { result in
switch result {
case .success:
// mobilePhoneVerified 将变为 true
break
case .failure(error: let error):
// 验证码不正确
print(error)
}
}
_ = LCUser.logIn(sessionToken: "anmlwi96s381m6ca7o7266pzf") { (result) in
switch result {
case .success(object: let user):
// 登录成功
print(user)
case .failure(error: let error):
// session token 无效
print(error)
}
}
_ = LCUser.requestPasswordReset(email: "tom@leancloud.rocks") { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
_ = LCUser.requestPasswordReset(mobilePhoneNumber: "+8618200008888") { (result) in
switch result {
case .success:
break
case .failure(error: let error):
print(error)
}
}
_ = LCUser.resetPassword(mobilePhoneNumber: "+8618200008888", verificationCode: "123456", newPassword: "cat!@#123") { result in
switch result {
case .success:
// 密码重置成功
break
case .failure(error: let error):
// 验证码不正确
print(error)
}
}
do {
guard let author = LCApplication.default.currentUser else {
return
}
let book = LCObject(className: "Book")
try book.set("title", value: "我的第五本书")
try book.set("author", value: author)
_ = book.save { result in
switch result {
case .success:
// 获取所有该作者写的书
let query = LCQuery(className: "Book")
query.whereKey("author", .equalTo(author))
_ = query.find { result in
switch result {
case .success(objects: let books):
// books 是包含同一作者所有 Book 对象的数组
break
case .failure(error: let error):
print(error)
}
}
case .failure(error: let error):
print(error)
}
}
} catch {
print(error)
}
_ = LCUser.logIn(username: "Tom", password: "cat!@#123") { result in
switch result {
case .success(object: let user):
// 试图修改用户名
try! user.set("username", "Jerry")
// 密码已被加密,这样做会获取到空字符串
let password = user.get("password")
// 可以执行,因为用户已鉴权
user.save()
// 绕过鉴权直接获取用户
let query = LCQuery(className: "_User")
_ = query.get(user.objectId) { result in
switch result {
case .success(object: let unauthenticatedUser):
try! unauthenticatedUser.set("username", "Toodle")
_ = unauthenticatedUser.save { result in
switch result {
.success:
// 无法执行,因为用户未鉴权
.failure:
// 操作失败
}
}
case .failure(error: let error):
print(error)
}
}
case .failure(error: let error):
print(error)
}
}
子类化推荐给进阶的开发者在进行代码重构的时候做参考。你可以用 LCObject 访问到所有的数据,用 get 方法获取任意字段,用 set 方法给任意字段赋值;你也可以使用子类化来封装 get 以及 set 方法,增强编码体验。子类化有很多优势,包括减少代码的编写量,具有更好的扩展性,和支持自动补全等等。
// Set name for LCObject
do {
let student = LCObject(className: "Student")
try student.set("name", value: "小明")
assert(student.save().isSuccess)
} catch {
print(error)
}
// Set name for Student
let student = Student()
student.name = LCString("小明")
assert(student.save().isSuccess)
数据存储开发指南 · Swift
数据存储(LeanStorage)是 LeanCloud 提供的核心功能之一,可用于存放和查询应用数据。下面的代码展示了如何创建一个对象并将其存入云端:
我们为各个平台或者语言开发的 SDK 在底层都是通过 HTTPS 协议调用统一的 REST API,提供完整的接口对数据进行各类操作。
SDK 安装与初始化
请阅读 Swift 安装指南。
对象
LCObject
LCObject
是 LeanCloud 对复杂对象的封装,每个LCObject
包含若干与 JSON 格式兼容的属性值对(也称键值对,key-value pairs)。这个数据是无模式化的(schema free),意味着你不需要提前标注每个LCObject
上有哪些 key,你只需要随意设置键值对就可以,云端会保存它。比如说,一个保存着单个 Todo 的
LCObject
可能包含如下数据:数据类型
LCObject
支持的数据类型包括String
、Number
、Boolean
、Object
、Array
、Date
、null
等等。你可以通过嵌套的方式在Object
或Array
里面存储更加结构化的数据。LCObject
还支持两种特殊的数据类型Pointer
和File
,可以分别用来存储指向其他LCObject
的指针以及二进制数据。LCObject
同时支持GeoPoint
,可以用来存储地理位置信息。参见 GeoPoint。以下是一些示例:
我们不推荐通过
LCData
在LCObject
里面存储图片、文档等大型二进制数据。每个LCObject
的大小不应超过 128 KB。如需存储大型文件,可创建LCFile
实例并将将其关联到LCObject
的某个属性上。参见 文件。注意:时间类型在云端将会以 UTC 时间格式存储,但是客户端在读取之后会转化成本地时间。
控制台 > 存储 > 数据 中展示的日期数据也会依据操作系统的时区进行转换。一个例外是当你通过 REST API 获得数据时,这些数据将以 UTC 呈现。你可以手动对它们进行转换。
若想了解 LeanCloud 是如何保护应用数据的,请阅读 数据和安全。
构建对象
下面的代码构建了一个 class 为
Todo
的LCObject
:在构建对象时,为了使云端知道对象属于哪个 class,需要将 class 的名字作为参数传入。你可以将 LeanCloud 里面的 class 比作关系型数据库里面的表。一个 class 的名字必须以字母开头,且只能包含数字、字母和下划线。
保存对象
下面的代码将一个 class 为
Todo
的对象存入云端:为了确认对象已经保存成功,我们可以到 控制台 > 存储 > 数据 >
Todo
里面看一下,应该会有一行新的数据产生。点一下这个数据的objectId
,应该能看到类似这样的内容:无需在 控制台 > 存储 > 数据 里面创建新的
Todo
class 即可运行前面的代码。如果 class 不存在,它将自动创建。以下是一些对象的内置属性,会在对象保存时自动创建,无需手动指定:
objectId
LCString
ACL
LCACL
createdAt
LCDate
updatedAt
LCDate
这些属性的值会在对象被存入云端时自动填入,代码中尚未保存的
LCObject
不存在这些属性。属性名(keys)只能包含字母、数字和下划线。自定义属性不得以双下划线(
__
)开头或与任何系统保留字段和内置属性(ACL
、className
、createdAt
、objectId
和updatedAt
)重名,无论大小写。属性值(values)可以是字符串、数字、布尔值、数组或字典(任何能以 JSON 编码的数据)。参见 数据类型。
我们推荐使用驼峰式命名法(CamelCase)为类和属性来取名。类,采用大驼峰法,如
CustomData
。属性,采用小驼峰法,如imageUrl
。获取对象
对于已经保存到云端的
LCObject
,可以通过它的objectId
将其取回:对象拿到之后,可以通过
get
方法来获取各个属性的值。注意objectId
、updatedAt
和createdAt
这三个内置属性不能通过get
获取或通过set
修改,只能由云端自动进行填充。尚未保存的LCObject
不存在这些属性。如果你试图获取一个不存在的属性,SDK 不会报错,而是会返回 `nil。
同步对象
当云端数据发生更改时,你可以调用
fetch
方法来刷新对象,使之与云端数据同步:刷新操作会强行使用云端的属性值覆盖本地的属性。因此如果本地有属性修改,
fetch
操作会丢弃这些修改。为避免这种情况,你可以在刷新时指定 需要刷新的属性,这样只有指定的属性会被刷新(包括内置属性objectId
、createdAt
和updatedAt
),其他属性不受影响。更新对象
要更新一个对象,只需指定需要更新的属性名和属性值,然后调用
save
方法。例如:LeanCloud 会自动识别需要更新的属性并将对应的数据发往云端,未更新的属性会保持原样。
有条件更新对象
通过传入
query
选项,可以按照指定条件去更新对象——当条件满足时,执行更新;条件不满足时,不执行更新并返回305
错误。例如,用户的账户表
Account
有一个余额字段balance
,同时有多个请求要修改该字段值。为避免余额出现负值,只有当金额小于或等于余额的时候才能接受请求:query
选项只对已存在的对象有效,不适用于尚未存入云端的对象。query
选项在有多个客户端需要更新同一属性的时候非常有用。相比于通过LCQuery
查询LCObject
再对其进行更新的方法,这样做更加简洁,并且能够避免出现差错。更新计数器
设想我们正在开发一个微博,需要统计一条微博有多少个赞和多少次转发。由于赞和转发的操作可能由多个客户端同时进行,直接在本地更新数字并保存到云端的做法极有可能导致差错。为保证计数的准确性,可以通过 原子操作 来增加或减少一个属性内保存的数字:
可以指定需要增加或减少的值。若未指定,则默认使用
1
。更新数组
更新数组也是原子操作。使用以下方法可以方便地维护数组类型的数据:
append(String, element: LCValue)
将指定对象附加到数组末尾。
append(String, element: LCValue, unique: Bool)
将指定对象附加到数组末尾,并且可以设置一个
unique
的bool
值表示只是确保唯一,不会重复添加。append(String, elements: [LCValue])
将指定对象数组附加到数组末尾。
append(String, elements: [LCValue], unique: Bool)
将指定对象数组附加到数组末尾,并且可以设置一个
unique
的bool
值表示只是确保唯一,不会重复添加。remove(String, element: LCValue)
从数组字段中删除指定对象的所有实例。
remove(String, elements: [LCValue])
从数组字段中删除指定的对象数组。
例如,
Todo
用一个alarms
属性保存所有闹钟的时间。下面的代码将多个时间加入这个属性:删除对象
下面的代码从云端删除一个
Todo
对象;删除对象是一个较为敏感的操作,我们建议你阅读 ACL 权限管理开发指南 来了解潜在的风险。熟悉 class 级别、对象级别和字段级别的权限可以帮助你有效阻止未经授权的操作。
批量操作
可以在一次请求中包含多个构建、保存、删除和同步对象的操作:
下面的代码将所有
Todo
的isComplete
设为true
:虽然上述方法可以在一次请求中包含多个操作,每一个分别的保存或同步操作在计费时依然会被算作一次请求,而所有的删除操作则会被合并为一次请求。
后台运行
细心的开发者已经发现,在所有的示例代码中几乎都是用了异步来访问 LeanStorage 云端,形如下面的用法都是提供给开发者在主线程调用用以实现后台运行的方法,因此开发者在主线程可以放心地调用这种命名方式的函数。另外,需要强调的是 回调函数的代码是在主线程执行。
数据模型
对象之间可以产生关联。拿一个博客应用来说,一个
Post
对象可以与许多个Comment
对象产生关联。LeanCloud 支持三种关系:一对一、一对多、多对多。一对一、一对多关系
一对一、一对多关系可以通过将
LCObject
保存为另一个对象的属性值的方式产生。比如说,让博客应用中的一个Comment
指向一个Post
。下面的代码会创建一个含有单个
Comment
的Post
:云端存储时,会将被指向的对象用
Pointer
的形式存起来。你也可以用objectId
来指向一个对象:请参阅 关系查询 来了解如何获取关联的对象。
多对多关系
想要建立多对多关系,最简单的办法就是使用 数组。在大多数情况下,使用数组可以有效减少查询的次数,提升程序的运行效率。但如果有额外的属性需要附着于两个 class 之间的关联,那么使用 中间表 可能是更好的方式。注意这里说到的额外的属性是用来描述 class 之间的关系的,而不是任何单一的 class 的。
我们建议你在任何一个 class 的对象数量超出 100 的时候考虑使用中间表。
我们知道设计数据模型并不容易,所以我们专门写了一篇 数据模型设计指南 来详细介绍这部分内容,推荐你去看一下。
序列化和反序列化
在实际的开发中,把
LCObject
当作参数传递的时候,会涉及到复杂对象的拷贝的问题,因此LCObject
也提供了序列化和反序列化的方法。序列化:
反序列化:
查询
我们已经了解到如何从云端获取单个
LCObject
,但你可能还会有一次性获取多个符合特定条件的LCObject
的需求,这时候就需要用到LCQuery
了。基础查询
执行一次基础查询通常包括这些步骤:
LCQuery
;下面的代码获取所有
lastName
为Smith
的Student
:查询条件
可以给
LCObject
添加不同的条件来改变获取到的结果。下面的代码查询所有
firstName
不为Jack
的对象:对于能够排序的属性(比如数字、字符串),可以进行比较查询:
可以在同一个查询中设置多个条件,这样可以获取满足所有条件的结果。可以理解为所有的条件是
AND
的关系:可以通过指定
limit
限制返回结果的数量(默认为100
):由于性能原因,
limit
最大只能设为1000
。即使将其设为大于1000
的数,云端也只会返回 1,000 条结果。如果只需要一条结果,可以直接用
getFirst
:可以通过设置
skip
来跳过一定数量的结果:把
skip
和limit
结合起来,就能实现翻页功能:需要注意的是,
skip
的值越高,查询所需的时间就越长。作为替代方案,可以通过设置createdAt
或updatedAt
的范围来实现更高效的翻页,因为它们都自带索引。对于能够排序的属性,可以指定结果的排序规则:
还可以为同一个查询添加多个排序规则;
下面的代码可用于查找包含或不包含某一属性的对象:
可以通过
.selected
指定需要返回的属性。下面的代码只获取每个对象的title
和content
(包括内置属性objectId
、createdAt
和updatedAt
):对于未获取的属性,可以通过对结果中的对象进行
fetch
操作来获取。参见 同步对象。字符串查询
可以用
.prefixedBy
来查找某一属性值以特定字符串开头的对象。和 SQL 中的LIKE
一样,你可以利用索引带来的优势:可以用
.matchedSubstring
来查找某一属性值包含特定字符串的对象:和
.prefixedBy
不同,.matchedSubstring
无法利用索引,因此不建议用于大型数据集。注意
.prefixedBy
和.matchedSubstring
都是 区分大小写 的,所以上述查询会忽略Lunch
、LUNCH
等字符串。如果想查找某一属性值不包含特定字符串的对象,可以使用
.matchedPattern
进行基于正则表达式的查询:不过我们并不推荐大量使用这类查询,尤其是对于包含超过 100,000 个对象的 class,因为这类查询无法利用索引,实际操作中云端会遍历所有对象来获取结果。如果有进行全文搜索的需求,可以了解一下 应用内搜索 功能。
使用查询时如果遇到性能问题,可参阅 查询性能优化。
数组查询
下面的代码查找所有数组属性
tags
包含工作
的对象:下面的代码查找所有数组属性
tags
同时包含工作
、销售
和会议
的对象:如需获取某一属性值包含一列值中任意一个值的对象,可以直接用
.containedIn
而无需执行多次查询。下面的代码构建的查询会查找所有priority
为1
或2
的 todo 对象:反过来,还可以用
.notContainedIn
来获取某一属性值不包含一列值中任何一个的对象。关系查询
查询关联数据有很多种方式,常见的一种是查询某一属性值为特定
LCObject
的对象,这时可以像其他查询一样直接用.equalTo
。比如说,如果每一条博客评论Comment
都有一个post
属性用来存放原文Post
,则可以用下面的方法获取所有与某一Post
相关联的评论:如需获取某一属性值为另一查询结果中任一
LCObject
的对象,可以用.matchedQuery
。下面的代码构建的查询可以找到所有包含图片的博客文章的评论:如需获取某一属性值不是另一查询结果中任一
LCObject
的对象,则使用.notMatchedQuery
。有时候可能需要获取来自另一个 class 的数据而不想进行额外的查询,此时可以在同一个查询上使用
.included
。下面的代码查找最新发布的 10 条评论,并包含各自对应的博客文章:可以用 dot 符号(
.
)来获取多级关系。比如说,在获取评论对应文章的同时获取文章作者:可以在同一查询上应用多次
.included
以包含多个属性。通过这种方法获取到的对象同样接受getFirst
、get
等LCQuery
辅助方法。通过
.included
进行多级查询的方式不适用于数组属性内部的LCObject
,只能包含到数组本身。dot 符号(
.
)还可用于.selected
以限制返回的关联对象的属性:关系查询的注意事项
LeanCloud 云端使用的并非关系型数据库,无法做到真正的联表查询,所以实际的处理方式是:先执行内嵌/子查询(和普通查询一样,
limit
默认为100
,最大1000
),然后将子查询的结果填入主查询的对应位置,再执行主查询。如果子查询匹配到的记录数量超出limit
,且主查询有其他查询条件,那么可能会出现没有结果或结果不全的情况,因为只有limit
数量以内的结果会被填入主查询。我们建议采用以下方案进行改进:
limit
设为1000
。skip
值来遍历所有记录(注意skip
的值较大时可能会引发性能问题,因此不是很推荐)。统计总数量
如果只需知道有多少对象匹配查询条件而无需获取对象本身,可使用
count
来代替find
。比如说,查询有多少个已完成的 todo:组合查询
组合查询就是把诸多查询条件用一定逻辑合并到一起(
OR
或AND
)再交给云端去查询。组合查询不支持在子查询中包含
GeoPoint
或其他非过滤性的限制(例如near
、withinGeoBox
、limit
、skip
、ascending
、descending
、include
)。OR 查询
OR 操作表示多个查询条件符合其中任意一个即可。 例如,查询优先级大于等于
3
或者已经完成了的 todo:使用 OR 查询时,子查询中不能包含
GeoPoint
相关的查询。AND 查询
使用 AND 查询的效果等同于往
LCQuery
添加多个条件。下面的代码构建的查询会查找创建时间在2016-11-13
和2016-12-02
之间的 todo:单独使用 AND 查询跟使用基础查询相比并没有什么不同,不过当把它和 OR 查询结合在一起的时候就不一样了。下面的代码构建的查询可以查找所有今天创建的 todo 中没有
location
的或priority
为3
的:缓存查询
缓存一些查询的结果到本地,这可以让你在无网络的时候展现一些数据给用户。
默认情况下 SDK 不会缓存查询结果,可以在 SDK 初始化时配置
HTTPURLCache
来开启查询结果缓存:默认情况下,SDK 的查询接口不会查询缓存,可以通过设置接口的
cachePolicy
来改变查询行为:缓存策略
为了满足多变的需求,SDK 提供了以下几种缓存策略:
onlyNetwork
onlyCache
networkElseCache
缓存相关的操作
可以参考 Apple 官方文档中关于 URLCache 的描述。
例如,清除所有本地缓存:
查询性能优化
影响查询性能的因素很多。特别是当查询结果的数量超过 10 万,查询性能可能会显著下降或出现瓶颈。以下列举一些容易降低性能的查询方式,开发者可以据此进行有针对性的调整和优化,或尽量避免使用。
count
(需要扫描所有数据)skip
跳过较多的行数(相当于需要先查出被跳过的那些行)LiveQuery
LiveQuery 衍生于
LCQuery
,并为其带来了更强大的功能。它可以让你无需编写复杂的逻辑便可在客户端之间同步数据,这对于有实时数据同步需求的应用来说很有帮助。设想你正在开发一个多人协作同时编辑一份文档的应用,单纯地使用
LCQuery
并不是最好的做法,因为它只具备主动拉取的功能,而应用并不知道什么时候该去拉取。想要解决这个问题,就要用到 LiveQuery 了。借助 LiveQuery,你可以订阅所有需要保持同步的
LCQuery
。订阅成功后,一旦有符合LCQuery
的LCObject
发生变化,云端就会主动、实时地将信息通知到客户端。LiveQuery 使用 WebSocket 在客户端和云端之间建立连接。WebSocket 的处理会比较复杂,而我们将其封装成了一个简单的 API 供你直接使用,无需关注背后的原理。
启用 LiveQuery
进入 控制台 > 存储 > 设置,在 其他 里面勾选 启用 LiveQuery,然后导入 LeanCloud Swift SDK:
Demo
下面是在使用了 LiveQuery 的网页应用和手机应用中分别操作,数据保持同步的效果:
使用我们的「LeanTodo」微信小程序和网页应用,可以实际体验以上视频所演示的效果,步骤如下:
注意按以上顺序操作。在网页应用中使用 Signup 注册的账户无法与小程序创建的账户相关联,所以如果颠倒以上操作顺序,则无法观测到数据同步效果。
LiveQuery 公开课 涵盖了许多开发者关心的问题和解答。
构建订阅
首先创建一个普通的
LCQuery
对象,添加查询条件(如有),然后进行订阅操作:LiveQuery 不支持内嵌查询,也不支持返回指定属性。
订阅成功后,就可以接收到和
LCObject
相关的更新了。假如在另一个客户端上创建了一个Todo
对象,对象的title
设为更新作品集
,那么下面的代码可以获取到这个新的Todo
:此时如果有人把
Todo
的content
改为把我最近画的插画放上去
,那么下面的代码可以获取到本次更新:事件处理
订阅成功后,可以选择监听如下几种数据变化:
create
update
enter
leave
delete
login
create
事件当有新的满足
LCQuery
查询条件的LCObject
被创建时,create
事件会被触发。下面的object
就是新建的LCObject
:update
事件当有满足
LCQuery
查询条件的LCObject
被更新时,update
事件会被触发。下面的object
就是有更新的LCObject
:enter
事件当一个已存在的、原本不符合
LCQuery
查询条件的LCObject
发生更新,且更新后符合查询条件,enter
事件会被触发。下面的object
就是进入LCQuery
的LCObject
,其内容为该对象最新的值:注意区分
create
和enter
的不同行为。如果一个对象已经存在,在更新之前不符合查询条件,而在更新之后符合查询条件,那么enter
事件会被触发。如果一个对象原本不存在,后来被构建了出来,那么create
事件会被触发。leave
事件当一个已存在的、原本符合
LCQuery
查询条件的LCObject
发生更新,且更新后不符合查询条件,leave
事件会被触发。下面的object
就是离开LCQuery
的LCObject
,其内容为该对象最新的值:delete
事件当一个已存在的、原本符合
LCQuery
查询条件的LCObject
被删除,delete
事件会被触发。下面的object
就是被删除的LCObject
的objectId
:login
事件当一个用户成功登录应用,
login
事件会被触发。下面的user
就是登录的LCUser
:取消订阅
如果不再需要接收有关
LCQuery
的更新,可以取消订阅。断开连接
断开连接有几种情况:
如上几种情况开发者无需做额外的操作,只要切回应用,SDK 会自动重新订阅,数据变更会继续推送到客户端。
而另外一种极端情况——当用户在移动端使用手机的进程管理工具,杀死了进程或者直接关闭了网页的情况下,SDK 无法自动重新订阅,此时需要开发者根据实际情况实现重新订阅。
LiveQuery 的注意事项
因为 LiveQuery 的实时性,很多用户会陷入一个误区,试着用 LiveQuery 来实现一个简单的聊天功能。我们不建议这样做,因为使用 LiveQuery 构建聊天服务会承担额外的存储成本,产生的费用会增加,后期维护的难度非常大(聊天记录、对话维护之类的代码会很混乱),并且 LeanCloud 已经提供了 即时通讯 的服务。LiveQuery 的核心还是提供一个针对查询的推拉结合的用法,脱离设计初衷容易造成前端的模块混乱。
文件
有时候应用需要存储尺寸较大或结构较为复杂的数据,这类数据不适合用
LCObject
保存,此时文件对象LCFile
便成为了更好的选择。文件对象最常见的用途是保存图片,不过也可以用来保存文档、视频、音乐等其他二进制数据。构建文件
可以通过字符串构建文件:
除此之外,还可以通过 URL 构建文件:
通过 URL 构建文件时,SDK 并不会将原本的文件转储到云端,而是会将文件的物理地址存储为字符串,这样也就不会产生任何文件上传流量。使用其他方式构建的文件会被保存在云端。
LeanCloud 会根据文件扩展名自动检测文件类型。如果需要的话,也可以手动指定
Content-Type
(一般称为 MIME 类型):与前面提到的方式相比,一个更常见的文件构建方式是从本地路径上传:
这里上传的文件名字叫做
avatar.jpg
。需要注意:objectId
,所以在一个应用内是允许多个文件重名的。LCFile
保存一个 PNG 格式的图像,那么扩展名应为.png
。application/octet-stream
。保存文件
将文件保存到云端后,便可获得一个永久指向该文件的 URL:
文件上传后,可以在
_File
class 中找到。已上传的文件无法再被修改。如果需要修改文件,只能重新上传修改过的文件并取得新的objectId
和 URL。已经保存到云端的文件可以关联到
LCObject
:上传进度监听
上传过程中可以实时向用户展示进度:
文件元数据
上传文件时,可以用
metaData
添加额外的属性。文件一旦保存,metaData
便不可再修改。图像缩略图
保存图像时,如果想在下载原图之前先得到缩略图,方法如下:
图片最大不超过 20 MB 才可以获取缩略图。
国际版不支持图片缩略图。
删除文件
下面的代码从云端删除一个文件:
默认情况下,文件的删除权限是关闭的,需要进入 控制台 > 存储 > 数据 >
_File
,选择 其他 > 权限设置 >delete
来开启。启用 HTTPS 域名
如果希望使用 HTTPS 域名来访问文件,需要进入 控制台 > 存储 > 设置 > 自定义文件域名,勾选 启用 HTTPS 域名。HTTPS 文件流量无免费的使用额度,收费标准将在该选项开启时显示。
「启用 HTTPS 域名」会影响到 API 返回的文件地址是 HTTPS 还是 HTTP 类型的 URL。需要注意的是,即使没有启用这一选项,终端仍然可以选择使用 HTTPS URL 来访问文件,但由此会产生 HTTPS 流量扣费。
在启用文件 HTTPS 域名之后,之前已保存在
_File
表中的文件的 URL 会自动被转换为以 HTTPS 开头。如果取消 HTTPS 域名,已经改为 HTTPS 域名的文件不会变回到 HTTP。LeanCloud 即时通讯组件也使用
LCFile
来保存消息的图片、音频等文件,并且把文件的地址写入到了消息内容中。当文件 HTTPS 域名被开启后,之前历史消息中的文件地址不会像_File
表那样被自动转换,而依然保持 HTTP。CDN 加速
中国节点的文件存储服务自带 CDN 加速访问,但不包括海外 CDN 加速。商用版应用可发工单申请开启海外 CDN 加速(开启后海外访问文件 http/https 流量费用分别上调为 0.40 元/GB、0.60 元/GB。 国际版没有现成的 CDN 加速访问服务,需要用户自行配置(步骤)。
以 CloudFront 加速服务为例,配置过程如下:
GeoPoint
LeanCloud 允许你通过将
LCGeoPoint
关联到LCObject
的方式存储折射真实世界地理位置的经纬坐标,这样做可以让你查询包含一个点附近的坐标的对象。常见的使用场景有「查找附近的用户」和「查找附近的地点」。要构建一个包含地理位置的对象,首先要构建一个地理位置。下面的代码构建了一个
LCGeoPoint
并将其纬度(latitude
)设为39.9
,经度(longitude
)设为116.4
:现在可以将这个地理位置存储为一个对象的属性:
地理位置查询
给定一些含有地理位置的对象,可以从中找出离某一点最近的几个,或者处于某一范围内的几个。要执行这样的查询,可以向普通的
LCQuery
添加.locatedNear
条件。下面的代码查找location
属性值离某一点最近的Todo
对象:像
.ascending
和.descending
这样额外的排序条件会获得比默认的距离排序更高的优先级。若要限制结果和给定地点之间的距离,可以参考 API 文档中的
.locatedNear
的from
和to
参数。若要查询在某一矩形范围内的对象,可以用
.locatedWithin
:GeoPoint 的注意事项
使用地理位置需要注意以下方面:
LCObject
数据对象中只能有一个LCGeoPoint
对象的属性。-90.0
到90.0
之间,经度的范围应该是在-180.0
到180.0
之间。如果添加的经纬度超出了以上范围,将导致程序错误。用户
用户系统几乎是每款应用都要加入的功能,我们为此专门提供了一个
LCUser
类来方便应用使用各项用户管理的功能。LCUser
是LCObject
的子类,这意味着任何LCObject
提供的方法也适用于LCUser
,唯一的区别就是LCUser
提供一些额外的用户管理相关的功能。每个应用都有一个专门的_User
class 用于存放所有的LCUser
。用户的属性
LCUser
相比一个普通的LCObject
多出了以下属性:username
:用户的用户名。password
:用户的密码。email
:用户的电子邮箱。emailVerfied
:用户的电子邮箱是否已验证。mobilePhoneNumber
:用户的手机号。mobilePhoneVerfied
用户的手机号是否已验证。在接下来对用户功能的介绍中我们会逐一了解到这些属性。
注册
用户第一次打开应用的时候,可以让用户注册一个账户。下面的代码展示了一个典型的使用用户名和密码注册的流程:
新建
LCUser
的操作应使用signUp
而不是save
,但以后的更新操作就可以用save
了。如果收到
202
错误码,意味着_User
表里已经存在使用同一username
的账号,此时应提示用户换一个用户名。除此之外,每个用户的email
和mobilePhoneNumber
也需要保持唯一性,否则会收到203
或214
错误。可以考虑在注册时把用户的username
设为与email
相同,这样用户可以直接 用邮箱重置密码。采用「用户名 + 密码」注册时需要注意:密码是以明文方式通过 HTTPS 加密传输给云端,云端会以密文存储密码(云端对密码的长度、复杂度不作限制),并且我们的加密算法是无法通过所谓「彩虹表撞库」获取的,这一点请开发者放心。换言之,用户的密码只可能用户本人知道,开发者不论是通过控制台还是 API 都是无法获取。另外我们需要强调 在客户端,应用切勿再次对密码加密,这会导致 重置密码 等功能失效。
手机号注册
对于移动应用来说,允许用户以手机号注册是个很常见的需求。实现该功能大致分两步,第一步是让用户提供手机号,点击「获取验证码」按钮后,该号码会收到一个六位数的验证码:
用户填入验证码后,用下面的方法完成注册:
username
将与mobilePhoneNumber
相同,password
会由云端随机生成。手机号格式
LCUser
接受的手机号以+
和国家代码开头,后面紧跟着剩余的部分。手机号中不应含有任何划线、空格等非数字字符。例如,+19490008888
是一个合法的美国或加拿大手机号(1
是国家代码),+8618200008888
是一个合法的中国手机号(86
是国家代码)。请参阅短信 SMS 服务使用指南中的 服务覆盖区域和价格 以了解 LeanCloud 支持的国家和地区。
登录
下面的代码用用户名和密码登录一个账户:
邮箱登录
下面的代码用邮箱和密码登录一个账户:
手机号登录
如果应用允许用户以手机号注册,那么也可以让用户以手机号配合密码或短信验证码登录。下面的代码用手机号和密码登录一个账户:
默认情况下,LeanCloud 允许所有关联了手机号的用户直接以手机号登录,无论手机号是否 通过验证。为了让应用更加安全,你可以选择只允许验证过手机号的用户通过手机号登录。可以在 控制台 > 存储 > 设置 里面开启该功能。
除此之外,还可以让用户通过短信验证码登录,适用于用户忘记密码且不愿重置密码的情况。和 通过手机号注册 的步骤类似,首先让用户填写与账户关联的手机号码,然后在用户点击「获取验证码」后调用下面的方法:
用户填写收到的验证码后,用下面的方法完成登录:
测试手机号和固定验证码
在开发过程中,可能会因测试目的而需要频繁地用手机号注册登录,然而运营商的发送频率限制往往会导致测试过程耗费较多的时间。
为了解决这个问题,可以在 控制台 > 消息 > 短信 > 设置 里面设置一个测试手机号,而云端会为该号码生成一个固定验证码。以后进行登录操作时,只要使用的是这个号码,云端就会直接放行,无需经过运营商网络。
测试手机号还可用于将 iOS 应用提交到 App Store 进行审核的场景,因为审核人员可能因没有有效的手机号码而无法登录应用来进行评估审核。如果不提供一个测试手机号,应用有可能被拒绝。
可参阅 短信 SMS 服务使用指南 来了解更多有关短信发送和接收的限制。
单设备登录
某些场景下需要确保用户的账户在同一时间只在一台设备上登录,也就是说当用户在一台设备上登录后,其他设备上的会话全部失效。可以按照以下方案来实现:
账户锁定
输入错误的密码或验证码会导致用户登录失败。如果在 15 分钟内,同一个用户登录失败的次数大于 6 次,该用户账户即被云端暂时锁定,此时云端会返回错误码
{ "code": 1, "error": "You have exceeded the maximum number of login attempts, please try again later, or consider resetting your password." }
,开发者可在客户端进行必要提示。锁定将在最后一次错误登录的 15 分钟之后由云端自动解除,开发者无法通过 SDK 或 REST API 进行干预。在锁定期间,即使用户输入了正确的验证信息也不允许登录。这个限制在 SDK 和云引擎中都有效。
验证邮箱
可以通过要求用户在登录或使用特定功能之前验证邮箱的方式防止恶意注册。默认情况下,当用户注册或变更邮箱后,
emailVerified
会被设为false
。在应用的 控制台 > 存储 > 设置 中,可以开启 从客户端注册邮箱或者更新邮箱时,发送验证邮件 选项,这样当用户注册或变更邮箱时,会收到一封含有验证链接的邮件。在同一设置页面还可找到阻止未验证邮箱的用户登录的选项。如果用户忘记点击链接并且在未来某一时刻需要进行验证,可以用下面的代码发送一封新的邮件:
用户点击邮件内的链接后,
emailVerified
会变为true
。如果用户的email
属性为空,则该属性永远不会为true
。验证手机号
和 验证邮箱 类似,应用还可以要求用户在登录或使用特定功能之前验证手机号。默认情况下,当用户注册或变更手机号后,
mobilePhoneVerified
会被设为false
。在应用的 控制台 > 存储 > 设置 中,可以开启 从客户端注册或更新手机号时,向注册手机号码发送验证短信 选项,这样当用户注册或变更手机号时,会收到一条含有验证码的短信。在同一设置页面还可找到阻止未验证手机号的用户登录的选项。可以随时用下面的代码发送一条新的验证码:
用户填写验证码后,调用下面的方法来完成验证。
mobilePhoneVerified
将变为true
:当前用户
用户登录后,SDK 会自动将会话信息存储到客户端,这样用户在下次打开客户端时无需再次登录。下面的代码检查是否有已经登录的用户:
会话信息会长期有效,直到用户主动登出:
设置当前用户
用户登录后,云端会返回一个 session token 给客户端,它会由 SDK 缓存起来并用于日后同一
LCUser
的鉴权请求。session token 会被包含在每个客户端发起的 HTTP 请求的 header 里面,这样云端就知道是哪个LCUser
发起的请求了。以下是一些应用可能需要用到 session token 的场景:
LCApplication.default.currentUser?.sessionToken?.value
获取到当前用户的 session token)。下面的代码使用 session token 登录一个用户(云端会验证 session token 是否有效):
请避免在外部浏览器使用 URL 来传递 session token,以防范信息泄露风险。
如果在 控制台 > 存储 > 设置 中勾选了 密码修改后,强制客户端重新登录,那么当一个用户修改密码后,该用户的 session token 会被重置。此时需要让用户重新登录,否则会遇到
403 (Forbidden)
错误。下面的代码检查 session token 是否有效:
重置密码
我们都知道,应用一旦加入账户密码系统,那么肯定会有用户忘记密码的情况发生。对于这种情况,我们为用户提供了多种重置密码的方法。
邮箱重置密码的流程如下:
首先让用户填写注册账户时使用的邮箱,然后调用下面的方法:
上面的代码会查询
_User
表中是否有对象的email
属性与前面提供的邮箱匹配。如果有的话,则向该邮箱发送一封密码重置邮件。之前提到过,应用可以让username
与email
保持一致,也可以单独收集用户的邮箱并将其存为email
。密码重置邮件的内容可在应用的 控制台 > 设置 > 邮件模版 中自定义。更多关于自定义邮件模板和验证链接的内容,请参考 自定义应用内用户重设密码和邮箱验证页面。
除此之外,还可以用手机号重置密码:
下面的代码向用户发送含有验证码的短信:
上面的代码会查询
_User
表中是否有对象的mobilePhoneNumber
属性与前面提供的手机号匹配。如果有的话,则向该号码发送验证码短信。可以在 控制台 > 存储 > 设置 中设置只有在
mobilePhoneVerified
为true
的情况下才能用手机号重置密码。用户输入验证码和新密码后,用下面的代码完成密码重置:
用户的查询
可以直接构建一个针对
_User
的LCQuery
来查询用户:为了安全起见,新创建的应用的
_User
表默认关闭了find
权限,这样每位用户登录后只能查询到自己在_User
表中的数据,无法查询其他用户的数据。如果需要让其查询其他用户的数据,建议单独创建一张表来保存这类数据,并开放这张表的find
查询权限。除此之外,还可以在 云引擎 里封装用户查询相关的方法,这样就无需开放_User
表的find
权限。可以参见 用户对象的安全 来了解
_User
表的一些限制,还可以阅读 数据和安全 来了解更多 class 级权限设置的方法。关联用户对象
关联
LCUser
的方法和LCObject
是一样的。下面的代码为一名作者保存了一本书,然后获取所有该作者写的书:用户对象的安全
LCUser
类自带安全保障,只有通过logIn
或者signUp
这种经过鉴权的方法获取到的LCUser
才能进行保存或删除相关的操作,保证每个用户只能修改自己的数据。这样设计是因为
LCUser
中存储的大多数数据都比较敏感,包括手机号、社交网络账号等等。为了用户的隐私安全,即使是应用的开发者也应避免直接接触这些数据。下面的代码展现了这种安全措施:
通过
LCApplication.default.currentUser
获取的LCUser
总是经过鉴权的。要查看一个
LCUser
是否经过鉴权,可以调用方法。通过经过鉴权的方法获取到的
LCUser
无需进行该检查。注意,用户的密码只能在注册的时候进行设置,日后如需修改,只能通过 重置密码 的方式进行。密码不会被缓存在本地。如果尝试直接获取已登录用户的密码,会得到
null
。其他对象的安全
对于给定的一个对象,可以指定哪些用户有权限读取或修改它。为实现该功能,每个对象都有一个由
LCACL
对象组成的访问控制表。请参阅 ACL 权限管理开发指南。第三方账户登录
LeanCloud 允许应用将用户账户与微信、QQ 等第三方平台关联起来,这样用户就可以直接用第三方账户登录应用。比如说允许用户使用微信登录,那么你的代码会像这样:
云端会验证传入的
authData
是否合法,并且查询是否已经存在与之关联的用户。如果有的话,则返回200 OK
状态码,同时附上用户的信息(包括sessionToken
)。如果
authData
没有和任何用户关联,客户端会收到201 Created
状态码,意味着新用户被创建,同时附上用户的objectId
、createdAt
、sessionToken
和一个自动生成的username
,例如:出于安全考虑,
authData
不会被返回给客户端,除非它属于当前用户。为确保每个
LCUser
只会关联每个平台一次,_User
class 中每个用户的authData.<SERVICE_NAME>.openid
需要保持独一无二。配置平台账号
开始使用前,需要在 控制台 > 组件 > 社交 配置相应平台的 应用 ID 和 应用 Secret Key。点击保存,自动生成 回调 URL 和 登录 URL。
以微博开放平台举例,它需要单独配置 回调 URL。在微博开放平台的 应用信息 > 高级信息 > OAuth2.0 授权设置 里的「授权回调页」中绑定生成的 回调 URL。测试阶段,在微博开放平台的 应用信息 > 测试信息 添加微博账号,在腾讯开放平台的 QQ 登录 > 应用调试者 里添加 QQ 账号即可。在应用通过审核后,可以获取公开的第三方登录能力。
配置平台账号的目的在于创建
LCUser
时,LeanCloud 云端会使用相关信息去校验authData
的合法性,确保LCUser
实际对应着一个合法真实的用户,确保平台安全性。如果想关闭自动校验authData
的功能,需要在 控制台 > 组件 > 社交 中 取消勾选「第三方登录时,验证用户 AccessToken 合法性」。鉴权数据
authData
是一个以平台名为键名,鉴权信息为键值的 JSON 对象。你需要自己完成鉴权流程(一般通过 OAuth 1.0 或 2.0)以从平台那边获取鉴权信息。一个关联了微信账户的用户应该会有下列对象作为
authData
:云端会自动验证部分服务的 access token 的有效性以防止伪造请求。如果验证失败,云端会返回
invalid authData
错误,关联不会被建立。对于云端无法识别的服务,你需要自己去验证 access token 的有效性。如果不希望云端自动验证 access token,可以在 控制台 > 存储 > 设置 里面取消勾选 第三方登录时,验证用户 AccessToken 合法性。扩展需求
新用户登录时必须填用户信息
满足需求:一个新用户使用第三方账号授权拿到相关信息后,仍然需要设置账号相关的用户名、手机号、密码等重要信息后,才被允许登录成功。
这时要使用
loginWithauthData
登录接口的failOnNotExist
参数并将其设置为ture
。服务端会判断是否已存在能匹配上的authData
,否的话,返回211
错误码和Could not find user
报错信息。开发者根据这个211
错误码,跳转到要求输入用户名、密码、手机号等信息的页面,实例化一个LCUser
对象,保存上述补充数据,再次调用loginWithauthData
接口进行登录,并 不再传入failOnNotExist
参数。示例代码如下:接入 UnionID 体系
随着第三方平台的账户体系变得日渐复杂,它们的
authData
出现了一些较大的变化。下面我们以最典型的微信开放平台为例来进行说明。当一个用户在移动应用内登录微信账号时,会被分配一个 OpenID;在微信小程序内登录账号时,又会被分配另一个不同的 OpenID。这样的架构会导致的问题是,使用同一个微信号的用户,也无法在微信开发平台下的移动应用和小程序之间互通。
微信官方为了解决这个问题,引入 UnionID 的体系,即:同一微信号,对同一个微信开放平台账号下的不同应用,不管是移动 app、网站应用还是小程序,UnionID 都是相同的。也就是说,UnionID 可以作为用户的唯一标识。
其他平台,如 QQ 的 UnionID 体系,和微信的设计保持一致。
LeanCloud 支持 UnionID 体系。你只需要给
loginWithauthData
和associateWithauthData
接口传入更多的参数,即可完成新 UnionID 体系的集成。要使用到的关键参数列表:
platform
weixinapp
、wxminiprogram
、qqapp1
等。unionIdPlatform
weixin
、weibo
和qq
等。unionId
asMainAccount
、unionIdPlatform
一起使用。asMainAccount
unionId
、unionIdPlatform
一起使用。接入新 UnionID 系统时,每次传入的
authData
必须包含成对的平台uid
或openid
和平台unionid
。示例代码如下:然后让我们来看看生成的
authData
的数据格式:当你想加入该 UnionID 下的一个新平台,比如
miniprogram1
时,再次登录后生成的数据为:可以看到,最终该
authData
实际包含了来自weixin
这个 UnionID 体系内的两个不同平台,weixinapp1
代表来自移动应用,miniprogram1
来自小程序。_weixin_unionid
这个字段的值就是用户在weixin
这个 UnionID 平台的唯一标识 UnionID 值。当一个用户以来自
weixinapp
的 OpenIDoTY851axxxgujsEl0f36Huxk
和 UnionIDox7NLs06ZGfdxxxxxe0F1po78qE
一起传入生成新的LCUser
后,接下来这个用户以来自miniprogram
不同的 OpenIDohxoK3ldpsGDGGSaniEEexxx
和同样的 UnionIDox7NLs06ZGfdxxxxxe0F1po78qE
一起传入时,LeanCloud 判定是同样的 UnionID,就直接把来自miniprogram
的新用户数据加入到已有authData
里了,不会再创建新的用户。这样一来,LeanCloud 后台通过识别平台性的用户唯一标识 UnionID,让来自同一个 UnionID 体系内的应用程序、小程序等不同平台的用户都绑定到了一个
LCUser
上,实现互通。已有 authData 应用接入 UnionID
先梳理一遍业务,看看是否在过去开发过程集成了移动应用程序、小程序等多个平台的
authData
,导致同一个用户的数据已经被分别保存为不同的LCUser
。如果没有的话,直接按前面 接入 UnionID 体系 小节的代码集成即可。
如果有的话,需要确认自身的业务需要,确定要以哪个已有平台的账号为主。比如决定使用某个移动应用上生成的账号,则在该移动应用程序更新版本时,使用
asMainAccount
参数。这个移动应用带着 UnionID 登录匹配或创建的账号将作为主账号,之后所有这个 UnionID 的登录都会匹配到这个账号。请注意,在第二种情况下
_User
表里会剩下一些用户数据,也就是没有被选为主账号的、其他平台的同一个用户的旧账号数据。这部分数据会继续服务于已经发布的但仍然使用 OpenID 登录的旧版应用。解除关联
可以通过将对应平台的
authData
设为null
的方式解除关联。下面的代码解除用户和微信账户的关联:角色
随着用户量的增长,你可能会发现相比于为每一名用户单独设置权限,将预先设定好的权限直接分配给一部分用户是更好的选择。为了迎合这种需求,LeanCloud 支持基于角色的权限管理。请参阅 ACL 权限管理开发指南。
子类化
子类化推荐给进阶的开发者在进行代码重构的时候做参考。你可以用
LCObject
访问到所有的数据,用get
方法获取任意字段,用set
方法给任意字段赋值;你也可以使用子类化来封装get
以及set
方法,增强编码体验。子类化有很多优势,包括减少代码的编写量,具有更好的扩展性,和支持自动补全等等。子类化的实现
要实现子类化,需要下面两个步骤:
LCObject
;objectClassName
,返回的字符串是原先要传递给LCObject(className:)
初始化方法的参数。如果不实现,默认返回的是类的名字。请注意:LCUser
子类化后必须返回_User
;application(_:didFinishLaunchingWithOptions:)
方法中调用static func register()
方法。下面是实现
Student
子类化的例子:将 Setter 以及 Getter 方法封装成属性
可以将
LCObject
的 Setter 和 Getter 方法封装成属性,需使用@objc dynamic var
来声明一个变量,且该变量的类型为LCValue
。如下所示,两段代码对
name
字段的赋值方式等价。应用内搜索
应用内搜索是一个针对应用数据进行全局搜索的接口,它基于搜索引擎构建,提供更强大的搜索功能。要深入了解其用法和阅读示例代码,请阅读 应用内搜索指南。
应用内社交
应用内社交,又称「事件流」,在应用开发中出现的场景非常多,包括用户间关注(好友)、朋友圈(时间线)、状态、互动(点赞)、私信等常用功能,请参考 应用内社交模块。