我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

我见过太多“能跑但难维护”的数据层:业务里到处飞的 SQL、同一段 ResultSet 解析复制粘贴十遍、字段改名全项目红一片。别急着叹气,这篇就按你给的大纲来——RDB 架构 → SQL 执行 → 数据模型映射——把 RdbStore / ValuesBucket / ResultSet 一网打尽,再顺手落一个轻量 ORM 封装(真·能抄能用那种)。写完你能做到:DDL/CRUD/事务有章法,查询更优雅,数据模型一处维护全局受益。

一、RDB 架构:把“库、表、连接、版本、事务”摆整齐

核心角色速记

  • RdbStore:数据库句柄(连接),你所有 SQL/CRUD 都从它发起。
  • StoreConfig:数据库配置(文件名、安全级别等),带版本升级回调
  • ValuesBucket:“一行数据”的键值容器,用于 insert/update
  • ResultSet:查询结果游标;自己迭代与 close,别泄漏句柄。
  • RdbPredicates:条件构造器,给 query/update/delete 传“WHERE/ORDER BY/LIMIT”。

推荐分层

/data
  rdb/Database.ts       // 获取 RdbStore + 版本升级
  rdb/Tx.ts             // 事务辅助
  orm/Entity.ts         // 元数据 + 映射工具
  orm/Repository.ts     // 通用 CRUD
  repo/TodoRepo.ts      // 业务仓库(组合查询)

二、起步:拿到 RdbStore,顺便把“升级”这事儿一次讲透

// data/rdb/Database.ts
import rdb from '@ohos.data.rdb'

export type Migrator = (db: rdb.RdbStore) => void

interface DbOptions {
  name: string
  version: number
  onCreate: Migrator
  onUpgrade?: (db: rdb.RdbStore, oldV: number, newV: number) => void
}

export class Database {
  private store?: rdb.RdbStore

  constructor(private opt: DbOptions) {}

  async open(ctx: Context) {
    const cfg: rdb.StoreConfig = { name: this.opt.name, securityLevel: rdb.SecurityLevel.S1 }
    this.store = await rdb.getRdbStore(ctx, cfg, this.opt.version, (db, oldV) => {
      if (oldV === 0) {
        this.opt.onCreate(db)
      } else if (this.opt.onUpgrade) {
        this.opt.onUpgrade(db, oldV, this.opt.version)
      }
    })
    return this.store!
  }

  get(): rdb.RdbStore {
    if (!this.store) throw new Error('DB not opened')
    return this.store
  }
}

初始化示例(包含 DDL 与升级)

// data/Bootstrap.ts
import rdb from '@ohos.data.rdb'
import { Database } from './rdb/Database'

export const db = new Database({
  name: 'app.db',
  version: 2,
  onCreate(db: rdb.RdbStore) {
    db.executeSql(`
      CREATE TABLE IF NOT EXISTS todos(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        title TEXT NOT NULL,
        done INTEGER NOT NULL DEFAULT 0,
        created_at INTEGER NOT NULL,
        updated_at INTEGER NOT NULL
      );
    `)
    db.executeSql(`CREATE INDEX IF NOT EXISTS idx_todos_done ON todos(done);`)
  },
  onUpgrade(db: rdb.RdbStore, oldV: number, newV: number) {
    if (oldV < 2) {
      db.executeSql(`ALTER TABLE todos ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;`)
      db.executeSql(`CREATE INDEX IF NOT EXISTS idx_todos_priority ON todos(priority);`)
    }
  }
})

小贴士

  • 版本升级回调里只做幂等 DDLIF NOT EXISTS),防止重复执行。
  • DDL 一定配索引(按查询条件上),别让搜索一跑就卡。

三、SQL 执行:从“裸 SQL”到“安全、可重复”的 CRUD

3.1 executeSql:DDL/原生 SQL、批处理、占位参数

const store = db.get()
// 占位参数用 ?,防注入
store.executeSql(
  'UPDATE todos SET done = ?, updated_at = ? WHERE id = ?',
  [1, Date.now(), 42]
)

3.2 insert/update/delete/query:配合 ValuesBucket/Predicates

import dataAbility from '@ohos.data.dataAbility'
import rdb from '@ohos.data.rdb'

// INSERT
function toValuesBucket(row: { [k: string]: any }): dataAbility.ValuesBucket {
  const v = new dataAbility.ValuesBucket()
  Object.keys(row).forEach(k => {
    const val = row[k]
    switch (typeof val) {
      case 'string': v.putString(k, val); break
      case 'number':
        // 简单区分整型/浮点,可按你的表结构裁剪
        Number.isInteger(val) ? v.putLong(k, val) : v.putDouble(k, val); break
      case 'boolean': v.putLong(k, val ? 1 : 0); break
      default: if (val == null) v.putNull(k); else v.putString(k, String(val))
    }
  })
  return v
}

// 新建 todo
export async function addTodo(title: string) {
  const now = Date.now()
  const row = { title, done: 0, created_at: now, updated_at: now, priority: 0 }
  const id = await store.insert('todos', toValuesBucket(row))
  return id
}

// 查询 todo 列表(ResultSet → 数组)
export async function listOpenTodos(limit = 50, offset = 0) {
  const pred = new rdb.RdbPredicates('todos')
    .equalTo('done', 0)
    .orderByAsc('priority')
    .orderByDesc('updated_at')
    .limitAs(limit)
    .offsetAs(offset)

  const rs = await store.query(pred, ['id', 'title', 'done', 'priority', 'updated_at'])
  const rows: any[] = []
  while (rs.goToNextRow()) {
    rows.push({
      id: rs.getLong(rs.getColumnIndex('id')),
      title: rs.getString(rs.getColumnIndex('title')),
      done: rs.getLong(rs.getColumnIndex('done')) === 1,
      priority: rs.getLong(rs.getColumnIndex('priority')),
      updatedAt: rs.getLong(rs.getColumnIndex('updated_at')),
    })
  }
  rs.close()
  return rows
}

// 更新(按主键)
export async function finishTodo(id: number) {
  const v = toValuesBucket({ done: 1, updated_at: Date.now() })
  const pred = new rdb.RdbPredicates('todos').equalTo('id', id)
  const changed = await store.update(v, pred)
  return changed
}

// 删除(批量条件)
export async function purgeDone(beforeMs: number) {
  const pred = new rdb.RdbPredicates('todos')
    .equalTo('done', 1)
    .lessThan('updated_at', beforeMs)
  return await store.delete(pred)
}

关键要领

  • ValuesBucket 就像一个类型感知的 Map,记得按列类型选择 putLong/putDouble/putString/putNull
  • ResultSet 必须 close();列值取用 getColumnIndex()getXxx(),稳。
  • Predicates 能覆盖大多数 WHEREequalTo/like/between/in/and/or/orderBy/limit/offset

3.3 事务:要么全成,要么全滚

export async function completeAll(ids: number[]) {
  store.beginTransaction()
  try {
    for (const id of ids) {
      const v = toValuesBucket({ done: 1, updated_at: Date.now() })
      const pred = new rdb.RdbPredicates('todos').equalTo('id', id)
      await store.update(v, pred)
    }
    store.commit()
  } catch (e) {
    store.rollBack()
    throw e
  }
}

事务内尽量只做本库写;有网络 / 跨源写入就分两阶段(或写出补偿日志)。


四、数据模型映射:给 SQL 穿件体面外套(轻量 ORM)

4.1 目标:定义一次表结构 + 字段映射,CRUD 自动带走

我们实现一套极轻的 ORM:

  • EntityMeta:表名、主键、列定义、字段转换器;
  • RowMapperResultSet → 实体
  • toValues实体 → ValuesBucket
  • Repository:泛型 CRUD + 查询辅助。
(1) 元信息定义
// data/orm/Entity.ts
import dataAbility from '@ohos.data.dataAbility'
import rdb from '@ohos.data.rdb'

export type ColType = 'INTEGER' | 'REAL' | 'TEXT' | 'BLOB'

export interface ColumnMeta<T=any> {
  name: string
  type: ColType
  fromDb?: (v: any) => T
  toDb?: (v: T) => any
}

export interface EntityMeta<T> {
  table: string
  pk: keyof T
  columns: Record<keyof T, ColumnMeta>
}

export function toValues<T>(meta: EntityMeta<T>, obj: Partial<T>): dataAbility.ValuesBucket {
  const v = new dataAbility.ValuesBucket()
  for (const k in obj) {
    const c = meta.columns[k as keyof T]
    if (!c) continue
    const raw = (obj as any)[k]
    const val = c.toDb ? c.toDb(raw) : raw
    if (val === undefined) continue
    switch (c.type) {
      case 'INTEGER': v.putLong(c.name, Number(val)); break
      case 'REAL': v.putDouble(c.name, Number(val)); break
      case 'TEXT': v.putString(c.name, val == null ? null : String(val)); break
      case 'BLOB': /* 自行实现二进制 */ break
    }
  }
  return v
}

export function rowFromRs<T>(meta: EntityMeta<T>, rs: rdb.ResultSet): T {
  const out: any = {}
  for (const k in meta.columns) {
    const c = meta.columns[k as keyof T]
    const idx = rs.getColumnIndex(c.name)
    let v: any = null
    switch (c.type) {
      case 'INTEGER': v = rs.getLong(idx); break
      case 'REAL': v = rs.getDouble(idx); break
      case 'TEXT': v = rs.getString(idx); break
      case 'BLOB': /* 读取 blob */ break
    }
    out[k] = c.fromDb ? c.fromDb(v) : v
  }
  return out as T
}
(2) 针对 Todo 的实体与表
// data/entities/Todo.ts
export interface Todo {
  id: number
  title: string
  done: boolean
  priority: number
  createdAt: number
  updatedAt: number
}

import { EntityMeta } from '../orm/Entity'

export const TodoMeta: EntityMeta<Todo> = {
  table: 'todos',
  pk: 'id',
  columns: {
    id:        { name: 'id',          type: 'INTEGER' },
    title:     { name: 'title',       type: 'TEXT'    },
    done:      { name: 'done',        type: 'INTEGER',
                 fromDb: (n) => n === 1, toDb: (b: boolean) => (b ? 1 : 0) },
    priority:  { name: 'priority',    type: 'INTEGER' },
    createdAt: { name: 'created_at',  type: 'INTEGER' },
    updatedAt: { name: 'updated_at',  type: 'INTEGER' },
  }
}
(3) 通用仓库(Repository)
// data/orm/Repository.ts
import rdb from '@ohos.data.rdb'
import { EntityMeta, toValues, rowFromRs } from './Entity'

export class Repository<T> {
  constructor(private store: rdb.RdbStore, private meta: EntityMeta<T>) {}

  async insert(partial: Omit<T, 'id'> & Partial<Pick<T, 'id'>>): Promise<number> {
    const v = toValues(this.meta, partial)
    return await this.store.insert(this.meta.table, v)
  }

  async updateById(id: number, partial: Partial<T>) {
    const v = toValues(this.meta, partial)
    const pred = new rdb.RdbPredicates(this.meta.table).equalTo(
      this.meta.columns[this.meta.pk].name, id
    )
    return await this.store.update(v, pred)
  }

  async deleteById(id: number) {
    const pred = new rdb.RdbPredicates(this.meta.table).equalTo(
      this.meta.columns[this.meta.pk].name, id
    )
    return await this.store.delete(pred)
  }

  async findById(id: number): Promise<T | null> {
    const pred = new rdb.RdbPredicates(this.meta.table).equalTo(
      this.meta.columns[this.meta.pk].name, id
    ).limitAs(1)
    const rs = await this.store.query(pred, Object.values(this.meta.columns).map(c => c.name))
    const row = rs.goToNextRow() ? rowFromRs<T>(this.meta, rs) : null
    rs.close()
    return row
  }

  async list(where: (p: rdb.RdbPredicates) => rdb.RdbPredicates, limit = 50, offset = 0): Promise<T[]> {
    let p = new rdb.RdbPredicates(this.meta.table)
    p = where(p).limitAs(limit).offsetAs(offset)
    const rs = await this.store.query(p, Object.values(this.meta.columns).map(c => c.name))
    const out: T[] = []
    while (rs.goToNextRow()) out.push(rowFromRs<T>(this.meta, rs))
    rs.close()
    return out
  }
}
(4) 在业务里用它
// repo/TodoRepo.ts
import { Repository } from '../data/orm/Repository'
import { Todo, TodoMeta } from '../data/entities/Todo'
import { db } from '../data/Bootstrap'
import rdb from '@ohos.data.rdb'

export class TodoRepo {
  private store!: rdb.RdbStore
  private repo!: Repository<Todo>

  async init(ctx: Context) {
    this.store = await db.open(ctx)
    this.repo = new Repository<Todo>(this.store, TodoMeta)
  }

  async add(title: string) {
    const now = Date.now()
    return await this.repo.insert({ title, done: false, priority: 0, createdAt: now, updatedAt: now } as any)
  }

  async markDone(id: number) {
    return await this.repo.updateById(id, { done: true, updatedAt: Date.now() } as any)
  }

  async listOpen(keyword = '', limit = 100, offset = 0) {
    return await this.repo.list(p => {
      let q = p.equalTo('done', 0).orderByAsc('priority').orderByDesc('updated_at')
      if (keyword) q = q.like('title', `%${keyword}%`)
      return q
    }, limit, offset)
  }
}

现在你业务里看不到 ValuesBucket/ResultSet 细节了,改表名/字段名只动实体元数据,映射自动跟上。舒服!


五、常用“姿势”合集:效率、正确性两手都要硬

5.1 分页 + 总数

export async function listWithCount(keyword = '') {
  const pred = new rdb.RdbPredicates('todos')
  if (keyword) pred.like('title', `%${keyword}%`)
  const rs = await store.query(pred, ['COUNT(1) AS c'])
  rs.goToNextRow()
  const count = rs.getLong(rs.getColumnIndex('c'))
  rs.close()

  const list = await repo.list(p => (keyword ? p.like('title', `%${keyword}%`) : p).limitAs(20))
  return { count, list }
}

5.2 批量写入:executeSql + 事务更快

store.beginTransaction()
try {
  const sql = `INSERT INTO todos(title, done, priority, created_at, updated_at) VALUES(?, ?, ?, ?, ?)`
  for (const t of batch) {
    store.executeSql(sql, [t.title, 0, 0, t.ts, t.ts])
  }
  store.commit()
} catch (e) { store.rollBack(); throw e }

5.3 乐观并发控制(更新时带版本/时间戳)

export async function safeUpdate(id: number, lastUpdated: number, patch: any) {
  const v = toValues(TodoMeta, { ...patch, updatedAt: Date.now() })
  const pred = new rdb.RdbPredicates('todos')
    .equalTo('id', id).equalTo('updated_at', lastUpdated)
  const changed = await store.update(v, pred)
  if (changed === 0) throw new Error('CONFLICT') // 有人抢先改过
  return changed
}

5.4 “读写合一”的小封装(避免忘 close)

export async function queryOne<T>(sql: string, args: any[], map: (rs: rdb.ResultSet)=>T): Promise<T|null> {
  const rs = await store.rawQuery(sql, args) // 若无 rawQuery,用 executeSql + 临时视图/或 predicates
  const row = rs.goToNextRow() ? map(rs) : null
  rs.close()
  return row
}

六、把它塞回 ArkUI 页面里跑一圈(最小演示)

// pages/TodoPage.ets
import { TodoRepo } from '../repo/TodoRepo'

@Entry
@Component
struct TodoPage {
  private repo: TodoRepo = new TodoRepo()
  @State items: Array<{ id:number; title:string; done:boolean }> = []
  @State title: string = ''

  async aboutToAppear() {
    await this.repo.init(getContext(this))
    this.items = await this.repo.listOpen()
  }

  build() {
    Column({ space: 12 }) {
      Row({ space: 8 }) {
        TextInput({ placeholder: 'Add a task…' }).onChange(v => this.title = v).layoutWeight(1)
        Button('Add').onClick(async () => {
          if (!this.title.trim()) return
          await this.repo.add(this.title.trim())
          this.items = await this.repo.listOpen()
          this.title = ''
        })
      }.padding(12)

      List() {
        ForEach(this.items, (it) => ListItem() {
          Row() {
            Checkbox({ name: `cb-${it.id}`, group: 'todo', selected: it.done })
              .onChange(async (_, checked) => {
                await this.repo.markDone(it.id)
                this.items = await this.repo.listOpen()
              })
            Text(it.title).opacity(it.done ? 0.5 : 1).decoration(it.done ? { type: TextDecorationType.LineThrough } : null)
          }.padding(12)
        }, it => String(it.id))
      }
    }.padding(12)
  }
}

七、踩坑复盘(我都踩过,真诚分享)

  1. ResultSet 忘记 close → 句柄泄漏、奇怪卡顿:统一用小工具函数兜住。
  2. 类型不一致putString/putLong 乱用:给每列明确类型,在 toDb/fromDb 里转换好。
  3. 大表无索引 → 查询像“走路去隔壁城”:把WHERE/ORDER涉及列建索引。
  4. 事务里做网络 → 回滚/一致性都悬:事务只管本地写
  5. 字段改名后大面积爆红 → 抽象成实体元数据,业务只用域模型字段名。
  6. 字符串拼 SQL 注入占位参数走起,Predicates 能覆盖的就别手搓。

八、扩展方向:让 ORM 再长点肌肉(按需挑)

  • 关系加载1:N 用懒加载或 JOIN 视图;避免 N+1。
  • 软删除deleted_at + 视图/Predicates 统一过滤。
  • 审计字段created_by/updated_by,谁改的写清楚。
  • 查询构建器:链式 select/where/order/limit,最后编译为 PredicatesrawQuery
  • 缓存层:热门读放内存(Map + 版本号),降低游标压力。
  • 迁移框架:以“编号脚本”管理 DDL,自动回滚/重跑。

结语|把数据库这摊事儿“制度化”,你会省掉 80% 的疲惫

别再让业务页直接摸 ResultSet、到处 copy ValuesBucket、每次都重新写 WHERE 了。RdbStore 只在数据层露面,ValuesBucket/ResultSet 交给封装实体元数据一处维护,仓库层帮你跑 CRUD。
  当你下次要“给 todos 加个 priority 并排序”时,不用通宵重构——加列、加索引、改一处映射,全项目自动受益。舒服到想给自己点个赞。👏

(未完待续)

Logo

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

更多推荐