RDB关系数据库实战:增删改查、事务、分页、版本迁移

本地数据库是鸿蒙应用里绕不开的能力。聊天记录、离线缓存、表单草稿、用户收藏,这些数据量大、结构复杂、需要条件查询的场景,用Preferences键值存储根本扛不住,必须上关系型数据库。很多开发者第一次接RDB时,总觉得SQLite相关的API繁琐:建表、增删改查、分页、事务、版本升级,一套下来步骤多,稍不注意就表结构出错。本文基于最新鸿蒙API,把RDB的完整落地流程拆成几个环节讲清楚,最后给一个可直接复用的工具类,代码可以直接复制到项目里跑。

01 数据库初始化与建表

RDB(Relational Database)是鸿蒙提供的关系型数据库,底层基于SQLite,支持标准SQL语句。使用前需要先配置数据库参数,创建数据库实例,然后建表。

import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

const STORE_CONFIG: relationalStore.StoreConfig = {
  name: 'my_database.db', // 数据库文件名
  securityLevel: relationalStore.SecurityLevel.S1,
  encrypt: false
};

let rdb: relationalStore.RelationalStore | null = null;

// 初始化数据库
async function initDatabase(context: common.Context) {
  rdb = await relationalStore.getRdbStore(context, STORE_CONFIG);

  // 建表SQL
  const CREATE_TABLE_SQL = `
    CREATE TABLE IF NOT EXISTS user (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      age INTEGER,
      email TEXT UNIQUE,
      created_at INTEGER,
      status INTEGER DEFAULT 1
    )
  `;

  await rdb.executeSql(CREATE_TABLE_SQL);
  console.info('数据库初始化完成');
}

几个关键点:

  • securityLevel:数据库安全级别,S1是基础级别,普通业务用S1就够了

  • CREATE TABLE IF NOT EXISTS:加上IF NOT EXISTS,重复执行不会报错,首次启动和版本升级都能安全执行

  • 主键自增:INTEGER PRIMARY KEY AUTOINCREMENT,id自动增长,不用手动赋值

建表SQL写在初始化里,每次启动都会执行一次。表已存在的话直接跳过,不会重建。

02 增删改查基础操作

RDB提供了insert、update、delete、query四个基础方法,对应SQL的增删改查。不用自己拼接完整SQL,用参数化的方式调用更安全。

async function insertUser(name: string, age: number, email: string) {
  if (!rdb) return;
  // 用ValuesBucket存数据,key是列名
  const valueBucket: relationalStore.ValuesBucket = {
    name: name,
    age: age,
    email: email,
    created_at: Date.now(),
    status: 1
  };
  // 插入,返回新行的id
  const rowId = await rdb.insert('user', valueBucket);
  console.info('插入成功,id:', rowId);
  return rowId;
}
async function updateUserStatus(userId: number, status: number) {
  if (!rdb) return;
  const valueBucket: relationalStore.ValuesBucket = {
    status: status
  };
  // 更新条件:id = ?
  const predicates = new relationalStore.RdbPredicates('user');
  predicates.equalTo('id', userId);
  // 返回受影响的行数
  const rows = await rdb.update(valueBucket, predicates);
  console.info('更新行数:', rows);
}
async function deleteUser(userId: number) {
  if (!rdb) return;
  const predicates = new relationalStore.RdbPredicates('user');
  predicates.equalTo('id', userId);
  const rows = await rdb.delete(predicates);
  console.info('删除行数:', rows);
}
async function queryUserById(userId: number) {
  if (!rdb) return;
  const predicates = new relationalStore.RdbPredicates('user');
  predicates.equalTo('id', userId);
  // 查询列,null表示查所有列
  const resultSet = await rdb.query(predicates, null);

  // 遍历结果集
  if (resultSet.goToFirstRow()) {
    const id = resultSet.getLong(resultSet.getColumnIndex('id'));
    const name = resultSet.getString(resultSet.getColumnIndex('name'));
    const age = resultSet.getLong(resultSet.getColumnIndex('age'));
    console.info('查询到用户:', id, name, age);
  }
  // 用完必须关闭结果集,不然内存泄漏
  resultSet.close();
}

RdbPredicates是条件构造器,equalTo、greaterThan、like、orderByAsc这些方法都有,不用自己拼WHERE语句。

结果集ResultSet用完一定要close(),这个最容易忘。不close的话,数据库游标一直占着内存,查多了就会泄漏。

03 分页查询实现

列表页数据多了不能一次全查出来,必须分页。RDB的分页用limit和offset实现,对应SQL的LIMIT offset, count。

async function queryUserPage(page: number, pageSize: number) {
  if (!rdb) return;
  // 页码从1开始,计算偏移量
  const offset = (page - 1) * pageSize;

  const predicates = new relationalStore.RdbPredicates('user');
  // 按创建时间倒序,最新的在前面
  predicates.orderByDesc('created_at');
  // 分页:offset是起始位置,pageSize是每页数量
  predicates.limitAs(pageSize, offset);

  const resultSet = await rdb.query(predicates, null);
  const list: Array<{id: number, name: string, age: number}> = [];

  while (resultSet.goToNextRow()) {
    list.push({
      id: resultSet.getLong(resultSet.getColumnIndex('id')),
      name: resultSet.getString(resultSet.getColumnIndex('name')),
      age: resultSet.getLong(resultSet.getColumnIndex('age'))
    });
  }

  resultSet.close();
  console.info('第', page, '页数据:', list);
  return list;
}

分页查询的几个注意点:

  • 页码从1开始,offset = (page - 1) * pageSize,别算错

  • 必须有排序,不然分页数据可能重复或者漏行

  • 总条数单独查,分页的同时查一下总数,前端才能显示共多少条

async function getUserCount(): Promise<number> {
  if (!rdb) return 0;
  const predicates = new relationalStore.RdbPredicates('user');
  const resultSet = await rdb.querySql('SELECT COUNT(*) as count FROM user');
  let count = 0;
  if (resultSet.goToFirstRow()) {
    count = resultSet.getLong(resultSet.getColumnIndex('count'));
  }
  resultSet.close();
  return count;
}

04 事务处理

批量操作的时候,一条一条插入性能很差,而且中间出错了,前面的数据已经写进去了,回滚不了。这时候就要用事务——要么全部成功,要么全部失败。

async function batchInsertUsers(users: Array<{name: string, age: number}>) {
  if (!rdb) return;

  // 开启事务
  rdb.begin();
  try {
    for (const user of users) {
      const valueBucket: relationalStore.ValuesBucket = {
        name: user.name,
        age: user.age,
        created_at: Date.now(),
        status: 1
      };
      await rdb.insert('user', valueBucket);
    }
    // 全部成功,提交事务
    rdb.commit();
    console.info('批量插入成功');
  } catch (e) {
    // 出错了,回滚事务,数据全部撤销
    rdb.rollBack();
    console.error('批量插入失败,已回滚:', e);
  }
}

事务的核心就是begin → commit / rollBack三个方法。批量插入、批量更新,一定要包在事务里,性能提升几十倍,而且数据安全。

有个细节:commit之后事务就结束了,不能再用同一个事务对象继续操作。要开新事务得重新begin。

05 数据库版本迁移

应用升级的时候,表结构经常要改:加字段、改字段类型、加索引。这时候不能直接删库重建,不然用户数据全丢了。RDB提供了版本升级的回调机制。

const STORE_CONFIG: relationalStore.StoreConfig = {
  name: 'my_database.db',
  securityLevel: relationalStore.SecurityLevel.S1,
  encrypt: false,
  // 数据库版本号,从1开始,每次升级加1
  version: 2
};

async function initDatabaseWithMigration(context: common.Context) {
  rdb = await relationalStore.getRdbStore(context, STORE_CONFIG);

  // 版本升级回调
  rdb.on('databaseUpgrade', (oldVersion: number, newVersion: number) => {
    console.info('数据库升级:', oldVersion, '→', newVersion);

    // 从版本1升级到版本2:加一个phone字段
    if (oldVersion === 1 && newVersion === 2) {
      rdb?.executeSql('ALTER TABLE user ADD COLUMN phone TEXT');
    }

    // 从版本2升级到版本3:加索引
    if (oldVersion === 2 && newVersion === 3) {
      rdb?.executeSql('CREATE INDEX IF NOT EXISTS idx_user_email ON user(email)');
    }
  });
}

版本迁移的规则:

  • version每次只加1,不要跳版本号,不然升级逻辑要处理跨版本的情况

  • ALTER TABLE加字段是最安全的,不影响老数据

  • 删字段、改字段类型比较麻烦,得建新表→搬数据→删旧表,慎用

  • 迁移SQL要加IF NOT EXISTS,防止重复执行报错

版本号写在StoreConfig里,系统自动对比当前数据库的版本,不一致就触发upgrade回调。不用自己判断版本号。

06 完整工具类封装

把上面的逻辑封装成一个通用的数据库工具类,项目里直接调用,不用每个页面都写一遍初始化和查询代码。

import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

class DBHelper {
  private static instance: DBHelper;
  private rdb: relationalStore.RelationalStore | null = null;

  private constructor() {}

  static getInstance(): DBHelper {
    if (!DBHelper.instance) {
      DBHelper.instance = new DBHelper();
    }
    return DBHelper.instance;
  }

  // 初始化,在Ability启动时调用一次
  async init(context: common.Context) {
    const config: relationalStore.StoreConfig = {
      name: 'my_database.db',
      securityLevel: relationalStore.SecurityLevel.S1,
      version: 1
    };
    this.rdb = await relationalStore.getRdbStore(context, config);
    // 建表
    await this.rdb.executeSql(`
      CREATE TABLE IF NOT EXISTS user (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age INTEGER,
        email TEXT UNIQUE,
        created_at INTEGER
      )
    `);
  }

  // 插入
  async insert(table: string, values: relationalStore.ValuesBucket): Promise<number> {
    if (!this.rdb) return -1;
    return await this.rdb.insert(table, values);
  }

  // 查询
  async query(table: string, predicates: relationalStore.RdbPredicates) {
    if (!this.rdb) return null;
    return await this.rdb.query(predicates, null);
  }

  // 更新
  async update(table: string, values: relationalStore.ValuesBucket, where: string) {
    if (!this.rdb) return 0;
    const predicates = new relationalStore.RdbPredicates(table);
    // 这里简化,实际项目里可以扩展更多条件
    return await this.rdb.update(values, predicates);
  }

  // 删除
  async delete(table: string, predicates: relationalStore.RdbPredicates) {
    if (!this.rdb) return 0;
    return await this.rdb.delete(predicates);
  }

  // 执行原生SQL
  async execSql(sql: string) {
    if (!this.rdb) return;
    await this.rdb.executeSql(sql);
  }
}

// 使用方式
const db = DBHelper.getInstance();
// 在EntryAbility的onCreate里调用db.init(context)
// 页面里直接 db.insert('user', {...})

这样封装之后,任何页面只需要拿DBHelper.getInstance()就能操作数据库,不用关心初始化和连接管理。

小结

RDB关系数据库的核心就是四件事:建表、增删改查、分页、事务。基础操作不复杂,容易踩坑的地方主要是结果集没close、分页没排序、批量操作没包事务、版本升级丢数据。实际项目中,把数据库操作封装在统一的工具类里,表结构和SQL集中管理。版本号每次升级只加1,迁移逻辑写清楚,老用户升级也不会丢数据。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐