鸿蒙Next版自带关系型数据库的使用研究1——SQLLite风格的SQL语句
一、问题起始
在手机助手这个项目中,为了要测试鸿蒙自带数据库的功能,笔者自建了一个测试页面,对
相关功能进行测试,但遇到了不少问题。
这里对其中遇到过的问题做一个总结梳理。
从大体上把握,遇到过的问题有:
- SQL语句的风格问题
- ChatGPT的使用方法问题
- 对数据库和程序的内在结构理解问题
- 实践中的细节问题
二、SQL语句的风格问题
最开始,没有特别关注鸿蒙自带SQL的语法风格问题,以为与MySQL差不多,结果在操作过程中就困难重重,一开始就无法正常运行。
后来才知道,对于鸿蒙自带的数据库,要注意,这是一种SQLLite风格的数据库,使用起来较MySql等其主流独立数据库是相对而言受到限制的。首先就体现在其支持的数据类型,这种类型是一个ValuesBucket的类型,这是我们在设定要使用数据库字段的类型需要界定的。下面是我们的测试案例中设计的接口:
// ValuesBucket 数据库支持的类型
interface NoteItem extends ValuesBucket {
id: number | null // 新增时设置 id 为空值 null,用于自增 id
title: string
content: string
date_added: number
}
在源码中可以看到以下语句定义:
export type ValuesBucket = Record<string, ValueType | Uint8Array | null>;
点进Record的用法,可见以下定义:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
这句话可以这样理解(参考ChatGPT):
这段代码是 TypeScript 中的泛型类型定义。让我们逐步解释它:
type Record<K extends keyof any, T> = { ... };
:这是一个 TypeScript 的类型别名定义。Record
是自定义的类型名称,后面的<K extends keyof any, T>
是泛型参数列表,其中K
是一个范型,表示一个键的集合,T
是另一个范型,表示该记录中每个键对应的值的类型。[P in K]: T;
:这部分是 TypeScript 中的映射类型(Mapped Type)语法。在这里,它定义了一个新的类型,该类型的每个属性名P
都必须是K
中的键,并且每个属性的值都是T
类型。综合起来,这段代码的作用是定义了一个通用的记录类型,其中的键集合由泛型
K
指定,每个键对应的值的类型由泛型T
指定。通过这种方式,可以创建具有指定键和相应值类型的记录类型,从而在 TypeScript 中实现更灵活和类型安全的数据结构定义。
上面的类型定义和相关说明中最核心的部分我总结如下:
(1)接口定义做出的是键值对的限定,所以最后我们需要落实到的数据也是键值对形式的数据
(2)通过泛型的设定,可以实现更灵活和类型安全的数据结构定义
三、ChatGPT的使用方法问题
对ChatGPT的提问,这也是有方法的,当确定了要处理的数据字段后,其实就可以通过ChatGPT的AI充当SQL语句写手,写出非常专业的语句。(以下的GPT均指豆包)
首先,基于我在个人的其他项目中已经设计好了接口形式,所以我这样问GPT
而根据以上写出的SQL建表语句,是难以使用的,不仅从字段取名上有了变化,而且字段的类型并不符合SQLLite风格的要求,自然以上结果是无法利用的。
后来经过技术交流讨论,从大佬处得知了SQLLite的一些特性以及SQLLite风格的SQL雨具在GPT中提问方法后,我进行了以下提问:
上面的问法中突出了几个信息:
数据库风格
什么情况下建表
id的性质
其他字段的性质
在对这些信息做出提点之后得到的SQLLite风格的SQL代码,CV即可使用。
四、对数据库和程序的内在结构的理解问题
在对鸿蒙自带数据库进行实践的过程中,我们主要设计以下八个功能的操作。
而对于以上八种功能,本篇篇幅暂且不详述,后续在《鸿蒙自带关系型数据库的使用研究》的其他板块中再接着补充。
这里只简要给出数据库实现所需的要素(代码要素)与能够实现的部分功能:
(一)对于整个数据库功能的实现,主要需要以下几个方面的功能模块
- 使用接口对数据库支持的数据类型进行限定
- 定义数据库的的表名
- 定义数据库语句(如建表语句)
- 设置操作数据库的管理对象,并编写获取数据库管理对象的方法(在这个方法中定义数据库文件名、设置数据库安全级别、执行数据库语句,最后返回数据库对象)
- 在UI部分通过对具体功能进行事件定义,执行特定的数据库功能
(二)能够实现的功能(例举常用功能)
- 创建/删除数据库文件
- 查询数据库表字段
- 新建数据
- 查询数据(所有数据详情、数据总条数)
- 删除数据
- 更新(修改)数据
五、实践中的细节问题
(一)注意数据库文件名和数据库表名的区分
前者简称库名,后者简称表名
库名一般以.db结尾,数据库文件生成后,存放路径如下图所示
(二)查询所有数据时的提取数据方法
有直接用索引提,和通过getColumnIndex两种提取方法。后者泛用性更强。
Button('四、查询所有数据')
.onClick(async () => {
// 获取操作数据库的对象
const store = await this.getStoreInstance()
// 谓词(条件)
const predicates = new relationalStore.RdbPredicates(this.tableName)
// resultSet 结果集
const resultSet = await store.query(predicates)
// 准备一个数组,用于存储数据库提取的数据
const list: NoteItem [] = []
// resultSet.goToNextRow() 指针移动到下一行
while (resultSet.goToNextRow()) {
// 移动指针的时候提取数据,按列下标提取数据
list.push({
// id: resultSet.getLong(0),
// title: resultSet.getString(1),
// content: resultSet.getString(2),
// date_added: resultSet.getLong(3),
//resultSet.getColumnIndex() 根据列名获取下标(索引)
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
date_added: resultSet.getLong(resultSet.getColumnIndex('date_added')),
})
}
// 循环结束后,获取所有数据
AlertDialog.show({ message: JSON.stringify(list, null, 2) })
})
(三)查询数据总条数
实际上,有许多数据的使用方法,包括倒序、正序、等于、查找多项、模糊匹配等等
Button('五、查询数据总条数')
.onClick(async () => {
// 获取操作数据库对象
const store = await this.getStoreInstance()
// 谓词(条件)
const predicates = new relationalStore.RdbPredicates(this.tableName)
// predicates.orderByDesc('id') // 倒序(由大到小,常用于排序)
predicates.orderByAsc('id') // 正序(小到大,常用于排序)
// predicates.equalTo('id', 1) // 等于(常用于详情页)
// predicates.in('id', [1, 3, 5]) // 查找多项(常用批量删除)
// predicates.like('title', '%数据库%') // 模糊匹配(常用于搜索)
// 还有很多...
// predicates.greaterThan('id', 3)
// 结果集
const resultSet = await store.query(predicates)
AlertDialog.show({ message: '数据总条(行rowCount)数:' + resultSet.rowCount })
})
(四)删除数据时可以设定删一条或批量删除
Button('六、删除数据')
.onClick(async ()=> {
// 获取操作数据库对象
const store = await this.getStoreInstance()
const predicates = new relationalStore.RdbPredicates(this.tableName)
// 注意!!!:记得添加predicates限定条件,否则会删除所有数据
// predicates.equalTo('id', 2) // equalTo 删除一条
predicates.in('id', [1, 2, 3, 4, 5]) // in 批量删除
const affectedRows = await store.delete(predicates)
AlertDialog.show({ message: '受影响行数:' + affectedRows })
})
六、总结
本次总结了使用鸿蒙自带关系型数据的过程中得出的一些问题和思路梳理,要点有:
(一)使用ChatGPT生成SQL语句时的提问方式要点
- 数据库风格
- 什么情况下建表
- id的性质
- 其他字段的性质
(二)数据库实现所需的功能模块 & 能实现的功能
一)对于整个数据库功能的实现,主要需要以下几个方面的功能模块
- 使用接口对数据库支持的数据类型进行限定
- 定义数据库的的表名
- 定义数据库语句(如建表语句)
- 设置操作数据库的管理对象,并编写获取数据库管理对象的方法(在这个方法中定义数据库文件名、设置数据库安全级别、执行数据库语句,最后返回数据库对象)
- 在UI部分通过对具体功能进行事件定义,执行特定的数据库功能
二)能够实现的功能(例举常用功能)
- 创建/删除数据库文件
- 查询数据库表字段
- 新建数据
- 查询数据(所有数据详情、数据总条数)
- 删除数据
- 更新(修改)数据
七、完整源码
RdbStoreTestPage.ets:
import { relationalStore, ValuesBucket } from '@kit.ArkData'
// ValuesBucket 数据库支持的类型
interface NoteItem extends ValuesBucket {
id: number | null // 新增时设置 id 为空值 null,用于自增 id
title: string
content: string
date_added: number
}
@Entry
@Component
struct RdbStoreTestPage {
// 表名
tableName: string = 'privacy_note'
sqlCreate: string = `CREATE TABLE IF NOT EXISTS ${this.tableName} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
date_added INTEGER NOT NULL
)`
// 操作数据库的管理对象
store: relationalStore.RdbStore | null = null
// 获取数据库管理对象
async getStoreInstance() {
// 如果已经存在,直接返回
if (this.store) {
return this.store
}
// 获取操作数据库的管理对象(如果数据库文件不存在,会自动创建数据库文件)
this.store = await relationalStore.getRdbStore(getContext(), {
name: `heima.db`, // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别
})
// 执行创建表的语句 execute 执行
this.store.executeSql(this.sqlCreate)
// 返回 store 对象
return this.store
}
build() {
Navigation(){
Scroll() {
Column({ space: 10 }){
Button('一、创建数据库文件')
.onClick(async () => {
// 获取操作数据库的管理对象
const store = await relationalStore.getRdbStore(getContext(),{
name: `heima.db`, // 数据库文件名
securityLevel: relationalStore.SecurityLevel.S1, // 数据库安全级别
})
// 执行创建表的语句 execute 执行
store.executeSql(this.sqlCreate)
AlertDialog.show({ message: '创建成功' })
})
Button('二、查询数据库表的字段')
.onClick(async () => {
// 获取操作数据库的对象
const store = await this.getStoreInstance()
// 谓词(条件),谓词类需要 new 实例化,传入表名 privacy_note
// const predicates = new relationalStore.RdbPredicates('user_alert_info')
const predicates = new relationalStore.RdbPredicates(this.tableName)
// query 查询,传入必传参数 谓词
const resultSet = await store.query(predicates)
AlertDialog.show({ message: '数据库的字段名:' + resultSet.columnNames })
})
Button('三、新建一条数据')
.onClick(async () => {
// 获取操作数据库的对象
const store = await this.getStoreInstance()
// 添加一条数据
const id = await store.insert(this.tableName, {
id: null, // 新增时设置 id 为空值 null,用于自增 id
title: '让亲们都来学习关系型数据库吧~!',
content: '111',
date_added: Date.now()
} as NoteItem)
AlertDialog.show({ message: '新增数组成功,数据的id为:' + id })
// 批量添加,传入数组
// store.batchInsert(表名, 数组)
})
Button('四、查询所有数据')
.onClick(async () => {
// 获取操作数据库的对象
const store = await this.getStoreInstance()
// 谓词(条件)
const predicates = new relationalStore.RdbPredicates(this.tableName)
// resultSet 结果集
const resultSet = await store.query(predicates)
// 准备一个数组,用于存储数据库提取的数据
const list: NoteItem [] = []
// resultSet.goToNextRow() 指针移动到下一行
while (resultSet.goToNextRow()) {
// 移动指针的时候提取数据,按列下标提取数据
list.push({
// id: resultSet.getLong(0),
// title: resultSet.getString(1),
// content: resultSet.getString(2),
// date_added: resultSet.getLong(3),
//resultSet.getColumnIndex() 根据列名获取下标(索引)
id: resultSet.getLong(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
date_added: resultSet.getLong(resultSet.getColumnIndex('date_added')),
})
}
// 循环结束后,获取所有数据
AlertDialog.show({ message: JSON.stringify(list, null, 2) })
})
Button('五、查询数据总条数')
.onClick(async () => {
// 获取操作数据库对象
const store = await this.getStoreInstance()
// 谓词(条件)
const predicates = new relationalStore.RdbPredicates(this.tableName)
// predicates.orderByDesc('id') // 倒序(由大到小,常用于排序)
predicates.orderByAsc('id') // 正序(小到大,常用于排序)
// predicates.equalTo('id', 1) // 等于(常用于详情页)
// predicates.in('id', [1, 3, 5]) // 查找多项(常用批量删除)
// predicates.like('title', '%数据库%') // 模糊匹配(常用于搜索)
// 还有很多...
// predicates.greaterThan('id', 3)
// 结果集
const resultSet = await store.query(predicates)
AlertDialog.show({ message: '数据总条(行rowCount)数:' + resultSet.rowCount })
})
Button('六、删除数据')
.onClick(async ()=> {
// 获取操作数据库对象
const store = await this.getStoreInstance()
const predicates = new relationalStore.RdbPredicates(this.tableName)
// 注意!!!:记得添加predicates限定条件,否则会删除所有数据
// predicates.equalTo('id', 2) // equalTo 删除一条
predicates.in('id', [1, 2, 3, 4, 5]) // in 批量删除
const affectedRows = await store.delete(predicates)
AlertDialog.show({ message: '受影响行数:' + affectedRows })
})
Button('七、更新(修改)数据')
.onClick(async () => {
// 获取操作数据库对象
const store = await this.getStoreInstance()
// 谓词(条件)
const predicates = new relationalStore.RdbPredicates(this.tableName)
// 注意!!!:记得添加 predicates 限定条件,否者修改全部
predicates.equalTo('id', 10)
// 待更新数据
const value = {
title: '新的标题',
content: '新的内容'
} as NoteItem
const affectedRows = await store.update(value, predicates)
AlertDialog.show({ message: '更新(修改)数据 - 受影响行数:' + affectedRows })
})
Button('八、删除数据库文件')
.onClick(async ()=>{
try{
await relationalStore.deleteRdbStore(getContext(),`${this.tableName}`)
AlertDialog.show({ message: '删除成功' })
} catch(error) {
AlertDialog.show({ message: JSON.stringify(error,null,2) })
}
})
}
.constraintSize({ minHeight: '100%' })
}
.width("100%")
.height("100%")
}
.title('关系型数据库')
.titleMode(NavigationTitleMode.Mini)
}
}
八、源码实测情况
对上面的代码进行测试,得出测试结果:一、二、三、四、五的功能可以正常使用,六、七存在优化空间、八存在一定问题。
相关详细分析和问题的优化解决分析,将在本系列的下一篇文章中给予展现。
转载自:https://juejin.cn/post/7375090667733631030