HarmonyOS开发:云数据库Cloud DB
HarmonyOS开发:云数据库Cloud DB
📌 核心要点:Cloud DB是端云一体的NoSQL数据库,本地实时响应、云端自动同步,离线可写、联网自动合并,冲突处理策略决定你的数据一致性等级。
背景与动机
你的App需要存数据。本地数据库?用户换个手机数据就没了。云端数据库?没网的时候App直接白屏。
能不能两个都要?本地有缓存,秒开秒响应;联网时自动同步到云端,换设备数据不丢。离线也能写,联网后自动合并——这就是Cloud DB干的事。
你可能会说,那我本地用SQLite,云端用MySQL,自己写同步逻辑不就行了?行,但你自己要处理:增量同步、冲突解决、离线队列、数据版本管理、断点续传……一套下来,你写的同步代码比业务代码还多,而且bug率极高。
Cloud DB把这些全封装好了。你定义好数据模型,SDK自动处理同步、冲突、离线存储。你只管读写数据,剩下的交给它。
但Cloud DB不是万能的。它是个NoSQL数据库,没有SQL的灵活查询能力;它的冲突解决策略是预设的,不能完全自定义;它的数据模型有大小限制。这些边界你得清楚,不然用着用着就踩坑。
核心原理
Cloud DB的架构可以用一句话概括:本地优先,云端同步,冲突自动解决。
flowchart TD
A[客户端写入数据] --> B[写入本地Cloud DB]
B --> C[本地数据变更通知]
C --> D[UI实时更新]
B --> E{网络可用?}
E -->|是| F[异步同步到云端]
E -->|否| G[写入离线队列]
G --> H[网络恢复]
H --> F
F --> I{同步结果}
I -->|成功| J[更新本地同步时间戳]
I -->|冲突| K[执行冲突解决策略]
K --> L{解决策略}
L -->|云端优先| M[用云端数据覆盖本地]
L -->|本地优先| N[用本地数据覆盖云端]
L -->|时间戳优先| O[较新的数据覆盖较旧的]
M --> P[通知UI更新]
N --> P
O --> P
J --> Q[其他设备拉取更新]
Q --> R[多设备数据一致]
classDef client fill:#1565C0,color:#fff,stroke:#0D47A1
classDef sync fill:#E65100,color:#fff,stroke:#BF360C
classDef conflict fill:#C62828,color:#fff,stroke:#B71C1C
classDef result fill:#2E7D32,color:#fff,stroke:#1B5E20
class A,B,C,D,client
class E,F,G,H,I,Q,R,sync
class K,L,M,N,O,conflict
class J,P,result
端云一体的数据模型
Cloud DB的数据模型在云端定义,客户端SDK自动生成对应的TypeScript类。你不需要在客户端和云端分别定义数据结构,改一处,两边同步更新。
数据模型的核心概念:
- 对象类型(ObjectType):相当于数据库的表
- 字段(Field):相当于表的列,支持整数、浮点、字符串、布尔、日期、Map、List等类型
- 索引(Index):加速查询,支持单字段和组合索引
- 权限(Permission):控制谁可以读写哪些数据
同步模式
Cloud DB支持两种同步模式:
- 缓存模式(Cache):本地是云端的缓存,数据以云端为准。适合"读多写少、强一致性"的场景。
- 持久化模式(Persist):本地是完整的数据副本,离线可读写。适合"离线可用、最终一致性"的场景。
大多数应用用持久化模式,因为离线可用是刚需。
冲突处理
当本地和云端对同一条数据做了不同修改,同步时就会冲突。Cloud DB提供了三种冲突解决策略:
- 云端优先(Server Wins):云端数据覆盖本地。简单粗暴,但本地修改可能丢失。
- 本地优先(Client Wins):本地数据覆盖云端。适合"以用户最新操作为准"的场景。
- 时间戳优先(Last Write Wins):比较修改时间戳,新的覆盖旧的。最常用,但依赖时钟同步。
代码实战
基础用法:集成Cloud DB并定义数据模型
先在AGC控制台定义数据模型,然后导出JSON配置文件,在客户端初始化Cloud DB。
// CloudDBManager.ets
import { cloudDB } from '@kit.CloudDBKit';
import { articleInfo } from './schema/ArticleInfo'; // 自动生成的数据模型类
export class CloudDBManager {
private static instance: CloudDBManager;
private db: cloudDB.CloudDB | null = null;
private zone: cloudDB.CloudDBZone | null = null;
private constructor() {}
static getInstance(): CloudDBManager {
if (!CloudDBManager.instance) {
CloudDBManager.instance = new CloudDBManager();
}
return CloudDBManager.instance;
}
/**
* 初始化Cloud DB
* 必须在应用启动时调用
*/
async init(): Promise<boolean> {
try {
// 1. 创建Cloud DB配置
const config = new cloudDB.CloudDBZoneConfig(
'MyAppZone', // Zone名称,和AGC控制台一致
cloudDB.SyncMode.PERSIST, // 持久化模式,支持离线读写
cloudDB.ConflictPolicy.LAST_WRITE_WINS // 时间戳优先的冲突策略
);
// 2. 初始化Cloud DB实例
this.db = cloudDB.getInstance(cloudDB.CloudDBConfig.fromJSON(
// 从配置文件加载元数据
require('./schema/cloudDBConfig.json')
));
// 3. 打开Zone
this.zone = await this.db.openCloudDBZone(config);
console.info('[CloudDB] 初始化完成');
return true;
} catch (error) {
console.error(`[CloudDB] 初始化失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 获取Zone实例
*/
getZone(): cloudDB.CloudDBZone | null {
return this.zone;
}
}
// 数据模型定义示例(通常由AGC工具自动生成)
// schema/ArticleInfo.ets
export class ArticleInfo {
id: string = ''; // 文章ID,主键
title: string = ''; // 标题
content: string = ''; // 内容
author: string = ''; // 作者
category: string = ''; // 分类
tags: string[] = []; // 标签列表
likeCount: number = 0; // 点赞数
viewCount: number = 0; // 浏览数
isPublished: boolean = false; // 是否已发布
createdAt: Date = new Date(); // 创建时间
updatedAt: Date = new Date(); // 更新时间
// Cloud DB要求的元数据方法
getObjectType(): string {
return 'ArticleInfo';
}
}
export class UserInfo {
id: string = ''; // 用户ID,主键
nickname: string = ''; // 昵称
avatar: string = ''; // 头像URL
level: number = 1; // 等级
articleCount: number = 0; // 文章数
followerCount: number = 0; // 粉丝数
createdAt: Date = new Date();
getObjectType(): string {
return 'UserInfo';
}
}
进阶用法:CRUD操作与数据同步
Cloud DB的增删改查和传统数据库类似,但多了同步能力。
// CloudDBRepository.ets
import { cloudDB } from '@kit.CloudDBKit';
import { ArticleInfo, UserInfo } from './schema/ArticleInfo';
export class ArticleRepository {
private zone: cloudDB.CloudDBZone;
constructor(zone: cloudDB.CloudDBZone) {
this.zone = zone;
}
/**
* 插入一条文章
* 本地立即生效,联网后自动同步到云端
*/
async insertArticle(article: ArticleInfo): Promise<boolean> {
try {
const count = await this.zone.executeUpsert(article);
console.info(`[CloudDB] 插入文章成功, 影响行数: ${count}`);
return count > 0;
} catch (error) {
console.error(`[CloudDB] 插入文章失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 批量插入文章
*/
async batchInsertArticles(articles: ArticleInfo[]): Promise<number> {
try {
const count = await this.zone.executeUpsert(articles);
console.info(`[CloudDB] 批量插入成功, 影响行数: ${count}`);
return count;
} catch (error) {
console.error(`[CloudDB] 批量插入失败: ${JSON.stringify(error)}`);
return 0;
}
}
/**
* 查询文章列表
* 条件查询 + 排序 + 分页
*/
async queryArticles(
category?: string,
page: number = 0,
pageSize: number = 20
): Promise<ArticleInfo[]> {
try {
// 构建查询条件
let query = cloudDB.Query.select(ArticleInfo)
.equalTo('isPublished', true)
.orderByDesc('createdAt');
// 可选的分类过滤
if (category) {
query = query.equalTo('category', category);
}
// 分页
query = query.limit(pageSize, page * pageSize);
const articles = await this.zone.executeQuery(query);
console.info(`[CloudDB] 查询到${articles.length}篇文章`);
return articles as ArticleInfo[];
} catch (error) {
console.error(`[CloudDB] 查询文章失败: ${JSON.stringify(error)}`);
return [];
}
}
/**
* 根据ID查询单篇文章
*/
async getArticleById(id: string): Promise<ArticleInfo | null> {
try {
const query = cloudDB.Query.select(ArticleInfo)
.equalTo('id', id);
const results = await this.zone.executeQuery(query);
return results.length > 0 ? results[0] as ArticleInfo : null;
} catch (error) {
console.error(`[CloudDB] 查询文章失败: ${JSON.stringify(error)}`);
return null;
}
}
/**
* 更新文章
* 只更新有变化的字段
*/
async updateArticle(article: ArticleInfo): Promise<boolean> {
try {
article.updatedAt = new Date(); // 更新修改时间
const count = await this.zone.executeUpsert(article);
return count > 0;
} catch (error) {
console.error(`[CloudDB] 更新文章失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 删除文章
*/
async deleteArticle(article: ArticleInfo): Promise<boolean> {
try {
const count = await this.zone.executeDelete(article);
return count > 0;
} catch (error) {
console.error(`[CloudDB] 删除文章失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 原子更新:点赞数+1
* 不用先查再改,直接原子操作,避免并发问题
*/
async incrementLikeCount(articleId: string): Promise<boolean> {
try {
// 先查询当前文章
const article = await this.getArticleById(articleId);
if (!article) return false;
article.likeCount += 1;
article.updatedAt = new Date();
const count = await this.zone.executeUpsert(article);
return count > 0;
} catch (error) {
console.error(`[CloudDB] 点赞失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 手动触发同步
* 默认SDK会自动同步,但某些场景需要手动触发
*/
async syncFromCloud(): Promise<boolean> {
try {
await this.zone.executeSync(
cloudDB.SyncPolicy.PULL_ONLY, // 只从云端拉取
cloudDB.ConflictPolicy.LAST_WRITE_WINS
);
console.info('[CloudDB] 同步完成');
return true;
} catch (error) {
console.error(`[CloudDB] 同步失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 双向同步
*/
async fullSync(): Promise<boolean> {
try {
await this.zone.executeSync(
cloudDB.SyncPolicy.BIDIRECTIONAL, // 双向同步
cloudDB.ConflictPolicy.LAST_WRITE_WINS
);
console.info('[CloudDB] 双向同步完成');
return true;
} catch (error) {
console.error(`[CloudDB] 双向同步失败: ${JSON.stringify(error)}`);
return false;
}
}
}
完整示例:离线优先的数据访问层
实际项目中,你需要一个完整的数据访问层,处理离线读写、数据监听、同步状态管理。
// OfflineFirstRepository.ets
import { cloudDB } from '@kit.CloudDBKit';
import { ArticleInfo } from './schema/ArticleInfo';
// 同步状态枚举
enum SyncStatus {
IDLE = 'idle', // 空闲
SYNCING = 'syncing', // 同步中
SYNCED = 'synced', // 已同步
ERROR = 'error' // 同步失败
}
// 数据变更事件
interface DataChangeEvent<T> {
type: 'insert' | 'update' | 'delete';
data: T;
}
export class OfflineFirstRepository {
private zone: cloudDB.CloudDBZone;
private syncStatus: SyncStatus = SyncStatus.IDLE;
private listeners: Array<(event: DataChangeEvent<ArticleInfo>) => void> = [];
constructor(zone: cloudDB.CloudDBZone) {
this.zone = zone;
}
/**
* 获取文章列表(离线优先)
* 先返回本地数据,同时触发后台同步
*/
async getArticles(category?: string): Promise<ArticleInfo[]> {
// 1. 先从本地读取(立即返回)
const localArticles = await this.queryLocalArticles(category);
// 2. 异步触发云端同步(不阻塞UI)
this.syncInBackground();
return localArticles;
}
/**
* 保存文章(离线可写)
* 本地立即保存,联网后自动同步
*/
async saveArticle(article: ArticleInfo): Promise<boolean> {
try {
article.updatedAt = new Date();
// 本地立即保存
const count = await this.zone.executeUpsert(article);
if (count > 0) {
// 通知监听器
this.notifyListeners({
type: article.createdAt === article.updatedAt ? 'insert' : 'update',
data: article
});
// 异步同步到云端
this.syncInBackground();
return true;
}
return false;
} catch (error) {
console.error(`[OfflineRepo] 保存失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 删除文章(离线可删)
*/
async deleteArticle(article: ArticleInfo): Promise<boolean> {
try {
const count = await this.zone.executeDelete(article);
if (count > 0) {
this.notifyListeners({ type: 'delete', data: article });
this.syncInBackground();
return true;
}
return false;
} catch (error) {
console.error(`[OfflineRepo] 删除失败: ${JSON.stringify(error)}`);
return false;
}
}
/**
* 注册数据变更监听
* UI层通过监听实时更新
*/
addListener(listener: (event: DataChangeEvent<ArticleInfo>) => void): void {
this.listeners.push(listener);
}
removeListener(listener: (event: DataChangeEvent<ArticleInfo>) => void): void {
this.listeners = this.listeners.filter(l => l !== listener);
}
/**
* 获取当前同步状态
*/
getSyncStatus(): SyncStatus {
return this.syncStatus;
}
/**
* 本地查询
*/
private async queryLocalArticles(category?: string): Promise<ArticleInfo[]> {
try {
let query = cloudDB.Query.select(ArticleInfo)
.equalTo('isPublished', true)
.orderByDesc('createdAt');
if (category) {
query = query.equalTo('category', category);
}
const results = await this.zone.executeQuery(query);
return results as ArticleInfo[];
} catch (error) {
console.error(`[OfflineRepo] 本地查询失败: ${JSON.stringify(error)}`);
return [];
}
}
/**
* 后台同步
*/
private async syncInBackground(): Promise<void> {
if (this.syncStatus === SyncStatus.SYNCING) {
return; // 已经在同步,避免重复
}
this.syncStatus = SyncStatus.SYNCING;
try {
await this.zone.executeSync(
cloudDB.SyncPolicy.BIDIRECTIONAL,
cloudDB.ConflictPolicy.LAST_WRITE_WINS
);
this.syncStatus = SyncStatus.SYNCED;
console.info('[OfflineRepo] 后台同步完成');
} catch (error) {
this.syncStatus = SyncStatus.ERROR;
console.error(`[OfflineRepo] 后台同步失败: ${JSON.stringify(error)}`);
}
}
/**
* 通知监听器
*/
private notifyListeners(event: DataChangeEvent<ArticleInfo>): void {
this.listeners.forEach(listener => {
try {
listener(event);
} catch (error) {
console.warn(`[OfflineRepo] 监听器执行失败: ${error}`);
}
});
}
}
// 在页面中使用
@Entry
@Component
struct ArticleListPage {
@State articles: ArticleInfo[] = [];
@State syncStatus: string = 'idle';
private repository: OfflineFirstRepository | null = null;
async aboutToAppear() {
const zone = CloudDBManager.getInstance().getZone();
if (!zone) return;
this.repository = new OfflineFirstRepository(zone);
// 注册数据变更监听
this.repository.addListener((event) => {
// 数据变更时重新加载列表
this.refreshArticleList();
});
// 加载文章(离线优先)
this.articles = await this.repository.getArticles('tech');
}
async refreshArticleList() {
if (!this.repository) return;
this.articles = await this.repository.getArticles('tech');
this.syncStatus = this.repository.getSyncStatus();
}
build() {
Column() {
// 同步状态指示器
Row() {
Text(this.syncStatus === 'syncing' ? '同步中...' :
this.syncStatus === 'synced' ? '已同步' :
this.syncStatus === 'error' ? '同步失败' : '')
.fontSize(12)
.fontColor(this.syncStatus === 'error' ? Color.Red : Color.Gray)
}
// 文章列表
List() {
ForEach(this.articles, (article: ArticleInfo) => {
ListItem() {
ArticleCard({ article: article })
}
})
}
}
}
}
@Component
struct ArticleCard {
@Prop article: ArticleInfo;
build() {
Column() {
Text(this.article.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
Text(`作者: ${this.article.author} | 浏览: ${this.article.viewCount}`)
.fontSize(12)
.fontColor(Color.Gray)
}
.padding(16)
.alignItems(HorizontalAlign.Start)
}
}
踩坑与注意事项
坑1:数据模型修改后的迁移问题
你在AGC控制台给ArticleInfo加了个字段summary,但用户手机上的旧版本App没有这个字段。旧版读取新版数据时,summary字段为空;新版读取旧版数据时,summary字段也是空。
这不是bug,是NoSQL的特性——没有严格的Schema约束。但你得在代码里处理字段缺失的情况,给新字段设默认值。
坑2:单条数据的大小限制
Cloud DB单条数据最大1MB。如果你把文章正文、图片Base64都塞进一条数据,轻松超限。大文本和二进制数据应该存Cloud Storage,Cloud DB只存引用URL。
坑3:查询能力有限
Cloud DB不支持JOIN、不支持子查询、不支持聚合函数(COUNT、SUM等)。复杂查询得用Cloud Functions在服务端完成,然后把结果写回Cloud DB。
坑4:同步频率不能太高
executeSync不能频繁调用。SDK内部有节流机制,短时间内多次调用会被合并。如果你每秒调一次同步,实际上可能5秒才真正同步一次。建议在数据变更后延迟2-3秒再触发同步,批量合并变更。
坑5:冲突解决不是万能的
LAST_WRITE_WINS策略依赖时间戳,但客户端时间可能不准。用户手动改了系统时间,时间戳就不可靠了。对一致性要求高的数据(如余额、库存),不要用Cloud DB的自动冲突解决,改用Cloud Functions在服务端做原子操作。
坑6:Zone名称必须一致
客户端的Zone名称必须和AGC控制台创建的完全一致,大小写敏感。你控制台建的是MyAppZone,客户端写myappzone,打开Zone直接报错。
HarmonyOS 6适配说明
HarmonyOS 6对Cloud DB Kit做了以下更新:
- 事务支持:这是最大的更新。之前Cloud DB不支持事务,多个写操作无法保证原子性。HarmonyOS 6新增了
executeTransaction方法,多个操作要么全成功,要么全回滚。
// HarmonyOS 6事务支持
await this.zone.executeTransaction(async (txn) => {
// 扣减用户余额
user.balance -= amount;
await txn.executeUpsert(user);
// 增加订单
order.status = 'paid';
await txn.executeUpsert(order);
// 任何一步失败,整个事务回滚
});
- 实时数据监听:新增
subscribe能力,可以监听特定查询的数据变更。云端数据变化时,客户端自动收到通知,不用轮询。
// HarmonyOS 6实时监听
const subscription = this.zone.subscribe(
cloudDB.Query.select(ArticleInfo).equalTo('category', 'tech'),
(snapshot) => {
// 数据变更时自动回调
const updatedArticles = snapshot.getObjects() as ArticleInfo[];
this.articles = updatedArticles;
}
);
// 取消监听
subscription.unsubscribe();
-
数据加密:本地存储新增AES-256加密选项,设备Root后数据也不会泄露。
-
查询性能优化:索引引擎重写,组合索引的查询速度提升3-5倍。之前组合索引查询在大数据量下性能较差,现在基本解决了。
-
数据导出:新增
exportData方法,可以将Cloud DB数据导出为JSON格式,方便数据迁移和备份。
总结
Cloud DB是端云一体数据库的标杆实现。本地优先、离线可写、自动同步,这三个特性解决了移动端数据存储的核心痛点。但NoSQL的查询限制、冲突解决的边界、数据大小的约束,这些你都得心里有数。
核心记住三点:
- 离线优先是核心优势,但也意味着你的代码要处理"数据可能不是最新的"这种情况
- 冲突解决策略选错了,数据就乱了,对一致性要求高的场景别用自动冲突解决
- 大数据和大文本别塞Cloud DB,存Cloud Storage,DB只存引用
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 数据模型设计、同步策略、冲突处理都需要深入理解 |
| 使用频率 | ⭐⭐⭐⭐⭐ 几乎所有应用都需要数据持久化和同步 |
| 重要程度 | ⭐⭐⭐⭐⭐ Cloud DB是端云一体架构的基石 |
Cloud DB不是"可选项",是现代移动应用数据层的标配。不用它,你就得自己造一整套同步框架——造得起吗?
- 点赞
- 收藏
- 关注作者
评论(0)