title: “PersistenceV2 数据库升级”
series: “HarmonyLedger 鸿蒙记账实战”
article_number: 13
git_tag: v0.1.3
category: HarmonyOS NEXT
tags: [HarmonyOS, PersistenceV2, ORM, 数据库]

PersistenceV2 数据库升级

本文是《HarmonyOS NEXT 企业级开发实战:30篇打造智能记账APP》系列的第 13 篇,对应 Git Tag v0.1.3。承接第 12 篇的 Preferences,本篇接入鸿蒙 PersistenceV2 ORM,把 Bill/Category/Budget 从内存 Map 切换到真实数据库,完成数据持久化。

前言

Preferences 适合少量设置,但 账单数据 是大量、结构化、需复杂查询的——这就需要 PersistenceV2。鸿蒙 PersistenceV2 是 ArkTS 原生 ORM,通过装饰器定义实体,自动建表、索引、CRUD,开发者无需写 SQL。

本文将带你:

  1. @Entity / @PrimaryKey / @Column 装饰器定义实体
  2. 配置 PersistenceV2 初始化与数据库版本
  3. 升级 BaseRepository 切换到 PersistenceV2 实现
  4. Bill/Category/Budget 三个 Repository 完成真实持久化
  5. 处理数据库版本升级与数据迁移

企业级核心原则:ORM 必须 实体声明式、API 异步化、版本可控。参考 鸿蒙 PersistenceV2 文档 了解官方约定。


一、PersistenceV2 基础

1.1 装饰器概览

装饰器 用途 示例
@Entity(tableName) 标记类为实体 @Entity('bill')
@PrimaryKey() 主键 @PrimaryKey() id: string
@Column(name?) 字段映射 @Column('money') money: number
@Index(fields) 索引 @Index(['date', 'type'])

1.2 基础用法

import { persistence, type PersistenceV2 } from '@kit.ArkData';

// 初始化
const db: PersistenceV2 = persistence.get();
await db.init(this.context, 'harmonyledger.db', 1);

// CRUD
await db.save<Bill>(Bill, bill);          // 新增/更新
const bill = await db.query<Bill>(Bill, 'id = ?', '1');  // 查询
await db.delete<Bill>(Bill, 'id = ?', '1');  // 删除
API 说明
persistence.get() 获取 PersistenceV2 单例
db.init(ctx, name, version) 初始化数据库(version 用于升级)
db.save(T, entity) 新增或更新(按主键幂等)
db.query(T, predicate, ...args) 条件查询
db.delete(T, predicate, ...args) 条件删除
db.update(T, predicate, setters) 字段更新

二、实体定义升级

2.1 Bill 实体

// model/Bill.ets(PersistenceV2 版)
import { persistence } from '@kit.ArkData';
import { BillType } from '../constants/BillType';

@ persistence
@Entity('bill')
export class Bill {
  @PrimaryKey()
  id: string = '';

  @Column()
  money: number = 0;          // 金额(分)

  @Column()
  type: BillType = BillType.EXPENSE;

  @Column()
  categoryId: string = '';

  @Column()
  remark: string = '';

  @Column()
  date: number = 0;

  @Column()
  createTime: number = 0;

  @Column()
  updateTime: number = 0;

  // 字段映射示例:源字段为 remarkDb,映射到 remark
  @Column('remark')
  remarkDb: string = '';
}

// 注册实体到 PersistenceV2
persistence.registerEntities(Bill);

2.2 Category 与 Budget 实体

// model/Category.ets(PersistenceV2 版)
import { persistence } from '@kit.ArkData';
import { BillType } from '../constants/BillType';

@persistence
@Entity('category')
export class Category {
  @PrimaryKey()
  id: string = '';

  @Column()
  name: string = '';

  @Column()
  iconRes: string = '';   // 图片资源名(Resource 无法直接持久化,存字符串名)

  @Column()
  color: string = '';

  @Column()
  sort: number = 0;

  @Column()
  type: BillType = BillType.EXPENSE;

  @Column()
  builtin: boolean = false;

  /** 取图标 Resource */
  getIcon(): Resource {
    return $r(`app.media.${this.iconRes}`);
  }
}

// model/Budget.ets(PersistenceV2 版)
@persistence
@Entity('budget')
export class Budget {
  @PrimaryKey()
  id: string = '';

  @Column()
  month: string = '';       // YYYY-MM

  @Column()
  budget: number = 0;

  @Column()
  used: number = 0;

  @Column()
  remain: number = 0;
}

设计要点Resource 无法直接持久化,需把资源名转字符串存储,运行时通过 $r() 重建 Resource。


三、数据库初始化

3.1 DatabaseManager 封装

// database/DatabaseManager.ets
import { persistence, type PersistenceV2 } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { Bill } from '../model/Bill';
import { Category } from '../model/Category';
import { Budget } from '../model/Budget';
import { AppConfig } from '../constants/app';

export class DatabaseManager {
  private static db: PersistenceV2 | null = null;

  /** 初始化数据库 */
  static async init(context: common.Context): Promise<void> {
    this.db = persistence.get();

    // 注册实体
    persistence.registerEntities(Bill, Category, Budget);

    // 初始化数据库(版本号用于升级)
    await this.db.init(context, AppConfig.DATABASE_NAME, AppConfig.DATABASE_VERSION);
  }

  /** 获取数据库实例 */
  static get(): PersistenceV2 {
    return this.db!;
  }

  /** 升级回调(version 增加时触发) */
  static async onUpgrade(targetVersion: number): Promise<void> {
    hilog.info(DOMAIN, TAG, `数据库升级到 v${targetVersion}`);
    // 后续版本在此添加字段迁移逻辑
  }
}

3.2 EntryAbility 集入

// entryability/EntryAbility.ets(追加数据库初始化)
import { DatabaseManager } from '../database/DatabaseManager';

async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
  await PreferenceUtil.init(this.context);
  await DatabaseManager.init(this.context);   // 新增:数据库初始化
  await ThemeManager.init();
  await this.handleFirstLaunch();
  // ...
}

四、BaseRepository 升级

4.1 切换为 PersistenceV2 实现

// repository/BaseRepository.ets(升级版)
import { persistence, type PersistenceV2 } from '@kit.ArkData';
import { DatabaseManager } from '../database/DatabaseManager';

export abstract class BaseRepository<T> {
  protected db: PersistenceV2 = DatabaseManager.get();
  protected abstract entityType: persistence.EntityType<T>;

  async findAll(): Promise<T[]> {
    return await this.db.query(this.entityType);
  }

  async findById(id: string): Promise<T | null> {
    const results = await this.db.query(this.entityType, 'id = ?', id);
    return results.length > 0 ? results[0] : null;
  }

  async save(entity: T): Promise<boolean> {
    try {
      await this.db.save(this.entityType, entity);
      return true;
    } catch (e) {
      hilog.error(DOMAIN, TAG, `save failed: ${e.message}`);
      return false;
    }
  }

  async saveAll(entities: T[]): Promise<boolean> {
    try {
      for (let i = 0; i < entities.length; i++) {
        await this.db.save(this.entityType, entities[i]);
      }
      return true;
    } catch (e) {
      return false;
    }
  }

  async delete(id: string): Promise<boolean> {
    try {
      await this.db.delete(this.entityType, 'id = ?', id);
      return true;
    } catch (e) {
      return false;
    }
  }

  async exists(id: string): Promise<boolean> {
    const e = await this.findById(id);
    return e !== null;
  }

  async clear(): Promise<void> {
    await this.db.delete(this.entityType);
  }
}

4.2 BillRepository 升级

// repository/BillRepository.ets(升级版)
export class BillRepository extends BaseRepository<Bill> {
  private static instance: BillRepository;
  protected entityType = Bill;

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

  async findTodayBills(): Promise<Bill[]> {
    const start = DateUtil.todayStart();
    const end = DateUtil.todayEnd();
    return await this.db.query(Bill, 'date >= ? AND date <= ?', start, end);
  }

  async findByDateRange(start: number, end: number): Promise<Bill[]> {
    return await this.db.query(Bill, 'date >= ? AND date <= ?', start, end);
  }

  async findByType(type: BillType): Promise<Bill[]> {
    return await this.db.query(Bill, 'type = ?', type);
  }

  async findByCategory(categoryId: string): Promise<Bill[]> {
    return await this.db.query(Bill, 'categoryId = ?', categoryId);
  }

  async search(keyword: string): Promise<Bill[]> {
    if (keyword.length === 0) return this.findAll();
    return await this.db.query(Bill, 'remark LIKE ?', `%${keyword}%`);
  }

  async sumTodayExpense(): Promise<number> {
    const today = await this.findTodayBills();
    return today
      .filter(b => b.type === BillType.EXPENSE)
      .reduce((sum, b) => sum + b.money, 0);
  }

  async sumTodayIncome(): Promise<number> {
    const today = await this.findTodayBills();
    return today
      .filter(b => b.type === BillType.INCOME)
      .reduce((sum, b) => sum + b.money, 0);
  }
}

五、CategoryRepository 与 BudgetRepository 升级

5.1 CategoryRepository

// repository/CategoryRepository.ets(升级版)
export class CategoryRepository extends BaseRepository<Category> {
  protected entityType = Category;

  async findByType(type: BillType): Promise<Category[]> {
    const all = await this.findAll();
    return all
      .filter(c => c.type === type)
      .sort((a, b) => a.sort - b.sort);
  }

  async initBuiltinCategories(): Promise<void> {
    const existing = await this.findAll();
    if (existing.length > 0) return;

    const builtin: Category[] = [
      this.makeBuiltin('food', '餐饮', BillType.EXPENSE, 'icon_category_food', AppColors.Expense),
      this.makeBuiltin('transport', '交通', BillType.EXPENSE, 'icon_category_transport', AppColors.Expense),
      this.makeBuiltin('salary', '工资', BillType.INCOME, 'icon_category_salary', AppColors.Income),
      // ... 其他内置分类 ...
    ];
    await this.saveAll(builtin);
  }

  private makeBuiltin(id: string, name: string, type: BillType, iconRes: string, color: string): Category {
    const cat = new Category();
    cat.id = id;
    cat.name = name;
    cat.type = type;
    cat.iconRes = iconRes;
    cat.color = color;
    cat.builtin = true;
    cat.sort = 0;
    return cat;
  }
}

5.2 BudgetRepository

// repository/BudgetRepository.ets(升级版)
export class BudgetRepository extends BaseRepository<Budget> {
  protected entityType = Budget;

  async findCurrentMonth(): Promise<Budget | null> {
    const month = DateUtil.formatDate(Date.now()).substring(0, 7);
    const results = await this.db.query(Budget, 'month = ?', month);
    return results.length > 0 ? results[0] : null;
  }

  async setMonthBudget(month: string, budget: number): Promise<Budget> {
    const results = await this.db.query(Budget, 'month = ?', month);
    if (results.length > 0) {
      const existing = results[0];
      existing.budget = budget;
      await this.save(existing);
      return existing;
    }
    const newBudget = new Budget();
    newBudget.id = UUIDUtil.generate();
    newBudget.month = month;
    newBudget.budget = budget;
    await this.save(newBudget);
    return newBudget;
  }
}

六、数据库版本升级

6.1 版本升级场景

// AppConfig 中管理版本号
static readonly DATABASE_VERSION: number = 1;

// 升级场景:
// v1 → v2:Bill 表新增 imageUrl 字段
// v2 → v3:新增 Setting 表
// v3 → v4:Category 表新增 color 字段

6.2 升级回调处理

// database/DatabaseManager.ets(升级回调)
static async init(context: common.Context): Promise<void> {
  this.db = persistence.get();
  persistence.registerEntities(Bill, Category, Budget);
  await this.db.init(context, AppConfig.DATABASE_NAME, AppConfig.DATABASE_VERSION, {
    onUpgrade: async (current: number, target: number) => {
      hilog.info(DOMAIN, TAG, `数据库升级 v${current} → v${target}`);
      for (let v = current + 1; v <= target; v++) {
        await this.applyMigration(v);
      }
    }
  });
}

private static async applyMigration(targetVersion: number): Promise<void> {
  switch (targetVersion) {
    case 2:
      // v2:新增 imageUrl 字段(已在实体声明中,无需手动迁移)
      hilog.info(DOMAIN, TAG, '迁移到 v2:新增 imageUrl');
      break;
    case 3:
      hilog.info(DOMAIN, TAG, '迁移到 v3:新增 Setting 表');
      break;
    default:
      break;
  }
}

七、最佳实践

7.1 Resource 持久化转换

// ❌ 错误:Resource 无法直接持久化
@Column()
icon: Resource;  // 报错

// ✅ 正解:存字符串名,运行时重建
@Column()
iconRes: string = '';   // 存储 'icon_category_food'

getIcon(): Resource {
  return $r(`app.media.${this.iconRes}`);  // 运行时重建
}

7.2 查询条件参数化

// ❌ 错误:字符串拼接易注入
const bills = await db.query(Bill, `remark LIKE '%${keyword}%'`);

// ✅ 正解:用 ? 占位符
const bills = await db.query(Bill, 'remark LIKE ?', `%${keyword}%`);

7.4 存储引擎对比

引擎 读写性能 事务支持 适用场景
PersistenceV2 结构化数据
Preferences 键值设置

根据数据特征选择正确的存储引擎是数据库设计的核心决策。

7.3 大批量保存优化

// 批量保存时用事务(如果 API 支持)
await db.transaction(async () => {
  for (let i = 0; i < bills.length; i++) {
    await db.save(Bill, bills[i]);
  }
});
// 或并行
await Promise.all(bills.map(b => db.save(Bill, b)));

八、运行验证

8.1 编译检查

hvigorw assembleHap --mode module -p product=default

8.2 功能验证

  1. 首次启动,hilog 输出"数据库初始化"
  2. 新增账单后关闭应用,重开首页仍显示该账单
  3. 删除账单后关闭应用,重开首页不再显示
  4. 内置分类初始化后,关闭应用重开仍存在
    在这里插入图片描述

九、常见问题

9.1 实体未注册

// 错误:未调用 registerEntities
await db.save(Bill, bill);  // 报错:Entity not registered

// 解决:init 前注册
persistence.registerEntities(Bill, Category, Budget);
await db.init(...);

9.2 Resource 持久化失败

// 报错:Cannot serialize Resource
// 解决:改存字符串名

9.3 查询条件错误

// 错误:占位符数量不匹配
db.query(Bill, 'date >= ? AND date <= ?', start);  // 缺第二个参数

// 正解:占位符与参数一一对应
db.query(Bill, 'date >= ? AND date <= ?', start, end);

9.4 版本升级失败

// 原因:实体字段变更但未声明升级回调
// 解决:在 onUpgrade 中处理字段迁移

十、Git 提交

10.1 Commit Message

git add .
git commit -m "feat(database): 接入 PersistenceV2 ORM 完成数据持久化

- 升级 Bill/Category/Budget 为 @Entity 装饰器实体
- 新增 DatabaseManager 封装数据库初始化与升级回调
- BaseRepository 切换为 PersistenceV2 实现(findAll/save/delete/query)
- BillRepository 升级为真实数据库查询
- CategoryRepository/BudgetRepository 升级为真实持久化
- EntryAbility 集入数据库初始化"

10.2 CHANGELOG

## [v0.1.3] - 2026-07-27
### Added
- database/DatabaseManager.ets:数据库管理器
- 数据库版本升级回调机制
### Changed
- model/Bill.ets、Category.ets、Budget.ets:升级为 @Entity 实体
- repository/*Repository.ets:切换为 PersistenceV2 实现
- entryability/EntryAbility.ets:集入数据库初始化

总结

本文完整介绍了 PersistenceV2 数据库升级,涵盖实体装饰器、DatabaseManager、BaseRepository 切换、三个 Repository 升级、版本迁移。通过本篇你可以:

  • @Entity/@PrimaryKey/@Column 声明式定义实体
  • 封装 DatabaseManager 处理初始化与升级
  • BaseRepository 切换持久化实现零侵入
  • 处理 Resource 无法持久化的转换
  • 用占位符查询避免 SQL 注入

下一篇预告:《账单编辑与删除》将开发 EditBillView 编辑账单页面,支持长按列表项弹删除菜单、滑动删除手势、二次确认对话框。


如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源

Logo

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

更多推荐