当前位置:首页>鸿蒙APP>鸿蒙开发日记——邮件收发工具(2)

鸿蒙开发日记——邮件收发工具(2)

  • 2026-10-21 16:57:52
鸿蒙开发日记——邮件收发工具(2)

在上一篇相关文章中(鸿蒙开发日记——邮件收发工具(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}`);    }  }}  
在数据库管理组件的实现中,有几个关键设计需要总结:
(1)采用单例模式进行数据库的管理,避免重复new对象,重复创建数据库。
(2)在init初始化方法中做幂等实现,如果数据库对象已经建立则不再重复建立,避免资源消耗。
(3)建表语句一定要用“Create Table If Not Exist”,避免建表时因表已存在抛出异常阻塞初始化工作,也算一定程度的幂等实现。
(4)数据库属性配置中,数据安全级别和是否加密方面,在开发期间为了调试的方便,邮件表和个人配置表都可以先设置为S1级别和不加密,真正发布release的时候,个人信息表为了保护敏感信息,要记得修改回来S4级别数据安全,并且对数据加密存储。如果需要定制加密密钥,还可以在属性中传入密钥对象,具体形式可以参考鸿蒙RDB的文档。
二、Dao、Model层的实现
相比于Database那曲折的实现过程,Dao层方法就简单很多了,基本上没有遇到什么困难,建立好对应的model数据对象,直接通过鸿蒙自己提供的RDB操作方法执行即可。
需要特殊说明的是,RDB的操作有两种方法:
一是直接在rdbStore对象上执行executeSql方法,十分类似Java语言中原生JDBC的方法逻辑。如果需要绑定变量,还需要在SQL中用‘?’占位,用ValueType数组来传入,注意传入的参数顺序要和SQL中问号出现的顺序一致。
//数据库后建表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);
二是通过鸿蒙自带的RdbPredicates对象来进行操作,这个就类似Hibernate这种将SQL参数化的方案,对工程师屏蔽掉复杂SQL编写,对那些SQL不熟练的程序员比较友好。
//示例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);
在这个邮件收发工具里,两种方案我都用了一下,也都保留了,二者在实现上没有什么区别。不过个人作为考下了Oracle OCP证书的人,更喜欢直接写SQL。
三、测试验证与问题排查
写完了Database和Dao的实现后,再配上单元测试和模拟机测试用例,执行模拟机用例时,已经看到RDB在模拟机上成功建立,并创建完成需要建立的表和执行增删改查的测试了。补充说一下,因为软件模拟机运行时,也是在模拟机的沙箱环境运行,如果想在DevEco Studio中直接查看模拟机中RDB数据库的内容,可以在菜单栏中,依次选择“视图 => 工具窗口 => 数据库检查器”,就可以直接连接模拟机的RDB,并且用SQL操作数据了。

开发过程中,在写模拟机单元测试的时候,遇到了一个奇怪的问题场景:在模拟机执行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 并建表),单例模式下首次调用后缓存 rdbStore      await MailDatabase.getInstance().init(context);      // 获取 MailDao 单例,后续测试用例通过它操作 MAIL 表      dao = MailDao.getInstance();    });

至此,底层的依赖的技术组件的大框架就算完成了,那么下一步就要开始主攻VIEW层和UI层了:VIEW层对上接受UI的点击或拖拽等操作,对下整合各种协议和逻辑的步骤;UI层需要进行视觉设计和自定义组件的开发。希望一切顺利。

最新文章

随机文章