作者:狼哥
团队:坚果派
团队介绍:坚果派由坚果等人创建,团队拥有12个华为HDE带领热爱HarmonyOS/OpenHarmony的开发者,以及若干其他领域的三十余位万粉博主运营。专注于分享HarmonyOS/OpenHarmony、ArkUI-X、元服务、仓颉。团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙原生应用,三方库60+,欢迎交流。

注意

当前API12的端云一体化开发工程仅支持手动签名

简介

此案例是一个简单的学生信息管理系统,使用到MVVM模式开发、本地使用关系型数据库存储数据、远程使用云数据库存储数据,并用云函数调用云数据库,应用启动时通过云函数调用云数据库,把云数据库数据更新到关系型数据库,应用切换到后台时,把本地关系型数据库数据,通过调用云函数T云数据库,把本地数据上传到云数据库。

知识点

  1. MVVM模式
  2. 关系型数据库
  3. 云函数T云数据库
  4. 案例讲解

1. MVVM模式

应用通过状态去渲染更新UI是程序设计中相对复杂,但又十分重要的,往往决定了应用程序的性能。程序的状态数据通常包含了数组、对象,或者是嵌套对象组合而成。在这些情况下,ArkUI采取MVVM = Model + View + ViewModel模式,其中状态管理模块起到的就是ViewModel的作用,将数据与视图绑定在一起,更新数据的时候直接更新视图。

  • Model层:存储数据和相关逻辑的模型。它表示组件或其他相关业务逻辑之间传输的数据。Model是对原始数据的进一步处理。
  • View层:在ArkUI中通常是@Component装饰组件渲染的UI。
  • ViewModel层:在ArkUI中,ViewModel是存储在自定义组件的状态变量、LocalStorage和AppStorage中的数据。

    • 自定义组件通过执行其build()方法或者@Builder装饰的方法来渲染UI,即ViewModel可以渲染View。
    • View可以通过相应event handler来改变ViewModel,即事件驱动ViewModel的改变,另外ViewModel提供了@Watch回调方法用于监听状态数据的改变。
    • 在ViewModel被改变时,需要同步回Model层,这样才能保证ViewModel和Model的一致性,即应用自身数据的一致性。
    • ViewModel结构设计应始终为了适配自定义组件的构建和更新,这也是将Model和ViewModel分开的原因。

2. 关系型数据库

关系型数据库(Relational Database,RDB)是一种基于关系模型来管理数据的数据库。关系型数据库基于SQLite组件提供了一套完整的对本地数据库进行管理的机制,对外提供了一系列的增、删、改、查等接口,也可以直接运行用户输入的SQL语句来满足复杂的场景需要。支持通过ResultSet.getSendableRow方法获取Sendable数据,进行跨线程传递。

为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。

大数据量场景下查询数据可能会导致耗时长甚至应用卡死,建议如下:

  • 单次查询数据量不超过5000条。
  • TaskPool中查询。
  • 拼接SQL语句尽量简洁。
  • 合理地分批次查询。

该模块提供以下关系型数据库相关的常用功能:

  • RdbPredicates: 数据库中用来代表数据实体的性质、特征或者数据实体之间关系的词项,主要用来定义数据库的操作条件。
  • RdbStore:提供管理关系数据库(RDB)方法的接口。
  • ResultSet:提供用户调用关系型数据库查询接口之后返回的结果集合。

3. 云函数T云数据库

官方文档提供了云函数的Demo包,您只需要简单配置后,在创建云函数时将其上传,就可以快速在应用中通过云函数进行数据库添加,删除和修改的操作。

Demo包的结构如下图所示。

img

开发者只需简单的配置并上传函数后就可以通过指令进行数据库各种查询操作。

  • CloudDBZoneWrapper.js:用于配置数据库相关信息和对数据库操作的逻辑。
  • resource:存放认证凭证json文件和一些资源文件。

完整定制流程如下图所示。

4. 案例讲解

4.1 效果图
4.2 代码结构
├──AppScope/resources                         
│  └──rawfile
│     └──agconnect-services.json              // 认证凭证
├──entry/src/main/ets                         // 代码区
│  ├──common
│  │  └──sql.ets                                // sql常量
│  ├──db
│  │  ├──cloud
│  │  │  └──StudentCloud.ets                  // 调用云函数操作
│  │  └──rdb
│  │     ├──RDB.ets                            // 关系型数据库获取
│  │     └──StudentDAO.ets                    // 调用关系型数据库操作
│  ├──entryability
│  │  └──EntryAbility.ets 
│  ├──model
│  │  └──Student.ets                          // 学生实例类
│  └──pages
│  │  └──Index.ets                            // 首页
│  └──view
│  │  ├──ListContent.ets                      // 列表内容项
│  │  └──ListTitle.ets                        // 列表标题项
│  └──viewmodel
│     ├──AddIcon.ets                            // 新增学生信息按钮
│     ├──Content.ets                            // 显示学生信息列表
│     ├──DialogContent.ets                    // 新增或编辑学生信息对话框
│     └──Header.ets                              // 页面标题
└──entry/src/main/resources                   // 应用资源目录
4.3 首页布局
Stack({alignContent: Alignment.BottomStart}) {
      Column() {
        // 标题
        Header({ title: "学生信息" })
        // 学生信息列表
        Content({
          data: this.data, 
          updck: (stu: Student) => {}, 
          delck: (stu: Student) => {}
        })
      }
      .height('100%')
      .width('100%')
      // 增加按钮
      AddIcon({
        okck: () => {}
      })
    }
    .width('100%')
    .height('100%')
4.4 页面内容列表
@Component
export struct Content {
  @Link data: Student[];
  updck: (stu: Student) => void = () => {}
  delck: (stu: Student) => void = () => {}
  build() {
    List() {
      ListTitle()
      ForEach(this.data, (item: Student) => {
        ListContent(item, this.updck, this.delck)
      }, (item: Student) => JSON.stringify(item))
    }
    .width('100%')
    .layoutWeight(1)
    .divider({strokeWidth: 1, color: '#ffecebeb'})
  }
}
@Builder
export function ListTitle() {
  Row() {
    Text('姓名').width('20%')
      .fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
    Text('年龄').width('15%')
      .fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
    Text('学校').width('30%')
      .fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
    Text('操作').width('35%')
      .fontSize(20).fontWeight(FontWeight.Bold).fontColor(Color.White)
      .textAlign(TextAlign.Center)
  }
  .width('100%').height(50).padding(5)
  .backgroundColor('rgba(0,0,0,0.4)')
  .justifyContent(FlexAlign.SpaceBetween)
}
@Builder
export function ListContent(stu: Student, updck:(stu: Student)=>void, delck:(stu:Student)=>void) {
  Row() {
    Text(stu.name).fontSize(20).width('20%')
    Text(stu.age+'').fontSize(20).width('15%')
    Text(stu.school).fontSize(20).width('30%')
    Row() {
      Text('更新').padding(10).borderRadius(5)
        .fontColor(Color.White).backgroundColor(Color.Blue)
        .onClick(() => {
          updck(stu)
        })
      Text('删除').padding(10).borderRadius(5)
        .fontColor(Color.White).backgroundColor(Color.Red)
        .onClick(() => {
          delck(stu)
        })
    }
    .width('35%')
    .justifyContent(FlexAlign.SpaceEvenly)
  }
  .width('100%').padding(5)
  .justifyContent(FlexAlign.SpaceBetween)
}
4.5 RDB关系型数据库初始化
import { relationalStore } from '@kit.ArkData'

const config: relationalStore.StoreConfig = {
  name: 'stu.db',
  securityLevel: relationalStore.SecurityLevel.S2
}
export async function getRdb():Promise<relationalStore.RdbStore> {
  return relationalStore.getRdbStore(globalThis.context, config);
}
4.6 StudentDAO关系型数据库操作
// 查询所有学生信息
export async function findAllStudents(): Promise<Student[]> {
  let store = await getRdb();
  let query = new relationalStore.RdbPredicates(TABLE_NAME);
  let rs = await store.query(query);
  let students = new Array<Student>();

  while (rs.goToNextRow()) {
    let stu = new Student(rs.getLong(rs.getColumnIndex("id")), rs.getString(rs.getColumnIndex("name")), rs.getLong(rs.getColumnIndex("age")), rs.getString(rs.getColumnIndex("school")));
    students.push(stu)
  }
  return students;
}
// 编辑学生信息
export async function updateStudent(stu: Student): Promise<number> {
  let store = await getRdb();
  let query = new relationalStore.RdbPredicates(TABLE_NAME);
  query.equalTo("id", stu.id);
  let values: relationalStore.ValuesBucket = {
    "name": stu.name,
    "age": stu.age,
    "school": stu.school
  }

  return store.update(values, query);
}
// 删除学生信息
export async function deleteStudent(stu: Student): Promise<number> {
  let store = await getRdb();
  let query = new relationalStore.RdbPredicates(TABLE_NAME);
  query.equalTo("id", stu.id);

  return store.delete(query);
}
// 新增学生信息
export async function addStudent(stu: Student): Promise<number> {
  let store = await getRdb();
  let values: relationalStore.ValuesBucket = {
    "name": stu.name,
    "age": stu.age,
    "school": stu.school
  }

  console.debug(TAG, `xx 插入到数据库内容:${JSON.stringify(values)}`)
  return store.insert(TABLE_NAME, values);
}
4.7 StudentCloud云函数操作
/**
 * 上传学生信息到云数据库
 * @returns
 */
export async function uploadStudents(): Promise<number> {
  console.debug(TAG, '开始上传本地数据到云数据库')
  let students: Student[] = await findAllStudents()
  console.debug(TAG, `上传数据到云数据库条数:${JSON.stringify(students.length)}`)
  //@TODO 调用云函数上传全部学生信息
  let params: Params = {
    action: "upsert",
    extraData: students
  } as Params

  return new Promise<number>((resolve, reject) => {
    cloudFunction.call({name: 'upload-students', data: params}).then(async (result) => {
      console.debug(TAG, `上传本地数据到云数据库结果:${JSON.stringify(result)}`)
      resolve(1)
    }).catch(async (err: BusinessError) => {
      console.debug(TAG, `上传本地数据到云数据库异常:${JSON.stringify(err)}`)
      reject(0)
    })
  })
}

/**
 * 同步云数据库学生信息到本地
 * @returns
 */
export async function downloadStudents(): Promise<number> {
  console.debug(TAG, '开始下载云数据库学生信息到本地')
  //@TODO 调用云函数下载全部学生信息
  let params: Params = {
    action: "all", // all,upsert,delete
    extraData: []
  } as Params
  return new Promise<number>((resolve, reject) => {
    cloudFunction.call({name: 'upload-students', data: params}).then(async (res) => {
      let cloudStudents: Student[] = res.result as Student[];
      console.debug(TAG, `下载云数据库学生信息人数:${cloudStudents.length}`)
      return batchUpdateStudent(cloudStudents)
    }).catch(async (err: BusinessError) => {
      console.debug(TAG, `下载云数据库学生信息异常:${JSON.stringify(err)}`)
      reject(-1)
    })
  })
}
4.8 EntryAbility初始化关系型数据库与AGC
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    hilog.info(0x0000, TAG, '%{public}s', 'Ability onCreate');
    globalThis.context = this.context;
    // 初始化数据库和表
    let store:relationalStore.RdbStore = await getRdb();
    store.executeSync(CREATE_TABLE)
    // 初始化SDK
    let input = await this.context.resourceManager.getRawFileContent('agconnect-services.json')
    let jsonString  = util.TextDecoder.create('utf-8', {
      ignoreBOM: true
    }).decodeWithStream(input, {
      stream: false
    });
    initialize(this.context, JSON.parse(jsonString));
  }
4.9 同步与下载数据

在onCreate回调函数同步云数据库数据到本地关系型数据库

    // 同步云数据库数据到本地数据库
    downloadStudents().then((num: number) => {
      hilog.info(0x0000, TAG, '%{public}s', `同步云数据库数据到本地数据库结果是:${num}`);
    }).catch((err: BusinessError) => {
      hilog.info(0x0000, TAG, '%{public}s', `同步云数据库数据到本地数据库异常:${JSON.stringify(err)}`);
    })

在onBackground回调函数上传本地关系型数据库数据到云数据库

// 上传本地数据到云数据库
    uploadStudents().then((num: number) => {
      hilog.info(0x0000, TAG, '%{public}s', `上传数据到云数据库结果是:${num}`);
    }).catch((err: BusinessError) => {
      hilog.info(0x0000, TAG, '%{public}s', `上传数据到云数据库异常:${JSON.stringify(err)}`);
    })

总结

通过学习此案例,学会使用MVVM模式开发,学会关系型数据库开发,学会调用云函数同步本地数据到云数据库与下载云数据库数据到本地数据库,在MVVM模式开发里,学到如何在@Builder注解function组件里传参事件,关于云函数调用云数据库操作,可以参考另一篇文章 轻松上手-Serverless模板-云存储-云数据库-云函数

约束与限制

1.本示例仅支持标准系统上运行,支持设备:华为手机。

2.HarmonyOS系统:HarmonyOS NEXT Developer Beta1及以上。

3.DevEco Studio版本:DevEco Studio NEXT Developer Beta1及以上。

4.HarmonyOS SDK版本:HarmonyOS NEXT Developer Beta1 SDK及以上。


狼哥
1 声望0 粉丝