HarmonyOS开发:数据层迁移——Room/CoreData到RDB
HarmonyOS开发:数据层迁移——Room/CoreData到RDB
📌 核心要点:Room的注解式ORM和CoreData的对象图模型在鸿蒙上都不存在,鸿蒙的RDB是更接近SQLite原生的关系型数据库,迁移的关键是理解"注解→手动建表"的思维转变。
背景与动机
你的App数据存在哪?Room?CoreData?SQLite?Realm?
不管你用哪个,数据层迁移都是最不能出错的部分。UI写错了顶多页面丑一点,网络层写错了顶多加载慢一点,数据层写错了——用户数据丢了,那就是生产事故。
鸿蒙的关系型数据库(RDB)本质上是对SQLite的封装,和Android的SQLiteOpenHelper类似,但比Room原始得多。没有@Entity注解自动建表,没有@Dao注解自动生成查询,没有@TypeConverter自动类型转换。
你可能会觉得这是退步。但从另一个角度看,RDB给了你更精细的控制——SQL语句你完全自己写,查询计划你完全自己优化,没有ORM的"惊喜"。
这篇文章,帮你把数据层从Room/CoreData迁移到RDB,同时封装出好用的数据访问层。
核心原理
数据层架构对比
graph TB
subgraph Room["Android Room 架构"]
R1["@Entity 注解定义表"] --> R2["@Dao 注解定义查询"]
R2 --> R3[RoomDatabase 抽象类]
R3 --> R4[SQLite 底层]
R4 --> R5[自动建表/迁移]
end
subgraph CoreData["iOS CoreData 架构"]
C1[".xcdatamodeld 模型文件"] --> C2[NSManagedObject 子类]
C2 --> C3[NSManagedObjectContext]
C3 --> C4[SQLite 底层]
C4 --> C5[自动Schema迁移]
end
subgraph RDB["鸿蒙 RDB 架构"]
H1[手动建表SQL] --> H2[手动CRUD方法]
H2 --> H3[relationalStore 封装]
H3 --> H4[SQLite 底层]
H4 --> H5[手动版本迁移]
end
R1 -.->|注解→手动SQL| H1
R2 -.->|自动→手动| H2
R5 -.->|自动→手动| H5
classDef roomStyle fill:#3DDC84,stroke:#2E7D32,color:#000,font-weight:bold
classDef coreStyle fill:#F05138,stroke:#C73A25,color:#fff,font-weight:bold
classDef rdbStyle fill:#0A59F7,stroke:#0A3CC7,color:#fff,font-weight:bold
class R1,R2,R3,R4,R5 roomStyle
class C1,C2,C3,C4,C5 coreStyle
class H1,H2,H3,H4,H5 rdbStyle
核心概念映射
| Room/CoreData概念 | RDB对应 | 说明 |
|---|---|---|
| @Entity | CREATE TABLE语句 | 手动写建表SQL |
| @PrimaryKey | PRIMARY KEY约束 | SQL中声明 |
| @ColumnInfo | 列定义 | SQL中声明 |
| @Dao | 数据访问类 | 手动封装CRUD方法 |
| @Insert | insert()方法 | 手动写insert语句 |
| @Update | update()方法 | 手动写update语句 |
| @Delete | delete()方法 | 手动写delete语句 |
| @Query | executeSql() | 手动写SQL查询 |
| @TypeConverter | 手动序列化 | JSON.stringify/parse |
| Migration | 版本迁移逻辑 | 手动写ALTER TABLE |
| NSManagedObject | TypeScript接口 | 纯数据结构 |
Room→RDB类型映射
| Room/SQLite类型 | RDB类型 | TypeScript类型 |
|---|---|---|
| INTEGER | INTEGER | number |
| REAL | REAL | number |
| TEXT | TEXT | string |
| BLOB | BLOB | ArrayBuffer |
| BOOLEAN | INTEGER(0/1) | number |
代码实战
基础用法:Room Entity迁移到RDB
先看一个Room的Entity定义:
// Room: Entity定义
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: String,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "email") val email: String,
@ColumnInfo(name = "age") val age: Int = 0,
@ColumnInfo(name = "avatar_url") val avatarUrl: String = "",
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)
// Room: Dao定义
@Dao
interface UserDao {
@Query("SELECT * FROM users")
suspend fun getAll(): List<User>
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getById(id: String): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("SELECT * FROM users WHERE name LIKE :keyword")
suspend fun searchByName(keyword: String): List<User>
}
迁移到RDB:
// 鸿蒙: 手动建表 + 数据访问类
import { relationalStore } from '@kit.ArkData'
// 数据接口(对应Room的Entity)
export interface UserInfo {
id: string
name: string
email: string
age: number
avatarUrl: string
createdAt: number
}
// 建表SQL(对应Room的@Entity注解)
const CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
age INTEGER DEFAULT 0,
avatar_url TEXT DEFAULT '',
created_at INTEGER DEFAULT 0
)
`
// 数据访问类(对应Room的@Dao)
export class UserDao {
private store: relationalStore.RdbStore | null = null
// 初始化数据库
async init(context: Context): Promise<void> {
let config: relationalStore.StoreConfig = {
name: 'app_database.db',
securityLevel: relationalStore.SecurityLevel.S1
}
this.store = await relationalStore.getRdbStore(context, config)
await this.store.executeSql(CREATE_TABLE_SQL)
}
// 查询全部(对应@Query("SELECT * FROM users"))
async getAll(): Promise<UserInfo[]> {
if (!this.store) return []
let predicates = new relationalStore.RdbPredicates('users')
let resultSet = await this.store.query(predicates)
let users: UserInfo[] = []
while (resultSet.goToNextRow()) {
users.push(this.rowToUser(resultSet))
}
resultSet.close()
return users
}
// 按ID查询(对应@Query("SELECT * FROM users WHERE id = :id"))
async getById(id: string): Promise<UserInfo | null> {
if (!this.store) return null
let predicates = new relationalStore.RdbPredicates('users')
predicates.equalTo('id', id)
let resultSet = await this.store.query(predicates)
let user: UserInfo | null = null
if (resultSet.goToFirstRow()) {
user = this.rowToUser(resultSet)
}
resultSet.close()
return user
}
// 插入(对应@Insert)
async insert(user: UserInfo): Promise<number> {
if (!this.store) return -1
let valueBucket: relationalStore.ValuesBucket = {
id: user.id,
name: user.name,
email: user.email,
age: user.age,
avatar_url: user.avatarUrl,
created_at: user.createdAt
}
return await this.store.insert('users', valueBucket)
}
// 更新(对应@Update)
async update(user: UserInfo): Promise<number> {
if (!this.store) return -1
let valueBucket: relationalStore.ValuesBucket = {
name: user.name,
email: user.email,
age: user.age,
avatar_url: user.avatarUrl,
created_at: user.createdAt
}
let predicates = new relationalStore.RdbPredicates('users')
predicates.equalTo('id', user.id)
return await this.store.update(valueBucket, predicates)
}
// 删除(对应@Delete)
async delete(id: string): Promise<number> {
if (!this.store) return -1
let predicates = new relationalStore.RdbPredicates('users')
predicates.equalTo('id', id)
return await this.store.delete(predicates)
}
// 搜索(对应@Query("SELECT * FROM users WHERE name LIKE :keyword"))
async searchByName(keyword: string): Promise<UserInfo[]> {
if (!this.store) return []
let predicates = new relationalStore.RdbPredicates('users')
predicates.like('name', `%${keyword}%`)
let resultSet = await this.store.query(predicates)
let users: UserInfo[] = []
while (resultSet.goToNextRow()) {
users.push(this.rowToUser(resultSet))
}
resultSet.close()
return users
}
// 行数据转对象
private rowToUser(resultSet: relationalStore.ResultSet): UserInfo {
return {
id: resultSet.getString(resultSet.getColumnIndex('id')),
name: resultSet.getString(resultSet.getColumnIndex('name')),
email: resultSet.getString(resultSet.getColumnIndex('email')),
age: resultSet.getLong(resultSet.getColumnIndex('age')),
avatarUrl: resultSet.getString(resultSet.getColumnIndex('avatar_url')),
createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at'))
}
}
}
代码量比Room多不少,但逻辑更透明——每一步你都清楚在干什么。
进阶用法:封装Room风格的ORM
手动写CRUD太繁琐?来封装一个轻量ORM:
// 鸿蒙: 轻量ORM封装
// 表定义接口
interface ColumnDef {
name: string
type: 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB'
primaryKey?: boolean
autoIncrement?: boolean
notNull?: boolean
defaultValue?: string | number
}
interface TableDef {
name: string
columns: ColumnDef[]
}
// ORM基类
abstract class BaseDao<T> {
protected store: relationalStore.RdbStore | null = null
protected abstract tableDef: TableDef
// 初始化
async init(store: relationalStore.RdbStore): Promise<void> {
this.store = store
await this.createTable()
}
// 自动建表
private async createTable(): Promise<void> {
if (!this.store) return
let columns = this.tableDef.columns.map(col => {
let def = `${col.name} ${col.type}`
if (col.primaryKey) def += ' PRIMARY KEY'
if (col.autoIncrement) def += ' AUTOINCREMENT'
if (col.notNull) def += ' NOT NULL'
if (col.defaultValue !== undefined) def += ` DEFAULT ${col.defaultValue}`
return def
}).join(', ')
let sql = `CREATE TABLE IF NOT EXISTS ${this.tableDef.name} (${columns})`
await this.store.executeSql(sql)
}
// 通用查询
async queryAll(): Promise<T[]> {
if (!this.store) return []
let predicates = new relationalStore.RdbPredicates(this.tableDef.name)
let resultSet = await this.store.query(predicates)
return this.resultSetToList(resultSet)
}
// 条件查询
async queryWhere(predicate: (predicates: relationalStore.RdbPredicates) => void): Promise<T[]> {
if (!this.store) return []
let predicates = new relationalStore.RdbPredicates(this.tableDef.name)
predicate(predicates)
let resultSet = await this.store.query(predicates)
return this.resultSetToList(resultSet)
}
// 按ID查询
async queryById(id: string | number): Promise<T | null> {
let pkColumn = this.tableDef.columns.find(c => c.primaryKey)
if (!pkColumn) return null
let results = await this.queryWhere(p => p.equalTo(pkColumn.name, id))
return results.length > 0 ? results[0] : null
}
// 插入
async insert(entity: T): Promise<number> {
if (!this.store) return -1
let valueBucket = this.entityToBucket(entity)
return await this.store.insert(this.tableDef.name, valueBucket)
}
// 批量插入
async insertBatch(entities: T[]): Promise<number> {
if (!this.store) return -1
let buckets = entities.map(e => this.entityToBucket(e))
return await this.store.batchInsert(this.tableDef.name, buckets)
}
// 更新
async update(entity: T, whereClause: (predicates: relationalStore.RdbPredicates) => void): Promise<number> {
if (!this.store) return -1
let valueBucket = this.entityToBucket(entity)
let predicates = new relationalStore.RdbPredicates(this.tableDef.name)
whereClause(predicates)
return await this.store.update(valueBucket, predicates)
}
// 删除
async deleteWhere(whereClause: (predicates: relationalStore.RdbPredicates) => void): Promise<number> {
if (!this.store) return -1
let predicates = new relationalStore.RdbPredicates(this.tableDef.name)
whereClause(predicates)
return await this.store.delete(predicates)
}
// 子类实现:行数据转对象
protected abstract rowToEntity(resultSet: relationalStore.ResultSet): T
// 子类实现:对象转ValueBucket
protected abstract entityToBucket(entity: T): relationalStore.ValuesBucket
// 通用:ResultSet转列表
private resultSetToList(resultSet: relationalStore.ResultSet): T[] {
let list: T[] = []
while (resultSet.goToNextRow()) {
list.push(this.rowToEntity(resultSet))
}
resultSet.close()
return list
}
}
// 使用:定义User的Dao
class UserDaoV2 extends BaseDao<UserInfo> {
protected tableDef: TableDef = {
name: 'users',
columns: [
{ name: 'id', type: 'TEXT', primaryKey: true, notNull: true },
{ name: 'name', type: 'TEXT', notNull: true },
{ name: 'email', type: 'TEXT', notNull: true },
{ name: 'age', type: 'INTEGER', defaultValue: 0 },
{ name: 'avatar_url', type: 'TEXT', defaultValue: '' },
{ name: 'created_at', type: 'INTEGER', defaultValue: 0 }
]
}
protected rowToEntity(resultSet: relationalStore.ResultSet): UserInfo {
return {
id: resultSet.getString(resultSet.getColumnIndex('id')),
name: resultSet.getString(resultSet.getColumnIndex('name')),
email: resultSet.getString(resultSet.getColumnIndex('email')),
age: resultSet.getLong(resultSet.getColumnIndex('age')),
avatarUrl: resultSet.getString(resultSet.getColumnIndex('avatar_url')),
createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at'))
}
}
protected entityToBucket(user: UserInfo): relationalStore.ValuesBucket {
return {
id: user.id,
name: user.name,
email: user.email,
age: user.age,
avatar_url: user.avatarUrl,
created_at: user.createdAt
}
}
// 自定义查询方法
async searchByName(keyword: string): Promise<UserInfo[]> {
return this.queryWhere(p => p.like('name', `%${keyword}%`))
}
async getUsersByAge(minAge: number, maxAge: number): Promise<UserInfo[]> {
return this.queryWhere(p => {
p.greaterThanOrEqualTo('age', minAge)
p.lessThanOrEqualTo('age', maxAge)
})
}
}
使用封装后的Dao:
// 初始化
let userDao = new UserDaoV2()
await userDao.init(store)
// CRUD操作
await userDao.insert({ id: '1', name: '张三', email: 'zhang@test.com', age: 25, avatarUrl: '', createdAt: Date.now() })
let allUsers = await userDao.queryAll()
let user = await userDao.queryById('1')
let searchResults = await userDao.searchByName('张')
await userDao.update(updatedUser, p => p.equalTo('id', '1'))
await userDao.deleteWhere(p => p.equalTo('id', '1'))
完整示例:数据库版本迁移
Room的Migration和CoreData的Lightweight Migration在RDB里都要手动实现:
// 鸿蒙: 数据库版本迁移
class DatabaseManager {
private store: relationalStore.RdbStore | null = null
private currentVersion: number = 3 // 当前最新版本
async init(context: Context): Promise<void> {
let config: relationalStore.StoreConfig = {
name: 'app_database.db',
securityLevel: relationalStore.SecurityLevel.S1
}
this.store = await relationalStore.getRdbStore(context, config)
let version = this.store.version
// 逐版本迁移
if (version === 0) {
// 全新安装,创建所有表
await this.createAllTables()
this.store.version = this.currentVersion
} else if (version < this.currentVersion) {
// 增量迁移
await this.migrate(version, this.currentVersion)
this.store.version = this.currentVersion
}
}
// 全量建表
private async createAllTables(): Promise<void> {
if (!this.store) return
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
age INTEGER DEFAULT 0,
avatar_url TEXT DEFAULT '',
created_at INTEGER DEFAULT 0,
phone TEXT DEFAULT '',
address TEXT DEFAULT ''
)
`)
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
amount REAL DEFAULT 0,
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`)
await this.store.executeSql(`
CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id)
`)
}
// 增量迁移
private async migrate(fromVersion: number, toVersion: number): Promise<void> {
if (!this.store) return
// V1 → V2: 给users表添加phone列
if (fromVersion < 2) {
await this.store.executeSql('ALTER TABLE users ADD COLUMN phone TEXT DEFAULT ""')
console.info('数据库迁移: V1 → V2 完成')
}
// V2 → V3: 给users表添加address列,创建orders表
if (fromVersion < 3) {
await this.store.executeSql('ALTER TABLE users ADD COLUMN address TEXT DEFAULT ""')
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
amount REAL DEFAULT 0,
status TEXT DEFAULT 'pending',
created_at INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`)
await this.store.executeSql('CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id)')
console.info('数据库迁移: V2 → V3 完成')
}
}
// 获取Store实例
getStore(): relationalStore.RdbStore | null {
return this.store
}
}
Room的Migration用注解声明,RDB的迁移用代码实现。虽然写起来更繁琐,但你可以做更复杂的迁移逻辑——数据清洗、表重构、索引优化,Room做不到的RDB都能做。
踩坑与注意事项
坑1:ResultSet必须close()
和HTTP请求必须destroy()一样,RDB的ResultSet必须手动close()。忘了关?游标泄漏,数据库连接耗尽。
// ❌ 错误:忘记close
let resultSet = await store.query(predicates)
while (resultSet.goToNextRow()) {
// 处理数据
}
// 没有close!
// ✅ 正确:用try/finally确保close
let resultSet = await store.query(predicates)
try {
while (resultSet.goToNextRow()) {
// 处理数据
}
} finally {
resultSet.close()
}
坑2:列名和属性名不一致
Room用@ColumnInfo映射列名和属性名,RDB里你需要在rowToEntity和entityToBucket中手动处理。
// 数据库列名: avatar_url(下划线风格)
// TypeScript属性名: avatarUrl(驼峰风格)
// 需要手动映射!
protected rowToEntity(resultSet: relationalStore.ResultSet): UserInfo {
return {
avatarUrl: resultSet.getString(resultSet.getColumnIndex('avatar_url')) // 列名→属性名
}
}
protected entityToBucket(user: UserInfo): relationalStore.ValuesBucket {
return {
avatar_url: user.avatarUrl // 属性名→列名
}
}
坑3:事务支持
RDB支持事务,但API和Room不同:
// 鸿蒙: 事务操作
await store.beginTransaction()
try {
await store.insert('users', userBucket)
await store.insert('orders', orderBucket)
store.commit()
} catch (e) {
store.rollBack()
throw e
}
坑4:没有@TypeConverter
Room的@TypeConverter可以自动处理复杂类型(Date→Long、List→JSON字符串)。RDB没有这个能力,你需要手动序列化:
// 手动类型转换
interface ArticleInfo {
id: string
tags: string[] // 数组类型,数据库不支持
}
// 存储时序列化
protected entityToBucket(article: ArticleInfo): relationalStore.ValuesBucket {
return {
id: article.id,
tags: JSON.stringify(article.tags) // 数组→JSON字符串
}
}
// 读取时反序列化
protected rowToEntity(resultSet: relationalStore.ResultSet): ArticleInfo {
let tagsStr = resultSet.getString(resultSet.getColumnIndex('tags'))
return {
id: resultSet.getString(resultSet.getColumnIndex('id')),
tags: JSON.parse(tagsStr) as string[] // JSON字符串→数组
}
}
坑5:数据库文件位置
鸿蒙的数据库文件存储在应用的沙箱目录中,路径和Android不同。你不能直接指定数据库文件路径,而是通过StoreConfig的name参数指定数据库名,系统自动管理路径。
HarmonyOS 6适配说明
HarmonyOS 6在数据层有几个重要改进:
-
relationalStore性能优化:批量插入性能提升约50%,大数据量查询的内存占用降低30%。
-
加密数据库支持:新增了数据库加密能力,SecurityLevel.S3及以上级别的数据库会自动加密。
-
WAL模式:默认启用Write-Ahead Logging,并发读写性能更好。
-
数据备份恢复:新增了数据库备份和恢复API,支持应用数据迁移。
-
ORM工具辅助:DevEco Studio 6新增了数据库可视化工具,可以查看表结构、执行SQL、浏览数据,类似Android的DB Browser。
总结
数据层迁移的核心是"注解→手动SQL"的思维转变。Room帮你省掉的代码,RDB需要你手动补回来。但手动写SQL的好处是——你对数据库有完全的控制权。
| 维度 | 学习难度 | 使用频率 | 重要程度 |
|---|---|---|---|
| 建表/CRUD | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| ORM封装 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 版本迁移 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 事务处理 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 类型转换 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| ResultSet管理 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
建议迁移时先封装一个BaseDao,把通用的CRUD逻辑抽出来。这样每个表的Dao只需要关注建表SQL和字段映射,代码量能减少60%以上。别在业务代码里直接写SQL,那和不用ORM有什么区别?
- 点赞
- 收藏
- 关注作者
评论(0)