应用数据持久化
应用数据持久化,是指应用将内存中的数据通过文件或数据库的形式保存到设备上。内存中的数据形态通常是任意的数据结构或数据对象,存储介质上的数据形态可能是文本、数据库、二进制文件等。
-
用户首选项(Preferences) :通常用于保存应用的配置信息。数据通过文本的形式保存在设备中,应用使用过程中会将文本中的数据全量加载到内存中,所以访问速度快、效率高,但不适合需要存储大量数据的场景。
-
键值型数据库(KV-Store) :一种非关系型数据库,其数据以“键值”对的形式进行组织、索引和存储,其中“键”作为唯一标识符。适合很少数据关系和业务关系的业务数据存储,同时因其在分布式场景中降低了解决数据库版本兼容问题的复杂度,和数据同步过程中冲突解决的复杂度而被广泛使用。相比于关系型数据库,更容易做到跨设备跨版本兼容。
-
关系型数据库(RelationalStore) :一种关系型数据库,以行和列的形式存储数据,广泛用于应用中的关系型数据的处理,包括一系列的增、删、改、查等接口,开发者也可以运行自己定义的SQL语句来满足复杂业务场景的需要。
用户首选项
用户首选项(Preference)为应用提供key-Value键值型的数据处理能力,支持应用持久化轻量级数据,并对其修改和查询。当用户希望有一个全局唯一存储的地方,可以采用用户首选项来进行存储。
Preferences会将该数据缓存在内存中,当用户读取的时候,能够快速从内存中获取数据。Preferences会随着存放的数据量越多而导致应用占用的内存越大,因此,Preferences不适合存放过多的数据,适用的场景一般为应用保存用户的个性化设置(字体大小,是否开启夜间模式) 等。
preferences必须使用模拟器,预览器不可以!!
使用
- 导入首选项模块
import preferences from '@ohos.data.preferences';
- 获取首选项实例,读取指定文件
//preferences.getPreferences(UIAbilityContent Preferences实例名称)
preferences.getPreferences(this.context,'MyAppPreferences')
.then(preferences=>{
// 获取成功
})
.catch(reason=>{
// 获取失败
})
- 数据操作
// 写入数据,如果已经存在则会覆盖,可利用.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=>{})
限制
-
Key为string类型,要求非空且长度不超过80字节
-
Value可以使string、number、boolean及以上类型数组,大小不超过8192字节
-
数据量建议不超过一万条
Demo
- 封装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
- 加载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')
}
}
- 使用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组件提供的本地数据库,用于管理应用中的结构化管理。例如:记账本、备忘录。
必须使用模拟器,预览器不可以!!
使用
当应用完成查询数据操作,不再使用结果集(ResultSet)时,请及时调用close方法关闭结果集,释放系统为其分配的内存。
- 初始化数据库
- 导入关系型数据库模块
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.准备数据
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.构建查询条件
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
- 封装关系型数据库
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;
- 加载关系型数据库
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)
}
}
- 使用关系型数据库
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)
}
}