针对:【https://harmonyosdev.csdn.net/user/discuss/6a938fc33bda720d4b37c1e2
的问答回复。

基于本项目(xiangcejihe)中 30+ 个 DAO 的真实代码总结,涉及 UserDaoTodoDaoContactDaoBackupDao 等,示例均取自 entry/src/main/ets/database/ 下的实际实现。


一、核心概念

概念说明
relationalStoreArkData 数据管理套件中的关系型数据库模块(@kit.ArkData),底层是 SQLite
RdbStore数据库实例,所有增删改查都在它上面执行
StoreConfig数据库配置:文件名 + 安全级别
RdbPredicates查询/更新/删除的条件构造器(WHERE 子句)
ValuesBucket插入/更新时的「列名 → 值」键值对
ResultSet查询结果集,需逐行遍历后手动 close
import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

二、建库与建表(初始化)

本项目中所有 DAO 都遵循同一套模式:单例复用 + CREATE TABLE IF NOT EXISTS + 建索引,可重复调用不会报错。

export class UserDao {
  private store: relationalStore.RdbStore | null = null;

  constructor(private context: common.Context) {}

  /** 初始化数据库:获取 RdbStore 实例并建表,可重复调用 */
  async init(): Promise<void> {
    if (this.store !== null) {
      return;
    }
    const config: relationalStore.StoreConfig = {
      name: 'demo.db',                                   // 数据库文件名,存于应用沙箱内
      securityLevel: relationalStore.SecurityLevel.S1,   // S1~S4,S1 最低安全级别
    };
    this.store = await relationalStore.getRdbStore(this.context, config);

    // 建表(IF NOT EXISTS 保证幂等)
    await this.store.executeSql(
      'CREATE TABLE IF NOT EXISTS user (' +
      'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +   // 自增主键
      'name TEXT NOT NULL, ' +
      'age INTEGER, ' +
      'phone TEXT' +
      ')'
    );
    // 为高频查询字段建索引(TodoDao 的实战做法)
    await this.store.executeSql(
      'CREATE INDEX IF NOT EXISTS idx_user_name ON user (name)'
    );
  }
}

要点:

  • 数据库文件保存在应用沙箱内,无需任何权限声明。
  • getRdbStore 是 async 的;securityLevel 可选 S1/S2/S3/S4,普通业务数据用 S1 即可。
  • 静态单例版(TodoDao / ContactDao 的写法):用 private static store?: relationalStore.RdbStore 缓存,getStore(context) 中判空复用。

三、增(INSERT)

/** 新增一条记录,返回自增 id */
async insert(name: string, age: number, phone: string): Promise<number> {
  const values: relationalStore.ValuesBucket = {
    'name': name,
    'age': age,
    'phone': phone,
  };
  // 注意:自增 id 不用传,insert 自动填充并返回新 id
  const rowId: number = await this.store!.insert('user', values);
  return rowId;
}

要点:

  • 入参是 表名 + ValuesBucket,返回新记录的自增 id(插入失败会 reject)。
  • ValuesBucket 的键必须与列名完全一致(含下划线,如 created_time)。
  • 批量插入(种子数据 / 备份恢复场景):循环 insert,配合事务(见第七节)可大幅提升性能。
// TodoDao 种子数据批量插入
for (const s of seed) {
  const values: relationalStore.ValuesBucket = {
    title: s.title, priority: s.priority, category: s.category,
    completed: s.completed, created_time: s.createdTime,
    completed_time: s.completed === 1 ? s.createdTime : 0,
    remark: s.remark,
  };
  await store.insert('todo', values);
}

四、删(DELETE)

删除必须用 RdbPredicates 指定条件,返回受影响行数(不是 true/false)。

/** 按 id 删除 */
async deleteById(id: number): Promise<number> {
  const predicates = new relationalStore.RdbPredicates('user');
  predicates.equalTo('id', id);
  return await this.store!.delete(predicates);
}

/** 清空整张表:不写任何条件即可 */
async deleteAll(): Promise<number> {
  const predicates = new relationalStore.RdbPredicates('user');
  return await this.store!.delete(predicates);
}

五、改(UPDATE)

update(ValuesBucket, RdbPredicates):只更新 ValuesBucket 中出现的列,其余列保持不变(天然支持部分更新)。

/** 按 id 更新全部业务字段 */
async update(id: number, name: string, age: number, phone: string): Promise<number> {
  const values: relationalStore.ValuesBucket = {
    'name': name, 'age': age, 'phone': phone,
  };
  const predicates = new relationalStore.RdbPredicates('user');
  predicates.equalTo('id', id);
  return await this.store!.update(values, predicates);  // 返回受影响行数
}

实战:只改两列的局部更新(TodoDao 切换完成状态)

/** 部分更新:只改 completed 与 completed_time */
static async toggleCompleted(context: common.Context, id: number, completed: number): Promise<number> {
  const store = await TodoDao.getStore(context);
  const values: relationalStore.ValuesBucket = {
    completed: completed,
    completed_time: completed === 1 ? Date.now() : 0,
  };
  const predicates = new relationalStore.RdbPredicates(TodoDao.TABLE);
  predicates.equalTo('id', id);
  return await store.update(values, predicates);
}

实战:按条件批量更新(CouponDao 把全部过期券置为失效)

static async expireAll(context: common.Context): Promise<number> {
  const store = await CouponDao.getStore(context);
  const values: relationalStore.ValuesBucket = { status: 2 };
  const predicates = new relationalStore.RdbPredicates(CouponDao.TABLE);
  predicates.equalTo('status', 0).lessThan('expire_time', Date.now());
  return await store.update(values, predicates);  // 一次更新所有过期券
}

六、查(SELECT)

6.1 条件构造器 RdbPredicates 常用方法

方法SQL 等价本项目出处
equalTo(col, v)col = v所有 DAO
notEqualTo(col, v)col != v
like(col, '%kw%')模糊匹配UserDao / ContactDao 搜索
greaterThan / greaterThanOrEqualTo> / >=CouponDao 查未过期券
lessThan / lessThanOrEqualTo< / <=CouponDao 查已过期券
between(col, a, b)BETWEEN a AND bLedgerDao 时间段查询
in(col, [v1,v2])IN (...)MediaDao 多类型筛选
isNull / isNotNullIS NULL
.and() / .or()条件连接链式调用时默认 AND,显式 .or() 切换 OR
orderByAsc / orderByDescORDER BY,可多级TodoDao 多字段排序
limitAs(n)LIMIT nUserDao 查第一条
countAs / sumAs / avgAs / maxAs / minAs聚合投影列ContactDao GROUP BY 统计
// 多字段 OR 模糊搜索(ContactDao)
predicates.like('name', `%${keyword}%`)
  .or()
  .like('phone', `%${keyword}%`)
  .orderByAsc('pinyin').orderByAsc('name');

// 时间范围查询(LedgerDao)
predicates.between('trade_time', start, end).orderByDesc('trade_time');

// IN 查询(MediaDao)
predicates.in('type', types).orderByDesc('rating');

// 多级排序(TodoDao:未完成在前 → 优先级高在前 → 新的在前)
predicates.orderByAsc('completed').orderByDesc('priority').orderByDesc('created_time');

6.2 查询全部 / 查询单条

/** 查询全部,按 id 倒序 */
async queryAll(): Promise<User[]> {
  const predicates = new relationalStore.RdbPredicates('user');
  predicates.orderByDesc('id');
  const resultSet: relationalStore.ResultSet = await this.store!.query(predicates);
  const users: User[] = this.parseResultSet(resultSet);
  resultSet.close();          // 必须关闭,防止内存泄漏
  return users;
}

/** 查询第一条,无记录返回 null(limitAs(1) 的用法) */
async queryFirst(): Promise<User | null> {
  const predicates = new relationalStore.RdbPredicates('user');
  predicates.limitAs(1);
  const resultSet = await this.store!.query(predicates);
  let user: User | null = null;
  if (resultSet.goToNextRow()) {
    user = this.rowToUser(resultSet);
  }
  resultSet.close();
  return user;
}

6.3 结果集遍历与行转对象

// 逐行遍历
private parseResultSet(resultSet: relationalStore.ResultSet): User[] {
  const users: User[] = [];
  while (resultSet.goToNextRow()) {      // 游标移动到下一行,无数据返回 false
    users.push(this.rowToUser(resultSet));
  }
  return users;
}

// 按列名取列下标,再按类型取值
private rowToUser(resultSet: relationalStore.ResultSet): User {
  return {
    id:    resultSet.getLong(resultSet.getColumnIndex('id')),
    name:  resultSet.getString(resultSet.getColumnIndex('name')),
    age:   resultSet.getLong(resultSet.getColumnIndex('age')),
    phone: resultSet.getString(resultSet.getColumnIndex('phone')),
  };
}

getLong / getString / getFloat / getBlob 按列的存储类型选择。本项目中时间戳(created_time)存 INTEGER,用 getLong 取出。

6.4 原生 SQL 查询(querySql)

复杂统计(聚合、CASE WHEN、GROUP BY)直接写 SQL:

/** 一条 SQL 出 4 个统计值(TodoDao) */
static async statistics(context: common.Context): Promise<TodoStats> {
  const store = await TodoDao.getStore(context);
  const result = await store.querySql(
    `SELECT COUNT(*) AS total,
       SUM(CASE WHEN completed=0 THEN 1 ELSE 0 END) AS pending,
       SUM(CASE WHEN completed=1 THEN 1 ELSE 0 END) AS done,
       SUM(CASE WHEN priority=2 AND completed=0 THEN 1 ELSE 0 END) AS high
      FROM todo`
  );
  let total = 0, pending = 0, done = 0, high = 0;
  if (result.goToNextRow()) {
    total   = result.getLong(result.getColumnIndex('total'));
    pending = result.getLong(result.getColumnIndex('pending'));
    done    = result.getLong(result.getColumnIndex('done'));
    high    = result.getLong(result.getColumnIndex('high'));
  }
  result.close();
  return { total, pending, done, high };
}

/** GROUP BY 分组统计(ContactDao 拼音分组人数) */
static async statistics(context: common.Context): Promise<ContactStats> {
  const store = await ContactDao.getStore(context);
  const groups: Record<string, number> = {};
  let total = 0;
  const result = await store.querySql(
    `SELECT pinyin, COUNT(*) AS cnt FROM contact GROUP BY pinyin ORDER BY pinyin`
  );
  while (result.goToNextRow()) {
    const p = result.getString(result.getColumnIndex('pinyin'));
    const cnt = result.getLong(result.getColumnIndex('cnt'));
    groups[p] = cnt;
    total += cnt;
  }
  result.close();
  return { total, groups };
}

执行非查询类原生 SQL(建表、建索引)用 executeSql(sql)


七、事务(批量操作必备)

涉及"多步写操作要么全成功要么全失败"的场景(备份恢复、菜谱+配料、保养记录+同步里程),用 beginTransaction / commit / rollBack

/** 备份恢复:清空表 + 批量导入,任一失败整体回滚(BackupDao) */
static async restoreJson(context: common.Context, json: string): Promise<number> {
  const store = await BackupDao.getStore(context);
  const parsed: SourceJsonRow[] = JSON.parse(json);
  let count = 0;
  try {
    await store.beginTransaction();          // 1. 开启事务
    const delPred = new relationalStore.RdbPredicates(BackupDao.SOURCE_TABLE);
    await store.delete(delPred);            // 先清空
    for (const r of parsed) {
      const values: relationalStore.ValuesBucket = {
        name: r.name, category: r.category, amount: r.amount,
        note: r.note, created_time: r.createdTime,
      };
      await store.insert(BackupDao.SOURCE_TABLE, values);
      count++;
    }
    await store.commit();                   // 2. 全部成功 → 提交
    return count;
  } catch (e) {
    await store.rollBack();                 // 3. 任何异常 → 回滚
    throw new Error(`恢复失败: ${JSON.stringify(e)}`);
  }
}

八、页面层调用示例(SqlDemo.ets)

页面通过 this.getUIContext().getHostContext() 获取上下文,初始化 DAO 后调用增删改查,并用 .then/.catch 处理结果:

@Entry
@Component
struct SqlDemo {
  private dao: UserDao | null = null;
  @State users: User[] = [];
  @State logText: string = '正在初始化数据库...';

  aboutToAppear(): void {
    let host = this.getUIContext().getHostContext();
    if (host === undefined) { return; }
    let context = host as common.UIAbilityContext;
    this.dao = new UserDao(context);
    this.dao.init().then(() => {
      this.logText = '✅ 数据库初始化成功';
      this.refresh();
    }).catch((err: BusinessError) => {
      this.logText = `❌ 初始化失败: ${JSON.stringify(err)}`;
    });
  }

  /** 增 */
  private onInsert(): void {
    this.dao!.insert('张三', 25, '13800138000').then((rowId: number) => {
      this.logText = `✅ 新增成功,自增 id = ${rowId}`;
      this.refresh();
    }).catch((err: BusinessError) => {
      this.logText = `❌ 新增失败: ${JSON.stringify(err)}`;
    });
  }

  /** 改(先查出目标记录,再按 id 更新) */
  private onUpdateFirst(): void {
    this.dao!.queryFirst().then((first: User | null) => {
      if (first === null) { return; }
      this.dao!.update(first.id, '李四', 30, '13900139000').then((rows: number) => {
        this.logText = `✅ 已更新 id=${first.id},受影响 ${rows}`;
        this.refresh();
      });
    });
  }

  /** 删 */
  private onDeleteById(id: number): void {
    this.dao!.deleteById(id).then((rows: number) => {
      this.logText = `✅ 已删除 id=${id},受影响 ${rows}`;
      this.refresh();
    });
  }

  /** 查 + 刷新 @State 驱动 UI 刷新 */
  private refresh(): void {
    this.dao!.queryAll().then((list: User[]) => {
      this.users = list;     // 赋值 @State,列表自动刷新
    });
  }

  build() {
    Column() {
      ForEach(this.users, (item: User) => {
        Text(`${item.id} ${item.name} ${item.age}`)
          .onClick(() => this.onDeleteById(item.id));
      }, (item: User) => `${item.id}-${item.name}`)
    }
  }
}

排查技巧(项目实战):把查询结果 JSON.stringify 到一个文本里显示出来——有 JSON 说明数据写入成功,列表没显示则是 UI 层问题


九、API 速查表

操作API返回
建库/建表relationalStore.getRdbStore(context, config)RdbStore(Promise)
执行 SQLstore.executeSql(sql)void(Promise)
原生查询store.querySql(sql, 列投影?)ResultSet(Promise)
store.insert(table, values)新记录自增 id
store.delete(predicates)受影响行数
store.update(values, predicates)受影响行数
store.query(predicates, 列投影?)ResultSet
事务store.beginTransaction() / commit() / rollBack()void
取值resultSet.getLong / getString / getFloat(colIndex)对应类型值
遍历resultSet.goToNextRow()boolean
释放resultSet.close()void

十、常见坑(本项目踩过的)

  1. ResultSet 忘记 close() → 内存泄漏,每次遍历完必须关闭。
  2. 条件构造器默认 AND → 要"或"的关系必须显式 .or(),且 .or() 放在两个条件之间。
  3. 列名大小写/下划线必须与建表 SQL 一致created_time 写成 createdTime 会报列不存在。
  4. insert 返回的是 id,不是布尔值update/delete 返回受影响行数,0 行不代表出错。
  5. ValuesBucket 里不要放主键自增列,放 id 会插入冲突或覆盖自增逻辑。
  6. 所有操作都是异步的,页面里必须 await.then/.catch,并处理 BusinessError
  7. 批量写操作要包事务,既保证原子性又显著提升性能。
  8. 高频查询字段建索引CREATE INDEX IF NOT EXISTS),排序/过滤字段收益最大。
Logo

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

更多推荐