DBeaver-鸿蒙PC技术解析
DBeaver 鸿蒙 PC 版:用 ArkTS + NAPI 重建一个通用数据库客户端
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址(AtomGit 仓库):https://atomgit.com/OpenHarmonyPCDeveloper/ohos_dbeaver
DBeaver 是使用最广的开源通用数据库客户端之一:连接会话、库表树、数据网格、SQL 编辑、结构管理、导入导出,一个工具覆盖了开发者与 DBA 的大部分日常。它上游是 Java / Eclipse RCP 技术栈,依赖 JVM、OSGi、SWT 和 JDBC——这四样在鸿蒙 PC 上一样都不存在。
本文讲的是这个项目在 HarmonyOS PC 上的一次「原生重建」:界面与领域逻辑用
ArkTS + ArkUI重写,数据库协议层把libmariadb / libpq / sqlite3交叉编译成 OHOS ARM64 动态库随 HAP 分发,中间用 NAPI 做一层统一的 provider 桥。文章侧重技术实现——架构怎么分层、句柄生命周期怎么管、C 库在 musl 工具链下怎么编、ArkTS 领域层几个关键模块(数据网格、行编辑缓冲、SQL 词法高亮、三方言元数据)是怎么设计的,以及踩过的那些坑。

一、项目定位:重写交互,复用协议
上游 DBeaver 的核心价值不在某个具体控件的实现,而在「数据库工作台」这套交互模型和十几年打磨出来的数据库访问层。前者是平台无关的,完全可以按规格用 ArkUI 重建;后者是纯协议实现,没必要、也没能力用 ArkTS 重写。所以路线选择很直接:
- 界面与领域逻辑:ArkTS + ArkUI 声明式范式,全部原创实现,不复制上游源码、图标与素材;
- 数据库协议:
libmariadb、libpq、sqlite3三套成熟 C 客户端库交叉编译进 HAP; - 桥接层:NAPI(C++17)统一 provider 接口,产物
libdbclient.so; - 交付形态:单一 HAP,
deviceTypes含2in1,装到鸿蒙 PC 上就是原生桌面 GUI 应用——没有独立进程、没有 HNP、没有终端命令。
| 层次 | 上游实现 | HarmonyOS PC 侧处理 |
|---|---|---|
| UI 框架 | Eclipse RCP / SWT | ArkTS + ArkUI 声明式范式(State Management V1) |
| 领域逻辑 | Java 插件(OSGi Bundle) | ArkTS 分层:viewmodel / catalog / editor / model / dbclient |
| 数据库驱动 | JDBC Driver | 交叉编译 C 客户端库:libmariadb 3.3.3、libpq 15.13、sqlite3 3.46.0 |
| 数据库桥接 | JDBC API | NAPI(C++17)统一 provider 接口,产物 libdbclient.so |
| TLS | JSSE | OpenSSL 3.3.1 交叉编译,libmariadb / libpq 共用 |
| 字符编码 | JVM 内置 | libiconv 1.17 + libintl(gettext 0.22.5,libpq 依赖) |
| 密码存储 | Eclipse Secure Storage | 系统密钥管理(HUKS)AES-256-GCM |
| 交付形态 | 桌面安装包 | arm64-v8a 签名 HAP,随通用设备安装 |
需要说明的边界:上游依赖 Java 生态的部分(上百个 JDBC 驱动、OSGi 扩展体系、ER 图与 AI 能力)不在本次范围内,后文能力分级一节如实列出。
二、架构分层与工程结构
整个系统的分层很干净:ArkTS 管一切看得见的,C 库只管协议,NAPI 做桥。
┌──────────────────────────────────────────────────────┐
│ components/ ArkUI 视图(数据网格/树/SQL 编辑器…) │
│ pages/ 单 Entry 主壳 + 状态机导航 │
├──────────────────────────────────────────────────────┤
│ viewmodel/ SQL 构建、事务编排、导入导出、结构加载 │
│ catalog/ 数据库树目录服务、字段类型注册表 │
│ editor/ SQL 词法高亮、格式化、补全 │
│ model/ 会话、连接参数、HUKS 加密、查询历史 │
├──────────────────────────────────────────────────────┤
│ dbclient/ ConnRegistry + DbConnection(ArkTS 侧)│
├──────────────────────────────────────────────────────┤
│ cpp/ NAPI 桥:DbProvider 统一接口 │
│ ├ mysql_provider → libmariadb │
│ ├ postgres_provider → libpq │
│ └ sqlite_provider → sqlite3 │
├──────────────────────────────────────────────────────┤
│ native/ 交叉编译 ARM64 .so(musl 兼容) │
└──────────────────────────────────────────────────────┘
对应工程目录(App 工程与交叉编译体系完全解耦):
HarmonDb/
├── AppScope/ # 应用级配置
├── build-profile.json5 # 工程级构建配置
├── entry/ # 唯一业务模块
│ └── src/main/
│ ├── ets/
│ │ ├── pages/ # 主壳(单 @Entry + 条件渲染导航)
│ │ ├── components/ # 19 个视图组件(DataGrid / SqlEditor / …)
│ │ ├── viewmodel/ # SQL 构建、行编辑缓冲、导入导出、结构加载
│ │ ├── catalog/ # 树目录服务、字段类型注册表
│ │ ├── editor/ # 高亮 / 格式化 / 补全 / 关键字表
│ │ ├── model/ # 会话、连接参数、HUKS、查询历史、错误映射
│ │ ├── dbclient/ # 连接注册表与 NAPI 的 OO 封装
│ │ └── entryability/ # EntryAbility(PC 窗口配置)
│ ├── cpp/ # NAPI 桥接层 → libdbclient.so
│ │ ├── db_provider.h/.cpp # 统一接口与工厂
│ │ ├── mysql_provider.cpp
│ │ ├── postgres_provider.cpp
│ │ ├── sqlite_provider.cpp
│ │ ├── napi_init.cpp
│ │ ├── CMakeLists.txt
│ │ └── types/libdbclient/ # Index.d.ts(ArkTS 侧类型声明)
│ └── module.json5 # deviceTypes 含 2in1、INTERNET 权限
└── native/ # 数据库客户端库交叉编译
├── build/
│ ├── env.sh # OHOS 工具链(clang / sysroot / lld)
│ └── build_libs.sh # 按依赖顺序编 7 步
├── compat/musl_compat.h # glibc 扩展符号的 musl 兼容桩
└── prebuilt/arm64-v8a/ # 产物:lib/*.so + include/
连接链路从前台一路贯通到 native 层:
EntryAbility(主窗口 1280×800)
└── pages/Index(单 Entry 主壳,条件渲染切换)
├── 会话管理视图 # 新建/编辑/测试连接
└── 工作台视图
├── DbTreeView # 数据库树(catalog 懒加载)
├── DataGrid # 数据网格(分页/排序/过滤/编辑)
├── SqlEditor # SQL 编辑器 + 结果面板
└── ConnRegistry # ArkTS 连接注册表(整数 id)
└── NAPI: libdbclient.so
├── mysql_provider → libmariadb.so
├── postgres_provider → libpq.so
└── sqlite_provider → libsqlite3.so
这条路线有三个工程上的收益:依赖库以预编译 .so 随 HAP 分发,设备上零预装;领域逻辑全在 ArkTS 层,换数据库只动 provider 一层;ArkTS 侧的演进也完全不触碰 C 库。
三、跨语言桥接:NAPI 怎么把 C 库变成 ArkTS 可用的对象
3.1 统一 Provider 接口
C++ 侧的核心抽象是一个纯虚接口,把三种数据库收敛成同一套方法面:
class DbProvider {
public:
virtual bool Connect(const std::map<std::string, std::string>& params,
std::string& err) = 0;
virtual void Disconnect() = 0;
virtual bool Ping(std::string& err) = 0;
virtual std::string GetServerVersion() = 0;
virtual std::string GetServerInfo() = 0;
virtual std::string EscapeString(const std::string& s) = 0;
virtual ResultSet Query(const std::string& sql) = 0;
virtual bool IsConnected() = 0;
};
工厂按 kind 分发(mysql / mariadb / postgres / postgresql / sqlite / sqlite3),ArkTS 侧完全不关心底下是哪套库:
std::shared_ptr<DbProvider> CreateProvider(const std::string& kind) {
if (kind == "sqlite" || kind == "sqlite3") return CreateSqliteProvider();
if (kind == "mysql" || kind == "mariadb") return CreateMysqlProvider();
if (kind == "postgres" || kind == "postgresql") return CreatePostgresProvider();
return nullptr;
}
接口的注释里写得很清楚:这是对标 org.jkiss.dbeaver.model.jdbc 的 C++ 等价物。换句话说,移植的是架构意图,不是代码。
3.2 句柄路由:跨语言生命周期怎么管
NAPI 层用「全局连接表 + 自增整型句柄」管对象生命周期。C++ 侧每个连接持有一个 std::shared_ptr<DbProvider>,只把一个 int32_t 句柄返回给 ArkTS,后续所有调用都通过句柄路由:
std::mutex g_mutex;
std::map<int32_t, std::shared_ptr<harmondb::DbProvider>> g_conns;
std::atomic<int32_t> g_nextHandle{1};
有个值得单独说的细节:移除连接时先把 shared_ptr 移出表,在锁外析构:
void RemoveProvider(int32_t h) {
std::shared_ptr<harmondb::DbProvider> p;
{
std::lock_guard<std::mutex> lk(g_mutex);
auto it = g_conns.find(h);
if (it != g_conns.end()) {
p = std::move(it->second);
g_conns.erase(it);
}
}
// 析构在锁外执行(避免 Provider 析构回调上层再抢同一把锁)
}
Provider 析构里会调 mysql_close / PQfinish,这些清理动作有可能触发回调路径;如果在持锁时析构,轻则卡顿、重则死锁。这种「锁外析构」是跨语言句柄管理里最容易漏掉的一环。
ArkTS 侧再用 DbConnection 包成面向对象的形式,业务代码不感知任何 NAPI 细节:
const conn = new DbConnection('mysql');
conn.connect({ host: '10.0.2.2', port: '3306', user: 'root', password: '***', database: 'test' });
const rs = conn.query('SELECT id, name FROM test.game_news;');
conn.close();
ConnRegistry 则在页面之间按整数 id 分发连接(registerConn / getConn / unregisterConn),只把 id 放进 @State。
3.3 ResultSet 归一化
ResultSet 的结构设计是「一次写、三库通用」的关键:所有列值统一以字符串承载,同时带一份与行同形状的 NULL 标记数组:
struct ResultSet {
std::vector<Column> columns;
std::vector<std::vector<std::string>> rows;
std::vector<std::vector<bool>> nulls; // true 表示 NULL
int64_t affectedRows = 0;
int64_t insertId = 0;
bool ok = true;
std::string error;
std::string sqlState;
int32_t errorCode = 0;
};
这个设计把类型转换后置到 ArkTS 侧按列类型自行解析,native 层不必为每个库维护不同的返回值结构;NAPI 出口时再把 nulls 为真的位置转成 JS 的 null。列元信息(dbTypeName / type / nullable / isPrimaryKey / maxLength)也一并带出,直接支撑上层的编辑与结构管理。
一个容易忽略的点:Query 不抛异常,失败时返回 ok=false 并填充 sqlState / errorCode。这不是偷懒,而是为了让多语句批量执行能逐条校验——一旦抛异常,循环就断了,还得靠 try/catch 判断是第几条失败。
NAPI 模块注册就是一张 9 个入口的方法表:
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"create", nullptr, M_Create, ...}, {"connect", ...}, {"disconnect", ...},
{"destroy", ...}, {"ping", ...}, {"getServerVersion", ...},
{"getServerInfo", ...}, {"escapeString", ...}, {"query", ...},
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
3.4 三个 provider 的差异都收敛在实现内部
MySQL / MariaDB:走 mysql_real_connect,mysqlDriver 参数区分两套认证策略——MariaDB 旧驱动设 MYSQL_DEFAULT_AUTH=mysql_native_password 并开启明文插件;MySQL 8 分支则交给静态链接进 libmariadb.so 的 caching_sha2_password 插件。列类型映射把 MYSQL_TYPE_* 收敛成 Integer / Real / Text / Binary / Date / Time / DateTime / Json / Enum / Set,PRI_KEY_FLAG 直接标出主键列,maxLength 取 fields[i].length,给上层的长文本截断提供依据。
PostgreSQL:用 PQconnectdb 拼 keyword 连接串,sslmode / sslrootcert / sslcert / sslkey 原样透传,另外硬加了一条 connect_timeout=10——没有超时的连接最伤 UI,网络不通时用户会以为应用卡死。类型映射维护了一张 PG 内置 OID 表(含 int2/int4/int8、bytea、jsonb、uuid 以及常见的数组类型 OID),PGRES_TUPLES_OK / PGRES_COMMAND_OK / 错误三种状态分别处理。
SQLite:sqlite3_open_v2 用 READWRITE | CREATE | NOMUTEX(文件不存在即创建,与 DBeaver 行为一致),打开后立刻 PRAGMA foreign_keys=ON。列类型先按 decltype 映射,表达式列没有 decltype 时保持 Unknown,在逐行 step 时按运行期值类型再细化一次——这是 SQLite 这种动态类型库必须做的兜底。BLOB 按原始字节读入 std::string(可能含 \0,长度由 sqlite3_column_bytes 决定),中途失败时清空已取数据,保证失败语义干净。
四、协议层:7 步交叉编译与 musl 兼容
界面可以重写,数据库协议不必重造轮子。libmariadb / libpq / sqlite3 都是成熟 C 库,只要能交叉编译到 OHOS ARM64,协议层就交给它们。
4.1 工具链封装(native/build/env.sh)
所有构建脚本只从一个文件取环境:SDK 自带 LLVM/Clang、--sysroot 指向 musl sysroot、链接器 ld.lld。其中两个三元组变量必须并存,这是最容易踩的坑:
# autoconf 系工程的 --host 只认标准三元组
export TARGET_HOST="aarch64-linux-gnu"
# clang 的 --target 用 ohos 三元组
export TARGET_TRIPLE="aarch64-linux-ohos"
export TARGET_FLAGS="--target=$TARGET_TRIPLE --sysroot=$SYSROOT \
-D__linux__ -D__MUSL__ -fPIC"
export LDFLAGS="--target=$TARGET_TRIPLE --sysroot=$SYSROOT -fuse-ld=$LLVM_BIN/ld.lld"
两个自定义宏很关键:-D__linux__(musl 上部分头文件靠它才暴露 API)、-D__MUSL__(自己的兼容桩与补丁全靠这个宏切分支)。
还有一个非常隐蔽的环境问题,值得单独提一句:DevEco command-line-tools 的 toolchains 目录自带 diff/sed 且排在 PATH 前面,其中 diff 对任何输入都返回 0,导致 autoconf 的 config.status 误判「配置未变化」,pg_config.h / config.h 根本没落盘。解法是构建脚本开头把系统工具链提到最前:
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:$PATH"
这不是编译错误,而是「configure 看起来成功、make 却找不到头文件」的玄学问题,第一次遇到很难往工具链污染上想。
4.2 版本锁定的 6 个库 + 1 步收尾
脚本按「先依赖后主库」的顺序编译,版本全部写死,保证可复现:
| 顺序 | 库 | 版本 | 说明 |
|---|---|---|---|
| 1 | SQLite | 3.46.0 | 纯 C amalgamation,一条 clang 命令编成 .so(开 FTS5 / JSON1 / RTREE) |
| 2 | libiconv | 1.17 | libpq 的字符集依赖,手工 lld 链接(libcharset + libiconv 两个 .so) |
| 3 | libintl | gettext 0.22.5 | 只编 gettext-runtime 子集,libpq 依赖 |
| 4 | OpenSSL | 3.3.1 | libmariadb / libpq 的 TLS 依赖 |
| 5 | libpq | PostgreSQL 15.13 | 只编 src/interfaces/libpq,不编 server 端 |
| 6 | libmariadb | 3.3.3 | MySQL / MariaDB 客户端,认证插件全部静态链接 |
| 7 | finalize | — | strip --strip-unneeded + 生成 SONAME 软链 + 产物汇总 |
脚本是幂等的:源码包已存在则跳过下载、已解压则跳过解压、产物已存在则跳过编译。
cd native/build
./build_libs.sh all # 按依赖顺序编译全部
./build_libs.sh libmariadb # 也可单独编某一个
OHOS_SDK_PATH=/path/to/sdk ./build_libs.sh all # 指定 SDK 路径
4.3 每个库都有自己的「特殊处理」
OpenSSL:不走 autoconf,用自己的 Configure——平台选 linux-aarch64,-D__MUSL__ 让它走 musl 路径,shared no-tests no-asm 减少坑面;make 时显式 CROSS_COMPILE="",安装阶段还要 unset CROSS_COMPILE,否则 make install 会拿交叉前缀去找本机不存在的工具。
libpq:--without-readline --without-zlib --without-icu --disable-nls 砍掉不必要依赖、--with-openssl 接 TLS,只编客户端接口层。这里有个 autoconf 旧式探测的经典误判——用无原型调用来检测 pthread_barrier_*,现代 clang 直接拒绝,于是 configure 误判 OHOS musl 缺这些函数,编出一套 fallback 实现并与 musl 头冲突。脚本用 cache 变量强制纠正:
export ac_cv_search_pthread_barrier_wait="none required"
export ac_cv_func_pthread_barrier_wait=yes
另外它采用 in-tree 构建:之前试过 out-of-tree,在 macOS 上 pg_config.h 会被后续 make 步骤异常清除(config.status 报 unchanged 却不落盘),排查成本很高,索性放弃。
libmariadb(坑最多的一个),三处处理:
CMAKE_SYSTEM_NAME=Linux必须显式指定,否则 CMake 在 macOS 上把它检测成 Darwin,交叉编译直接翻车;- 交叉环境下
FIND_PACKAGE(OpenSSL)内部的TRY_RUN无法执行,OPENSSL_FOUND拿不到值、TLS 会被静默裁掉——脚本用sed往它的CMakeLists.txt注入一段OHOS_CROSS_COMPILE_HACK,跳过探测、直接注入头文件与库路径; - 认证插件全部静态链接(
CLIENT_PLUGIN_caching_sha2_password=STATIC等)。这是真机上最容易撞上的坑:MySQL 8+ 默认认证就是caching_sha2_password,插件若靠动态加载,运行时在应用沙箱里找不到插件文件,连接直接报错;GSSAPI、ed25519 等用不到的直接 OFF。
4.4 musl 兼容桩(native/compat/musl_compat.h)
这些库默认按 glibc 假设编译,交叉链接时会报一串 undefined symbol。逐个改库源码既脏又不可持续——每个库每个版本都要重新打补丁。最终解法是一个兼容桩头,通过 -include 强制注入到所有编译单元:
export CFLAGS="$CFLAGS -include $NATIVE_ROOT/compat/musl_compat.h"
| 符号 | 处理 |
|---|---|
backtrace / backtrace_symbols | musl 无 <execinfo.h>,libmariadb 崩溃诊断路径会引用,提供空桩 |
malloc_usable_size | glibc 扩展,用于内存统计,返回 0 |
program_invocation_name / program_invocation_short_name | glibc 全局变量,声明为外部符号 |
__libc_current_sigrtmin / sigrtmax | libpq 代码路径引用,返回常量 |
secure_getenv | musl 无,转发到 getenv |
PTHREAD_MUTEX_ADAPTIVE_NP / ROBUST_NP | 缺失宏,补 0 |
库源码零改动,升级库版本时不需要重新维护补丁集。
4.5 组包:rpath、SONAME 与插件目录
NAPI 模块的 CMake 指向 native/prebuilt/arm64-v8a/,链接 sqlite3 / mariadb / pq / ssl / crypto 加 SDK 的 ace_napi.z / hilog_ndk.z,并用 POST_BUILD 把运行期依赖的 .so 自动拷进 libdbclient.so 同目录:
target_link_options(dbclient PRIVATE "-Wl,-rpath,\$ORIGIN")
add_custom_command(TARGET dbclient POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PREBUILT_DIR}/lib" "$<TARGET_FILE_DIR:dbclient>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${PREBUILT_DIR}/lib/libpq.so.5.15"
"$<TARGET_FILE_DIR:dbclient>/libpq.so.5")
最后那条 copy_if_different 是个很具体的坑:动态链接器按 SONAME 加载,而 libpq 实体文件名是 libpq.so.5.15、SONAME 是 libpq.so.5,只拷实体文件会在运行时找不到库。所以必须额外补一份按 SONAME 命名的副本。libmariadb / libssl / libcrypto 恰好 SONAME 与实体名一致,才没暴露这个问题。
CMake 里还有一段条件编译:只有 libpq.so 与 libpq-fe.h 都在,才定义 HARMONDB_WITH_LIBPQ 并链接 pq,否则 PostgreSQL provider 退化为桩实现。这样即使某个库还没编好,UI 与其余功能也不会被阻塞构建。
4.6 连接失败要给人话,不要给 mysql_error 原文
真机连库失败时,把原始错误原样抛给用户体验很差。MySQL provider 会识别错误特征并给出可操作提示:
if (originalErr.find("caching_sha2_password") != std::string::npos ||
originalErr.find("Authentication plugin") != std::string::npos) {
err = "MySQL 8.0+ 默认使用 caching_sha2_password 认证...请在连接配置中"
"将驱动类型切换为「MySQL 8.0+ (新版)」,或在服务器端执行: "
"ALTER USER '用户名'@'主机' IDENTIFIED WITH mysql_native_password BY '密码';";
}
PostgreSQL 侧同理,对 password authentication failed / does not exist / Connection refused 三类高频错误分别给出中文诊断。这类「错误文案工程」在桌面工具里价值很高——它把排查成本从「搜错误码」降到「照着提示改配置」。
五、ArkTS 领域层的几个关键设计
5.1 数据网格:不是一句 SELECT *
数据网格是数据库工具的体验核心,它的数据流由 DataGridLoader 管理。一次刷新分三步:
- 探针查询拿列结构:首次进入用
LIMIT 0, 1探一次,拿到列名、类型、是否主键、最大长度; - 分页取数据:按
pageSize(默认 100)构造SELECT ... LIMIT n OFFSET m; - 单独统计总数:另发一条
SELECT COUNT(*)(同样带过滤条件)驱动分页控件。
SQL 全部在 ArkTS 侧拼接,排序、过滤、列裁剪都在这一层:
SELECT <列清单> FROM `db`.`table` WHERE <过滤条件> ORDER BY `col` DESC LIMIT 100 OFFSET 0
三个值得注意的设计:
- 列裁剪的触发条件很克制:只有表存在主键时才逐列写清单;无主键或全短列直接
SELECT *。原因很简单——逐列写清单是为了给超长文本列套LEFT()/SUBSTR()截断,没有主键时这套改写对用户没有收益,反而增加 SQL 复杂度。 - 长文本截断:对
TEXT / JSON / ENUM / SET / ARRAY这类文本列,若maxLength > 1000或长度未知(0),只取前 1000 个字符,避免把超大字段整个拉回内存拖垮网格。SQLite 用SUBSTR,MySQL / PG 用LEFT。 - 过滤操作符结构化:
eq / ne / like / notlike / gt / lt / gte / lte / isnull / isnotnull / raw。数值列走数字比较、文本列走转义后的引号比较,还支持直接写原生WHERE子句。标识符引用按方言区分:MySQL 用反引号,PG / SQLite 用双引号。
渲染侧用了 LazyForEach + 自定义 IDataSource:
class DisplayRowSource {
private rows: DisplayRow[] = [];
totalCount(): number { return this.rows.length; }
getData(index: number): Object { return this.rows[index]; }
// onDataReloaded / onDataAdded / onDataChanged …
}
DisplayRow 是「服务端行 + 编辑态」的合并快照。Loader 与 RowEditBuffer 都是长生命周期业务对象、刻意不进 @State,组件只把合并后的结果拷贝进 @State displayRows,再通知数据源刷新。这样 1000 行数据也不会一次性生成 1000 个组件节点,可视窗口之外的行按需调度。

5.2 行编辑缓冲区:桌面级编辑的「脏数据」模型
数据网格支持行内编辑 + 批量事务提交 / 回滚,核心是 RowEditBuffer。它把用户在网格上的改动先暂存,点「提交」前不写库,点「回滚」丢弃全部待提交编辑:
export interface PendingEdit {
rowKey: string; // 行的唯一 key
op: 'update' | 'insert' | 'delete';
original: (string | null)[]; // 原始值(UPDATE/DELETE 的 WHERE 定位用)
current: (string | null)[]; // 编辑后的值
changedColumns: Set<string>; // 被改过的列
}
几个关键决策:
- rowKey 生成:优先用主键列值拼接(
||分隔,NULL 用\N占位);无主键时退化为全部列值拼接——这也是「无主键表只能受限编辑」的由来; - 新增行的临时 key:
__new_<时间戳>_<随机数>,避免与真实主键冲突;删除一个尚未提交的新增行时直接把它从缓冲区移除,不生成 DELETE; - 值语义约定清晰:空串
''和NULL是两件事,NULL只能通过显式入口写入;数值列的空串在生成 SQL 时兜底按NULL处理; - 提交顺序固定:
INSERT → UPDATE → DELETE,并用单个事务包起来(BEGIN → 逐条 → 失败 ROLLBACK → COMMIT)。
SqlBuilder 负责把缓冲区变成 SQL。这里有两个在真机上被验证过的「小修复」,非常值得写出来:
// 1) WHERE 里 NULL 必须用 IS NULL("col" = NULL 恒为假 → 永远命中 0 行)
if (v === null) {
clauses.push(this.quoteIdent(c.name) + ' IS NULL');
} else {
clauses.push(this.quoteIdent(c.name) + ' = ' + this.formatValue(v, c));
}
// 2) DELETE 影响 0 行不是成功,而是并发冲突信号
if (s.op === 'delete' && rs.affectedRows === 0) {
this.conn.query('ROLLBACK');
return { ok: false, executedCount: executed,
error: 'DELETE 未匹配到任何行(该行可能已被其他会话修改或删除)' };
}
第一条是 SQL 语义层面的经典错误,第二条是产品语义层面的判断:桌面数据库工具经常多窗口、多人协作,把 0 行删除当成功会静默丢操作。这里选择回滚并明确报错,本质是一套轻量的乐观并发检测。
5.3 SQL 编辑器:自研词法高亮、格式化与补全降级
编辑器没有依赖现成高亮库,而是写了一个词法扫描器 SqlHighlighter.ets:把 SQL 切成 whitespace / comment / string / quotedIdent / number / keyword / function / datatype / operator / punctuation / identifier 十一种 token,再按 token 类型生成带颜色的 HTML 交给 RichText 渲染,明暗双主题各有一套色板。
扫描器只做词法、不做语法树,但该尊重的上下文都尊重了:-- 与 # 单行注释、/* */ 多行注释、单引号字符串(含反斜杠转义)、双引号、MySQL 反引号标识符、十六进制与科学计数法数字、以及「后跟 ( 才算函数名」的判定。
formatSql 也基于同一套 token 流,规则化地处理主关键字换行、括号层级缩进、列清单逗号换行、关键字大小写(upper / lower / preserve 三档可配):
export interface FormatOptions {
keywordCase: 'upper' | 'lower' | 'preserve';
indentUnit: 'spaces' | 'tabs';
indentSize: number;
}
配套的 SqlExecutor 负责按 ; 拆多语句并逐条执行。拆分是词法级的:字符串、三种引号、单行/多行注释里的分号都不拆,反斜杠转义后的字符跳过。执行时统计每条语句的耗时、影响行数与错误,支持「首条失败即中止后续」:
const { results, summary } = executor.execute(sql, /* abortOnError */ true);
// summary: { totalStatements, successCount, failedCount, totalElapsedMs, aborted }
自动补全则是一个诚实的降级实现,注释里写得很明白:ArkTS TextArea 当时不暴露光标位置 API,也没有语法解析树,所以只能做「文本尾端倒推前缀 + 候选集 startsWith 匹配」:
- 从 SQL 文本尾部倒推最后一个 word 作为待补全前缀;
- 候选来源:关键字 / 类型 / 函数 Set + 传入的表名列表 +(表名 → 列名)字典;
- 轻量扫描 SQL 中
FROM / JOIN / UPDATE / INTO / TABLE之后出现的表名,优先补这些表的列; - 单次最多返回 60 条,按类别着色,选中后替换原前缀。
它不完美,但「能补关键字、能补当前表列、不会卡住输入」已经覆盖了编辑器 80% 的补全需求。这类「先降级再演进」的取舍在平台能力不完整时比硬做更划算。

5.4 结构元数据:三套方言,一个领域模型
结构管理不是把 SHOW CREATE TABLE 原样展示,而是把字段、索引、外键读成统一的 FieldDefinition / IndexDefinition / ForeignKeyDefinition,编辑后做 diff,再生成 DDL。StructureLoader 为此维护了三套查询:
| 元数据 | MySQL / MariaDB | PostgreSQL | SQLite |
|---|---|---|---|
| 字段 | information_schema.columns(含 column_key / extra / column_comment) | information_schema.columns + 主键列集合查询 | PRAGMA table_info |
| 索引 | information_schema.statistics(按 seq_in_index 归并多列索引) | pg_index + pg_class + pg_get_indexdef | PRAGMA index_list + PRAGMA index_info |
| 外键 | key_column_usage + referential_constraints | pg_constraint + pg_get_constraintdef | PRAGMA foreign_key_list |
几处细节很能体现移植的复杂度:
- PG 的
pg_get_constraintdef返回一整段FOREIGN KEY (...) REFERENCES ... ON DELETE ...,只能写解析器把它拆成列、引用表、引用列与两个动作; - PG 的
data_type是character varying这种全称,需要归一化成VARCHAR才和编辑器里的类型下拉对得上; - SQLite 的
origin='pk'表示主键自动索引,不可编辑,DDL 生成时必须跳过; - MySQL 的
column_key='PRI'与 PG 的主键集合查询走的是完全不同的路径,但最终都落到同一个isPrimaryKey字段。
方言差异集中在加载与 DDL 构建层,UI 完全不感知——这也是这套设计最值得称赞的地方:同一个表结构编辑器,底下换数据库只需要换 loader 与 builder。



5.5 密码安全存储(HUKS)
SecureSecretStore 基于 @ohos.security.huks 实现,替代 Windows 上的 DPAPI / Eclipse Secure Storage 思路:
- 密钥由 HUKS 生成并持久化存储,密钥永不出 HUKS,加解密都在 HUKS 内部完成;
- 算法 AES-256-GCM,
HUKS_PADDING_NONE,12 字节随机 IV + 16 字节认证标签; - 走 HUKS 标准三阶段会话:
initSession → updateSession → finishSession; - 密文按
Base64(IV + ciphertext + tag)拼接存储,每条密文独立; - 解密时把 tag 拆出来放进
HUKS_TAG_AE_TAG、密文单独喂入——GCM 的 tag 位置在 HUKS 接口里是有约定的,搞错就是「加密能过、解密必失败」; - 解密失败(密钥变更或数据被篡改)直接抛异常,不静默返回脏数据。
const initRes = await huks.initSession(KEY_ALIAS, initOpts);
await huks.updateSession(initRes.handle, { data: updateData });
const finishRes = await huks.finishSession(initRes.handle, { data: new Uint8Array(0) });
// 拼装:IV(12) + ciphertext(含 tag),整体 Base64
另外还有一个 testHuksAvailable() 自检:加密一段固定字符串再解密回环。密钥服务不可用时,这个自检能把「密码存不进去」与「连接参数写错了」这两类问题快速分开。
六、PC 形态适配与状态管理经验
「让它像桌面应用、而不是大号手机应用」是重点投入的部分。
窗口与主题:module.json5 的 deviceTypes 显式声明 2in1,随通用设备一起安装;EntryAbility.onWindowStageCreate 里主窗口拉起即 win.resize(1280, 800),符合桌面工作区习惯。这里有个必须知道的单位问题——resize 的参数是 px 不是 vp,在鸿蒙 PC 的高密度屏上若严格按 vp 布局设计,需要先用 display.getDefaultDisplaySync() 取 densityPixels 做换算,否则窗口实际只有设计尺寸的一半,模态对话框会超出窗口被裁剪。主题色全部引用集中色板,品牌常量单独收敛,保证明暗主题切换时颜色不散落。
右键菜单与键鼠交互:数据库树与表格统一走右键上下文菜单(刷新、新建对象、编辑、导出等),数据网格的行与列区分鼠标按键行为,列宽拖拽是独立组件并带悬停高亮,同时保持触控可用。
状态管理 V1 的两个真实坑:
- 长生命周期对象不能进
@State:DbConnection、DataGridLoader、DbTreeLoader、RowEditBuffer全部用private字段持有。原因很直接——@State会用 Proxy 包裹对象,破坏private字段访问,还会让 NAPI 句柄这种原生资源变得不可控。页面之间只传整数connId,需要时从ConnRegistry取连接。 - 嵌套字段写入不具备可观察性:
this.vm.editing.kind = 'sqlite'这种写法不会触发依赖editing的表达式重新求值,界面会停在旧状态。正确做法是把结构性联动收敛成方法,重建对象并整体重新赋值:
changeKind(kind: DbKind): void {
const next: SessionEditing = { /* 逐字段显式拷贝 */ };
next.kind = kind;
this.editing = next; // 整体赋值才会触发依赖 editing 的表达式重新求值
}
配套还有「结果拷贝进 @State + refreshTick++ 手动重建」的模式:Loader 算完数据后同步到 @State 快照,再递增一个 tick 触发重渲染。这不是优雅方案,但在 V1 装饰器体系下是可靠方案。
七、功能走查
以下按用户使用路径整理,截图均来自鸿蒙 PC 实机。
7.1 连接管理
应用启动先进入会话管理:左侧会话列表(新建 / 保存 / 删除),右侧「连接属性」面板覆盖名称、类型(MySQL / MariaDB、PostgreSQL、SQLite)、主机、端口、用户、密码、数据库、字符集,以及驱动类型、SSL 模式、查询超时、Keep-Alive 等高级项。连接前可先「测试连接」,成功后回显服务器版本与 SSL 状态。密码经 HUKS 加密存储,不以明文落盘。

7.2 工作台与数据库树
连接成功后进入工作台:左侧数据库树按「实例 → 数据库 → 表 / 视图」分层展开,支持按名称过滤;树节点绑定统一的右键菜单。树是懒加载的——展开时才发查询,flattenVisible 把展开节点拍平成渲染行,过滤时保留匹配节点及其祖先链。SQLite 这类单库数据源会收敛成虚拟库节点,交互与 MySQL / PostgreSQL 保持一致。
7.3 SQL 编辑器与查询历史
SQL 脚本标签支持关键字高亮(多主题可切换)、多语句一次提交、结果按语句分区展示;左侧竖排工具条提供执行、执行模式(全部 / 选中 / 当前语句)、清空、格式化、查询历史与数据源标识。查询历史独立持久化,可回看历史 SQL 与执行时间。

7.4 建表与新建对象
新建对象走统一菜单(表 / 视图 / 存储过程 / 触发器 / 事件等),表设计器按字段、索引、外键分区编辑,保存时由 AlterTableBuilder / IndexFkBuilder 生成 DDL。


7.5 数据导入导出
数据可导出为 SQL 文件或 CSV,CSV 亦可导入。导入是逐条 INSERT 并逐条检查 rs.ok 的:批量拼接多句 INSERT 时,只要中间某句失败,后续语句和整体结果都会被吞掉,所以这里宁可慢一点也要拿到准确的行号错误,失败时回滚并给出提示。


7.6 对象与运维
对象编辑器延伸到视图 / 存储过程 / 触发器 / 事件,另有用户管理与偏好设置(字体、字号、每页行数、NULL 显示、驱动、超时、高亮主题等)。偏好链路前端到后端统一用 @Prop + @Watch,保存后重新构造子组件参数触发热更新。




八、环境要求与构建步骤
| 项目 | 要求 | 备注 |
|---|---|---|
| IDE / SDK | DevEco Studio(含 HarmonyOS SDK) | 提供 OHOS LLVM/Clang、musl sysroot、hvigor、CMake |
| 交叉编译工具 | cmake、make、curl、tar、xz、unzip | cmake 优先用 SDK 自带 |
| 目标设备 | 2in1(鸿蒙 PC)及通用设备 | deviceTypes = ["default", "2in1"] |
| 权限 | ohos.permission.INTERNET | 连接远程数据库必需,已在 module.json5 带用途说明声明 |
整个流程是「交叉编译 C 库 → 构建签名 HAP → 装到鸿蒙 PC」:
# 1. 交叉编译数据库客户端 C 库(仓库已附带预编译产物,常规开发可跳过)
cd native/build && ./build_libs.sh all
# 2. 构建签名 HAP(DevEco 里 Signing Configs 点 Fix 生成调试签名后 Run,或命令行)
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk
export PATH="$JAVA_HOME/bin:/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin:$PATH"
hvigorw assembleHap
# 3. 安装并启动
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
产物落在 native/prebuilt/arm64-v8a/(.so + 头文件),native 依赖库全部随 HAP 打包,单文件即可分发安装。native/prebuilt 随仓库分发,日常开发无需每次重跑交叉编译。
首次使用建议先用 SQLite:只需填一个本地文件路径,最容易跑通「连接 → 浏览 → 查询 → 编辑」全链路。
九、能力分级与当前边界
仓库内的 README.OpenHarmony_CN.md 按社区规范给出了完整的 T0 / T1 / T2 能力分级,这里给出结论性摘要。
T0(基础能力,已达成):HAP 构建 / 签名 / 安装 / 启动;2in1 桌面窗口与主题适配;NAPI 桥接闭环;三套 C 客户端库 musl 交叉编译;MySQL / MariaDB(TCP + TLS)、PostgreSQL(TCP + TLS)、SQLite(本地文件)三类连接;密码加密存储。
T1(主要能力,已达成):会话管理(新建 / 编辑 / 复制 / 删除 / 测试连接);数据库树分层导航、过滤与右键菜单;数据网格浏览 / 编辑双模式、列宽拖拽、快速与条件过滤、行内编辑 + 批量事务提交 / 回滚、脏数据守卫、无主键表风险告警、BLOB / TEXT 专用编辑器;SQL 编辑器高亮、多语句执行、执行模式切换、查询历史、格式化;SQL / CSV 导出与 CSV 导入。
T2(增强能力,部分达成):数据库 / 表对象编辑器、视图 / 存储过程 / 触发器 / 事件管理、用户管理、偏好设置、TLS 连接配置已交付;SQL 自动补全为降级实现;SSH 隧道、ER 图 / 数据建模、任务调度 / 仪表盘监控未排期;Oracle / SQL Server / MongoDB / Redis 等其余数据源按 provider 模式逐步扩展。
需要如实说明两点边界:数据编辑的批量提交依赖目标表具备主键或唯一键,无主键表提供的是带风险告警的受限编辑;上游的 JDBC 驱动框架与 OSGi 插件体系与原生架构不兼容,本项目以「源码级 provider 扩展」替代「装 JDBC 驱动包」这条路。
十、总结分析
把 DBeaver 搬到鸿蒙 PC,真正的工作量并不在「画界面」,而在三处结构性差异上。
第一,复用边界要划在协议层,而不是 UI 层。 数据库客户端最容易低估的是协议实现:认证插件、TLS、字符集、类型映射、错误码,每一项都是十几年打磨的结果。这里的取舍是——UI 与工作流 100% 重写,协议 100% 复用 C 库,桥接层只做统一形状,不做业务。结果是加一个新数据库只需要写一个 provider 和一套元数据查询,UI 一行不改。
第二,跨语言桥接的关键是「形状收敛」,不是「功能映射」。 ResultSet 把列值统一成字符串 + 独立 NULL 标记,看起来放弃了类型信息,实际上换来的是三个 provider 完全一致的出口结构,类型解析集中在一层做。句柄表 + shared_ptr + 锁外析构这套组合,解决了原生资源跨 GC 边界的管理问题。这两点比多写几个 API 重要得多。
第三,平台差异要当设计输入,而不是事后补丁。 musl 兼容桩、三元组双写、CMake 的 TRY_RUN、SONAME 命名、resize 的 px/vp 单位、状态管理 V1 的嵌套刷新——每一个都不是「代码写错了」,而是「平台假设不同」。把它们集中到脚本与工具层(env.sh、musl_compat.h、build_libs.sh),而不是散落到业务代码里,是这类移植能持续维护的前提。
回头看,这个项目最值得同类项目借鉴的可能是那条**「先降级、再演进」**的节奏:补全先做前缀匹配,虚拟化先做 LazyForEach,PostgreSQL 先做条件编译桩——每一步都保证「能编译、能安装、能启动、核心链路可用」,再把能力一格一格补上。桌面工具的用户预期很高,但只要「连库、查数、改数据」这条主线是通的,它就已经是一个可日常使用的工具。
开源鸿蒙 PC 需要的正是这类基础设施软件。项目代码已托管在 AtomGit,无论是 provider 扩展、数据网格性能打磨,还是新场景的适配反馈,都欢迎参与共建。
更多推荐



所有评论(0)