使用关系型数据库实现目标管理
本文介绍了一个基于关系型数据库的运动计划管理应用实现案例。案例中使用了SQLite关系型数据库,构建了运动表和计划表两张数据表,通过封装的RDBStoreUtil工具类实现了增删改查等基本操作。文章详细展示了数据库创建、表结构设计、SQL语句封装、数据操作接口实现等关键代码,并说明了如何将数据库操作与ArkUI界面组件结合,实现计划列表展示、条件筛选、新增编辑等功能。同时,文章还介绍了关系型数据库
一、案例实现
1. 13.4.2.2 案例运用的知识点
- 关系型数据库:基于关系模型来管理数据的数据库,提供了增、删、改、查等接口,也可运行输入的SQL语句满足复杂场景需要。
2. 13.4.2.3 数据库的创建
- 分析
该案例使用了关系型数据库,涉及两张数据表:运动表和计划表。
运动表包括以下字段:id(主键)、sport_name(运动名称)、sport_type(运动类型)。
计划表包括以下字段:id(主键)、sport_id(外键,关联运动表的主键)、duration(运动时长)、status(运动完成状态)。
具体实现功能如下:
- 创建运动表和计划表,并向运动表中插入一些数据。
- 联表查询,查询出所有计划,以及计划对应的运动名和运动类型。
- 查询所有计划中的时长,用于刷新作为筛选条件的时长下拉框选项。
- 根据筛选条件,筛选部分计划。
- 新增一条计划。
- 编辑某条计划。
- 删除某条计划。
- 工具类的封装
// 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()
- 常量提取
为了提高代码的可阅读性,可以将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 = "'
}
- 查询结果的实体类
// 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 应用数据库工具类构建页面
- 在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)
// ...
}
// ...
}
- 页面路由
- 配置路由表
// 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 开发步骤
- 使用关系型数据库实现数据持久化,需要获取一个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实例后,再进行数据库的增、删、改、查等操作
}
}
- 获取到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}`)
}
)
}
- 根据谓词指定的实例对象,对数据进行修改或删除。
调用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}`)
}
)
}
- 根据谓词指定的查询条件查找数据。
调用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()
}
)
}
- 在同路径下备份数据库。
关系型数据库支持两种手动备份和自动备份(仅系统应用可用)两种方式。
此处以手动备份为例:
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.`)
})
}
- 从备份数据库中恢复数据。
关系型数据库支持两种方式:恢复手动备份数据和恢复自动备份数据(仅系统应用可用)。
此处以调用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.`)
})
}
- 删除数据库。
调用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.')
})
更多推荐
所有评论(0)