文档管理中心
我的

如何读取项目工程下预置的SQLite数据库

问题现象

应用开发时很多时候由于涉及数据库表过多,需要在项目中提前预置SQLite数据库文件(rawfile或resfile目录下),再在应用中无需创建相关表直接访问数据库。针对此类预置在项目工程中的数据库文件,应该如何实现在应用中获取数据库并操作其中的表?

背景知识

  • 应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为“应用沙箱目录”。
  • rawfile目录支持创建多层子目录,子目录名称可以自定义,文件夹内可以自由放置各类资源文件。目录中的资源文件会被直接打包进应用,不经过编译,也不会分配资源ID。通过指定文件路径和文件名访问。
  • resfile目录支持创建多层子目录,子目录名称可以自定义,文件夹内可以自由放置各类资源文件。目录中的资源文件会被直接打包进应用,不经过编译,也不会分配资源ID。应用安装后,resfile资源会被解压到应用沙箱路径,通过Context属性resourceDir获取到resfile资源目录后,可通过文件路径访问,且该路径仅能以只读方式访问。
  • 关系型数据库(Relational Database,RDB)是一种基于关系模型来管理数据的数据库。关系型数据库基于SQLite组件提供了一套完整的对本地数据库进行管理的机制,对外提供了一系列的增、删、改、查等接口,也可以直接运行用户输入的SQL语句来满足复杂的场景需要。

解决方案

关系型数据库(Relational Database,RDB)不支持直接打开rawfile或resfile目录下预置的SQLite数据库,需要先将对应的SQLite数据库文件拷贝到应用沙箱目录,再使用@ohos.data.relationalStore进行相关数据库操作。具体步骤如下:

  1. 若数据库预置在rawfile目录下,可使用resourceManager.getRawFdSync接口获取对应资源的文件描述符(fd),再使用fs.readSync接口读取文件内容,结合fs.writeSync接口将读取的内容写入到沙箱指定文件中。(可参考下述示例的saveRawFileToSandbox代码)
  2. 若数据库预置在resfile目录下,可使用fs.openSync接口获取对应资源的文件描述符(fd),再使用fs.readSync接口读取文件内容,结合fs.writeSync接口将读取的内容写入到沙箱指定文件中。(可参考下述示例的saveResFileToSandbox代码)
  3. 将在项目rawfile或resfile目录下预置的SQLite数据库文件通过上述方案拷贝到应用沙箱databaseDir目录下后,即可在应用中使用relationalStore.getRdbStore接口获取数据库实例进行数据库相关操作。(可参考下述示例的initDataBaseDir代码)
    说明

    需要注意以下几点:

    • 在创建或打开数据库时若不指定StoreConfig的rootDir时,则会默认在${context.databaseDir}/rdb/${customDir}目录下创建或打开。若指定rootDir,则会在${rootDir}/${customDir}目录下创建或打开数据库,通过设置此参数打开的数据库为只读模式。rootDir配置从API18版本开始提供。
    • rawfile目录下文件需使用resourceManager.getRawFdSync获取文件描述符或使用resourceManager.getRawFileContentSync获取其文件数据。
    • resfile目录路径可使用${this.context.resourceDir}获取,其目录下的文件可直接使用@ohos.file.fs (文件管理)的能力打开读取。

完整示例代码如下:

收起
自动换行
深色代码主题
复制
  1. import { common } from '@kit.AbilityKit';
  2. import { fileIo, ReadOptions } from '@kit.CoreFileKit';
  3. import { relationalStore } from '@kit.ArkData';
  4. import { BusinessError } from '@kit.BasicServicesKit';
  5. import { resourceManager } from '@kit.LocalizationKit';
  6. @Entry
  7. @Component
  8. struct Index {
  9. private context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext;
  10. private promptAction = this.getUIContext().getPromptAction();
  11. private resManage: resourceManager.ResourceManager = this.context.resourceManager;
  12. private store: relationalStore.RdbStore | undefined = undefined;
  13. private dbName: string = 'test.db';
  14. private tableName: string = 'testTable';
  15. private storeConfig: relationalStore.StoreConfig = {
  16. name: this.dbName,
  17. securityLevel: relationalStore.SecurityLevel.S1,
  18. };
  19. /**
  20. * 创建或打开数据库的路径
  21. * 知识点1:
  22. * 若不指定StoreConfig的rootDir,则默认在${this.context.databaseDir}/rdb路径下创建或打开数据库;
  23. * 若需要自定义数据库的路径,则需指定StoreConfig的rootDir
  24. */
  25. private dbFilePath: string = `${this.context.databaseDir}/rdb`;
  26. build() {
  27. Column({ space: 20 }) {
  28. Button('rawfile目录下预置的数据库')
  29. .type(ButtonType.ROUNDED_RECTANGLE)
  30. .onClick(() => {
  31. // 需要替换为实际预置在rawfile目录下的db文件相对路径
  32. let rawfilePath: string = 'rdb/test.db'; // rawfile/rdb/test.db
  33. this.saveRawFileToSandbox(rawfilePath, this.dbFilePath, this.dbName);
  34. this.initDataBaseDir();
  35. });
  36. Button('操作resfile目录下预置的数据库')
  37. .type(ButtonType.ROUNDED_RECTANGLE)
  38. .onClick(() => {
  39. // 需要替换为实际预置在resfile目录下的db文件相对路径
  40. let resfilePath: string = `${this.context.resourceDir}/rdb/test.db`;
  41. this.saveResFileToSandbox(resfilePath, this.dbFilePath, this.dbName);
  42. this.initDataBaseDir();
  43. });
  44. Button('删除已创建的默认路径数据库文件')
  45. .type(ButtonType.ROUNDED_RECTANGLE)
  46. .onClick(() => {
  47. this.deleteRdb();
  48. });
  49. }
  50. .justifyContent(FlexAlign.Center)
  51. .width('100%')
  52. .height('100%');
  53. }
  54. private deleteRdb() {
  55. relationalStore.deleteRdbStore(this.context, this.storeConfig).then(() => {
  56. // 数据库删除成功后,已初始化的RdbStore实例将无法继续使用。
  57. // 及时将相关变量置空以释放资源。
  58. this.store = undefined;
  59. this.promptAction.showToast({ message: `删除数据库成功` });
  60. console.info('Delete RdbStore successfully.');
  61. }).catch((err: BusinessError) => {
  62. console.error(`Delete RdbStore failed, code is ${err.code},message is ${err.message}`);
  63. });
  64. }
  65. saveRawFileToSandbox(rawfilePath: string, dbFileDir: string, dbFileName: string) {
  66. let destFile: fileIo.File | undefined = undefined;
  67. try {
  68. /**
  69. * 知识点2:rawfile目录下文件需使用resourceManager的getRawFdSync或getRawFileContentSync获取其文件数据
  70. */
  71. // 1.获取rawfile目录下预置的数据库文件
  72. const srcFile = this.resManage.getRawFdSync(rawfilePath);
  73. // 2.将rawfile目录下数据库文件写入指定沙箱目录下
  74. // 如果目录不存在则开始创建
  75. if (!fileIo.accessSync(dbFileDir)) {
  76. console.info(`沙箱路径【${dbFileDir}】不存在,开始创建目录。`);
  77. fileIo.mkdirSync(dbFileDir, true);
  78. }
  79. let dbFilePath = `${dbFileDir}/${dbFileName}`;
  80. destFile = fileIo.openSync(dbFilePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
  81. this.copyFile(srcFile.fd, srcFile.offset, destFile);
  82. console.info(`拷贝rawfile目录下预置的数据库文件到沙箱成功!`);
  83. } catch (err) {
  84. this.promptAction.showToast({ message: `拷贝数据库文件到沙箱失败` });
  85. console.error(`拷贝rawfile目录下预置的数据库文件到沙箱失败, code is ${err.code},message is ${err.message}`);
  86. } finally {
  87. if (rawfilePath) {
  88. this.resManage.closeRawFdSync(rawfilePath);
  89. }
  90. if (destFile) {
  91. fileIo.closeSync(destFile);
  92. }
  93. }
  94. }
  95. copyFile(fd: number, currentOffset: number, destFile: fileIo.File) {
  96. // 读取缓冲区大小
  97. let bufferSize = 30000;
  98. let buffer = new ArrayBuffer(bufferSize); // 创建buffer缓冲区
  99. // 要copy的文件的offset和length
  100. let readOption: ReadOptions = {
  101. offset: currentOffset,
  102. length: bufferSize // 每次期望读取数据的长度。可选,默认缓冲区长度
  103. };
  104. // 后面len会一直减,直到没有
  105. while (true) {
  106. // 读取buffer容量的内容
  107. let readLength = fileIo.readSync(fd, buffer, readOption);
  108. // 写入buffer容量的内容
  109. fileIo.writeSync(destFile.fd, buffer, { length: readLength });
  110. // 判断后续内容,修改读文件的参数
  111. // buffer没读满代表文件读完了
  112. if (readLength < bufferSize) {
  113. break;
  114. }
  115. if (readOption.offset != undefined) {
  116. readOption.offset += readLength;
  117. }
  118. }
  119. }
  120. saveResFileToSandbox(resfilePath: string, dbFileDir: string, dbFileName: string) {
  121. let srcFile: fileIo.File | undefined = undefined;
  122. let destFile: fileIo.File | undefined = undefined;
  123. try {
  124. /**
  125. * 知识点3:resfile目录路径可使用${context.resourceDir}获取,可直接使用文件管理的能力打开读取
  126. */
  127. // 1.获取resfile目录下预置的数据库文件
  128. srcFile = fileIo.openSync(resfilePath, fileIo.OpenMode.READ_ONLY);
  129. // 2.将resfile目录下数据库文件写入指定沙箱目录下
  130. // 如果目录不存在则开始创建
  131. if (!fileIo.accessSync(dbFileDir)) {
  132. console.info(`沙箱路径【${dbFileDir}】不存在,开始创建目录。`);
  133. fileIo.mkdirSync(dbFileDir, true);
  134. }
  135. let dbFilePath = `${dbFileDir}/${dbFileName}`;
  136. destFile = fileIo.openSync(dbFilePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
  137. this.copyFile(srcFile.fd, 0, destFile);
  138. console.info(`拷贝resfile目录下预置的数据库文件到沙箱成功!`);
  139. } catch (err) {
  140. this.promptAction.showToast({ message: `拷贝数据库文件到沙箱失败` });
  141. console.error(`拷贝resfile目录下预置的数据库文件到沙箱失败, code is ${err.code},message is ${err.message}`);
  142. } finally {
  143. if (srcFile) {
  144. fileIo.closeSync(srcFile);
  145. }
  146. if (destFile) {
  147. fileIo.closeSync(destFile);
  148. }
  149. }
  150. }
  151. initDataBaseDir() {
  152. try {
  153. relationalStore.getRdbStore(this.context, this.storeConfig)
  154. .then(async (rdbStore: relationalStore.RdbStore) => {
  155. this.store = rdbStore;
  156. console.info('Get RdbStore successfully.');
  157. // 操作数据库增删改查
  158. this.operateRdb();
  159. })
  160. .catch((err: BusinessError) => {
  161. this.promptAction.showToast({ message: `打开数据库失败` });
  162. console.error(`Get RdbStore failed, code is ${err.code},message is ${err.message}`);
  163. });
  164. } catch (err) {
  165. this.promptAction.showToast({ message: `打开数据库失败` });
  166. console.error(`initDataBaseDir failed, code is ${err.code},message is ${err.message}`);
  167. }
  168. }
  169. operateRdb() {
  170. if (!this.store) {
  171. console.error('RdbStore is undefined.');
  172. return;
  173. }
  174. try {
  175. // 进行数据的相关操作
  176. // 插入数据
  177. const valueBucket: relationalStore.ValuesBucket = {
  178. 'NAME': 'rdbTest',
  179. 'AGE': 10,
  180. };
  181. let rowId: number = this.store.insertSync(this.tableName, valueBucket);
  182. console.info(`Insert is successful, rowId = ${rowId}`);
  183. // 查询数据
  184. let resultSet = this.store.querySqlSync(`SELECT * FROM ${this.tableName} limit 10`);
  185. console.info(`ResultSet column names: ${resultSet.columnNames}, row count: ${resultSet.rowCount}`);
  186. if (resultSet.rowCount > 0) {
  187. resultSet.getRows(resultSet.rowCount).then((rows: Array<relationalStore.ValuesBucket>) => {
  188. console.info(`querySqlSync successful rows = ${JSON.stringify(rows)}`);
  189. this.promptAction.showToast({ message: `查询数据库成功,查询结果:${rows.length}` });
  190. }).catch((err: BusinessError) => {
  191. console.error(`querySqlSync failed, code is ${err.code},message is ${err.message}`);
  192. }).finally(() => {
  193. resultSet.close();
  194. });
  195. } else {
  196. resultSet.close();
  197. }
  198. } catch (err) {
  199. console.error(`operateRdb failed, code is ${err.code},message is ${err.message}`);
  200. }
  201. }
  202. }

代码运行效果: