HarmonyOS 4.0——应用数据持久化及Demo

152 阅读9分钟

应用数据持久化

应用数据持久化,是指应用将内存中的数据通过文件或数据库的形式保存到设备上。内存中的数据形态通常是任意的数据结构或数据对象,存储介质上的数据形态可能是文本、数据库、二进制文件等。

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

  • 键值型数据库(KV-Store) :一种非关系型数据库,其数据以“键值”对的形式进行组织、索引和存储,其中“键”作为唯一标识符。适合很少数据关系和业务关系的业务数据存储,同时因其在分布式场景中降低了解决数据库版本兼容问题的复杂度,和数据同步过程中冲突解决的复杂度而被广泛使用。相比于关系型数据库,更容易做到跨设备跨版本兼容

  • 关系型数据库(RelationalStore) :一种关系型数据库,以行和列的形式存储数据,广泛用于应用中的关系型数据的处理,包括一系列的增、删、改、查等接口,开发者也可以运行自己定义的SQL语句来满足复杂业务场景的需要

用户首选项

用户首选项(Preference)为应用提供key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。当用户希望有一个全局唯一存储的地方,可以采用用户首选项来进行存储。

Preferences会将该数据缓存在内存中,当用户读取的时候,能够快速从内存中获取数据。Preferences会随着存放的数据量越多而导致应用占用的内存越大,因此,Preferences不适合存放过多的数据,适用的场景一般为应用保存用户的个性化设置(字体大小,是否开启夜间模式) 等。

preferences必须使用模拟器,预览器不可以!!

image.png

使用

  1. 导入首选项模块
import preferences from '@ohos.data.preferences';
  1. 获取首选项实例,读取指定文件
//preferences.getPreferences(UIAbilityContent  Preferences实例名称)
preferences.getPreferences(this.context,'MyAppPreferences')
  .then(preferences=>{
    // 获取成功
  })
  .catch(reason=>{
    // 获取失败
  })
  1. 数据操作
// 写入数据,如果已经存在则会覆盖,可利用.has()判断是否存在
preferences.put('key',val)
  .then(()=>preferences.flush()) // 刷到磁盘
  .catch(reson=>{}) // 处理异常
  
// 查询数据
preferences.get('key','defaultValue')
  .then(()=>console.log('查询成功'))) 
  .catch(reason=>console.log('查询失败')))
  
// 删除数据
preferences.delete('key')
  .then(()=>{}) 
  .catch(reason=>{})

限制

  • Keystring类型,要求非空且长度不超过80字节

  • Value可以使string、number、boolean及以上类型数组,大小不超过8192字节

  • 数据量建议不超过一万条

Demo

  1. 封装preferences

src/main/ets/common/util/UserPreferencesUtil.ets

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

class PreferencesUtil {
  prefMap: Map<string, preferences.Preferences> = new Map() //记录多个preferences实例

  // 加载
  async loadPreference(context, name: string) {
    try { // 加载preferences
      let pref = await preferences.getPreferences(context, name)
      this.prefMap.set(name, pref)
      console.log('testTag', `加载Preferences[${name}]成功`)
    } catch (e) {
      console.log('testTag', `加载Preferences[${name}]失败`, JSON.stringify(e))
    }
  }

  // 新增
  async putPreferenceValue(name: string, key: string, value: preferences.ValueType) {
    if (!this.prefMap.has(name)) {
      console.log('testTag', `Preferences[${name}]尚未初始化!`)
      return
    }
    try {
      let pref = this.prefMap.get(name)
      // 写入数据
      await pref.put(key, value)
      // 刷盘
      await pref.flush()
      console.log('testTag', `保存Preferences[${name}.${key} = ${value}]成功`)
    } catch (e) {
      console.log('testTag', `保存Preferences[${name}.${key} = ${value}]失败`, JSON.stringify(e))
    }
  }

  // 读取
  async getPreferenceValue(name: string, key: string, defaultValue: preferences.ValueType) {
    if (!this.prefMap.has(name)) {
      console.log('testTag', `Preferences[${name}]尚未初始化!`)
      return
    }
    try {
      let pref = this.prefMap.get(name)
      // 读数据
      let value = await pref.get(key, defaultValue)
      console.log('testTag', `读取Preferences[${name}.${key} = ${value}]成功`)
      return value
    } catch (e) {
      console.log('testTag', `读取Preferences[${name}.${key} ]失败`, JSON.stringify(e))
    }
  }

  // 删除
  async deletePreferenceValue(name: string, key: string) {
    if (!this.prefMap.has(name)) {
      console.log('testTag', `Preferences[${name}]尚未初始化!`)
      return
    }
    try {
      let pref = this.prefMap.get(name)
      let value = await pref.delete(key)
      // 刷盘
      await pref.flush()
      console.log('testTag', `删除Preferences[${name}.${key} = ${value}]成功`)
      return value
    } catch (e) {
      console.log('testTag', `删除Preferences[${name}.${key} ]失败`, JSON.stringify(e))
    }
  }
}

const preferencesUtil = new PreferencesUtil()

export default preferencesUtil as PreferencesUtil
  1. 加载preferences

src/main/ets/entryability/EntryAbility.ets

import PreferencesUtil from '../common/util/UserPreferencesUtil'

export default class EntryAbility extends UIAbility {
  async onCreate(want, launchParam) {
    //   加载用户首选项
    await PreferencesUtil.loadPreference(this.context, 'MyPreferences')
    //   可以加载多个
    //await PreferencesUtil.loadPreference(this.context, 'xxx')
  }
}
  1. 使用preferences

src/main/ets/pages/UserPreferencesPage.ets

import PreferencesUtil from '../common/util/UserPreferencesUtil'
import UserPreferencesPanel from '../view/UserPreferencesPanel'

@Entry
@Component
struct UserPreferencesPage {
  @Provide fontSize: number = 15
  @State isShowPanel: boolean = false

  async aboutToAppear() {
    this.fontSize = await PreferencesUtil.getPreferenceValue('MyPreferences', 'GlobalFontSize', 15) as number
  }

  build() {
    Column() {
      this.Header()
      this.Content()
      Button('删除首选项')
        .onClick(async () => {
          await PreferencesUtil.deletePreferenceValue('MyPreferences', 'GlobalFontSize')
          this.fontSize = 15
        })
      if (this.isShowPanel) {
        UserPreferencesPanel()
          .transition({
            translate: { y: 115 }
          })
      }
    }
    .width('100%')
    .height('100%')

  }

  @Builder
  Header() {
    Row() {
      Text('用户首选项')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .height(80)
      Image($r('app.media.ic_public_text_filled'))
        .width(24)
        .onClick(() => {
          animateTo({ duration: 500, curve: Curve.EaseOut }, () => this.isShowPanel = !this.isShowPanel)
        })
    }
    .justifyContent(FlexAlign.SpaceBetween)
    .width('90%')
  }

  @Builder
  Content() {
    Text('文本内容文本内容文本内容文本内容文本内容')
      .fontSize(this.fontSize)
      .layoutWeight(1)
  }
}

src/main/ets/view/UserPreferencesPanel.ets

import PreferencesUtil from "../common/util/UserPreferencesUtil"

@Component
export default struct UserPreferencesPanel {
  @Consume fontSize: number
  fontSizeLabel: object = {
    15: '特小',
    20: '小',
    25: '标准',
    30: '大',
    35: '特大'
  }

  build() {
    Column() {
      Text(this.fontSizeLabel[this.fontSize]).fontSize(20)
      Row({ space: 5 }) {
        Text('A').fontSize(14).fontWeight(FontWeight.Bold)
        Slider({
          min: 15,
          max: 35,
          step: 5,
          value: this.fontSize
        })
          .showSteps(true)
          .trackThickness(6)
          .layoutWeight(1)
          .onChange(val => {
            // 修改字体大小
            this.fontSize = val
            // 写入Preferences
            PreferencesUtil.putPreferenceValue('MyPreferences', 'GlobalFontSize', val)
          })
        Text('A').fontSize(20).fontWeight(FontWeight.Bold)
      }.width('100%')
    }
    .width('100%')
    .padding(15)
    .backgroundColor('#fff1f0f0')
    .borderRadius(20)
  }
}

键值型数据库

关系型数据库

关系型数据库(RDB)是基于SQLite组件提供的本地数据库,用于管理应用中的结构化管理。例如:记账本、备忘录。

必须使用模拟器,预览器不可以!!

image.png

使用

当应用完成查询数据操作,不再使用结果集(ResultSet)时,请及时调用close方法关闭结果集,释放系统为其分配的内存。

  1. 初始化数据库
  • 导入关系型数据库模块
import relationalStore from '@ohos.data.relationalStore';
  • 初始化数据库表
//   1.rdb配置
const config = {
  name: 'MyApplication.db', // 数据库文件名
  securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别
}
//   2.初始化SQL语句   (项目启动创建表)
const sql = `CREATE TABLE IF NOT EXISTS TASK (
            ID INTEGER PRIMARY KEY AUTOINCREMENT,
            NAME TEXT NOT NULL,
            FINISHED bit
            )`
//   3.获取rdb
relationalStore.getRdbStore(this.context, config, (err, rdbStore) => {
  //   执行Sql   后续的增删改查都是使用的rdbStore对象
  rdbStore.executeSql(sql) 
})
  1. 增、删、改数据
  • 新增数据
//   1.准备数据
let task = {id: 1, name: '任务1', finished: false}
//   1.新增
this.rdbStore.insert(this.tableName, task)
  • 修改数据
//   1.要更新的数据
let data = { 'finished': true }
//   2.更新的条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('ID', id)
//   3.更新操作
this.rdbStore.update(data, predicates)
  • 删除数据
//   1.删除的条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
predicates.equalTo('ID', id)
//   3.更新操作
this.rdbStore.delete(predicates)
  1. 查询数据
  • 查询数据
//   1.构建查询条件
let predicates = new relationalStore.RdbPredicates(this.tableName)
//   2.查询
let result = await this.rdbStore.query(predicates, ['ID', 'NAME', 'FINISHED'])
  • 解析结果
//   1. 准备数组保存结果
let tasks: TaskInfo[] = []
//   2. 循环遍历结果集,判断是否遍历到最后一行
while (!result.isAtLastRow) {
  //    指定移动到下一行 (指针,默认指向-1,数据从0开始有)
  result.goToNextRow()
  //    根据字段名获取字段index,从而获取字段值
  let id = result.getLong(result.getColumnIndex('ID'))
  let name = result.getString(result.getColumnIndex('NAME'))
  tasks.push({ id, name })
}

限制

  • 系统默认日志方式是WAL(Write Ahead Log)模式,系统默认落盘方式是FULL模式。

  • 数据库中连接池的最大数量4个,用以管理用户的读操作。

  • 为保证数据的准确性,数据库同一时间只能支持一个写操作

  • 当应用被卸载完成后,设备上的相关数据库文件及临时文件会被自动清除

Demo

  1. 封装关系型数据库

src/main/ets/model/TaskModel.ets

import relationalStore from '@ohos.data.relationalStore';
// ts文件不能导入ets,所以TaskModel需定义为ets文件
import TaskInfo from '../viewmodel/TaskInfo';

class TaskModel {
  private rdbStore: relationalStore.RdbStore
  private tableName: string = 'TASK'

  /**
   * 初始化任务表
   */
  initTaskDB(context) {
    // 1.rdb配置
    const config = {
      name: 'MyApplication.db', // 数据库文件名
      securityLevel: relationalStore.SecurityLevel.S1  // 数据库安全级别
    }
    // 2.初始化SQL语句 (项目启动创建表)
    const sql = `CREATE TABLE IF NOT EXISTS TASK (
                  ID INTEGER PRIMARY KEY AUTOINCREMENT,
                  NAME TEXT NOT NULL,
                  FINISHED bit
                 )`
    // 3.获取rdb
    relationalStore.getRdbStore(context, config, (err, rdbStore) => {
      if (err) {
        console.log('testTag', '获取rdbStore失败!')
        return
      }
      // 执行Sql
      rdbStore.executeSql(sql)  // 创建数据表
      console.log('testTag', '创建task表成功!')
      // 保存rdbStore
      this.rdbStore = rdbStore
    })
  }

  /**
   * 查询任务列表
   */
  async getTaskList() {
    // 1.构建查询条件
    let predicates = new relationalStore.RdbPredicates(this.tableName)
    // 2.查询
    let result = await this.rdbStore.query(predicates, ['ID', 'NAME', 'FINISHED'])
    // 3.解析查询结果
    // 3.1.定义一个数组,组装最终的查询结果
    let tasks: TaskInfo[] = []
    // 3.2.遍历封装
    while (!result.isAtLastRow) {
      // 3.3.指针移动到下一行
      result.goToNextRow()
      // 3.4.获取数据
      let id = result.getLong(result.getColumnIndex('ID'))
      let name = result.getString(result.getColumnIndex('NAME'))
      let finished = result.getLong(result.getColumnIndex('FINISHED'))
      // 3.5.封装到数组
      tasks.push({ id, name, finished: !!finished })
    }
    console.log('testTag', '查询到数据:', JSON.stringify(tasks))
    return tasks
  }

  /**
   * 添加一个新的任务
   * @param name 任务名称
   * @returns 任务id
   */
  addTask(name: string): Promise<number> {
    return this.rdbStore.insert(this.tableName, { name, finished: false })
  }

  /**
   * 根据id更新任务状态
   * @param id 任务id
   * @param finished 任务是否完成
   */
  updateTaskStatus(id: number, finished: boolean) {
    // 1.要更新的数据
    let data = { finished }
    // 2.更新的条件
    let predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('ID', id)
    // 3.更新操作
    return this.rdbStore.update(data, predicates)
  }

  /**
   * 根据id删除任务
   * @param id 任务id
   */
  deleteTaskById(id: number) {
    // 1.删除的条件
    let predicates = new relationalStore.RdbPredicates(this.tableName)
    predicates.equalTo('ID', id)
    // 2.删除操作
    return this.rdbStore.delete(predicates)
  }
}

let taskModel = new TaskModel();

export default taskModel as TaskModel;
  1. 加载关系型数据库

src/main/ets/entryability/EntryAbility.ets

// 由于ts文件不能引入ets,这里EntryAbility.ts需改成ets文件
import TaskModel from '../model/TaskModel'

export default class EntryAbility extends UIAbility {
  async onCreate(want, launchParam) {
    //   初始化任务表
    TaskModel.initTaskDB(this.context)
  }
}
  1. 使用关系型数据库

src/main/ets/pages/TaskManagePage.ets

import { Header } from '../common/components/Header'
import TaskList from '../views/task/TaskList'
import TaskStatistics from '../views/task/TaskStatistics'

@Entry
@Component
struct TaskManagePage {
  @State totalTask: number = 0
  @State finishTask: number = 0

  build() {
    Column({ space: 10 }) {
      // 1.顶部导航
      Header({ title: '任务列表' })
      // 2.任务进度卡片
      TaskStatistics({ totalTask: this.totalTask, finishTask: this.finishTask })
      // 3.任务列表
      TaskList({ totalTask: $totalTask, finishTask: $finishTask })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F2F3')
  }
}

src/main/ets/viewmodel/TaskInfo.ets

@Observed
export default class TaskInfo{
  id: number
  // 任务名称
  name: string
  // 任务状态:是否完成
  finished: boolean

  constructor(id: number, name: string) {
    this.id = id
    this.name = name
    this.finished = false
  }
}

src/main/ets/common/components/Header.ets

@Component
export struct Header {
  private title: string

  build() {
    Row({ space: 5 }) {
      Image($r('app.media.ic_public_back'))
        .width(24)
        .onClick(() => {
          // router.back()
        })
      if (this.title) {
        Text(this.title)
          .fontSize(24)
      }
      Blank()
      Image($r('app.media.ic_public_refresh'))
        .width(24)
    }
    .width('98%')
    .height(30)
  }
}

src/main/ets/views/task/TaskItem.ets

import TaskModel from '../../model/TaskModel'
import TaskInfo from '../../viewmodel/TaskInfo'

@Component
export default struct TaskItem {
  @ObjectLink item: TaskInfo
  onTaskChange: (item: TaskInfo) => void

  build() {
    Row(){
      if(this.item.finished){
        Text(this.item.name)
          .finishedTask()
      }else{
        Text(this.item.name)
      }
      Checkbox()
        .select(this.item.finished)
        .onChange(async val => {
          // 1.更新当前任务状态
          TaskModel.updateTaskStatus(this.item.id, val)
            .then(() => {
              this.item.finished = val
              // 2.更新已完成任务数量
              this.onTaskChange(this.item)
            })
            .catch(error => console.log('testTag', '更新任务状态失败, id = ', this.item.id, JSON.stringify(error)))

        })
    }
    .card()
    .justifyContent(FlexAlign.SpaceBetween)
  }
}

// 任务完成样式
@Extend(Text) function finishedTask(){
  .decoration({type:TextDecorationType.LineThrough})
  .fontColor('#B1B2B1')
}

// 统一的卡片样式
@Styles function card(){
  .width('95%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4})
}

src/main/ets/views/task/TaskList.ets

import TaskInfo from '../../viewmodel/TaskInfo'
import TaskItem from './TaskItem'
import TaskModel from '../../model/TaskModel'

@Component
export default struct TaskList {
  // 总任务数量
  @Link totalTask: number
  @Link finishTask: number
  // 任务数组
  @State tasks: TaskInfo[] = []

  idx: number = 1
  // 任务信息弹窗
  dialogController: CustomDialogController = new CustomDialogController({
    builder: TaskInfoDialog({onTaskConfirm: this.handleAddTask.bind(this)}),
  })

  aboutToAppear(){
    // 查询任务列表
    console.log('testTag', '初始化组件,查询任务列表')
    TaskModel.getTaskList()
      .then(tasks => {
        this.tasks = tasks
        // 更新任务状态
        this.handleTaskChange()
      })
  }

  handleTaskChange(){
    // 1.更新任务总数量
    this.totalTask = this.tasks.length
    // 2.更新已完成任务数量
    this.finishTask = this.tasks.filter(item => item.finished).length
  }

  handleAddTask(name: string){
    // 1.新增任务
    TaskModel.addTask(name)
      .then(id => {
        console.log('testTag', '处理新增任务: ', name)
        // 回显到数组页面
        this.tasks.push(new TaskInfo(id, name))
        // 2.更新任务完成状态
        this.handleTaskChange()
        // 3.关闭对话框
        this.dialogController.close()
      })
      .catch(error => console.log('testTag', '新增任务失败:', name, JSON.stringify(error)))

  }

  build() {
    Column(){
      // 1.新增任务按钮
      Button('新增任务')
        .width(200)
        .margin({bottom: 10})
        .onClick(() => {
          // 打开新增表单对话框
          this.dialogController.open()
        })

      // 2.任务列表
      List({space: 10}){
        ForEach(
          this.tasks,
          (item: TaskInfo, index) => {
            ListItem(){
              TaskItem({item: item, onTaskChange: this.handleTaskChange.bind(this)})
            }
            .swipeAction({end: this.DeleteButton(index, item.id)})
          }
        )
      }
      .width('100%')
      .layoutWeight(1)
      .alignListItem(ListItemAlign.Center)
    }
  }

  @Builder DeleteButton(index: number, id: number){
    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(() => {
      // 删除任务
      TaskModel.deleteTaskById(id)
        .then(() => {
          this.tasks.splice(index, 1)
          console.log('testTag', `尝试删除任务,index: ${index}`)
          this.handleTaskChange()
        })
        .catch(error => console.log('testTag', '删除任务失败,id = ', id, JSON.stringify(error)))

    })
  }
}

@CustomDialog
struct TaskInfoDialog{

  name: string = ''
  onTaskConfirm : (name: string) => void
  controller: CustomDialogController

  build(){
    Column({space: 20}){
      TextInput({placeholder: '输入任务名称'})
        .onChange(val => this.name = val)
      Row(){
        Button('确定')
          .onClick(() => {
            this.onTaskConfirm(this.name)
          })
        Button('取消')
          .backgroundColor(Color.Grey)
          .onClick(() => {
            this.controller.close()
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
    }
    .width('100%')
    .padding(20)
  }
}

src/main/ets/views/task/TaskStatistics.ets

// 统一的卡片样式
@Styles function card(){
  .width('95%')
  .padding(20)
  .backgroundColor(Color.White)
  .borderRadius(15)
  .shadow({radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4})
}

@Component
export default struct TaskStatistics {
  @Prop totalTask: number
  @Prop finishTask: number

  build() {
    Row(){
      Text('任务进度:')
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
      Stack(){
        Progress({
          value: this.finishTask,
          total: this.totalTask,
          type: ProgressType.Ring
        })
          .width(100)
        Row(){
          Text(this.finishTask.toString())
            .fontSize(24)
            .fontColor('#36D')
          Text(' / ' + this.totalTask.toString())
            .fontSize(24)
        }
      }
    }
    .card()
    .margin({top: 5, bottom: 10})
    .justifyContent(FlexAlign.SpaceEvenly)
  }
}