遇到报错,网上基本差不到,自己看开发文档agc开发文档
云数据库
概念
不是关系型数据库,是基于对象模型的数据库
- 对象类型:用来定义存储对象的集合,不同的对象类型对应不同的数据结构,类似关系型数据库中的表
- 对象:基本操作单元,每一个对象是一条完整的数据记录
- 存储区:类似于数据库,有多张表。多份存储区的意义在于隔离测试、正式数据
字段的数据类型:
两种字符串string,text,四种整数,两种浮点数,字节数组(存储二进制数据),整数自增类型(只有正整数)两种(并不是所有客户端语言都支持,ArkTS可以)
数据类型 | 取值范围 | 说明 | 排序方式 |
---|---|---|---|
String | 最大长度200 | 如果字符串长度超过200,建议使用Text类型。 | 采用 UTF-8 编码的字节顺序 |
Boolean | true/false | - | false < true |
Byte | ( − 2 7 ) ∼ ( 2 7 − 1 ) (-2^7)\sim(2^7-1) (−27)∼(27−1) | - | 数字顺序 |
Short | ( − 2 15 ) ∼ ( 2 15 − 1 ) (-2^{15})\sim(2^{15}-1) (−215)∼(215−1) | - | |
Integer | ( − 2 31 ) ∼ ( 2 31 − 1 ) (-2^{31})\sim(2^{31}-1) (−231)∼(231−1) | - | |
Long | ( − 2 63 ) ∼ ( 2 63 − 1 ) (-2^{63})\sim(2^{63}-1) (−263)∼(263−1) | 由于JavaScript不支持数据类型“Long”,Web SDK通过引入第三方开源组件实现支持数据类型“Long”的能力。“Long”类型的使用方法请参考https://github.com/dcodeIO/long.js。 | |
Float | -3.40E+38 ~ +3.40E+38 | - | |
Double | -1.79E+308 ~ +1.79E+308 | - | |
ByteArray | - | 一般用于文件类型的数据存储,如图片、文档和视频等。在端侧时,使用Android开发应用时,以byte[]表示为字节数组。 | 采用 UTF-8 编码的字节顺序 |
Text | - | - | |
Date | - | - | 时间顺序 |
IntAutoIncrement | 1 ∼ ( 2 31 − 1 ) 1\sim(2^{31}-1) 1∼(231−1) | Android、HarmonyOS(Java)和iOS不支持此数据类型。 | 数字顺序 |
LongAutoIncrement | 1 ∼ ( 2 63 − 1 ) 1\sim(2^{63}-1) 1∼(263−1) | Android、HarmonyOS(Java)和iOS不支持此数据类型。 | 数字顺序 |
假设有自增类型 id,执行 upsert (insert+update)
- id 值在数据库有,更新 {id:1, name:‘李四’ , age:20}
- id 值在数据库无,按自增添加 {id:5, name:‘王五’ , age:16} 不会按你给的id增加
- id 值不给,也按自增添加 {name:‘赵六’ , age:16}
student 表
id name age
1 李四 20
2 王五 16
3 赵六 16
权限管理:
【角色】共有四种
- World 所有人 app管理者,使用者(已登录、未登录)
- Authenticated 已认证
- Creator 创建者 (是这个用户创建的)
- Administrator 管理员
【权限】共有三种:Read 查询、Upsert 新增和修改、Delete 删除
例如:
并没有字段去记录是哪个人创建的,这是隐式的
article 表
id title
用户 111
1 探索自我成长的道路:我的心路历程
2 如何在忙碌的生活中保持身心健康
3 分享我的旅行经历:探索世界的美
用户 222
4 金融投资的新趋势:数字货币的崛起
5 未来科技发展展望:人工智能对社会的影响
它的权限设置如下:
"permissions": [
{
"rights": ["Read"],
"role": "World"
},
{
"rights": ["Read","Upsert"],
"role": "Authenticated"
},
{
"rights": ["Read","Upsert","Delete"],
"role": "Creator"
},
{
"rights": ["Read","Upsert","Delete"],
"role": "Administrator"
}
]
- 用户111 未登录,查询所有文章 1 2 3 4 5 属于world所有人 有查询功能
- 用户333 未登录,查询所有文章 1 2 3 4 5
- 用户111 未登录,添加新文章 失败 权限不足
- 用户111 已登录,添加新文章 成功 已登录就可以upsert
- 用户111 已登录,删除文章1 成功 是他创建的,可以删
- 用户111 已登录,删除文章4 失败 不是他创建的,不能删
- 用户111 已登录,修改文章4 成功 匹配authenticated,可以修改,这不对,得通过业务代码去处理
准备云数据库
开通->新增存储区(数据库)->新建对象类型(表) 直接在云端操作版:
在开发工具里操作版:
CloudProgram/clouddb/objectType(表)或dataentry(数据)
自带的两个json文件并没有什么用,可以删掉
- 新建表 json文件
fields 定义表中的字段
indexes 定义表中的索引
objectTypeName
permission表的权限
这个json文件他自动生成了一堆东西,我们在上面改就行:
belongPrimaryKey如果为true就是主键
filedName 字段名字
filedType 是数据类型 可以提示出来类型
isNeddEncrypt是否加密
notNull 非空 如果true必须加default
indexName 索引名字 id_idx
indexList:对应哪个字段、索引排序
permission就算这个粗糙的权限和角色 - 添加对象数据
cloudDBZoneName 存储区名字,要写好不然就会新创一个存储区
objectTypeName:表名
objects:数据(创建时间是毫秒值 - 部署到云端
db-config.json 修改默认存储区
点cloud右键有一个部署,提示部署成功
调用云数据库
端侧调用
在arkts中进行增删改查
准备
添加依赖,entry/oh-package.json5加依赖,其中的long依赖就是对long类型的支持
开通新服务要重新下载agc配置文件
初始化agc连接器,自动生成,在entryability.ts的oncreate里
建立模型
操作数据库之前要建立一个模型类(实体类),用js建,不需要自己写,可以借助agc自动生成。
导出,选择js格式,js文件类型选js,云端的js选serverSDK,就会生成一个zip文件,下载到本地,解压,得到两个js文件(视频的例子是两个表)
在ets里建立一个文件夹model,放进去
导出schema
schema指云数据库中表的结构(字段、权限等
和导出模型文件类似,选择json文件,下载到本地,_数字是版本号
把这个json文件放到rawfile里
ctrl+alt+shift+L格式化(不是删掉那个格式化,是变得格式起来)
schemaVersion 版本
permission权限
objectType 有表的结构(索引,表名,字段)
弹幕说,目录下自己就有这个json文件
初始化database
要用database对象去操作,先初始化一下
// @ts-ignore
import schema from ‘./com.example.myapplication_21_cn.json’
@Entry
@Component
struct XXX {
// …
private database: Database
async aboutToAppear() {
try {
// 根据 schema 和 zoneName 创建数据库对象
this.database = cloud.database({ objectTypeInfo: schema, zoneName: “shopping” })
} catch (e) {
hilog.error(0, ‘User Query’, ‘error:’ + JSON.stringify(e))
}
}
导包,新建一个Database的对象
在aboutToAppear里写,调用cloud.database()
内部需要dataconfig类型:表信息,存储区名字,可选的两项
忽略错误
database()只有一个collection方法,选择操作哪张表,把模型类作为参数
查询
.query()返回DatabaseZoneQuery对象,这里面的结构有一个get方法,返回Promise对象,其中是一个T数组(学生)
展示查询到的学生数据:
效果:
问题1:只展示了姓名
原因:Text组件不能直接写数字,需要先转化toString(),或者拼接方式变成字符串
问题2:顺序和数据库不一样
原因:query().orderByAsc(“字段名”).get()
修改后的效果:
改进:
分页查询功能,上滑屏幕显示后续的记录
搜索(向下滑动屏幕会出现一个搜索框,模拟器没法输入汉字)
主界面里包括三个东西Row,List,Divider
效果:
整体代码:
import cloud, { Database } from '@hw-agconnect/cloud'
// @ts-ignore
import schema from '../../resources/rawfile/schema.json'
import { t_student } from '../model/t_student'
import hilog from '@ohos.hilog'
@Entry
@Component
struct StudentPage {
@State studentList: t_student[] = []
private database: Database = null
private dataOffset: number = 0
@State searchAge: string = ''
@State showSearch: boolean = false
private startY: number = 0
private startIndex: number = 0
aboutToAppear() {
this.database = cloud.database({
zoneName: 'test',
objectTypeInfo: schema
})
this.search('', '', '', this.dataOffset) //查询全部
}
//offset偏移量,limit每页多少条记录,给一个默认值
async search(id: string, name: string, age: string, offset: number, limit: number = 10) {
try {
const query = this.database.collection(t_student).query() //拿到database对象,选择表,query查询
if (id.length > 0) {
// where id=?
query.equalTo("id", Number(id))
}
if (name.length > 0) {
// where name like ?
query.contains("name", name)
}
if (age.length > 0) {
// where age=?
query.equalTo("age", Number(age))
}
query.limit(limit, offset) //参数是:一页几条,每页偏移位置
query.orderByAsc("id") //id升序
const list: t_student[] = await query.get() //真正到云数据库去查询
this.studentList.push(...list)
hilog.info(0, 'Query', `${list.map(s => s.id)}`)
} catch (e) {
hilog.error(0, 'Query', JSON.stringify(e))
}
}
build() {
Column({ space: 26 }) {
Row() {
Text(`编号`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('white')
.textAlign(TextAlign.Center)
.width('33%')
Text(`姓名`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('white')
.textAlign(TextAlign.Center)
.width('34%')
Text(`年龄`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('white')
.textAlign(TextAlign.Center)
.width('33%')
}
.width('100%')
.height(36)
.backgroundColor('black')
if (this.showSearch) {
Search()
.searchButton('搜索')
.onChange(value => {
this.searchAge = value
})
.onSubmit(() => {
this.studentList = []
this.dataOffset = 0
this.search('', '', this.searchAge, this.dataOffset)
this.showSearch = false
})
}
List({ space: 52 }) {
ForEach(this.studentList, (stu: t_student) => {
ListItem() {
Row() {
Text(`${stu.getId()}`)
.fontSize(18)
.textAlign(TextAlign.Center)
.width('33%')
Text(stu.getName())
.fontSize(18)
.textAlign(TextAlign.Center)
.width('34%')
Text(`${stu.getAge()}`)
.fontSize(18)
.textAlign(TextAlign.Center)
.width('33%')
}
.width('100%')
}
})
}
.width('100%')
.height('82%')
.onReachEnd(() => { //触底时,查询第二页记录,注意,记录一起的高度要比list高度高,否则无法触底
this.dataOffset += 10
this.search('', '', '', this.dataOffset)
})
.onScrollIndex((start, end) => { //list可视区的顶部索引值,底部索引值
hilog.info(0, 'Scroll', `start: ${start} end:${end}`)
this.startIndex = start
})
.onTouch(event => { //下滑显示搜索框
switch (event.type) {
case TouchType.Down: //按下屏幕 up是松开 event.touches触点,可以拿到坐标
this.startY = event.touches[0].y //起始的y坐标需要记录,终止的不需要记录
break;
case TouchType.Move: //滑动
const endY = event.touches[0].y
if (endY - this.startY >= 35 && this.startIndex === 0) { //必须滑动到顶部才显示搜索框
this.showSearch = true
} else if (this.startY - endY >= 25) {
this.showSearch = false
}
break;
}
})
Divider()
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
}
- 搜索方法:传入的是string,真的去查再转成number
- 有搜索组件Search,有方法.onSubmit()
- onTouch触屏事件
新增
对话框效果:
需要加@CustomDialog 装饰器 @Preview可以配合预览器使用,export抛出,自定义对话框必须有controller,控制打开关闭。
@CustomDialog
export struct StudentInsertDialog {
private name: string = ''
private age: number = 18
controller: CustomDialogController
confirm: (name: string, age: number) => void //只声明,由将来对话框的使用者去定义
private static range = (start, stop, step = 1) => Array.from({
length: (stop - start - 1) / step + 1
}, (_, i) => start + (i * step))
static AgeRange: string[] = StudentInsertDialog.range(16, 60).map(i => String(i))
build() {
Column({ space: 15 }) {
Row() {
Text('姓名')
.width('20%')
TextInput({ text: this.name })
.width('80%')
.onChange(value => {
this.name = value
})
}
.width('80%')
.justifyContent(FlexAlign.SpaceAround)
Row() {
Text('年龄')
.width('20%')
//范围,选中的索引值
TextPicker({ range: StudentInsertDialog.AgeRange, selected: 2 })
.width('80%')
.onChange(value => {
this.age = Number(value)
})
}
.width('80%')
Row() {
Button('取消')
.onClick(() => {
this.controller.close() //关闭对话框
})
.backgroundColor(0xcccccc)
Button('确定')
.onClick(() => {
//database.collection(t_student).upsert(学生对象),但这样写耦合程度太高,希望分离开
this.confirm(this.name, this.age) //用回调函数实现
this.controller.close()
})
}
.width('80%')
.justifyContent(FlexAlign.SpaceAround)
}
.justifyContent(FlexAlign.SpaceAround)
.width('80%')
.height('55%')
.margin({ top: 15 })
}
}
删除
左滑记录,点击可选修改和删除
@Builder和页面渲染相关
@Builder
showOperation(stu: t_student) {
Row({ space: 12 }) {
Image($r("app.media.ic_public_edit_filled"))
.width(20)
.onClick(() => {
this.openStudentUpdateDialog(stu)
})
Image($r("app.media.ic_public_delete_filled"))
.width(21)
.onClick(() => {
this.openStudentDeleteDialog(stu)
})
}
.width(80)
.justifyContent(FlexAlign.Start)
}
加到listitem里面
ListItem() {
Row() {
Text(`${stu.getId()}`)
.fontSize(18)
.textAlign(TextAlign.Center)
.width('33%')
Text(stu.getName())
.fontSize(18)
.textAlign(TextAlign.Center)
.width('34%')
Text(`${stu.getAge()}`)
.fontSize(18)
.textAlign(TextAlign.Center)
.width('33%')
}
.width('100%')
}
.swipeAction({ end: this.showOperation.bind(this, stu) })
//函数作为参数传递时,会出现this丢失问题,必须调用bind方法
//有剩余参数问题,必须调用bind方法
this丢失现象说明:a和print的关系断了,bind就是用来绑定他们的
剩余参数问题说明:
openStudentDeleteDialog(stu: t_student) {
AlertDialog.show({
title: '请确认',
message: '真的要删除该用户吗?',
confirm: {
value: '确定',
action: async () => {
try {
await this.database.collection(t_student).delete(stu)
AlertDialog.show({ message: '删除学生成功' })
hilog.info(0, 'Student Delete', 'Success')
//更新显示
this.dataOffset = 0
this.studentList = []
this.search('', '', '', this.dataOffset)
} catch (e) {
hilog.error(0, 'Student Delete', JSON.stringify(e))
}
}
}
})
}
效果:
修改
import { StudentInsertDialog } from './StudentInsertDialog'
@CustomDialog
export struct StudentUpdateDialog {
private sid: number//不能叫id,因为自己有一个id会冲突
private name: string
private age: number
controller: CustomDialogController
confirm: (id: number, name: string, age: number) => void
build() {
Column({ space: 15 }) {
Text(`${this.sid}`)
Row() {
Text('姓名')
.width('20%')
TextInput({ text: this.name })
.width('80%')
.onChange(value => {
this.name = value
})
}
.width('80%')
.justifyContent(FlexAlign.SpaceAround)
Row() {
Text('年龄')
.width('20%')
TextPicker({ range: StudentInsertDialog.AgeRange, selected: 2 }) //value:`${this.age}`
.width('80%')
.onChange(value => {
this.age = Number(value)
})
}
.width('80%')
Row() {
Button('取消')
.onClick(() => {
this.controller.close()
})
.backgroundColor(0xcccccc)
Button('确定')
.onClick(() => {
this.confirm(this.sid, this.name, this.age)
this.controller.close()
})
}
.width('80%')
.justifyContent(FlexAlign.SpaceAround)
}
.justifyContent(FlexAlign.SpaceAround)
.width('80%')
.height('65%')
.margin({ top: 15 })
}
}
openStudentUpdateDialog(stu: t_student) {
this.studentUpdateDialogController = new CustomDialogController({
builder: StudentUpdateDialog({
sid: stu.id, name: stu.name, age: stu.age,
confirm: async (id, name, age) => { //把初始值给到这三个属性
try {
const stu = new t_student()
stu.setId(id)
stu.setName(name)
stu.setAge(age)
await this.database.collection(t_student).upsert(stu)
AlertDialog.show({ message: '修改学生成功' })
hilog.info(0, 'Student Update', 'Success')
this.dataOffset = 0
this.studentList = []
this.search('', '', '', this.dataOffset)
} catch (e) {
hilog.error(0, 'Student Update', JSON.stringify(e))
}
}
})
})
this.studentUpdateDialogController.open()
}
效果:
整体studentPage代码:
import cloud, { Database } from '@hw-agconnect/cloud'
// @ts-ignore
import schema from '../../resources/rawfile/schema.json'
import { t_student } from '../model/t_student'
import hilog from '@ohos.hilog'
import { StudentInsertDialog } from './StudentInsertDialog'
import router from '@ohos.router'
import { StudentUpdateDialog } from './StudentUpdateDialog'
@Entry
@Component
struct StudentPage {
@State studentList: t_student[] = []
private database: Database = null
private dataOffset: number = 0
@State searchAge: string = ''
@State showSearch: boolean = false
private startY: number = 0
private startIndex: number = 0
//对话框的控制器
private studentInsertDialogController: CustomDialogController = new CustomDialogController({
builder: StudentInsertDialog({ //使用这个对话框,需要传入函数
confirm: async (name, age) => {
try {
const stu = new t_student()
stu.setName(name)
stu.setAge(age)
await this.database.collection(t_student).upsert(stu)
AlertDialog.show({ message: '添加学生成功' }) //给用户一个提示
hilog.info(0, 'Student Insert', 'Success')
this.dataOffset = 0 //重新查询
this.studentList = []
this.search('', '', '', this.dataOffset)
} catch (e) {
hilog.error(0, 'Student Insert', JSON.stringify(e))
}
}
})
})
private studentUpdateDialogController: CustomDialogController
openStudentUpdateDialog(stu: t_student) {
this.studentUpdateDialogController = new CustomDialogController({
builder: StudentUpdateDialog({
sid: stu.id, name: stu.name, age: stu.age,
confirm: async (id, name, age) => {
try {
const stu = new t_student()
stu.setId(id)
stu.setName(name)
stu.setAge(age)
await this.database.collection(t_student).upsert(stu)
AlertDialog.show({ message: '修改学生成功' })
hilog.info(0, 'Student Update', 'Success')
this.dataOffset = 0
this.studentList = []
this.search('', '', '', this.dataOffset)
} catch (e) {
hilog.error(0, 'Student Update', JSON.stringify(e))
}
}
})
})
this.studentUpdateDialogController.open()
}
openStudentDeleteDialog(stu: t_student) {
AlertDialog.show({
title: '请确认',
message: '真的要删除该用户吗?',
confirm: {
value: '确定',
action: async () => {
try {
await this.database.collection(t_student).delete(stu)
AlertDialog.show({ message: '删除学生成功' })
hilog.info(0, 'Student Delete', 'Success')
this.dataOffset = 0
this.studentList = []
this.search('', '', '', this.dataOffset)
} catch (e) {
hilog.error(0, 'Student Delete', JSON.stringify(e))
}
}
}
})
}
@Builder
showOperation(stu: t_student) {
Row({ space: 12 }) {
Image($r("app.media.ic_public_edit_filled"))
.width(20)
.onClick(() => {
this.openStudentUpdateDialog(stu)
})
Image($r("app.media.ic_public_delete_filled"))
.width(21)
.onClick(() => {
this.openStudentDeleteDialog(stu)
})
}
.width(80)
.justifyContent(FlexAlign.Start)
}
aboutToAppear() {
this.database = cloud.database({
zoneName: 'test',
objectTypeInfo: schema
})
this.search('', '', '', this.dataOffset)
}
async search(id: string, name: string, age: string, offset: number, limit: number = 10) {
try {
const query = this.database.collection(t_student).query()
if (id.length > 0) {
// where id=?
query.equalTo("id", Number(id))
}
if (name.length > 0) {
// where name like ?
query.contains("name", name)
}
if (age.length > 0) {
// where age=?
query.equalTo("age", Number(age))
}
query.limit(limit, offset)
query.orderByAsc("id")
const list: t_student[] = await query.get()
this.studentList.push(...list)
hilog.info(0, 'Student Query', `${list.map(s => s.id)}`)
} catch (e) {
hilog.error(0, 'Student Query', JSON.stringify(e))
}
}
build() {
Column() {
Row() {
Text(`编号`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('white')
.textAlign(TextAlign.Center)
.width('33%')
Text(`姓名`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('white')
.textAlign(TextAlign.Center)
.width('34%')
Text(`年龄`)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('white')
.textAlign(TextAlign.Center)
.width('33%')
}
.width('100%')
.height(36)
.backgroundColor('black')
if (this.showSearch) {
Search()
.searchButton('搜索')
.onChange(value => {
this.searchAge = value
})
.onSubmit(() => {
this.studentList = []
this.dataOffset = 0
this.search('', '', this.searchAge, this.dataOffset)
this.showSearch = false
})
}
List({ space: 48 }) {
ForEach(this.studentList, (stu: t_student) => {
ListItem() {
Row() {
Text(`${stu.getId()}`)
.fontSize(18)
.textAlign(TextAlign.Center)
.width('33%')
Text(stu.getName())
.fontSize(18)
.textAlign(TextAlign.Center)
.width('34%')
Text(`${stu.getAge()}`)
.fontSize(18)
.textAlign(TextAlign.Center)
.width('33%')
}
.width('100%')
}
.swipeAction({ end: this.showOperation.bind(this, stu) })
})
}
.width('100%')
.height('78%')
.margin({ top: 24, bottom: 24 })
.onReachEnd(() => {
this.dataOffset += 10
this.search('', '', '', this.dataOffset)
})
.onScrollIndex((start, end) => {
hilog.info(0, 'Scroll', `start: ${start} end:${end}`)
this.startIndex = start
})
.onTouch(event => {
switch (event.type) {
case TouchType.Down:
this.startY = event.touches[0].y
break;
case TouchType.Move:
const endY = event.touches[0].y
if (endY - this.startY >= 25 && this.startIndex === 0) {
this.showSearch = true
} else if (this.startY - endY >= 5 || this.startIndex === 0) {
this.showSearch = false
}
break;
}
})
Button('新增', { type: ButtonType.Normal })
.width('100%')
.margin({ bottom: 6 })
.onClick(async () => {
this.studentInsertDialogController.open()
})
Button('登出', { type: ButtonType.Normal })
.width('100%')
.backgroundColor(0xcccccc)
.margin({ bottom: 6 })
.onClick(async () => {
try {
await cloud.auth().signOut()
hilog.info(0, 'SignOut', 'Success')
router.replaceUrl({ url: 'pages/MyLoginCustom' })
} catch (e) {
hilog.error(0, 'SignOut', JSON.stringify(e))
}
})
Divider()
}
.width('100%')
.justifyContent(FlexAlign.Start)
}
}
云侧调用
arkts先调云函数,云函数再调云数据库