在上一篇相关文章中(鸿蒙开发日记——邮件收发工具(1)),简单描述了开发一个纯血鸿蒙系统下运行的邮件收发工具的设想,Gitee上搭建了开源仓库,并且在前期的工作中完成了POP3、SMTP、IMAP三大核心邮件协议的开发实现和单元测试,拿QQ邮箱试了一下,效果还是可用的。

做完了邮件的收发,下一步就是邮件数据的持久化,同步带来的就是用户配置的持久化存储。
一、Database层的实现
邮件数据的保存,属于典型的大数据保存,最适合用数据库来进行存储,那么就要找一个鸿蒙OS能用的数据库。特别惊喜的是鸿蒙系统在设计之初就已经考虑过应用需要数据库这个问题了,系统本身就自带了一个关系型数据库:RelationalStore,俗称RDB。RDB数据库脱胎于众多周知的轻量级开源数据库SQLLite,甚至数据文件都支持SQLLite的客户端读取,同时数据库的数据是随应用存储在应用自己的沙箱中,只要不是手动把数据库放到外边,那么软件自己的数据对其他应用完全屏蔽。如果能够熟练操作常用关系型数据库(如Oracle、DB2、OceanBase等等),那么RDB会很快上手,最多就是了解下如何用ArkTS调用罢了。
除了邮件数据外,还有用户的个人配置,比如邮箱信息,登录账号密码,UI目录信息等等。鸿蒙系统内部除了RDB外,还提供了KV形式的轻量存储,特别像Redis的用法。但是用户个人邮箱的账户配置属于敏感隐私数据,KV存储还要考虑保密的问题。了解了一些可用的方案,最终决定还继续用RDB,因为RDB有两个特别有用的属性:securityLevel和encrypt,数据安全级别和是否数据加密,对敏感数据再合适不过,唯一要注意的就是因为数据库属性不一样,使用时需要区别于邮件库,单独另建立一个库。

两个库的创建逻辑几乎完全一致,RDB的使用也十分简单,以邮件数据库为例,必需的核心方法就那么几个,其余的可以根据自身需要再定制。
//首先从ohos里引入对应的模块import relationalStore from '@ohos.data.relationalStore';export class MailDatabase {//单例模式访问private static instance: MailDatabase;//数据库核心对象private rdbStore: relationalStore.RdbStore | undefined;//初始化数据库后建表private static CREATE_MAIL_DB_DDL: string =`CREATE TABLE IF NOT EXISTS MAIL (...略...);`//单例模式需要把构造方法对外禁用private constructor() {}//配置数据库的基本属性private static STORE_CONFIG: relationalStore.StoreConfig = {name: 'mail.db',securityLevel: relationalStore.SecurityLevel.S1,encrypt: false};//获取和创建单例的方法public static getInstance(): MailDatabase {if (!MailDatabase.instance) {MailDatabase.instance = new MailDatabase();}return MailDatabase.instance;}//异步初始化流程public async init(context: common.UIAbilityContext): Promise<void> {//初始化幂等控制if (this.rdbStore) {return;}try {this.rdbStore = await relationalStore.getRdbStore(context, MailDatabase.STORE_CONFIG);await this.createTables();Log.info('MailDatabase', '数据库初始化成功');} catch (err) {Log.error('MailDatabase', `数据库初始化失败: ${err.message}`);throw new MailException(MailConstant.ERROR_DB_INIT_FAILED, `数据库初始化失败: ${err.message}`);}}}
//数据库后建表DDL(类内私有静态string变量)private static CREATE_TABLE_DDL: string =`CREATE TABLE IF NOT EXISTS TABLE_NAME(...略...);`//直接通过rdbStore对象执行SQLawait this.rdbStore.executeSql(ClassName.CREATE_TABLE_DDL);//待执行SQL,需要用?绑定待传入参数let sql : string = "DELETE FROM TABLE WHERE ID = ? AND USER = ?";//参数值必须按 ? 的顺序放入数组let bindArgs: Array<relationalStore.ValueType> = [3, "user"];//按绑定变量方式执行SQL,返回结果需要用结果集对象收集let resultSet: relationalStore.ResultSet = await rdbStore.executeSql(sql, bindArgs);
//示例SQL:SELECT * FROM USERS WHERE ACCOUNT = ? AND AGE > ? OR STATUS = ?let predicates = new relationalStore.RdbPredicates('USERS');predicates.equalTo('ACCOUNT', 'user123').and().greaterThan('AGE', 18).or().equalTo('STATUS', 'active');//获取结果集let resultSet = await rdbStore.query(predicates);
开发过程中,在写模拟机单元测试的时候,遇到了一个奇怪的问题场景:在模拟机执行Dao对象的模拟单元测试的时候,数据库初始化总是失败,但是直接部署到模拟机里运行,RDB能成功拉起来,Dao层方法也能成功执行增删改查,还能够通过数据库连接工具读取到虚拟机数据库里的数据。
经排查,这个问题的主要原因是,直接部署模拟机和模拟机单元测试在RDB的运行机制不一样,两种情况下应用的上下文对象不一致。如果想要在模拟机单元测试中运行,需要在执行每个单元测试类的所有用例前,手动调整一下上下文对象,否则Dao和Database会隶属到不同上下文,无法互相访问。
beforeAll(async () => {// 通过 AbilityDelegator 获取当前运行的 TestAbility,其 context 是 UIAbilityContext// 不能用 getAppContext(),因为它返回的是 ApplicationContext,relationalStore.getRdbStore() 不接受const ability = await abilityDelegatorRegistry.getAbilityDelegator().getCurrentTopAbility();// ability.context 类型为 common.Context,需要显式断言为 UIAbilityContext 才能传给 init()const context = ability.context as common.UIAbilityContext;// 初始化 MailDatabase(创建 mail.db 并建表),单例模式下首次调用后缓存 rdbStoreawait MailDatabase.getInstance().init(context);// 获取 MailDao 单例,后续测试用例通过它操作 MAIL 表dao = MailDao.getInstance();});
至此,底层的依赖的技术组件的大框架就算完成了,那么下一步就要开始主攻VIEW层和UI层了:VIEW层对上接受UI的点击或拖拽等操作,对下整合各种协议和逻辑的步骤;UI层需要进行视觉设计和自定义组件的开发。希望一切顺利
。