前言

随着鸿蒙生态蓬勃发展,各种实用的鸿蒙三方库已成为开发者高效开发的利器。它不仅能拓展应用功能边界,还能大幅提升开发效率,不管你是刚接触鸿蒙开发的小白,还是开发时长两年半的老司机,掌握这些三方库,绝对能让你在鸿蒙开发上更加方便!博主在日常的工作开发中也也是如此,今天给大家分享的是storm三方库。

介绍

Storm是直接基于纯TypeScript编写的高效简洁的轻量级OpenHarmonyOS SQL ORM框架,提供了强类型的SQL DSL,直接将低级bug暴露在编译期。

安装

在命令行中执行以下命令。

ohpm install @zxhhyj/storm

快速上手

这里以用户表和用户个人信息表为示例:

1.定义用户信息表

创建UserInfo.(ts/ets)文件,并实现以下代码:

export interface UserInfo {
id?: number
name: string
info: string
}

export class UserInfoTable extends Table<UserInfo> {
  override readonly tableName = 't_user_info'
  readonly id = Column.integer('id').primaryKey(true).bindTo(this, 'id')
  readonly name = Column.text('name').notNull().bindTo(this, 'name')
  readonly info = Column.text('info').bindTo(this, 'info')
}

export const userInfoTable = new UserInfoTable()

其中UserInfo为实体模型,UserInfoTable为表的Scheme,在其中使用ColumnAPI来定义列,最后导出表实例。

2.定义用户表

创建User.(ts/ets)文件,并实现以下代码:

export interface User {
id?: number
email: string
userInfo: UserInfo
createDataTime: Date
}

export class UserTable extends Table<User> {
  readonly tableName = 't_user'
  readonly id = Column.integer('id').primaryKey(true).bindTo(this, 'id')
  readonly email = Column.text('email').notNull().bindTo(this, 'email')
  readonly userInfo = Column.references('user_info_id', userInfoTable).bindTo(this, 'userInfo')
  readonly createDataTime = Column.date('create_data_time').notNull().bindTo(this, 'createDataTime')
}

export const userTable = new UserTable()

这里使用了Column.referencesAPI将UserTableUserInfoTable进行关联。

3.定义并初始化数据库

创建AppDatabase.(ts/ets)文件,并实现以下代码:

class AppDatabase extends Database {
  readonly userDao = DatabaseDao.form(this).select(userTable)
  readonly userInfoDao = DatabaseDao.form(this).select(userInfoTable)

  protected initDb(context: Context) {
    return relationalStore.getRdbStore(context, { name: 'app.db', securityLevel: relationalStore.SecurityLevel.S1 })
  }

  protected onCreate(_rdbStore: RdbStoreWrapper): void {
  }

  protected onDatabaseCreate(rdbStore: RdbStoreWrapper): void {
    rdbStore.executeSync(SQL.createTableAndIndex(userTable))
    rdbStore.executeSync(SQL.createTableAndIndex(userInfoTable))
  }
}

export const appDatabase = new AppDatabase(1)

这段代码的大致作用:

  1. 你需要重写initDb函数并在其中返回你的relationalStore.getRdbStore
  2. onCreate将在数据库完全初始化完毕时被调用(一般在onDatabaseCreate之后)。
  3. onDatabaseCreate将在你的RdbStore版本为0
    时被调用,你需要在这里执行初始化你的数据库,执行完毕后版本号为Database
    构造函数中输入的整数值。

最后需要调用init函数进行初始化:

export class AppAbilityStage extends AbilityStage {
  async onCreate() {
    await appDatabase.init(this.context)
  }

  onAcceptWant(_want: Want): string {
    return 'AppAbilityStage'
  }
}

4.使用 DatabaseDao 访问 Database

1.使用 insert API 插入数据

const userInfo: UserInfo = {
  name: '浩',
  info: '无限进步'
}
const user: User = {
  email: 'zxhhyj@qq.com',
  userInfo: userInfo,
  createDataTime: new Date(),
}
await appDatabase.userInfoDao.insert(userInfo)
await appDatabase.userDao.insert(user)
// insert API存在一个副作用,插入成功后会自动将自增主键回写到实体中
// 另外在表中存在 references 列时,建议开启事务,且需要保证 references 列的插入删除先于它的所在的表
// 就像示例中一样,先插入 userInfo 再插入 user

2.使用 delete & deleteIf API 删除数据

const userInfo: UserInfo = {
  name: '浩',
  info: '无限进步'
}
await appDatabase.userInfoDao.insert(userInfo)
// 使用实体,需要在插入后使用或者确保其中存在主键
await appDatabase.userInfoDao.delete(userInfo)

// 使用谓词
await appDatabase.userInfoDao.deleteIf(it => it.equalTo(userInfoTable.id, 1))

3.使用 update & updateIf API 更新数据

const userInfo: UserInfo = {
  name: '浩',
  info: '无限进步'
}
await appDatabase.userInfoDao.insert(userInfo)
userInfo.name = '真的科幻吗?'
// 使用实体,需要在插入后使用或者确保其中存在主键
await appDatabase.userInfoDao.update(userInfo)

// 使用谓词
await appDatabase.userInfoDao.updateIf(it => it.equalTo(userInfoTable.id, 1), {
  info: '真的无限进步吗?'
})

4.使用 query & queryOne API 查询数据

查询多个数据:

const userInfos = await appDatabase.userInfoDao.query(it => it.limitAs(30))
for (const userInfo of userInfos) {
  console.log(userInfo.name)
}

查询单个数据:

const bookcase = await appDatabase.bookcaseDao.queryOne(it => it.isNotNull(bookTable.name))

5.使用 toList & first API 查询数据

查询多个数据:

const userInfos = await appDatabase.userInfoDao.toList(it => it.limitAs(30))
const haoUser = userInfos.find(item => item.name === '浩')
// 设置模型字段值并立即更新至数据库中对应的记录
await haoUser?.set('name', 'Hao')
// 立即将模型的所有字段更新至数据库中对应的记录
await haoUser?.flushChanges()
// 删除当前模型在数据库中对应的记录
await haoUser?.delete()

查询单个数据:

const haoUser = await appDatabase.userInfoDao.first(it => it.equalTo(userInfoTable.name, '浩'))
// 设置模型字段值并立即更新至数据库中对应的记录
await haoUser?.set('name', 'Hao')
// 立即将模型的所有字段更新至数据库中对应的记录
await haoUser?.flushChanges()
// 删除当前模型在数据库中对应的记录
await haoUser?.delete()

5.使用 launchTransaction API 开启事务

// 底层使用 beginTransaction
appDatabase.launchTransaction(async it => {
  const haoUser = await it.userInfoDao.first(it => it.equalTo(userInfoTable.name, '浩'))
  await haoUser.set('name', 'Hao')
})
// 底层使用 beginTrans
appDatabase.userInfoDao.launchTransaction(async it => {
  const haoUser = await it.first(it => it.equalTo(userInfoTable.name, '浩'))
  await haoUser.set('name', 'Hao')
})

5.使用 SQLiteDatabase 访问 Database

SQLiteDatabase相比DatabaseDao,API自由度更高,不局限与操作单个表,适合需要一次操作多个表时使用。

1.在 Database 中声明

以之前的AppDatabase为例:

class AppDatabase extends Database {
  readonly userDao = DatabaseDao.form(this).select(userTable)
  readonly userInfoDao = DatabaseDao.form(this).select(userInfoTable)
  // 声明 SQLiteDatabase
  readonly db = SQLiteDatabase.form(this)

  protected initDb(context: Context) {
    return relationalStore.getRdbStore(context, { name: 'app.db', securityLevel: relationalStore.SecurityLevel.S1 })
  }

  protected onCreate(_rdbStore: RdbStoreWrapper): void {
  }

  protected onDatabaseCreate(rdbStore: RdbStoreWrapper): void {
    rdbStore.executeSync(SQL.createTableAndIndex(userTable))
    rdbStore.executeSync(SQL.createTableAndIndex(userInfoTable))
  }
}

export const appDatabase = new AppDatabase(1)

SQLiteDatabase的增删改API与DatabaseDao使用基本一致,这里不再赘述,但查询的API比较特殊。

2. 使用 querySql 查询数据

简单示例:

// 查询 email 不为空的数据
const emails = await appDatabase.db.querySql(SQL
  .select(userTable.id, userTable.email)
  .form(userTable)
  .where()
  .isNull(userTable.email))
for (const emailsElement of emails) {
  // 同时类型被推断为
  // {
  //   id: number;
  //   email: string;
  // }
  console.log(`${emailsElement.id}`)
  console.log(emailsElement.email)
}

6.使用 Dao 封装 Database 的访问

有些时候我们需要对数据库的访问进行封装。

创建UserDao.(ts/ets)文件,并实现以下代码:

export class UserDao extends Dao {
  // 将 userDao & userInfoDao 私有化
  private readonly userDao = DatabaseDao.form(this).select(userTable)
  private readonly userInfoDao = DatabaseDao.form(this).select(userInfoTable)

  /**
   * 供外部调用
   */
  createUser() {
    appDatabase.launchTransaction(async () => {
      const userInfo: UserInfo = {
        name: '浩',
        info: '无限进步'
      }
      const user: User = {
        email: 'zxhhyj@qq.com',
        userInfo: userInfo,
        createDataTime: new Date(),
      }
      await this.userInfoDao.insert(userInfo)
      await this.userDao.insert(user)
    })
  }
}

然后在Database中声明。
以之前的AppDatabase为例:

class AppDatabase extends Database {
  // 声明 UserDao
  readonly userDao = Dao.form(this).select(UserDao)

  protected initDb(context: Context) {
    return relationalStore.getRdbStore(context, { name: 'app.db', securityLevel: relationalStore.SecurityLevel.S1 })
  }

  protected onCreate(_rdbStore: RdbStoreWrapper): void {
  }

  protected onDatabaseCreate(rdbStore: RdbStoreWrapper): void {
    rdbStore.executeSync(SQL.createTableAndIndex(userTable))
    rdbStore.executeSync(SQL.createTableAndIndex(userInfoTable))
  }
}

export const appDatabase = new AppDatabase(1)

7.升级数据库

例如,需要对前文的UserTable中新增一个字段password

export interface User {
id?: number
email: string
// 新增 password
password: string
userInfo: UserInfo
createDataTime: Date
}

export class UserTable extends Table<User> {
  readonly tableName = 't_user'
  readonly id = Column.integer('id').primaryKey(true).bindTo(this, 'id')
  readonly email = Column.text('email').notNull().bindTo(this, 'email')
  // 新增 password
  readonly password = Column.text('password').notNull().bindTo(this, 'password')
  readonly userInfo = Column.references('user_info_id', userInfoTable).bindTo(this, 'userInfo')
  readonly createDataTime = Column.date('create_data_time').notNull().bindTo(this, 'createDataTime')
}

export const userTable = new UserTable()

然后修改AppDatabase

class AppDatabase extends Database {
  readonly userDao = DatabaseDao.form(this).select(userTable)
  readonly userInfoDao = DatabaseDao.form(this).select(userInfoTable)

  protected initDb(context: Context) {
    return relationalStore.getRdbStore(context, { name: 'app.db', securityLevel: relationalStore.SecurityLevel.S1 })
  }

  protected onCreate(_rdbStore: RdbStoreWrapper): void {
  }

  protected onDatabaseCreate(rdbStore: RdbStoreWrapper): void {
    rdbStore.executeSync(SQL.createTableAndIndex(userTable))
    rdbStore.executeSync(SQL.createTableAndIndex(userInfoTable))
  }

  /**
   * 定义迁移函数,并使用 @Migration 注解,参数分别为起始版本号和结束版本号
   */
  @Migration(1,2)
  protected migration_1_2(rdbStore: RdbStoreWrapper) {
    // 执行迁移逻辑
    rdbStore.executeSync(SQL.alterTable(userTable, sql => sql.addColumn(userTable.password)))
  }
}

// 修改最新版本
export const appDatabase = new AppDatabase(2)

这下就大功告成了。

8.快速生成 SQL

可以使用SQLAPI来快速生成SQL语句:

// 生成创建表和索引的SQL语句
SQL.createTableAndIndex(userInfoTable)

// 生成创建表的SQL语句
SQL.createTable(userInfoTable)

// 生成创建索引的SQL语句
SQL.createIndex(userInfoTable)

// 生成新增列的SQL语句
SQL.alterTable(userInfoTable, sql => sql.addColumn(userInfoTable.info))

// 生成更新列的SQL语句
SQL.alterTable(userInfoTable, sql => sql.alterColumn(userInfoTable.info))

9.多数据源

参考定义并初始化数据库,创建不同的Database并修改initDb返回的RdbStore即可。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐