【鸿蒙HarmonyOS NEXT】数据存储之关系型数据库RDS

【鸿蒙HarmonyOS NEXT】数据存储之关系型数据库RDS

一、环境说明

  1. DevEco Studio 版本:

  2. API版本:以12为主

二、关系型数据库RDS介绍

1. RDS关系型数据库简介

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

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

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

  1. 单次查询数据量不超过5000条。
  • 在TaskPool中查询。
  • 拼接SQL语句尽量简洁。
  • 合理地分批次查询。
  • 该模块提供以下关系型数据库相关的常用功能:
  1. RdbPredicates: 数据库中用来代表数据实体的性质、特征或者数据实体之间关系的词项,主要用来定义数据库的操作条件。
  2. RdbStore:提供管理关系数据库(RDB)方法的接口。
  3. ResultSet:提供用户调用关系型数据库查询接口之后返回的结果集合。

2. 基本概念

  • 谓词:数据库中用来代表数据实体的性质、特征或者数据实体之间关系的词项,主要用来定义数- 据库的操作条件。

  • 结果集:指用户查询之后的结果集合,可以对数据进行访问。结果集提供了灵活的数据访问方式,可以更方便地拿到用户想要的数据。

3. RDS应用场景:

关系型数据库基于SQLite组件,适用于存储包含复杂关系数据的场景,比如一个班级的学生信息,需要包括姓名、学号、各科成绩等,又或者公司的雇员信息,需要包括姓名、工号、职位等,由于数据之间有较强的对应关系,复杂程度比键值型数据更高,此时需要使用关系型数据库来持久化保存数据。

4. 运作机制介绍:

关系型数据库对应用提供通用的操作接口,底层使用SQLite作为持久化存储引擎,支持SQLite具有的数据库特性,包括但不限于事务、索引、视图、触发器、外键、参数化查询和预编译SQL语句。

三、示例代码加以说明

沿用【鸿蒙HarmonyOS NEXT】页面之间相互传递参数博文中的代码,进行测试。
使用场景模拟将用户登录的信息存储到关系型数据库中,并将RDS中的数据库抽取展示出来。

代码改写如下:

  1. 新增一个名为model的目录

  2. model目录下创建名称为UserInfo的Ark TS文件,用于封装用户信息表的数据,代码如下:

    typescript 复制代码
    /**
     * 构造数据库表CLIENT_USER对应的实体类
     */
    export default class UserInfo {
      public id: number
      public account: string
      public password: string
    
      constructor(id: number, account: string, password: string) {
        this.id = id;
        this.account = account;
        this.password = password;
      }
    }
  3. LoginPage完整代码如下:

    typescript 复制代码
    import { router } from '@kit.ArkUI';
    
    // 引入Context相关
    import { common } from '@kit.AbilityKit';
    
    // 引入RDB相关
    import { relationalStore } from '@kit.ArkData';
    import { BusinessError } from '@kit.BasicServicesKit';
    
    
    @Preview
    @Entry
    @Component
    struct LoginPage {
      @State message: string = '登录页';
      @State btnMsg: string = '登录';
      @State account: string = ''; // 账号状态变量
      @State password: string = ''; // 密码状态变量
    
      // 获取Context
      private context = getContext(this) as common.UIAbilityContext;
      // 定义RdbStore
      private rdbStore: relationalStore.RdbStore | undefined = undefined;
    
      aboutToAppear(): void {
    
        // 构造StoreConfig对象
        const STORE_CONFIG: relationalStore.StoreConfig = {
          name: 'RdbTest.db', // 数据库文件名
          securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别
          encrypt: false, // 可选参数,指定数据库是否加密,默认不加密
          customDir: 'customDir/subCustomDir', // 可选参数,数据库自定义路径。数据库将在如下的目录结构中被创建:context.databaseDir + '/rdb/' + customDir,其中context.databaseDir是应用沙箱对应的路径,'/rdb/'表示创建的是关系型数据库,customDir表示自定义的路径。当此参数不填时,默认在本应用沙箱目录下创建RdbStore实例。
          isReadOnly: false // 可选参数,指定数据库是否以只读方式打开。该参数默认为false,表示数据库可读可写。该参数为true时,只允许从数据库读取数据,不允许对数据库进行写操作,否则会返回错误码801。
        };
    
        // 判断数据库版本,如果不匹配则需进行升降级操作
        // 默认数据库版本为0,表结构:CLIENT_USER (ID INTEGER PRIMARY KEY AUTOINCREMENT, ACCOUNT TEXT NOT NULL, PASSWORD TEXT NOT NULL)'
        const SQL_CREATE_TABLE =
          'CREATE TABLE IF NOT EXISTS CLIENT_USER (ID INTEGER PRIMARY KEY AUTOINCREMENT, ACCOUNT TEXT NOT NULL, PASSWORD TEXT NOT NULL)'; // 建表Sql语句
    
        // 获得一个相关的RdbStore,操作关系型数据库,用户可以根据自己的需求配置RdbStore的参数,然后通过RdbStore调用相关接口可以执行相关的数据操作,使用callback异步回调。
        relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, store) => {
          if (err) {
            console.error(`At LoginPage Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
            return;
          }
          console.info('At LoginPage Succeeded in getting RdbStore.');
          // 当数据库创建时,数据库默认版本为0
          if (store.version === 0) {
            store.executeSql(SQL_CREATE_TABLE); // 创建数据表
            // 设置数据库的版本,入参为大于0的整数
            store.version = 1;
          }
          // 赋值给当前的rdbStore对象,确保获取到RdbStore实例后,再进行数据库的增、删、改、查等操作
          this.rdbStore = store
        });
      }
    
      build() {
        Column() {
          Text(this.message)
            .id('HelloWorld')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .width('100%')
            .height(50)
            .textAlign(TextAlign.Center)
            .backgroundColor(0xF1F3F5)
    
          Image($r('app.media.startIcon'))
            .width(150)
            .height(150)
            .margin({ top: 40, bottom: 40 })
    
          TextInput({ placeholder: '请输入手机号' })
            .maxLength(11)// 最大长度
            .type(InputType.Number)// 输入类型为数字
            .inputStyle()// 应用自定义样式
            .onChange((value: string) => {
              this.account = value; // 更新账号状态
            })
          Line().lineStyle() // 应用自定义Line样式
    
          // 密码输入框
          TextInput({ placeholder: '请输入密码' })
            .maxLength(12)// 最大长度
            .type(InputType.Password)// 输入类型为密码
            .inputStyle()// 应用自定义样式
            .onChange((value: string) => {
              // TODO: 生产环境需要使用正则表达式对手机号进行验证
              this.password = value; // 更新密码状态
            })
          Line().lineStyle() // 应用自定义Line样式
    
    
          Button(this.btnMsg)
            .width('80%')
            .margin({ top: 100 })
            .height(50)
            .onClick(() => {
              if (this.account === undefined || this.account === '') {
                console.info('请输入账号')
                return
              }
    
              if (this.password === undefined || this.password === '') {
                console.info('请输入密码')
                return
              }
    
              // 使用ArkData RDB数据库 以建表方式存储用户信息数据
              if (this.rdbStore !== undefined) {
                // 构造insert函数所需ValuesBucket对象
                const userInfo: relationalStore.ValuesBucket = {
                  ACCOUNT: this.account,
                  PASSWORD: this.password
                };
                // 插入数据到指定表中
                this.rdbStore.insert('CLIENT_USER', userInfo, (err: BusinessError, rowId: number) => {
                  // 如果成功插入,则返回该行所在表中的行号,否则返回-1
                  if (rowId === -1) {
                    console.error(`At LoginPage,插入数据失败,错误代码:${err.code},错误提示:${err.message}`)
                    return
                  }
                  console.info(`At LoginPage,插入数据成功,rowId为 ${rowId}`)
                })
              }
    
              // 跳转到首页
              router.pushUrl({
                url: 'pages/HomePage',
                params: {
                  account: this.account,
                  password: this.password
                }
              })
            })
        }
        .height('100%')
        .width('100%')
        .padding(0)
      }
    }
    
    // TextInput组件的自定义样式扩展
    @Extend(TextInput)
    function inputStyle() {
      .placeholderColor(Color.Gray) // 占位符颜色
      .height(50) // 输入框高度
      .fontSize(15) // 字体大小
      .backgroundColor(0xF1F3F5) // 背景颜色
      .width('90%') // 宽度为父组件的100%
      .padding({ left: 12 }) // 左侧填充
      .margin({ top: 15 }) // 上方边距
    }
    
    // Line组件的自定义样式扩展
    @Extend(Line)
    function lineStyle() {
      .width('100%') // 宽度为父组件的100%
      .height(1) // 高度
      .backgroundColor(0xF1F3F5) // 背景颜色
    }
  4. HomePage完整代码如下:

    typescript 复制代码
    import { router } from '@kit.ArkUI';
    import { relationalStore } from '@kit.ArkData';
    import { common } from '@kit.AbilityKit'
    import { BusinessError } from '@kit.BasicServicesKit';
    import UserInfo from '../model/UserInfo';
    
    
    @Preview
    @Entry
    @Component
    struct HomePage {
      @State message: string = '首页';
      // 定义List组件所需展示的数组,初始化为null
      @State listUserInfo: Array<UserInfo> = []
      // 获取Context
      private context = getContext(this) as common.UIAbilityContext;
      // 定义RdbStore
      private rdbStore: relationalStore.RdbStore | undefined = undefined;
    
      aboutToAppear(): void {
        // 使用ArkData RDB数据库 获取用户数据
        // 构造StoreConfig对象
        const STORE_CONFIG: relationalStore.StoreConfig = {
          name: 'RdbTest.db', // 数据库文件名
          securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别
          encrypt: false, // 可选参数,指定数据库是否加密,默认不加密
          customDir: 'customDir/subCustomDir', // 可选参数,数据库自定义路径。数据库将在如下的目录结构中被创建:context.databaseDir + '/rdb/' + customDir,其中context.databaseDir是应用沙箱对应的路径,'/rdb/'表示创建的是关系型数据库,customDir表示自定义的路径。当此参数不填时,默认在本应用沙箱目录下创建RdbStore实例。
          isReadOnly: false // 可选参数,指定数据库是否以只读方式打开。该参数默认为false,表示数据库可读可写。该参数为true时,只允许从数据库读取数据,不允许对数据库进行写操作,否则会返回错误码801。
        };
    
        // 获得一个相关的RdbStore,操作关系型数据库,用户可以根据自己的需求配置RdbStore的参数,然后通过RdbStore调用相关接口可以执行相关的数据操作,使用callback异步回调。
        relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, store) => {
          if (err) {
            console.error(`At HomePage Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
            return;
          }
          console.info('At HomePage Succeeded in getting RdbStore.');
          // 当数据库创建时,数据库已更新版本为1
          // 赋值给当前的rdbStore对象,确保获取到RdbStore实例后,再进行数据库的增、删、改、查等操作
          if (store.version === 1) {
            this.rdbStore = store
            // 测试查询出来看看
            //let clientUserPredicate = new relationalStore.RdbPredicates('CLIENT_USER') // CLIENT_USER 为表名称
            this.rdbStore.querySql('SELECT ID,ACCOUNT,PASSWORD FROM CLIENT_USER ORDER BY ID DESC;',
              (err: BusinessError, resultSet) => {
                if (err) {
                  console.error(`LoginPage 读取数据库表失败,错误代码:${err.code},错误信息为:${err.message}`)
                  return
                }
                console.info(`读取的结果为(对象形式): ${resultSet}`)
                // resultSet是一个数据集合的游标,默认指向第-1个记录,有效的数据从0开始。
                while (resultSet.goToNextRow()) {
                  const id = resultSet.getLong(resultSet.getColumnIndex('ID'));
                  const account = resultSet.getLong(resultSet.getColumnIndex('ACCOUNT'));
                  const passwd = resultSet.getString(resultSet.getColumnIndex('PASSWORD'));
                  console.info(`id=${id}, account=${account}, passwd=${passwd}`);
                  let userInfo = new UserInfo(id, account.toString(), passwd)
                  this.listUserInfo.push(userInfo)
                }
                // 释放数据集的内存
                resultSet.close();
              })
          }
        });
      }
    
      build() {
        Column() {
          Text(this.message)
            .fontSize(30)
            .width('100%')
            .height(50)
            .textAlign(TextAlign.Center)
            .backgroundColor(0xF1F3F5)
    
          Blank().height(10)
    
          List({ initialIndex: 0, space: 5 }) {
            ForEach(this.listUserInfo, (user: UserInfo, index: number) => {
              ListItem() {
                Row() {
                  Column() {
                    Image($r('app.media.goods_icon_svg')).width(80).height(60)
                  }.width('10%')
    
                  Column() {
                    Text(`ID: ${user.id}`).width('100%').padding({left: 10})
                    Text(`账号: ${user.account}`).width('100%').padding({left: 10})
                    Text(`密码:${user.password}`).width('100%').padding({left: 10})
                  }.width('90%')
                }
                .alignItems(VerticalAlign.Center)
                .width('100%')
              }
              .height(80)
              .width('100%')
              .backgroundColor(0xF1F3F5)
    
            }, (item: UserInfo, index: number) => index.toString())
          }
          .height('80%')
          .width('100%')
    
          Button('返回上一页')
            .width('80%')
            .margin({ top: 10 })
            .height(50)
            .onClick(() => {
              // 返回登录页面
              router.showAlertBeforeBackPage({ message: '确认返回上一页吗?' })
              router.back({
                url: 'pages/LoginPage',
                params: {
                  msg: 'homepage'
                }
              })
            })
        }
        .height('100%')
        .width('100%')
      }
    }

测试步骤如下:

  1. 打开模拟器,并将代码部署到模拟器上,当模拟器正常运行代码后,输入用户名和密码

  2. 点击手机模拟器上应用的登录按钮,跳转到了首页,然后再从首页返回登录页,查看控制台日志,内容如截图红色框或者首页文字所示:

四、小结

通过上述的说明和示例演示,相信大家已经很清楚关系型数据库RDS读写数据的用法了。细心的读者朋友可能会问,如何删除数据呢?或者有何约束和限制呢?感兴趣的读者朋友可以对照鸿蒙官网上的示例尝试下,欢迎大家的留言,我们在留言区进行讨论。

相关推荐
SameX2 小时前
HarmonyOS Next 安全生态构建与展望
前端·harmonyos
SameX3 小时前
HarmonyOS Next 打造智能家居安全系统实战
harmonyos
Random_index10 小时前
#Uniapp篇:支持纯血鸿蒙&发布&适配&UIUI
uni-app·harmonyos
鸿蒙自习室14 小时前
鸿蒙多线程开发——线程间数据通信对象02
ui·harmonyos·鸿蒙
SuperHeroWu716 小时前
【HarmonyOS】鸿蒙应用接入微博分享
华为·harmonyos·鸿蒙·微博·微博分享·微博sdk集成·sdk集成
zhangjr057518 小时前
【HarmonyOS Next】鸿蒙实用装饰器一览(一)
前端·harmonyos·arkts
诗歌难吟4641 天前
初识ArkUI
harmonyos
SameX1 天前
HarmonyOS Next 设备安全特性深度剖析学习
harmonyos
郭梧悠1 天前
HarmonyOS(57) UI性能优化
ui·性能优化·harmonyos
郝晨妤2 天前
鸿蒙原生应用开发元服务 元服务是什么?和App的关系?(保姆级步骤)
android·ios·华为od·华为·华为云·harmonyos·鸿蒙