一、案例实现

1. 13.4.2.2 案例运用的知识点

  • 关系型数据库:基于关系模型来管理数据的数据库,提供了增、删、改、查等接口,也可运行输入的SQL语句满足复杂场景需要。

2. 13.4.2.3 数据库的创建

  1. 分析

该案例使用了关系型数据库,涉及两张数据表:运动表和计划表。
运动表包括以下字段:id(主键)、sport_name(运动名称)、sport_type(运动类型)。
计划表包括以下字段:id(主键)、sport_id(外键,关联运动表的主键)、duration(运动时长)、status(运动完成状态)。

具体实现功能如下:

  • 创建运动表和计划表,并向运动表中插入一些数据。
  • 联表查询,查询出所有计划,以及计划对应的运动名和运动类型。
  • 查询所有计划中的时长,用于刷新作为筛选条件的时长下拉框选项。
  • 根据筛选条件,筛选部分计划。
  • 新增一条计划。
  • 编辑某条计划。
  • 删除某条计划。
  1. 工具类的封装
// entry/src/main/ets/common/database/RDBStoreUtil.ets
import { relationalStore } from '@kit.ArkData'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { GoalItem } from '../../viewmodel/GoalItem'
import CommonConstants from '../constants/CommonConstants'

const TAG: string = 'RDBStoreUtil'  // 日志标识标签

/**
 * RDBStoreUtil 是一个工具类,封装了关系型数据库的增删查改操作(CRUD)
 */
export class RDBStoreUtil {
  // 用于存储数据库连接实例
  objectiveRDB?: relationalStore.RdbStore

  /**
   * 创建数据库连接实例
   * @param context 应用上下文,用于获取 RDB 存储
   */
  createObjectiveRDB(context: Context) {
    const STORE_CONFIG: relationalStore.StoreConfig = {
      name: 'Objective.db', // 数据库名称
      securityLevel: relationalStore.SecurityLevel.S1 // 安全等级
    }

    relationalStore.getRdbStore(
      context, 
      STORE_CONFIG, 
      (err: BusinessError, rdbStore: relationalStore.RdbStore) => {
        this.objectiveRDB = rdbStore
        if (err) {
          hilog.error(
            0x0000, 
            TAG, 
            `获取 RdbStore 失败,code: ${err.code}, message: ${err.message}`
          )
          return
        }
        hilog.error(0x0000, TAG, '成功获取 RdbStore 实例。')
      })
  }

  /**
   * 创建运动表
   */
  createSportTable() {
    this.objectiveRDB?.execute(CommonConstants.CREATE_SPORTS_TABLE_SQL)
      .then(() => {
        hilog.info(0x0000, TAG, `创建 SPORTS 表成功`)
      })
      .catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `创建 SPORTS 表失败,code: ${err.code}, message: ${err.message}`
        )
      })
  }

  /**
   * 创建计划表
   */
  createPlanTable() {
    this.objectiveRDB?.execute(CommonConstants.CREATE_PLANS_TABLE_SQL)
      .then(() => {
        hilog.info(0x0000, TAG, `创建 PLANS 表成功`)
      })
      .catch((err: BusinessError) => {
        hilog.error(
          0x0000, TAG, 
          `创建 PLANS 表失败,code: ${err.code}, message: ${err.message}`
        )
      })
  }

  /**
   * 初始化运动表,插入默认数据
   */
  initSportTable() {
    const sportDataOne: relationalStore.ValuesBucket = {
      'ID': 0,
      'NAME': '晨跑',
      'TYPE': '有氧运动'
    }
    const sportDataTWO: relationalStore.ValuesBucket = {
      'ID': 1,
      'NAME': '瑜伽',
      'TYPE': '柔韧运动'
    }
    const sportDataThree: relationalStore.ValuesBucket = {
      'ID': 2,
      'NAME': '游泳',
      'TYPE': '有氧运动'
    }

    // 批量插入三条运动数据
    let valueBuckets = new Array(sportDataOne, sportDataTWO, sportDataThree)
    this.objectiveRDB?.batchInsert('SPORTS', valueBuckets)
      .then((insertNum: number) => {
        hilog.info(0x0000, TAG, `批量插入成功,共插入 ${insertNum} 条数据`)
      }).catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `批量插入失败,code: ${err.code}, message: ${err.message}`
        )
      })
  }

  /**
   * 查询所有计划的运动时长(只返回时长列表)
   */
  async queryAllPlansDuration(): Promise<number[]> {
    let plansSet: Array<number> = []
    await this.objectiveRDB?.querySql(CommonConstants.QUERY_ALL_DURATIONS_SQL)
      .then((resultSet: relationalStore.ResultSet) => {
        while (resultSet.goToNextRow()) {
          let duration: number = resultSet.getValue(
            resultSet.getColumnIndex('DURATION')
          ) as number
          plansSet.push(duration)
        }
        resultSet.close()
      }).catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `查询失败,
          code: ${err.code}, message: ${err.message}`
        )
      })
      return plansSet
  }

  /**
   * 查询所有计划信息(完整对象列表)
   */
  async queryAllPlans(): Promise<GoalItem[]> {
    let plansSet: Array<GoalItem> = []
    await this.objectiveRDB?.querySql(CommonConstants.QUERY_ALL_PLANS_SQL)
      .then((resultSet: relationalStore.ResultSet) => {
        while (resultSet.goToNextRow()) {
          const id 
            = resultSet.getValue(resultSet.getColumnIndex('ID')) as number
          const name 
            = resultSet.getValue(resultSet.getColumnIndex('NAME')) as string
          const type = 
            resultSet.getValue(resultSet.getColumnIndex('TYPE')) as string
          const duration 
            = resultSet.getValue(resultSet.getColumnIndex('DURATION')) as number
          const status 
            = resultSet.getValue(resultSet.getColumnIndex('STATUS')) as string
          plansSet.push(new GoalItem(id, name, type, duration, status))
        }
        resultSet.close()
      }).catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `查询失败,code: ${err.code}, message: ${err.message}`
        )
      })
    return plansSet
  }

  /**
   * 插入一条新的计划记录
   * @param sportID 对应运动项目 ID
   * @param duration 计划持续时间
   * @param status 状态:是否完成
   */
  async insertPlan(sportID: number, duration: number, status: boolean) {
    const sportData: relationalStore.ValuesBucket = {
      'SPORT_ID': sportID,
      'DURATION': duration,
      'STATUS': status ? '已完成' : '未完成'
    }

    await this.objectiveRDB?.insert('PLANS', sportData,
      relationalStore.ConflictResolution.ON_CONFLICT_REPLACE)
      .then((rowId: number) => {
        hilog.info(0x0000, TAG, `插入成功,rowId = ${rowId}`)
      }).catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `插入失败,code: ${err.code}, message: ${err.message}`
        )
      })
  }

  /**
   * 更新已有的计划记录
   * @param planID 要更新的计划 ID
   * @param duration 新的持续时间
   * @param status 新的状态
   */
  async updatePlan(planID: number, duration: number, status: boolean) {
    const planData: relationalStore.ValuesBucket = {
      'DURATION': duration,
      'STATUS': status ? '已完成' : '未完成'
    }

    let predicates = new relationalStore.RdbPredicates('PLANS')
    predicates.equalTo('ID', planID)

    await this.objectiveRDB?.update(planData, predicates,
      relationalStore.ConflictResolution.ON_CONFLICT_REPLACE)
      .then(async (rows: Number) => {
        hilog.info(0x0000, TAG, `更新成功,影响行数:${rows}`)
      }).catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `更新失败,code: ${err.code}, message: ${err.message}`
        )
      })
  }

  /**
   * 删除指定的计划记录
   * @param planID 要删除的计划 ID
   */
  async deletePlan(planID: number) {
    let predicates = new relationalStore.RdbPredicates('PLANS')
    predicates.equalTo('ID', planID)
    await this.objectiveRDB?.delete(predicates)
      .then((rows: Number) => {
        hilog.info(0x0000, TAG, `删除成功,删除行数:${rows}`)
      }).catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `删除失败,code: ${err.code}, message: ${err.message}`
        )
      })
  }

  /**
   * 条件查询计划记录(按时长和状态过滤)
   * @param duration 持续时间,可选
   * @param status 状态(已完成/未完成),可选
   * @returns 返回符合条件的 GoalItem 数组
   */
  async conditionalPlansQuery(
    duration: number | undefined, 
    status: boolean | undefined
  ): Promise<GoalItem[]> {
    let plansSet: Array<GoalItem> = []
    let querySql: string = ''

    // 根据传入参数组合查询 SQL
    if (status === undefined) {
      querySql = duration === undefined
        ? CommonConstants.QUERY_ALL_PLANS_SQL
        : CommonConstants.QUERY_PLANS_BY_DURATION_SQL + duration
    } else {
      let statusCondition: string = status ? '已完成' : '未完成'
      querySql = duration === undefined
        ? CommonConstants.QUERY_PLANS_BY_STATUS_SQL + statusCondition + '"'
        : CommonConstants.QUERY_PLANS_BY_STATUS_SQL 
          + statusCondition + '" and p.DURATION = ' + duration
    }

    await this.objectiveRDB?.querySql(querySql)
      .then((resultSet: relationalStore.ResultSet) => {
        while (resultSet.goToNextRow()) {
          const id 
            = resultSet.getValue(resultSet.getColumnIndex('ID')) as number
          const name 
            = resultSet.getValue(resultSet.getColumnIndex('NAME')) as string
          const type 
            = resultSet.getValue(resultSet.getColumnIndex('TYPE')) as string
          const duration 
            = resultSet.getValue(resultSet.getColumnIndex('DURATION')) as number
          const status 
            = resultSet.getValue(resultSet.getColumnIndex('STATUS')) as string
          plansSet.push(new GoalItem(id, name, type, duration, status))
        }
        resultSet.close()
      }).catch((err: BusinessError) => {
        hilog.error(
          0x0000, 
          TAG, 
          `条件查询失败,code: ${err.code}, message: ${err.message}`
        )
      })

    return plansSet
  }
}

// 单例导出
export default new RDBStoreUtil()
  1. 常量提取

为了提高代码的可阅读性,可以将SQL语句提取常量。我们在如右图所示的路径中创建一个名为CommonConstants.ets文件,保存我们需要的SQL语句。

// entry/src/main/ets/common/constants/CommonConstants.ets
/**
 * 所有功能模块的通用常量类。
 */
export default class CommonConstants {
  // 创建运动表的 SQL 语句
  static readonly CREATE_SPORTS_TABLE_SQL: string =
    'CREATE TABLE IF NOT EXISTS SPORTS (ID INTEGER PRIMARY KEY AUTOINCREMENT, ' +
    'NAME TEXT NOT NULL, ' +
    'TYPE TEXT NOT NULL)'

  // 创建计划表的 SQL 语句
  static readonly CREATE_PLANS_TABLE_SQL: string =
    'CREATE TABLE IF NOT EXISTS PLANS (ID INTEGER PRIMARY KEY AUTOINCREMENT, ' +
    'SPORT_ID INTEGER NOT NULL, DURATION INTEGER NOT NULL, ' +
    'STATUS TEXT NOT NULL)'

  // 查询所有不重复计划时长的 SQL 语句
  static readonly QUERY_ALL_DURATIONS_SQL: string =
    'SELECT DISTINCT DURATION FROM PLANS'

  // 查询所有计划的 SQL 语句
  static readonly QUERY_ALL_PLANS_SQL: string =
    'SELECT p.id AS ID,s.NAME AS NAME,s.type AS TYPE,p.DURATION AS DURATION,' +
    'p.STATUS AS STATUS FROM SPORTS AS s ' +
    'JOIN PLANS AS p WHERE s.ID = P.SPORT_ID'

  // 根据计划时长查询的 SQL 语句(需拼接时长值)
  static readonly QUERY_PLANS_BY_DURATION_SQL: string =
    'SELECT p.id AS ID,s.NAME AS NAME,s.type AS TYPE,p.DURATION AS DURATION,' +
    'p.STATUS AS STATUS FROM SPORTS AS s ' +
    'JOIN PLANS AS p WHERE s.ID = P.SPORT_ID AND p.DURATION = '

  // 根据计划完成状态查询的 SQL 语句(需拼接"已完成"/"未完成")
  static readonly QUERY_PLANS_BY_STATUS_SQL: string =
    'SELECT p.id AS ID,s.NAME AS NAME,s.type AS TYPE,p.DURATION AS DURATION,' +
    'p.STATUS AS STATUS FROM SPORTS AS s ' +
    'JOIN PLANS AS p WHERE s.ID = P.SPORT_ID AND p.STATUS = "'
}
  1. 查询结果的实体类
// entry/src/main/ets/viewmodel/GoalItem.ets
export class GoalItem {
  id: number
  sportName: string
  type: string
  duration: number
  status: string

  constructor(
    id: number, sportName: string, type: string, 
    duration: number, status: string
  ) {
    this.id = id
    this.sportName = sportName
    this.type = type
    this.duration = duration
    this.status = status
  }
}

3. 13.4.2.7 应用数据库工具类构建页面

  1. 在EntryAbility里创建数据库
// entry/src/main/ets/entryability/EntryAbility.ets
// ...
import RDBStoreUtil from '../common/database/RDBStoreUtil'

export default class entryAbility extends UIAbility {
  // ...

  onWindowStageCreate(windowStage: window.WindowStage): void {
    RDBStoreUtil.createObjectiveRDB(this.context)
    // ...
  }

  // ...
}
  1. 页面路由
  • 配置路由表
// entry/src/main/resources/base/profile/route_map.json
{
  "routerMap": [
    // ...
    {
      "name": "GoalPage",
      "pageSourceFile": "src/main/ets/pages/GoalPage.ets",
      "buildFunction": "GoalPageBuilder",
      "data": {
        "description" : "this is GoalPage"
      }
    }
  ]
}
  • 定义pathStack

一定要在LoginPage里定义pathStack,其他页面共享使用一个pathStack。

// entry/src/main/ets/pages/LoginPage.ets
// ...
@Entry
@ComponentV2
struct LoginPage {
  //...
  @Provider() pathStack: NavPathStack = new NavPathStack()
  // ...
}
  • 在Home组件里添加进入“我的目标”界面

以下只给出了涉及本案例的关键代码。全部代码请参阅完整案例代码。

// entry/src/main/ets/view/Home.ets

// 消费LoginPage里提供的pathStack
@Consumer() pathStack: NavPathStack = new NavPathStack()

GridItem() {
  // ...
}
.onClick(() => { 
  if (index === 4) { // 当点击“我的目标”图标时路由
    this.pathStack.pushPathByName('GoalPage', null)
  }
})
  • “我的目标”功能实现
// entry/src/main/ets/pages/GoalPage.ets
import { hilog } from '@kit.PerformanceAnalysisKit'
import RDBStoreUtil from '../common/database/RDBStoreUtil'
import { GoalItem } from '../viewmodel/GoalItem'

const TAG = 'GoalPage'

@Builder
export function GoalPageBuilder() {
  GoalPage()
}

/**
 * Goal page
 */
@ComponentV2
export struct GoalPage {
  @Local plansSet: Array<GoalItem> = []
  @Local plan: GoalItem = new GoalItem(0, '', '', 0, '')
  @Local durationSelectOptions: Array<SelectOption> = []
  @Local durationSet: Array<number> = []
  @Local currentDuration: number | undefined = undefined
  @Local currentStatus: boolean | undefined = undefined
  @Local isUpdateShow: boolean = false
  @Local isInsetShow: boolean = false
  @Local currentNumber: number = 0
  @Local currentPlan: GoalItem = new GoalItem(0, '', '', 0, '')
  @Local duration: number = 0
  @Local status: boolean = false
  @Local addSportID: number = 0
  @Local addDuration: number = 0
  @Local addStatus: boolean = false

  @Builder
  updateBindSheet(planData: GoalItem) {
    Column() {
      Row() {
        Text($r('app.string.sport_name'))
          .font({
            size: '16fp',
            weight: 500
          })
          .opacity(0.9)
        Select([
          { value: $r('app.string.Morning_jogging') }, 
          { value: $r('app.string.Yoga') },
          { value: $r('app.string.Swimming') }])
          .value(planData.sportName)
          .focusable(false)
          .width('150vp')
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width('90%')
      .margin({
        top: '15vp',
        bottom: '15vp'
      })

      Row() {
        Text($r('app.string.Exercise_duration'))
          .font({
            size: '16fp',
            weight: 400
          })
          .opacity(0.9)
        TextInput({ text: planData.duration.toString() })
          .onChange((data: string) => {
            this.duration = Number.parseInt(data)
          })
          .width('150vp')
      }
      .width('90%')
      .margin({ bottom: '15vp' })
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text($r('app.string.Completion_Status'))
          .font({
            size: '16fp',
            weight: 400
          })
          .opacity(0.9)
        Select([
          { value: $r('app.string.Completed') },
          { value: $r('app.string.Uncompleted') }
        ])
          .width('150vp')
          .value(planData.status)
          .onSelect((index: number) => {
            if (index === 0) {
              this.status = true
            } else {
              this.status = false
            }
          })
      }
      .width('90%')
      .margin({ bottom: '15vp' })
      .justifyContent(FlexAlign.SpaceBetween)

      Button($r('app.string.submit'))
        .width('288vp')
        .onClick(async () => {
          await RDBStoreUtil.updatePlan(planData.id, this.duration, this.status)
          await RDBStoreUtil.queryAllPlans().then((value) => {
            this.plansSet = value
          })
          this.refreshSelectOption()
          this.isUpdateShow = false
        })
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
  }

  @Builder
  insertBindSheet() {
    Column() {
      Row() {
        Text($r('app.string.sport_name'))
          .font({
            size: '16fp',
            weight: 500
          })
          .opacity(0.9)
        Select([
          { value: $r('app.string.Morning_jogging') },
          { value: $r('app.string.Yoga') },
          { value: $r('app.string.Swimming') }
        ])
          .value($r('app.string.Morning_jogging'))
          .onSelect((index: number) => {
            this.addSportID = index
          })
          .width('170vp')
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .width('90%')
      .margin({
        top: '15vp',
        bottom: '15vp'
      })

      Row() {
        Text($r('app.string.Exercise_duration'))
          .font({
            size: '16fp',
            weight: 400
          })
          .opacity(0.9)
        TextInput({ placeholder: $r('app.string.enter_duration') })
          .onChange((data: string) => {
            this.addDuration = Number.parseInt(data)
          })
          .width('170vp')
      }
      .width('90%')
      .margin({ bottom: '15vp' })
      .justifyContent(FlexAlign.SpaceBetween)

      Row() {
        Text($r('app.string.Completion_Status'))
          .font({
            size: '16fp',
            weight: 400
          })
          .opacity(0.9)
        Select([
          { value: $r('app.string.Completed') },
          { value: $r('app.string.Uncompleted') }
        ])
          .value($r('app.string.Uncompleted'))
          .onSelect((index: number) => {
            if (index === 0) {
              this.addStatus = true
            } else {
              this.addStatus = false
            }
          })
          .width('170vp')
      }
      .width('90%')
      .margin({ bottom: '15vp' })
      .justifyContent(FlexAlign.SpaceBetween)

      Button($r('app.string.submit'))
        .width('288vp')
        .onClick(async () => {
          await RDBStoreUtil.insertPlan(
            this.addSportID, 
            this.addDuration, 
            this.addStatus
          )
          await RDBStoreUtil.queryAllPlans().then((value) => {
            this.plansSet = value
          })
          this.refreshSelectOption()
          this.isInsetShow = false
        })
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
  }

  async aboutToAppear(): Promise<void> {
    RDBStoreUtil.createSportTable()
    RDBStoreUtil.initSportTable()
    RDBStoreUtil.createPlanTable()
    await RDBStoreUtil.queryAllPlans().then((value) => {
      this.plansSet = value
    })
    this.refreshSelectOption()
  }

  async refreshSelectOption() {
    await RDBStoreUtil.queryAllPlansDuration().then((value) => {
      this.durationSet = value
    })
    this.durationSelectOptions = [{ value: $r('app.string.ALL') }]
    for (let i = 0; i < this.durationSet.length; i++) {
      this.durationSelectOptions.push({ value: this.durationSet[i] + 'min' })
    }
  }

  build() {
    NavDestination() {
      Column() {
        Column() {
          Text($r('app.string.my_goal'))
            .width('100%')
            .font({
              size: '30fp',
              weight: 700
            })
            .margin({
              top: '48vp',
              bottom: '32vp'
            })
          Row() {
            Select(this.durationSelectOptions)
              .onSelect((index: number) => {
                if (index === 0) {
                  this.currentDuration = undefined
                } else {
                  this.currentDuration = this.durationSet[index-1]
                }
              })
              .value($r('app.string.Duration'))
              .margin({ right: '25vp' })
            Select([{ value: $r('app.string.ALL') },
              { value: $r('app.string.Completed') },
              { value: $r('app.string.Uncompleted') }])
              .onSelect((index: number) => {
                if (index === 0) {
                  this.currentStatus = undefined
                } else if (index === 1) {
                  this.currentStatus = true
                } else {
                  this.currentStatus = false
                }
              })
              .value($r('app.string.status'))
              .margin({ right: '25vp' })
            Image($r('app.media.reset'))
              .width(40)
              .onClick(async () => {
                await RDBStoreUtil.queryAllPlans().then((value) => {
                  this.plansSet = value
                })
              })
              .margin({ right: '16vp' })
            Image($r('app.media.sift'))
              .width(40)
              .onClick(async () => {
                await RDBStoreUtil.conditionalPlansQuery(
                  this.currentDuration, 
                  this.currentStatus
                ).then((value) => {
                  this.plansSet = value
                })
              })
              .margin({ right: '16vp' })
          }
          .width('100%')
          .margin({ bottom: '8vp' })

          List() {
            ForEach(this.plansSet, (item: GoalItem, index: number) => {
              ListItem() {
                Row() {
                  Column() {
                    Text(item.sportName)
                      .font({
                        size: '16fp',
                        weight: 500
                      })
                      .opacity(0.9)
                    Text(item.type)
                      .font({
                        size: '14fp',
                        weight: 400
                      })
                      .opacity(0.6)
                  }
                  .alignItems(HorizontalAlign.Start)
                  .margin({ left: '16vp' })

                  Text(item.duration + 'min')
                    .font({
                      size: '14vp',
                      weight: 400
                    })
                    .opacity(0.6)
                  Text(item.status)
                    .font({
                      size: '14vp',
                      weight: 400
                    })
                    .opacity(0.6)
                  Image($r('app.media.edit'))
                    .width('40vp')
                    .onClick(() => {
                      this.currentNumber = index
                      this.currentPlan = item
                      this.isUpdateShow = true
                    })
                  Image($r('app.media.delete'))
                    .width('40vp')
                    .margin({ right: '16vp' })
                    .onClick(() => {
                      AlertDialog.show(
                        {
                          message: $r('app.string.delete_question'),
                          autoCancel: true,
                          alignment: DialogAlignment.Bottom,
                          gridCount: 4,
                          offset: { dx: 0, dy: -20 },
                          primaryButton: {
                            value: $r('app.string.Cancel'),
                            action: () => {
                              hilog.info(
                                0x0000, 
                                TAG, 
                                'Callback when the first button is clicked'
                              )
                            }
                          },
                          secondaryButton: {
                            enabled: true,
                            defaultFocus: true,
                            style: DialogButtonStyle.HIGHLIGHT,
                            value: $r('app.string.delete'),
                            action: async () => {
                              await RDBStoreUtil.deletePlan(item.id)
                              this.refreshSelectOption()
                              await RDBStoreUtil.queryAllPlans()
                                .then((value) => {
                                  this.plansSet = value
                                })
                            }
                          },
                          cancel: () => {
                            hilog.info(0x0000, TAG, 'Closed callbacks')
                          },
                          onWillDismiss: (
                            dismissDialogAction: DismissDialogAction
                          ) => {
                            if (dismissDialogAction.reason 
                                == DismissReason.PRESS_BACK) {
                              dismissDialogAction.dismiss()
                            }
                            if (
                              dismissDialogAction.reason 
                              == DismissReason.TOUCH_OUTSIDE
                            ) {
                              dismissDialogAction.dismiss()
                            }
                          }
                        }
                      )

                    })
                }
                .width('100%')
                .height('100%')
                .justifyContent(FlexAlign.SpaceBetween)
              }
              .width('100%')
              .height('60vp')
            }, (item: GoalItem) => JSON.stringify(item))
          }
          .width('100%')
          .height('')
          .divider({
            strokeWidth: 1,
            color: '#1a000000',
            startMargin: 16,
            endMargin: 16
          })
          .bindSheet(this.isUpdateShow, this.updateBindSheet(this.currentPlan), {
            height: '320vp',
            title: { title: $r('app.string.bind_title') },
            onDisappear: () => {
              this.isUpdateShow = false
            }
          })
        }

        Button($r('app.string.Add_Plans'))
          .width('100%')
          .margin({ bottom: '32vp' })
          .onClick(() => {
            this.isInsetShow = true
          })
          .bindSheet(this.isInsetShow, this.insertBindSheet(), {
            height: '320vp',
            title: { title: $r('app.string.Add_Plans') },
            onDisappear: () => {
              this.isInsetShow = false
            }
          })
      }
      .padding({
        left: '12vp',
        right: '12vp'
      })
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.SpaceBetween)
    }
    .backgroundColor('#F1F3F5')
    .hideTitleBar(true)
  }
}

关键代码说明:

  • 在自定义组件的生命周期中,完成数据库的创建、数据表创建以及数据表初始化的操作,将存储在数据库中的内容读取出来,并存储在状态变量中以便循环的渲染,这里主要包括所有的计划,以及计划所涉及的时长。
  • 使用List循环渲染所有计划以及补充下拉框的内容。
  • 在重置、筛选、删除、新增、编辑按钮的点击事件中使用封装好的方法。

二、关系型数据库基础知识

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

(一) 13.2.1 基本概念

  • 谓词:数据库中用来代表数据实体的性质、特征或者数据实体之间关系的词项,主要用来定义数据库的操作条件。
  • 结果集:指用户查询之后的结果集合,可以对数据进行访问。结果集提供了灵活的数据访问方式,可以更方便地拿到用户想要的数据。

(二) 13.2.2 运作机制

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

(三) 13.2.3 约束限制

  • 系统默认日志方式是WAL(Write Ahead Log)模式,系统默认落盘方式是FULL模式。
  • 数据库中有4个读连接和1个写连接,线程获取到空闲读连接时,即可进行读取操作。当没有空闲读连接且有空闲写连接时,会将写连接当做读连接来使用。
  • 为保证数据的准确性,数据库同一时间只能支持一个写操作。
  • 当应用被卸载完成后,设备上的相关数据库文件及临时文件会被自动清除。
  • ArkTS侧支持的基本数据类型:number、string、二进制类型数据、boolean。
  • 为保证插入并读取数据成功,建议一条数据不要超过2M。超出该大小,插入成功,读取失败。

(四) 13.2.4 接口说明

以下是关系型数据库持久化功能的相关接口,大部分为异步接口。异步接口均有callback和Promise两种返回形式,下表均以callback形式为例,更多接口及使用方式请见关系型数据库。

接口名称

描述

getRdbStore(context: Context, config: StoreConfig, callback: AsyncCallback<RdbStore>): void

获得一个RdbStore,操作关系型数据库,用户可以根据自己的需求配置RdbStore的参数,然后通过RdbStore调用相关接口可以执行相关的数据操作。

executeSql(sql: string, bindArgs: Array<ValueType>, callback: AsyncCallback<void>):void

执行包含指定参数但不返回值的SQL语句。

insert(table: string, values: ValuesBucket, callback: AsyncCallback<number>):void

向目标表中插入一行数据。

update(values: ValuesBucket, predicates: RdbPredicates, callback: AsyncCallback<number>):void

根据predicates的指定实例对象更新数据库中的数据。

delete(predicates: RdbPredicates, callback: AsyncCallback<number>):void

根据predicates的指定实例对象从数据库中删除数据。

query(predicates: RdbPredicates, columns: Array<string>, callback: AsyncCallback<ResultSet>):void

根据指定条件查询数据库中的数据。

deleteRdbStore(context: Context, name: string, callback: AsyncCallback<void>): void

删除数据库。

(五) 13.2.5 开发步骤

  1. 使用关系型数据库实现数据持久化,需要获取一个RdbStore,其中包括建库、建表、升降级等操作。

示例代码如下所示:

import { relationalStore } from '@kit.ArkData' // 导入模块
import { UIAbility } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { window } from '@kit.ArkUI'

// 此处示例在Ability中实现,使用者也可以在其他合理场景中使用
class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    const STORE_CONFIG :relationalStore.StoreConfig= {
      name: 'RdbTest.db', // 数据库文件名
      securityLevel: relationalStore.SecurityLevel.S3, // 数据库安全级别
      encrypt: false, // 可选参数,指定数据库是否加密,默认不加密

       // 可选参数,数据库自定义路径。
       // 数据库将在如下的目录结构中被创建:context.databaseDir 
       // + '/rdb/' + customDir,其中context.databaseDir是应用沙箱对应的路径,
       // '/rdb/'表示创建的是关系型数据库,customDir表示自定义的路径。
       // 当此参数不填时,默认在本应用沙箱目录下创建RdbStore实例。
      customDir: 'customDir/subCustomDir',

      // 可选参数,指定数据库是否以只读方式打开。该参数默认为false,表示数据库可读可写。
      // 该参数为true时,只允许从数据库读取数据,不允许对数据库进行写操作,否则会返回错误码801。
      isReadOnly: false
    }
    
    // 判断数据库版本,如果不匹配则需进行升降级操作
    // 假设当前数据库版本为3,表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, IDENTITY)

    // 建表Sql语句, IDENTITY为bigint类型,sql中指定类型为UNLIMITED INT
    const SQL_CREATE_TABLE = 'CREATE TABLE IF NOT EXISTS EMPLOYEE ('
      + 'ID INTEGER PRIMARY KEY AUTOINCREMENT,'
      + 'NAME TEXT NOT NULL, AGE INTEGER,'
      + 'SALARY REAL, CODES BLOB, '
      + 'IDENTITY UNLIMITED INT)'
    relationalStore.getRdbStore(this.context, STORE_CONFIG, (err, store) => {
      if (err) {
        console.error('Failed to get RdbStore. ' 
                      + `Code:${err.code}, message:${err.message}`)
        return
      }
      console.info('Succeeded in getting RdbStore.')

      // 当数据库创建时,数据库默认版本为0
      if (store.version === 0) {
        store.executeSql(SQL_CREATE_TABLE) // 创建数据表
        // 设置数据库的版本,入参为大于0的整数
        store.version = 3
      }

      // 如果数据库版本不为0且和当前数据库版本不匹配,需要进行升降级操作
      // 当数据库存在并假定版本为1时,例应用从某一版本升级到当前版本,
      // 数据库需要从1版本升级到2版本
      if (store.version === 1) {
        // version = 1:表结构:EMPLOYEE (NAME, SALARY, CODES, ADDRESS) => 
        // version = 2:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, ADDRESS)
        (store as relationalStore.RdbStore)
          .executeSql('ALTER TABLE EMPLOYEE ADD COLUMN AGE INTEGER')
        store.version = 2
      }

      // 当数据库存在并假定版本为2时,例应用从某一版本升级到当前版本,
      // 数据库需要从2版本升级到3版本
      if (store.version === 2) {
        // version = 2:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES, ADDRESS) => 
        // version = 3:表结构:EMPLOYEE (NAME, AGE, SALARY, CODES)
        (store as relationalStore.RdbStore)
          .executeSql('ALTER TABLE EMPLOYEE DROP COLUMN ADDRESS TEXT')
        store.version = 3
      }
    })
    // 请确保获取到RdbStore实例后,再进行数据库的增、删、改、查等操作
  }
}
  1. 获取到RdbStore后,调用insert()接口插入数据。

示例代码如下所示:

let store: relationalStore.RdbStore | undefined = undefined

let value1 = 'Lisa'
let value2 = 18
let value3 = 100.5
let value4 = new Uint8Array([1, 2, 3, 4, 5])
let value5 = BigInt('15822401018187971961171')
// 以下三种方式可用
const valueBucket1: relationalStore.ValuesBucket = {
  'NAME': value1,
  'AGE': value2,
  'SALARY': value3,
  'CODES': value4,
  'IDENTITY': value5,
}
const valueBucket2: relationalStore.ValuesBucket = {
  NAME: value1,
  AGE: value2,
  SALARY: value3,
  CODES: value4,
  IDENTITY: value5,
}
const valueBucket3: relationalStore.ValuesBucket = {
  "NAME": value1,
  "AGE": value2,
  "SALARY": value3,
  "CODES": value4,
  "IDENTITY": value5,
}

if (store !== undefined) {
  (store as relationalStore.RdbStore).insert(
    'EMPLOYEE', 
    valueBucket1, 
    (err: BusinessError, rowId: number) => {
      if (err) {
        console.error('Failed to insert data. '
                      + `Code:${err.code}, message:${err.message}`)
        return
      }
      console.info(`Succeeded in inserting data. rowId:${rowId}`)
    }
  )
}
  1. 根据谓词指定的实例对象,对数据进行修改或删除。

调用update()方法修改数据,调用delete()方法删除数据。示例代码如下所示:

let value6 = 'Rose'
let value7 = 22
let value8 = 200.5
let value9 = new Uint8Array([1, 2, 3, 4, 5])
let value10 = BigInt('15822401018187971967863')
// 以下三种方式可用
const valueBucket4: relationalStore.ValuesBucket = {
  'NAME': value6,
  'AGE': value7,
  'SALARY': value8,
  'CODES': value9,
  'IDENTITY': value10,
}
const valueBucket5: relationalStore.ValuesBucket = {
  NAME: value6,
  AGE: value7,
  SALARY: value8,
  CODES: value9,
  IDENTITY: value10,
}
const valueBucket6: relationalStore.ValuesBucket = {
  "NAME": value6,
  "AGE": value7,
  "SALARY": value8,
  "CODES": value9,
  "IDENTITY": value10,
}

// 修改数据
// 创建表'EMPLOYEE'的predicates
let predicates1 = new relationalStore.RdbPredicates('EMPLOYEE') 
// 匹配表'EMPLOYEE'中'NAME'为'Lisa'的字段
predicates1.equalTo('NAME', 'Lisa')
if (store !== undefined) {
  (store as relationalStore.RdbStore).update(
    valueBucket4, 
    predicates1, 
    (err: BusinessError, rows: number) => {
      if (err) {
        console.error('Failed to update data. '
                      + Code:${err.code}, message:${err.message})
        return
      }
      console.info(`Succeeded in updating data. row count: ${rows}`)
    }
  )
}

// 删除数据
predicates1 = new relationalStore.RdbPredicates('EMPLOYEE')
predicates1.equalTo('NAME', 'Lisa')
if (store !== undefined) {
  (store as relationalStore.RdbStore).delete(
    predicates1, 
    (err: BusinessError, rows: number) => {
      if (err) {
        console.error('Failed to delete data. '
                      + `Code:${err.code}, message:${err.message}`)
        return
      }
      console.info(`Delete rows: ${rows}`)
    }
  )
}
  1. 根据谓词指定的查询条件查找数据。

调用query()方法查找数据,返回一个ResultSet结果集。示例代码如下所示:

let predicates2 = new relationalStore.RdbPredicates('EMPLOYEE')
predicates2.equalTo('NAME', 'Rose')
if (store !== undefined) {
  (store as relationalStore.RdbStore).query(
    predicates2, 
    ['ID', 'NAME', 'AGE', 'SALARY', 'IDENTITY'], 
    (err: BusinessError, resultSet) => {
      if (err) {
        console.error('Failed to query data. '
                      + `Code:${err.code}, message:${err.message}`)
        return
      }
      console.info('ResultSet column names: ${resultSet.columnNames}, '
                   + `column count: ${resultSet.columnCount}`)
      // resultSet是一个数据集合的游标,默认指向第-1个记录,有效的数据从0开始。
      while (resultSet.goToNextRow()) {
        const id = resultSet.getLong(resultSet.getColumnIndex('ID'))
        const name = resultSet.getString(resultSet.getColumnIndex('NAME'))
        const age = resultSet.getLong(resultSet.getColumnIndex('AGE'))
        const salary = resultSet.getDouble(resultSet.getColumnIndex('SALARY'))
        const identity = resultSet.getValue(resultSet.getColumnIndex('IDENTITY'))
        console.info(`id=${id}, name=${name}, age=${age}, ` 
                     + `salary=${salary}, identity=${identity}`)
      }
      // 释放数据集的内存
      resultSet.close()
    }
  )
}
  1. 在同路径下备份数据库。

关系型数据库支持两种手动备份和自动备份(仅系统应用可用)两种方式。

此处以手动备份为例:

if (store !== undefined) {
  // "Backup.db"为备份数据库文件名,默认在RdbStore同路径下备份。
  // 也可指定路径:customDir + "backup.db"
  (store as relationalStore.RdbStore)
    .backup("Backup.db", (err: BusinessError) => {
      if (err) {
        console.error(`Failed to backup RdbStore. ` 
                      + `Code:${err.code}, message:${err.message}`)
        return
      }
      console.info(`Succeeded in backing up RdbStore.`)
    })
}
  1. 从备份数据库中恢复数据。

关系型数据库支持两种方式:恢复手动备份数据和恢复自动备份数据(仅系统应用可用)。

此处以调用restore接口恢复手动备份数据为例:

if (store !== undefined) {
  (store as relationalStore.RdbStore)
    .restore("Backup.db", (err: BusinessError) => {
    if (err) {
      console.error(`Failed to restore RdbStore. ` 
                    + `Code:${err.code}, message:${err.message}`)
      return
    }
    console.info(`Succeeded in restoring RdbStore.`)
  })
}
  1. 删除数据库。

调用deleteRdbStore()方法,删除数据库及数据库相关文件。示例代码如下:

relationalStore
  .deleteRdbStore(this.context, 'RdbTest.db', (err: BusinessError) => {
    if (err) {
      console.error(`Failed to delete RdbStore. ` 
                    + Code:${err.code}, message:${err.message})
      return
    }
    console.info('Succeeded in deleting RdbStore.')
  })

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐