在自动化脚本中操作本地数据库

在自动化脚本里处理数据,绕不开持久化这个环节。脚本跑完就丢的变量只能应付简单场景,一旦需要记录运行状态、缓存采集结果或者保存配置,就得让数据落下来。冰狐平台提供的本地数据库模块,把嵌入式数据库封装成了一套面向对象的接口,不用额外装任何东西,也不用 Root 权限,直接在脚本里 new Database('名字') 就能开始读写。这套接口一共只有八个方法,但覆盖了从建库、建表、增删改查到事务控制的完整链路。下面围绕这八个方法,结合一个完整的日志记录场景,把冰狐本地数据库的使用方式拆开来说。

先看清这套接口的边界

冰狐本地数据库的 API 设计非常克制,所有操作都挂在 Database 实例上,没有游离的函数。官方文档列出的方法清单是:Database、close、delete、isTableExist、exeSql、query、beginTransaction、endTransaction。理解这几个方法的职责划分,比记住参数更重要。

Database(dbName) 是入口,作用是打开或创建一个本地数据库文件。如果同名数据库已经存在,拿到的就是现有库的实例;如果不存在,则新建一个。isTableExist(tableName) 返回布尔值,用来判断表是否已经建过,这是避免重复执行建表语句的标准手段。exeSql(sql) 承担所有非查询类 SQL 的执行,包括建表、插入、更新、删除,返回值是一个布尔值,表示语句是否执行成功。query(sql) 只处理 SELECT,返回的是 JSON 数组,数组里每个元素对应一行记录,字段名就是对象的键。beginTransaction() 和 endTransaction() 成对出现,用来把多步写操作包成一个原子单元;close() 释放数据库连接;delete() 则是直接把整个数据库文件删掉。

有一点需要提前知道:冰狐的本地数据库是轻量型嵌入式实现,SQL 语法遵循标准 SQLite 的规范,建表时常用的 INTEGER PRIMARY KEY AUTOINCREMENT、VARCHAR、DATETIME 都可以正常使用。但它是移动端专有的,服务端脚本里不能调用这套 API,服务端的数据操作需要走微服务或远程数据库那条路。

从建库到建表:让脚本自己管理结构

一个容易被忽略的习惯是:每次脚本启动都先判断表是否存在,再决定是否建表。如果不做这个判断,直接 CREATE TABLE,第二次运行就会因为表已存在而报错。下面的结构处理方式可以作为一个固定模板来用:

复制代码
var db = new Database('script_runtime');
if (!db.isTableExist('run_log')) {
    db.exeSql(`create table run_log(
        id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        task_name VARCHAR(64) NULL,
        status VARCHAR(16) NULL,
        detail TEXT NULL,
        created_at DATETIME(4) NULL
    )`);
}

这里 created_at 用了 DATETIME(4),冰狐文档的示例中也是这么写的,可以保留毫秒级的精度。isTableExist 的存在让这段代码可以反复执行而不会产生副作用,脚本入口处放一次即可。

写入数据:exeSql 的返回值要用起来

插入一条日志记录,用 exeSql 就够了。但很多人只写不检查返回值,SQL 拼错了、字段名写错了,脚本照样往下跑,直到查询的时候才发现数据压根没进去。exeSql 返回的是布尔值,虽然不能拿到自增 ID,但至少能告诉你这条语句有没有被数据库接受。下面是一个带判断的插入片段:

复制代码
var insertOk = db.exeSql(
    `insert into run_log(task_name, status, detail, created_at) 
     values('daily_check', 'running', '开始执行巡检', datetime('now'))`
);
if (!insertOk) {
    console.log('插入日志失败,后续逻辑中止');
    db.close();
    return;
}

需要注意 SQL 语句里的字符串拼接。如果值来自变量,尤其是可能包含单引号的内容,直接模板字符串拼进去有 SQL 注入的风险,也可能因为引号嵌套导致语法错误。稳妥的做法是在写入前对字符串做转义,或者用参数化写法------冰狐的 exeSql 本身不暴露参数绑定接口,所以手动处理引号是必要的防护步骤。

查询:query 返回的是普通 JSON 数组

query 的返回值是数组,不是游标,也不是结果集对象。拿到之后可以直接用 length、下标、forEach 处理,不需要额外的 next() 或 fetch() 步骤。下面的查询取出最近五条失败记录:

复制代码
var failedLogs = db.query(
    `select id, task_name, detail, created_at 
     from run_log 
     where status = 'failed' 
     order by id desc 
     limit 5`
);

if (failedLogs.length > 0) {
    for (var i = 0; i < failedLogs.length; i++) {
        console.log(`第${i + 1}条: ${failedLogs[i].task_name} / ${failedLogs[i].detail}`);
    }
} else {
    console.log('没有失败记录');
}

这里有一个细节:如果查询结果为空,query 返回的是空数组而不是 null,所以直接取 .length 是安全的。字段名的大小写和建表时保持一致,SQLite 默认对字段名大小写不敏感,但写代码时保持统一可以减少排查成本。

查询语句本身也要注意性能。本地数据库虽然轻量,但如果表里数据量大了,select * 不带条件照样会拖慢脚本。能用 where 就用 where,能加 limit 就加 limit,尤其是在循环里执行查询时。

事务:批量写入时绕不开的一层

单条插入用 exeSql 没问题,但如果有十几条甚至几十条记录要一次性写入,逐条执行 exeSql 的效率并不高------每条语句都可能触发一次磁盘同步。更重要的是,如果中途有一条失败,前面已经写进去的数据不会自动回滚,数据库会停在一个不完整的状态。这时候就需要 beginTransaction 和 endTransaction。

事务的用法很直接,但在冰狐的这套 API 里,endTransaction 没有参数,它只负责提交,不负责回滚。也就是说,一旦进入事务,所有 exeSql 都执行完后再调用 endTransaction,事务内的操作才会被提交;如果中间某条语句返回 false,需要开发者自己决定是否要中止并清理。下面是一个批量写入日志的写法:

复制代码
db.beginTransaction();

var tasks = [
    ['task_a', 'success', '完成'],
    ['task_b', 'failed', '超时'],
    ['task_c', 'success', '完成']
];

var allOk = true;
for (var j = 0; j < tasks.length; j++) {
    var t = tasks[j];
    var r = db.exeSql(
        `insert into run_log(task_name, status, detail, created_at) 
         values('${t[0]}', '${t[1]}', '${t[2]}', datetime('now'))`
    );
    if (!r) {
        allOk = false;
        break;
    }
}

db.endTransaction();

if (!allOk) {
    console.log('批量写入过程中出现失败,事务已结束,需检查数据完整性');
}

事务把多条写入压成一个提交单元,减少了中间状态的暴露时间。对于日志记录这类场景,即使不追求严格的原子性,事务也能显著减少磁盘 I/O 次数。

收尾:close 与 delete 的区别

close() 释放当前 Database 实例持有的连接资源。脚本结束时不调用它,一般不会立刻报错,但长期运行的脚本反复打开而不关闭,会积累资源占用。delete() 则是把整个数据库文件从设备上删掉,调用之后库里的所有表和数据都不存在了。两者是完全不同的操作:close 是"关上门",delete 是"拆掉房子"。下面的收尾代码先关闭再删除,顺序不能反过来:

复制代码
db.close();

if (needResetAll) {
    db.delete();
}

如果脚本在后续逻辑里还需要用同一个数据库,就不能在 close() 之后直接 delete(),因为 delete 调用时 db 已经处于关闭状态。需要先重新 new Database('script_runtime') 拿到实例,再执行删除。

几个容易踩的坑

SQL 字符串拼接的安全边界。 exeSql 和 query 都只接受一个 SQL 字符串参数,没有参数绑定接口。任何来自外部或者用户输入的值,拼进 SQL 之前必须做引号转义,否则单引号就会导致语句断裂。

事务内不要做无关操作。 beginTransaction 到 endTransaction 之间只放需要原子化的写语句。如果在中间夹了耗时的 query 或者 console.log,事务的持有时间会被拉长,反而增加锁冲突的风险。

查询结果的字段名依赖建表结构。 query 返回的对象键名直接对应表的列名。如果建表时用了 VARCHAR(45) 这类类型声明,字段名不会自动变化,但大小写敏感度取决于 SQLite 的默认行为。建议建表和查询时用同一套字段命名写法,避免"表里叫 taskName、查询写成 taskname"这类问题。

脚本入口与作用域。 冰狐脚本以 main 为入口,数据库操作代码需要放在 main 函数体内或 main 调用的自定义函数中。在顶层直接写 new Database 在某些执行环境下可能拿不到预期的作用域,把数据库实例的创建和关闭都收在 main 内部是更稳妥的做法。

冰狐这套本地数据库 API 的体量很小,八个方法,没有 ORM,没有查询构建器,也没有连接池。但正是这种克制让它在自动化脚本里不容易出幺蛾子:建表前判断一次,写入时检查返回值,批量操作包一层事务,用完关闭,需要彻底清理时再删除。把这几个动作固定成脚本里的习惯,数据持久化这部分就不需要再花额外的心思了。

相关推荐
Decay__1 小时前
09_MySQL 闪回与时间点恢复 —— binlog 回放、mysqlbinlog、PITR 与误删恢复
运维·数据库·sql·mysql·架构
太理摆烂哥1 小时前
MySQL用户管理
数据库·mysql
LMT流氓兔2 小时前
第五单元——第十六课:给书店助手加上网页界面
java·开发语言·jvm·人工智能·oracle·langchain·1024程序员节
代码方舟2 小时前
零信任架构实战:基于天远企业法人四要素高级版构建自动化电子印章授权与签约准入网关
人工智能·架构·自动化·区块链
NailiConveyor3 小时前
柔性物流方案如何适配工厂升级?自动化输送流水线定制要点
大数据·运维·自动化·制造·业界资讯
梦影_3 小时前
MySQL进阶教程(五)—— SQL调优(下)
android·大数据·数据库·后端·sql·mysql·database
考虑考虑3 小时前
mariadb数据库
数据库·后端·mariadb
螺丝钉tina5 小时前
[Playwright] [02]常用命令与调试:Debug UI、Mode和 Trace Viewer
开发语言·javascript·测试工具·ui·自动化
John_Hui5 小时前
Windows 10 安装版本:mysql-8.1.0-winx64.zip
数据库·windows·mysql