HarmonyOS数据管理

一、HarmonyOS数据管理之用户首选项

1.1.用户首选项

用户首选项(Preferences):提供了轻量级配置数据的持久化能力,并支持订阅数据变化的通知能力。不支持分布式同步,常用于保存应用配置信息、用户偏好设置等。

通常用于保存应用的配置信息。数据通过文本的形式保存在设备中,应用使用过程中会将文本中的数据全量加载到内存中,所以访问速度快、效率高,但不适合需要存储大量数据的场景。

1.2.运作机制

首选项的特点是:

  • 以Key-Value形式存储数据:Key是不重复的关键字,Value是数据值。
  • 非关系型数据库区别于关系型数据库,它不保证遵循ACID(Atomicity, Consistency, Isolation and Durability)特性,数据之间无关系。

进程中每个文件仅存在一个Preferences实例,应用获取到实例后,可以从中读取数据,或者将数据存入实例中。通过调用flush方法可以将实例中的数据回写到文件里。如下图所示,用户程序通过JS接口调用用户首选项读写对应的数据文件。开发者可以将用户首选项持久化文件的内容加载到Preferences实例,每个文件唯一对应到一个Preferences实例,系统会通过静态容器将该实例存储在内存中,直到主动从内存中移除该实例或者删除该文件。

  • Key键为string类型,要求非空且长度不超过80个字节。

  • 如果Value值为string类型,请使用UTF-8编码格式,可以为空,不为空时长度不超过8192个字节。

  • 内存会随着存储数据量的增大而增大,所以存储的数据量应该是轻量级的,建议存储的数据不超过一万条,否则会在内存方面产生较大的开销。

1.4.案例说明

基于 Slider 组件和首选项,实现通过拖动滑块调节应用内字体大小的功能。效果图如下:

使用说明

  • 字体大小调节原理 :通过组件Slider滑动,获取滑动数值,将这个值通过首选项进行持久化,页面的字体通过这个值去改变大小。
  • 首选项: 首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。
  • 应用包含“设置”、“字体大小设置”两个页面。
  • 通过点击设置页的“设置字体大小”项,可以进入到字体大小设置页。
  • 在字体大小设置页拖动滑块,设置页的设置项文字以及字体大小设置页的聊天文字大小会同步变化。

1.3.使用说明

1.3.1.导入模块

import data_preferences from '@ohos.data.preferences';

1.3.2.常用方法

获取Preferences实例

getPreferences(context: Context, name: string): Promise<Preferences>

注意:

  • context - 指示应用程序或功能的上下文,就是UIAbility

  • name - 表示首选项文件名。这个自定义即可

1.3.3.Preferences对象常用方法

①.获取键对应的值,如果值为null或者非默认值类型,返回默认数据defValue

get(key: string, defValue: ValueType): Promise<ValueType>

说明:

  • key:要修改的存储的Key,不能为空。

  • value:默认返回值。支持number、string、boolean、Array<number>、Array<string>、Array<boolean>类型。

②.将数据写入Preferences实例,可通过flush将Preferences实例持久化,使用Promise异步回调

put(key: string, value: ValueType): Promise<void>

说明:

  • key:要修改的存储的Key,不能为空。

  • value:存储的新值。支持number、string、boolean、Array<number>、Array<string>、Array<boolean>类型。

③.将当前Preferences实例的数据异步存储到用户首选项的持久化文件中,写入后需要调用

flush(): Promise<void>

④.是否包含指定的key(has)通过has方法判断首选项中是否包含指定的key,保证指定的key不会被重复保存,相关代码实现如下:

has(key: string): Promise<boolean>

说明:

  • 要检查的存储key名称,不能为空。检查Preferences实例是否包含名为给定Key的存储键值对,使用Promise异步回调。

⑤.从Preferences实例中删除名为给定Key的存储键值对,使用Promise异步回调。

delete(key: string): Promise<void>

说明:

  • key:要删除的key名,不能为空

1.4.案例说明

基于 Slider 组件和首选项,实现通过拖动滑块调节应用内字体大小的功能。效果图如下:

 

使用说明

  • 字体大小调节原理 :通过组件Slider滑动,获取滑动数值,将这个值通过首选项进行持久化,页面的字体通过这个值去改变大小。

  • 首选项: 首选项为应用提供Key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。

  • 应用包含“设置”、“字体大小设置”两个页面。

  • 通过点击设置页的“设置字体大小”项,可以进入到字体大小设置页。

  • 在字体大小设置页拖动滑块,设置页的设置项文字以及字体大小设置页的聊天文字大小会同步变化。

1.4.1.封装用户首选项类

在common包下创建子包utils,然后创建PreferencesUtil.ets,在里面封装Preferences新增、删除和查询的方法代码如下:

import preferences from '@ohos.data.preferences';
​
/**
 * PreferencesUtil 提供创建、保存和查询的首选项
 */
export class PreferencesUtil {
  //创建Map用来保存多个Preferences,如果只是变量,则只能保存一个,新的会替换之前的,所以用Map保持多个
  preferencesMap:Map<string, preferences.Preferences> = new Map();
​
  /**
   * 方式一:异步写法
   * 加载preferences实例,
   * @param context UIAbility
   * @param name preferences名称
   */
  /*loadPreferences(context,name:string){
    //返回的值是Promise<preferences.Preferences>
    preferences.getPreferences(context,name)
      .then((pref)=>{//成功回调取值
        //这里就需要将获取到的Preferences保存下来
        this.preferencesMap.set(name,pref)
        console.log("testTag", `加载[${name}].Preferences成功`)
      })
      .catch((reason)=>{
        console.log("testTag", `加载[${name}].Preferences失败`,JSON.stringify(reason))
      })
  }*//**
   * 方式二:同步写法
   * async:表示该方法为异步方法
   * @param context
   * @param name
   */
  async loadPreferences(context,name:string){
    try { //返回的值是Promise<preferences.Preferences>,所以同步的写法则是使用await关键字,等到结果返回之后再给赋值
      let pref = await preferences.getPreferences(context, name)
      this.preferencesMap.set(name, pref)
      console.log("testTag", `加载[${name}].Preferences成功`)
    } catch (e) {
      console.log("testTag", `加载[${name}].Preferences失败`,JSON.stringify(e))
    }
  }
​
  /**
   * 在Preferences中新增值
   * @param name Preferences名称
   * @param key 新增的key
   * @param value 新增的value,类型preferences.ValueType的值为: string | number | boolean | number[] | string[] | boolean[]
   */
  async putPreferencesValue(name:string,key:string,value:preferences.ValueType){
    //先判断,如果没有Preferences名称,如果没有则无法新增值,直接return
    if(!this.preferencesMap.has(name)){
      console.log("testTag", `[${name}].Preferences不存在`)
      return
    }
    try { //如果存在了,则新增数据
      let pref = this.preferencesMap.get(name)
      await pref.put(key, value)
      //刷入磁盘
      await pref.flush()
      console.log("testTag", `在[${name}].Preferences保存${key},${value}成功`)
    } catch (e) {
      console.log("testTag", `在[${name}].Preferences保存:${key},${value}失败`,JSON.stringify(e))
    }
  }
​
  /**
   * 根据key查询值
   * @param name Preferences的名称
   * @param key key的名称
   * @param defaultValue 默认值
   * @returns 返回查询的值
   */
  async getPreferencesValue(name:string,key:string,defaultValue:preferences.ValueType){
    //先判断,如果没有Preferences名称,如果没有则无法取值,直接return
    if(!this.preferencesMap.has(name)){
      console.log("testTag", `[${name}].Preferences不存在`)
      return
    }
    try { //如果存在了,则新增数据
      let pref = this.preferencesMap.get(name)
      //根据key在preferences中获取值
      let value =  await pref.get(key,defaultValue)
      console.log("testTag", `在[${name}].Preferences查询获取${key},${value}成功`)
      return value
    } catch (e) {
      console.log("testTag", `在[${name}].Preferences查询获取${key}失败`,JSON.stringify(e))
    }
  }
​
  /**
   * 根据key删除值
   * @param name 数据的key
   * @param key 值
   */
  async deletePreferencesValue(name:string,key:string){
    //先判断,如果没有Preferences名称,如果没有则无法取值,直接return
    if(!this.preferencesMap.has(name)){
      console.log("testTag", `[${name}].Preferences不存在`)
      return
    }
    try { //如果存在了,则删除数据
      let pref = this.preferencesMap.get(name) //获取preferences
      //根据key从preferences中删除值
      await pref.delete(key)
      console.log("testTag", `在[${name}].Preferences删除${key}成功`)
    } catch (e) {
      console.log("testTag", `在[${name}].Preferences删除${key}失败`,JSON.stringify(e))
    }
  }
​
​
}
​
// 导出 PreferencesUtil 的实例。
export default new PreferencesUtil();

1.4.3.加载Preferences

在entryAbility的onCreate方法,调用PreferencesUtil.loadPreferences在项目启动的时候完成Preferences加载, 这里需要添加await等待完成,代码如下:

import UIAbility from '@ohos.app.ability.UIAbility';
import hilog from '@ohos.hilog';
import window from '@ohos.window';
import PreferencesUtil from '../common/utils/PreferencesUtil';
​
export default class EntryAbility extends UIAbility {
  //添加async表示为异步加载
  async onCreate(want, launchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
    //项目启动的时候加载Preferences, 这里需要添加await等待完成
    await PreferencesUtil.loadPreferences(this.context,"MyFontSizePreferences")
  }
}

注意:这里entryAbility.ts中调用上面PreferencesUtil.ets封装的Preferences相关方法的时候,由于ts文件不能调用ets文件中方法,这个时候最简单的办法,就是将entryAbility.ts改为entryAbility.ets文件类型即可

1.4.4.在Preferences中获取保存的字体大小

在Index.ets中,字体大小使用首选项中定义的key,代码如下:

import router from '@ohos.router';
import hilog from '@ohos.hilog';
import CommonConstants from '../common/constants/CommonConstants';
import StyleConstants from '../common/constants/StyleConstants'
import { SettingItemComponent } from '../view/SettingItemComponent'
import { TitleBarComponent } from '../view/TitleBarComponent'
import HomeViewModel from '../viewmodel/HomeViewModel';
import { SettingData } from '../viewmodel/SettingData';
import PreferencesUtil from '../common/utils/PreferencesUtil';
​
@Entry
@Component
struct Index {
  //获取设置页面各项数据
  private settingArr = HomeViewModel.initSettingData();
​
  //字体大小:默认为16
  @State changeFontSize:number = CommonConstants.SET_SIZE_NORMAL
​
  //页面生命周期构造,在build渲染之前加载,获取系统中保存的字体大小
  async aboutToAppear(){
    this.changeFontSize = await PreferencesUtil.getPreferencesValue("MyFontSizePreferences","IndexFontSize",CommonConstants.SET_SIZE_NORMAL) as number
  }
​
  /**
   * 只有被@Entry装饰的组件才可以调用页面的生命周期
   * 这里如果不添加,在进入字体页面后,修改字体大小。在返回这个页面,设置不会生效
   */
  async onPageShow(){
    this.changeFontSize = await PreferencesUtil.getPreferencesValue("MyFontSizePreferences","IndexFontSize",CommonConstants.SET_SIZE_NORMAL) as number
  }
​
  build() {
    Column(){
      //标题栏组件
      TitleBarComponent({isBack:false, title:$r("app.string.home_title")})
​
      //列表选项:显示和亮度
      Row(){
        SettingItemComponent({
          setting: this.settingArr[CommonConstants.DISPLAY_INDEX],//指定显示的数据
          changeFontSize: $changeFontSize,//字体大小
          itemClick:()=>{
          }
        })
      }
      //自定义组件设置样式
      .blockBackground(StyleConstants.BLOCK_TOP_MARGIN_FIRST_PERCENT) //'0.5%'
//列表选项:声音
      Row(){
        SettingItemComponent({
          setting: this.settingArr[CommonConstants.VOICE_INDEX],//指定显示的数据
          changeFontSize: $changeFontSize,//字体大小
          itemClick:()=>{
​
          }
        })
      }
      //自定义组件设置样式
      .blockBackground(StyleConstants.BLOCK_TOP_MARGIN_SECOND_PERCENT) //'1.5%'
//应用管理、存储、隐私、字体大小
      Row(){
        this.SettingItems()
      }.blockBackground(StyleConstants.BLOCK_TOP_MARGIN_SECOND_PERCENT) //外上边距:'1.5%'
​
    }
    .width(StyleConstants.FULL_WIDTH)
    .height(StyleConstants.FULL_HEIGHT)
    .backgroundColor($r("sys.color.ohos_id_color_sub_background"))
  }
​
  /**
   * 定义组件,循环渲染产生:应用管理、存储、隐私、字体大小
   */
  @Builder SettingItems(){
    List(){
      ForEach(this.settingArr.slice(CommonConstants.START_INDEX,CommonConstants.END_INDEX),(item:SettingData,index?:number)=>{
        ListItem(){
          //循环产生
          SettingItemComponent({setting:item, changeFontSize:$changeFontSize,itemClick:()=>{
            //如果index为3,则给 设置字体大小 选项设置路由
            if (index === CommonConstants.SET_FONT_INDEX) {
              router.pushUrl({
                url: CommonConstants.SET_URL
              }).catch((error: Error) => {
                //如果有异常,则输出日志
                hilog.info(0xFFFF, "TAG", 'HomePage push error' + JSON.stringify(error));
              });
            }
          }})
        }
      },(item:SettingData)=>JSON.stringify(item))
    }
  }
}
​
@Extend(Row) function blockBackground(marginTop: string) {
  .backgroundColor(Color.White)//设置背景
  .borderRadius($r("app.float.block_background_radius"))//设置圆角半径
  .margin({top:marginTop})//设置外上边距
  .width(StyleConstants.BLOCK_WIDTH_PERCENT)//设置宽度
  .padding({top:$r("app.float.block_vertical_padding"), bottom:$r("app.float.block_vertical_padding")})//设置内上、下边距
}

在view包下创建SettingItemComponent.ets作为列表项目组件

/**
 * 设置列表项目组件
 */
import StyleConstants from '../common/constants/StyleConstants'
import { SettingData } from '../viewmodel/SettingData';
@Component
export struct SettingItemComponent {
  //列表项数据
  setting:SettingData = new SettingData()
  //字体大小
  @Link changeFontSize:number;
​
  //等会使用这个给设置路由
  itemClick:Function = ()=>{}
​
  build() {
    Row(){
      Image(this.setting.settingImage)
        .height($r('app.float.setting_item_ic_size'))
        .width($r('app.float.setting_item_ic_size'))
        .margin({ left: $r('app.float.setting_ic_margin_left'), right: $r('app.float.setting_ic_margin_right') })
​
      Text(this.setting.settingName)
        .fontSize(this.changeFontSize + StyleConstants.UNIT_FP)
        .fontColor($r("app.color.text"))
        .fontWeight(FontWeight.Medium)//设置合适的宽度
    }
    .width(StyleConstants.FULL_WIDTH)
    .height($r("app.float.setting_item_height")) //高度
    .onClick(()=>{
      this.itemClick()
    })
  }
}

在view包下创建TitleBarComponent.ets,在里面定义标题栏组件

/**
 * 标题栏组件
 */
import router from '@ohos.router';
import StyleConstants from '../common/constants/StyleConstants';
@Component
export struct TitleBarComponent {
  isBack: boolean = true; //控制是否需要显示返回按钮
  title: Resource = $r("app.string.empty")
  build() {
    Row(){
      //这里要组件要重复利用,如果进入子页面需要显示返回按钮
      if(this.isBack){
        Image($r("app.media.ic_public_back"))
          .height($r("app.float.title_ic_size"))
          .width($r("app.float.title_ic_size"))
          .margin({right:$r("app.float.title_ic_margin")})
          .onClick(()=>{
            router.back()//返回上一页
          })
      }
​
      Text(this.title)
        .fontSize(Color.Black)
        .fontSize($r("sys.float.ohos_id_text_size_headline8"))
        .fontWeight(FontWeight.Medium) //FontWeight.Medium字体粗细适中
        .margin({left:$r("app.float.title_text_margin_left")})
​
    }
    .width(StyleConstants.FULL_WIDTH)
    .height(StyleConstants.TITLE_BAR_HEIGHT_PERCENT)
    .padding({left:$r("app.float.title_padding_left")})
  }
}

1.4.5.在Preferences中修改字体大小后保存到首选项

在SetFontSizePage.ets中,修改字体大小后保存到首选项,代码如下:

import CommonConstants from '../common/constants/CommonConstants';
import StyleConstants from '../common/constants/StyleConstants';
import PreferencesUtil from '../common/utils/PreferencesUtil';
import { ChatItemComponent } from '../view/ChatItemComponent';
import { SliderLayoutComponent } from '../view/SliderLayoutComponent';
import { TitleBarComponent } from '../view/TitleBarComponent';
import { ChatData } from '../viewmodel/ChatData';
import SetViewModel from '../viewmodel/SetViewModel';
​
@Entry
@Component
struct SetFontSizePage {
  //获取聊天信息
  @State contentArr : Array<ChatData> = [];
​
  //设置字体大小
  @State changeFontSize: number = 16;
  @State fontSizeText: Resource = $r("app.string.set_size_normal"); //赋值:标准
/**
   * 在渲染之前加载完成
   */
  async aboutToAppear(){
    this.contentArr = SetViewModel.initChatData()
    this.changeFontSize = await PreferencesUtil.getPreferencesValue("MyFontSizePreferences","IndexFontSize",CommonConstants.SET_SIZE_NORMAL) as number
  }
​
  build() {
    Row() {
      Column() {
        //标题
        TitleBarComponent({title:$r("app.string.set_title")})
​
        //判断
        if(this.changeFontSize > 0){
          //循环渲染聊天记录
          List(){
            ForEach(this.contentArr, (item:ChatData)=>{
              ListItem(){
                //聊天信息组件
                ChatItemComponent({item:item, changFontSize:$changeFontSize})
              }
            },(item:ChatData) => JSON.stringify(item))
          }
          .layoutWeight(StyleConstants.WEIGHT_FULL)
        }
​
        //滑动条
        SliderLayoutComponent({fontSizeText:$fontSizeText,changeFontSize:$changeFontSize})
​
      }
      .width('100%')
    }
    .height('100%')
  }
}

在view包下创建ChatItemComponent.ets,在里面封装聊天信息组件,如下:

import StyleConstants from '../common/constants/StyleConstants'
import { ChatData } from '../viewmodel/ChatData'
import { ItemDirection } from '../viewmodel/ItemDirection'
@Component
export struct ChatItemComponent {
​
  //聊天数据对象
  item:ChatData = new ChatData()
  //字体大小
  @Link changFontSize: number
​
  build() {
    Row(){
      //聊天用户头像
      Image(this.item.itemDirection === ItemDirection.RIGHT?$r("app.media.right_head"):$r("app.media.left_head"))
        .width(StyleConstants.SET_CHAT_HEAD_SIZE_PERCENT)//宽度'8.9%'
        .aspectRatio(StyleConstants.HEAD_ASPECT_RATIO) //指定当前组件的宽高比为1。
        .margin({
          left:this.item.itemDirection === ItemDirection.RIGHT?StyleConstants.HEAD_LEFT_PERCENT:StyleConstants.HEAD_RIGHT_PERCENT,
          right:this.item.itemDirection === ItemDirection.RIGHT?StyleConstants.HEAD_RIGHT_PERCENT:StyleConstants.HEAD_LEFT_PERCENT
        })
      //聊天信息
      ChatContent({item:this.item,changFontSize:$changFontSize})
    }
    .alignItems(VerticalAlign.Top)
    .width(StyleConstants.FULL_WIDTH) //占据100%
    //设置滑动方向。枚举值支持逻辑与(&)和逻辑或(|)。如果this.item.itemDirection值为RIGHT,则元素从右向左排列,否则元素从左到右布局
    .direction(this.item.itemDirection === ItemDirection.RIGHT?Direction.Rtl:Direction.Ltr)
    .margin({top:StyleConstants.CHAT_TOP_MARGIN_PERCENT})
  }
}
​
/**
 * 用户聊天信息组件
 */
@Component
struct ChatContent {
  item:ChatData = new ChatData();
  @Link changFontSize:number;
  @State isLineFeed: boolean = false;
​
  build() {
    Row(){
      Text(this.item.content)
        .fontColor($r("app.color.text"))//设置字体颜色
        .fontSize(this.changFontSize + StyleConstants.UNIT_FP)
        .fontWeight(FontWeight.Medium)//设置字体宽度
        .onAreaChange((oldValue:Area, newValue:Area)=>{//当此组件的大小或位置更改完成时,将触发此回调。
          //设置完成后获取组件的高度,如果高度大于28,则设置this.isLineFeed为true
          this.isLineFeed = newValue.height > StyleConstants.DOUBLE_ROW_MIN;
        })
​
      //如果为true,则设置容器主轴上空白填充组件的最小尺寸。
      if (this.isLineFeed){
        //容器主轴上空白填充组件的最小尺寸。
        Blank().layoutWeight(StyleConstants.WEIGHT_FULL)
      }
    }
    //最大宽度
    .constraintSize({maxWidth:StyleConstants.MAX_CHAT_WIDTH_PERCENT})
    //设置滑动方向:元素从左到右排列。
    .direction(Direction.Ltr)
    .padding({//设置内边距
      left:$r("app.float.set_chat_content_vertical_padding"),
      right:$r("app.float.set_chat_content_vertical_padding"),
      top: $r("app.float.set_chat_content_horizontal_padding"),
      bottom: $r("app.float.set_chat_content_horizontal_padding")
    })
    .backgroundColor(//如果获取出来的值为RIGHT
      this.item.itemDirection === ItemDirection.RIGHT? $r("app.color.set_chat_right_bg"):$r("app.color.set_chat_left_bg")
    )
    //设置圆角矩形
    .borderRadius($r("app.float.set_chat_content_bg_radius"))
  }
}

在view包下创建SliderLayoutComponent.ets,在里面封装滑动条组件,如下:

import CommonConstants from '../common/constants/CommonConstants'
import StyleConstants from '../common/constants/StyleConstants'
import PreferencesUtil from '../common/utils/PreferencesUtil';
/**
 * 滑动条组件
 */
@Component
export struct SliderLayoutComponent {
  @Link fontSizeText: Resource;
  //控制字体大小
  @Link changeFontSize:number
​
  fontSizeLabel:object = {14:"小",16:"标准",18:"大",20:"特大"}
​
  build() {
    Column(){
      Text(this.fontSizeLabel[this.changeFontSize])
        .fontSize(20)
​
      Row(){
        Text($r('app.string.set_char_a'))
          .fontColor($r('app.color.text'))
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .textAlign(TextAlign.End)
          .width(StyleConstants.A_WIDTH_PERCENT)
          .padding({ right: $r('app.float.a_right_padding') })
​
        //滑动条组件
        Slider({
          //当前进度值
          value:this.changeFontSize,
          min:CommonConstants.SET_SLIDER_MIN,//设置进度条最小值
          max:CommonConstants.SET_SLIDER_MAX,//设置进度条最大值
          step:CommonConstants.SET_SLIDER_STEP,//设置Slider滑动步长
          style:SliderStyle.InSet //SliderStyle.InSet设置Slider的滑块与滑轨显示样式滑块在滑轨内。。默认值:SliderStyle.OutSet
        })
          .showSteps(true)//设置当前是否显示步长刻度值。
          .width(StyleConstants.SLIDER_WIDTH_PERCENT) //设置宽度占75%
          .onChange(async (value: number) => {//value是当前滑动进进度值
            this.changeFontSize = value
            //将字体大小,写入Preferences
            PreferencesUtil.putPreferencesValue("MyFontSizePreferences","IndexFontSize",value)
          })
​
        Text($r('app.string.set_char_a'))
          .fontColor($r('app.color.text'))
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .width(StyleConstants.A_WIDTH_PERCENT)
          .padding({ left: $r('app.float.a_left_padding') })
      }
    }
    .margin({bottom:50})
    .width("100%")
    .justifyContent(FlexAlign.Start)
    .alignItems(HorizontalAlign.Center)
  }
}

其余细节代码比较多,如果需要跟我联系,发送完整的代码

二、HarmonyOS数据管理之关系型数据库

2.1.关系型数据库是什么?

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

2.2.接口说明

以下是关系型数据库持久化功能的相关接口,大部分为异步接口。异步接口均有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 根据RdbPredicates的指定实例对象更新数据库中的数据。
delete(predicates: RdbPredicates, callback: AsyncCallback<number>):void 根据RdbPredicates的指定实例对象从数据库中删除数据。
query(predicates: RdbPredicates, columns: Array<string>, callback: AsyncCallback<ResultSet>):void 根据指定条件查询数据库中的数据。
deleteRdbStore(context: Context, name: string, callback: AsyncCallback<void>): void 删除数据库。

2.3.常用功能

2.3.1.导入模块

import relationalStore from '@ohos.data.relationalStore'

2.3.2.初始化数据库

 初始化数据库需要在页面渲染之前就完成

//1.rdb配置
const config = {
  name:"MyTaskList.db",//数据库文件名
  securityLevel: relationalStore.SecurityLevel.S1 //数据库安全级别
}
//2.初始化SQL
const sql = `CREATE TABLE IF NOT EXISTS TASK(
                ID INTEGER PRIMARY KEY AUTOINCREMENT,
                TASKCONENT TEXT NOT NULL,
                TASKSTATUS bit)`
//3.获取rdb
relationalStore.getRdbStore(context,config,(err,rdbStore)=>{
  if(err){
    console.log("testTag","获取rdbStore失败")
    return
  }
  //执行SQL,后续的增删改查都是通过rdbStore完成
  rdbStore.executeSql(sql)
  console.log("testTag","创建TASK表成功")
  //保存rdbStore后续都使用
  this.rdbStore = rdbStore
})

2.3.3.新增、修改、删除数据

这里需要通过rdbStore来调用新增、修改和删除方法

//新增
this.rdbStore.insert("TASK", { TASKCONENT:name, TASKSTATUS:false })

//修改
//1.指定需要修改的数据,键值对
let data = {TaskStatus:false}
//2.更新的条件
let predicates = new relationalStore.RdbPredicates("TASK")
predicates.equalTo("ID",id)//根据ID修改
//3.更新操作
this.rdbStore.update(data,predicates)

//删除
//1.删除的条件
let predicates = new relationalStore.RdbPredicates("TASK")
predicates.equalTo("ID",id)//根据ID删除
//3.更新操作
this.rdbStore.delete(predicates)

2.3.4.查询数据

数据在查询的出来解析的时候,有一个指针的概念,默认指针式-1的位置,读取一行就需要移动一次指针循环读取,直到没有数据为止,,如下所示:

img

所以下面通过 result.isAtLastRow方法判断,是否有下一行

//1.构建查询条件
let predicates = new relationalStore.RdbPredicates("TASK")
//2.执行查询,指定查询的字段,字段名要和表中保持一致
let result = await this.rdbStore.query(predicates,["ID", "TASKCONENT", "TASKSTATUS"])
//3.解析查询结果
//3.1.准备数组保存结果
let tasks:TaskModel[] = []
//3.2.循环遍历结果集,判断是否结果是否遍历到了最后一行
while (!result.isAtLastRow){//没有到最后一行就继续往后读取
  //指针移动到下一行数据
  result.goToNextRow()
  //根据字段名获取字段index,从而获取字段值
  let id = result.getLong(result.getColumnIndex("ID"))
  let taskContent = result.getString(result.getColumnIndex("TASKCONENT"))
  let taskStatus = result.getLong(result.getColumnIndex("TASKSTATUS"))//返回是1或者0,这里需要给转换一下
  //将获取到的数据添加到数组
  tasks.push({taskId:id,taskContent:taskContent,taskStatus:taskStatus?true:false})
}

2.3.常用功能

对于之前的任务列表功能进行重构,将数据的保存通过关系型数据库来实现持久化存储,这样在软件关闭,在此启动后,依然可以看到之前的数据,效果如下:

2.3.1.封装关系数据库操作工具类

在model包下创建 TaskDataModel.ets 内容如下:

import relationalStore from '@ohos.data.relationalStore';
import { TaskModel } from '../viewmodel/TaskModel';

export class TaskDataModel{
  //定义rdbStore,给后面查询、删除、修改、新增使用
  private rdbStore:relationalStore.RdbStore

  /**
   * 初始化任务数据表
   */
  initTaskDB(context){

    //1.rdb配置
    const config = {
      name:"MyTaskList.db",//数据库文件名
      securityLevel: relationalStore.SecurityLevel.S1 //数据库安全级别
    }
    //2.初始化SQL
    const sql = `CREATE TABLE IF NOT EXISTS TASK(
                    ID INTEGER PRIMARY KEY AUTOINCREMENT,
                    TASKCONENT TEXT NOT NULL,
                    TASKSTATUS bit)`
    //3.获取rdb
    relationalStore.getRdbStore(context,config,(err,rdbStore)=>{
      if(err){
        console.log("testTag","获取rdbStore失败")
        return
      }
      //执行SQL,后续的增删改查都是通过rdbStore完成
      rdbStore.executeSql(sql)
      console.log("testTag","创建TASK表成功")
      //保存rdbStore后续都使用
      this.rdbStore = rdbStore
    })
  }

  /**
   * 查询任务列表
   */
  async getTaskList(){
    //1.构建查询条件
    let predicates = new relationalStore.RdbPredicates("TASK")
    //2.执行查询,指定查询的字段,字段名要和表中保持一致
    let result = await this.rdbStore.query(predicates,["ID", "TASKCONENT", "TASKSTATUS"])
    //3.解析查询结果
    //3.1.准备数组保存结果
    let tasks:TaskModel[] = []
    //3.2.循环遍历结果集,判断是否结果是否遍历到了最后一行
    while (!result.isAtLastRow){//没有到最后一行就继续往后读取
      //指针移动到下一行数据
      result.goToNextRow()
      //根据字段名获取字段index,从而获取字段值
      let id = result.getLong(result.getColumnIndex("ID"))
      let taskContent = result.getString(result.getColumnIndex("TASKCONENT"))
      let taskStatus = result.getLong(result.getColumnIndex("TASKSTATUS"))//返回是1或者0,这里需要给转换一下
      //将获取到的数据添加到数组
      tasks.push({taskId:id,taskContent:taskContent,taskStatus:taskStatus?true:false})
    }
    console.log("testTag",`查询到数据:${tasks}`)
    return tasks
  }

  /**
   * 添加任务列表
   * @param name 任务名称
   */
  addTask(name:string):Promise<number>{
    console.log("能不能走到这里");
    //主键让自动生成,默认状态为false, 需要拿到主键,需要指定返回值Promise<number>
    return this.rdbStore.insert("TASK", { TASKCONENT:name, TASKSTATUS:false })
  }

  /**
   * 根据ID更新任务状态
   * @param id
   * @param finished
   */
  updateTaskStatus(id:number,finished:boolean):Promise<number>{
    //1.指定需要修改的数据,键值对
    let data = {TaskStatus:finished}
    //2.更新的条件
    let predicates = new relationalStore.RdbPredicates("TASK")
    predicates.equalTo("ID",id)
    //3.更新操作
    return this.rdbStore.update(data,predicates)
  }

  /**
   * 根据id删除任务
   * @param id
   */
  deleteTaskById(id:number):Promise<number>{
    //1.删除的条件
    let predicates = new relationalStore.RdbPredicates("TASK")
    predicates.equalTo("ID",id)
    //3.更新操作
    return this.rdbStore.delete(predicates)
  }
}

export default new TaskDataModel();

2.3.2.初始化数据库表

需要在项目启动的时候,完成数据库表的初始化(创建),可以在 EntryAbility.ts中 onWindowStageCreate或者onCreate方法中完成,这里在onWindowStageCreate方法完成,代码如下:

img

注意:这里由于EntryAbility默认是ts文件,而我们封装的数据在ets文件中,ts文件是无法调用ets文件中的方向,这时候就需要将EntryAbility改为ets文件

2.3.3.创建任务实体类

在viewmodel包下创建 TaskModel.ets,代码如下:

/**
 * 任务实体类
 */
export class TaskModel{
  //任务id
  taskId: number;
  //任务内容
  taskContent: string;
  //控制任务是否完成
  taskStatus:boolean;

  constructor(taskId: number,taskContent: string,taskStatus:boolean) {
    this.taskId = taskId
    this.taskContent = taskContent
    this.taskStatus = taskStatus
  }
}

2.3.4.编写主页

在pages包下 Index.ets,编写主页代码如下:

import { AddComponent } from '../components/AddComponent';
import { TaskListComponent } from '../components/TaskListComponent';
import { TaskModel } from '../viewmodel/TaskModel';

@Entry
@Component
struct Index {
  //控制是否显示添加页面
  @State displayAddPage: boolean = false;

  //任务列表
  @State taskList:Array<TaskModel> = [];

  //总任务数量
  @State totalTask: number = 0;

  //已完成任务数量
  @State finishTask:number = 0;

  build() {
    //判断,是否实现添加页面
    if(this.displayAddPage){
      //调用添加页面组件
      AddComponent({
        displayAddPage:$displayAddPage,
        taskList:$taskList,
        totalTask:$totalTask,
        finishTask:$finishTask
      })
    }else {
      //则显示任务列表
      TaskListComponent({
        displayAddPage:$displayAddPage,
        taskList:$taskList,
        totalTask:$totalTask,
        finishTask:$finishTask
      })
    }
  }
}

2.3.5.自定义组件

在components包下 AddComponent.ets,编写新增任务组件,代码如下:

import promptAction from '@ohos.promptAction';
import TaskDataModel from '../model/TaskDataModel';
import { TaskModel } from '../viewmodel/TaskModel';

@Component
export struct AddComponent {
  //控制是否显示添加页面
  @Link displayAddPage: boolean;
  //任务列表
  @Link taskList:Array<TaskModel>;
  //总任务数量
  @Link totalTask: number;
  //已完成任务数量
  @Link finishTask:number;

  //用户输入的任务内容
  @State taskContent:string = ""

  build() {
    Column({space:20}){
      Stack(){
        //返回按钮
        Column(){
          //返回按钮式图片
          Image($r("app.media.ic_public_back"))
            .width(30).height(40)
            .fillColor(Color.Black)
            .onClick(()=>{
              //控制是否显示新增页面
              this.displayAddPage = false
            })
        }
        .width("100%")
        .alignItems(HorizontalAlign.Start) //水平方向靠左

        //标题文字: 新增任务
        Text($r("app.string.Add_task"))
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
      }

      //用户自定义任务名字
      TextArea({text:this.taskContent,placeholder:$r("app.string.enter_task_content")})
        .backgroundColor(Color.White) //设置背景
        .width("95%")
        .height(200)
        .borderRadius(10)
        .borderColor(Color.Gray) //设置线条为灰色
        .onChange((value)=>{
          //将获取的值赋值给保存的变量
          this.taskContent = value;
        })

      //提交按钮
      Button({
        stateEffect:true, //按钮按下时开启按压态显示效果,
        type:ButtonType.Normal //默认按钮样式
      }){
        //按钮名称
        Text($r("app.string.submit"))
          .fontColor(Color.White)
          .fontSize(20)
      }
      .width("95%") //宽度
      .height(40) //高度
      .borderRadius(8) //圆角半径
      .borderColor($r("app.color.button_background"))//线条背景颜色
      .onClick(()=>{//点击后触发
        //判断输入框内容是否为空,为空时点击触发函数弹窗提示
        if(this.taskContent.length === 0){
          try {
            //显示提示信息
            promptAction.showToast(
              {
                message:$r("app.string.not_empty"),//弹窗内容
                duration:2000 //显示时间
              }
            )
          }catch (error){
            console.error(`showToast args error code is ${error.code}, message is ${error.message}`)
          }
          return
        }
        //如果输入有内容,则将任务添加到任务列表中,这里获取系统时间作为id, 这里是数组存值
        /*systemDateTime.getCurrentTime(true)
          .then((currentTime)=>{
            //1.创建一个任务模型对象
            let task = new TaskModel(currentTime,this.taskContent,false)

            //2.将新任务添加到任务列表中
            this.taskList.push(task) //将新添加的任务添加到数组中
            //3.更新任务总数量
            this.totalTask = this.taskList.length

            //隐藏添加任务页面
            this.displayAddPage = false
          })*/


        //这里采用数据库保存:只传入名称即可,id让自增,状态默认是false
        TaskDataModel.addTask(this.taskContent)
          .then((id)=>{//是新增后的数据id
            //这里将数据添加到本地数组中,在页面查询
            this.taskList.push(new TaskModel(id,this.taskContent,false))
            //3.更新任务总数量
            this.totalTask = this.taskList.length

            //隐藏添加任务页面
            this.displayAddPage = false
            console.log("testTag",`新增${this.taskContent}任务成功`);
          }).catch((error)=>{
          console.log("testTag",`新增${this.taskContent}任务失败, 失败的原因:${error}`);
        })

      })
    }
    .width("100%")
    .height("100%")
    .backgroundColor($r("app.color.background"))
    .padding({ left:20, top:30, right:20, bottom:20 })
  }
}

在components包下 TaskListComponent.ets,编写 任务列表组件,代码如下:

import TaskDataModel from '../model/TaskDataModel';
import { TaskModel } from '../viewmodel/TaskModel';

//统一卡片样式
@Styles
function card(){
  .width("95%")
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(10)
  //
  .shadow({
    radius:10,//模糊半径
    color:"#1F000000",//阴影的颜色
    offsetX:2,//x轴的偏移量
    offsetY:4//y轴的偏移量
  })
}

@Component
export struct TaskListComponent {
  //控制是否显示添加页面
  @Link displayAddPage: boolean;
  //任务列表
  @Link taskList:Array<TaskModel>;
  //总任务数量
  @Link totalTask: number;
  //已完成任务数量
  @Link finishTask:number;

  //任务类型。默认为全部
  @State taskType:string = "all"

  //更新:任务数量、完成数
  handleTaskChange(){
    //2.更新任务总量
    this.totalTask = this.taskList.length
    //更新已完成任务数量
    this.finishTask = this.taskList.filter((item:TaskModel)=>{
      //判断数量
      if(item.taskStatus){
        return true
      }
    }).length
  }

  //页面渲染前从数据库查询出来任务列表
  aboutToAppear(){
    TaskDataModel.getTaskList().then((tasks)=>{
      //从数据库中查询,然后赋值
      this.taskList = tasks
      console.log("testTag",`查询数据:${tasks}`)
      //更新任务数据
      this.handleTaskChange()
    })
  }

  //根据全部、待办、完成来区分显示的数据
  getTaskList():Array<TaskModel>{
    //如果任务类型为all,则返回任务列表
    if (this.taskType === "all"){
      return this.taskList
    }else if(this.taskType === "todo"){
      //filter返回满足回调函数中指定条件的数组元素。
      return this.taskList.filter((item)=>{
        //如果任务未完成,则返回true
        // item.taskStatus取反如果为true,则返回true,表示该数据item.taskStatus为false,是未完成待办的数据
        if(!item.taskStatus){
          return true
        }
      })
      //finish表示已经完成的任务
    }else if(this.taskType === "finish"){
      //filter返回满足回调函数中指定条件的数组元素。
      return this.taskList.filter((item)=>{
        if(item.taskStatus){
          return true
        }
      })
    }
  }

  build() {
    Column({space:10}){
      //标题
      Text($r("app.string.my_task"))
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      //三个按钮选项: 全部、待办、完成
      Row(){
        //全部
        Column(){
          Text($r("app.string.all"))
          //单选框: 组为taskType
          Radio({value:"all",group: "taskType"})
            .onChange((isSelect)=>{//单选框选中状态改变时触发回调。
              if(isSelect){
                //任务类型设置为all
                this.taskType = "all"
              }
            })
            //设置是否被选中
            .checked(this.taskType == "all"? true:false)
        }

        //待办
        Column(){
          Text($r("app.string.todo"))
          //单选框: 组为taskType
          Radio({value:"todo",group: "taskType"})
            .onChange((isSelect)=>{//单选框选中状态改变时触发回调。
              if(isSelect){
                //任务类型设置为all
                this.taskType = "todo"
              }
            })
              //设置是否被选中
            .checked(this.taskType == "todo"? true:false)
        }

        //完成
        Column(){
          Text($r("app.string.finish"))
          //单选框: 组为taskType
          Radio({value:"finish",group: "taskType"})
            .onChange((isSelect)=>{//单选框选中状态改变时触发回调。
              if(isSelect){
                //任务类型设置为all
                this.taskType = "finish"
              }
            })
              //设置是否被选中
            .checked(this.taskType == "finish"? true:false)
        }
      }
      .width("100%")
      .justifyContent(FlexAlign.SpaceAround)

      //任务进度卡片
      Row(){
        Text("任务进度:")
          .fontSize(25)
          .fontWeight(FontWeight.Bold)
        Stack(){
          //环形进度条组件,用于显示内容加载或操作处理等进度。
          Progress({
            value:this.finishTask,//指定当前进度值
            total:this.totalTask,//指定进度总长
            type:ProgressType.Ring //指定进度条类型,ProgressType.Ring表示环形无刻度样式,环形圆环逐渐显示至完全填充效果
          })
            .width(70)

          //数字显示 例如  已完成 / 总量
          Row(){
            Text(this.finishTask.toString())//已完成任务数
              .fontSize(18)
              .fontColor($r("app.color.button_background"))
            Text("/ " + this.totalTask.toString())//任务总数
              .fontSize(18)
              .fontColor($r("app.color.button_background"))
          }
        }
      }
      .card()
      .margin({top:10,bottom:10})
      .justifyContent(FlexAlign.SpaceEvenly)

      //显示分割线
      Text()
        .width("95%")
        .height(1.5)
        .backgroundColor($r("app.color.Line_Between"))

      //显示任务列表(可滑动)
      Column({space:10}){
        List({space:10}){
          ForEach(this.getTaskList(),(item:TaskModel, index:number)=>{
            ListItem(){
              //任务列表
              Row(){
                //完成待办后的样式
                Text(item.taskContent)
                  .margin({left:10})//设置左边距
                  .decoration({//三目运算符、根据任务是否完成,设置文本样式, 已完成TextDecorationType.LineThrough使用中划线,为false,则没有划线TextDecorationType.None
                    type: item.taskStatus? TextDecorationType.LineThrough:TextDecorationType.None
                  })
                  .width("70%")//设置宽度
                  .maxLines(1) //设置文本溢出时的样式,默认情况下,文本是自动折行的
                  .textOverflow({overflow:TextOverflow.Ellipsis}) // 超出maxLines展示省略号

                //添加一个空行
                Blank()

                //创建复选框
                Checkbox()//设置复选框的值
                  .select(item.taskStatus) //当复选框的值发送变化的时候,执行回调函数
                  .onChange((value)=>{
                    //从数据库中修改:
                    TaskDataModel.updateTaskStatus(item.taskId,value).then(()=>{
                      //判断任务列表中是否存在某个元素,如果存在,则返回索引
                      let idx = this.taskList.indexOf(item)
                      //构建:根据遍历信息,构建任务对象,修改数组中状态,但是注意只是修改状态
                      let newTask = new TaskModel(item.taskId,item.taskContent,value)
                      //根据索引index,在数组中找到数据,替换数据
                      this.taskList[idx] = newTask
                      //更新任务数量
                      this.handleTaskChange()
                      console.log("testTag",`修改任务${item.taskContent}成功`);
                    }).catch((error)=>{
                      console.log("testTag",`修改任务${item.taskContent}失败,原因:${error}`);
                    })

                  })
              }
              .card()
              .justifyContent(FlexAlign.SpaceBetween)
            }
            //设置列表项沿列表横轴方向滑动时出现的操作项
            .swipeAction({end: this.deleteButton(item)})//设置滑动删除数据
          })
        }
        .width("100%")
        .height("55%")
        .alignListItem(ListItemAlign.Center) //在交叉轴方向的布局方式
      }
      .margin({top:10})

      //新增任务按钮
      Column(){
        Button({
          type:ButtonType.Circle, //设置圆形按钮
          stateEffect: true //设置按钮状态
        })
        {
          Text("+")
            .fontSize(50)
            .fontColor(Color.White)
        }
          .width(60)
          .height(60)
          .borderColor($r("app.color.button_background"))
          .onClick(()=>{
            //设置为true显示添加页面
            this.displayAddPage = true
            //跟新数据
            this.handleTaskChange()
          })
      }
      .width("100%")
      .alignItems(HorizontalAlign.End)
    }
    .width("100%")
    .height("100%")
    .backgroundColor($r("app.color.background"))
    .padding(20)
  }

  /**
   * 删除任务函数
   */
  deleteTask(item:TaskModel){
    //弹出提示框
    AlertDialog.show({
      title:$r("app.string.delete"),
      message:$r("app.string.sure_delete"),
      autoCancel:true,
      alignment:DialogAlignment.Bottom,
      //偏移量
      offset:{ dx:0, dy:-20 },
      //主要按钮
      primaryButton:{
        value: $r("app.string.no"),
        action:()=>{}
      },
      //次要按钮
      secondaryButton:{
        value:$r("app.string.yes"),
        action:()=>{
          //从数据库中删除
          TaskDataModel.deleteTaskById(item.taskId)
            .then(()=>{
              //从任务数组中删除
              //由于是分三个:全部、待办、完成,顺序不一致,所以这里需要获取数据,查询出来再任务列表的索引
              let idx = this.taskList.indexOf(item)
              //从任务列表中删除
              this.taskList.splice(idx,1)
              //更新数据
              this.handleTaskChange()
              console.log("testTag",`删除任务${item.taskContent}成功`);
            }).catch((error)=>{
              console.log("testTag",`删除任务${item.taskContent}失败,原因:${error}`);
          })
        }
      }
    }
    )
  }

  /**
   * 删除按钮组件
   * @param taskId
   */
  @Builder
  deleteButton(item:TaskModel){
    //删除按钮
    Button(){
      Image($r("app.media.ic_public_delete_filled"))
        .fillColor(Color.White)
        .width(20)
    }
    .width(40)
    .height(40)
    .type(ButtonType.Circle)//设置为圆形按钮
    .backgroundColor(Color.Red)//设置按钮为红色
    .margin(5)
    .onClick(()=>{

      //调用从数组中删除函数
      this.deleteTask(item)
    })
  }
}
posted @ 2024-01-26 22:00  酒剑仙*  阅读(433)  评论(0)    收藏  举报