SQLite 与 db_manager 集成关键概念详解

这篇文章是对 RFID 仓库管理系统中 db_manager 模块(SQLite 数据库集成)的全流程总结。涵盖了从 SQL 执行、回调机制、内存管理、JSON 解析到 TCP 发送的每一个关键点。


一、SQLite 两种执行方式

1.1 sqlite3_exec --- 回调方式(适合 SELECT 查询)

复制代码
int rc = sqlite3_exec(g_db, sql, callback, user_data, &err_msg);

流程:

  1. 编译 SQL
  2. 执行 SQL
  3. 每查到一行数据,调用一次你的回调函数
  4. 所有行处理完毕,返回

1.2 sqlite3_prepare_v2 + step --- 预处理方式(适合有参数的查询)

复制代码
sqlite3_stmt *stmt = NULL;
sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM t WHERE batch_no = ?", -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, batch_no, -1, SQLITE_STATIC);
sqlite3_step(stmt);
int count = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);

流程:

  1. prepare --- 编译 SQL,带 ? 占位符
  2. bind --- 把实际值填进 ?
  3. step --- 执行,SQLITE_ROW 表示有结果
  4. column --- 取出结果
  5. finalize --- 释放资源

为什么用两步? ? 占位符需要先"挖坑",再"填值"。没有 ? 的话可以直接 exec


二、回调函数参数全解析

复制代码
static int collect_batch_callback(
    void *data,        // 你自己的数据(如 BatchListData 的指针)
    int col_count,     // SELECT 后面有几个字段
    char **col_values, // 当前行的数据(字符串数组)
    char **col_names   // 各列的列名(字符串数组)
)

参数来源

参数 谁给的 例子
data sqlite3_exec 第4个参数) &bd
col_count SQLite 编译时算的 SELECT a,b → 2
col_values SQLite 执行时从数据库读的 ["RK20260610-003"]
col_names SQLite 从 CREATE TABLE 读的 ["batch_no"]

col_count 是一个数字,决定两个数组的长度

sql

复制代码
SELECT batch_no FROM inbound_records;        -- col_count = 1
SELECT batch_no, warehouse FROM ...;         -- col_count = 2
SELECT batch_no, warehouse, goods_name FROM ...; -- col_count = 3

col_count 跟数据无关(即使值是 NULL,列数不变),只跟 SELECT 写了几个字段有关。

回调的调用次数 = 表中的行数

sql

复制代码
-- inbound_records 表有 3 行数据
SELECT batch_no FROM inbound_records;

-- 回调被调用 3 次,每次一行:
-- 第1次:col_values = ["RK20260610-003"]
-- 第2次:col_values = ["RK20260610-002"]
-- 第3次:col_values = ["RK20260610-001"]

col_names 是列名

sql

复制代码
CREATE TABLE inbound_records (
    batch_no    TEXT,     ← 这个 "batch_no" 就是 col_names 的值的来源
    warehouse   TEXT,     ← "warehouse"
    goods_name  TEXT      ← "goods_name"
);

SELECT batch_no FROM inbound_records;
-- col_names = ["batch_no"]  ← 来自表结构定义

三、为什么必须 strdup?

复制代码
static int collect_batch_callback(void *data, ..., char **col_values, ...)
{
    // col_values[0] 指向 SQLite 内部的临时内存
    // 回调返回后,这个内存会被回收

    // ✅ 正确做法:strdup 复制到堆
    bd->list[bd->count] = strdup(col_values[0]);

    // ❌ 错误做法:只存指针
    bd->list[bd->count] = col_values[0];  // 回调返回后变野指针!
}

strdup 干了什么

复制代码
// strdup = malloc + memcpy
char *strdup(const char *s) {
    char *copy = malloc(strlen(s) + 1);  // 申请新内存
    strcpy(copy, s);                     // 复制字符串过去
    return copy;                         // 返回新内存的地址
}

类比

复制代码
SQLite 临时缓冲区(借你用的纸条)
  ┌──────────────────┐
  │ RK20260610-003   │  ← col_values[0] 指向这里
  └──────────────────┘
     回调结束后,这张纸条就被回收了

strdup 复制一份到堆上(你抄到自己笔记本上)
  ┌──────────────────┐
  │ RK20260610-003   │  ← 永远有效,直到你 free
  └──────────────────┘

四、realloc 和 strdup 的关系

两个不同的概念

函数 作用 比喻
realloc 扩大 指针数组(增加"格子"数量) 书架不够用了,买个更大的书架
strdup 复制 字符串数据 到新内存 买了本新书,放在仓库里

内存结构

复制代码
char **list = realloc(NULL, 16 * sizeof(char*));
// list 是指针数组,每个元素存一个地址(4或8字节)

bd->list(指针数组,realloc 管理的)
┌──────┬──────┬──────┬──────┐
│  ●   │  ●   │  ●   │      │  ← 每个格子 4/8 字节,只存地址
└──│───┴──│───┴──│───┴──────┘
   │      │      │
   │      │      └────→ strdup("RK20260610-001") → 15字节,在另一块内存
   │      │
   │      └──────────→ strdup("RK20260610-002")
   │
   └────────────────→ strdup("RK20260610-003")

为什么不能合并

因为 指针数组的格子只能放指针,放不下 15 个字符的字符串。

复制代码
list[0] = "RK20260610-001";  // list[0] 存的是地址,不是15个字符

realloc 扩再大,每个格子的大小仍然是 sizeof(char*) = 4 或 8 字节。需要 strdup 在别处申请内存存真正的字符串。


五、先 realloc 再 strdup 的顺序

正确顺序

复制代码
// 1. 先确保指针数组有格子
if(bd->count >= bd->capacity) {
    bd->capacity *= 2;
    bd->list = realloc(bd->list, bd->capacity * sizeof(char*));
}

// 2. 再把数据放进去
bd->list[bd->count] = strdup(col_values[0]);
bd->count++;

为什么不能反过来

如果先 strduprealloc,一旦 realloc 失败返回 NULL,之前 strdup 的内存就没人管了(内存泄漏)。


六、TCP 发送时的 malloc

为什么不能用固定数组

复制代码
// tcp_send_tag 可以用固定数组,因为格式固定:
void tcp_send_tag(const char *epc, const char *tid) {
    char buf[256];  // 固定大小,够用
    snprintf(buf, sizeof(buf),
             "{\"cmd\":\"tag_found\",\"epc\":\"%s\",\"tid\":\"%s\"}",
             epc, tid);
    tcp_send(buf);
}

// tcp_send_batch_list 必须用 malloc,因为长度不确定:
void tcp_send_batch_list(char **list, int count) {
    int buf_size = 128;           // 固定部分
    for(int i = 0; i < count; i++)
        buf_size += strlen(list[i]) + 16;  // 每个批次号 + 格式字符
    char *buf = malloc(buf_size); // 按需申请
    snprintf(buf, ...);
    tcp_send(buf);
    free(buf);                    // 用完即释
}

+16 是什么

每个批次号在 JSON 里要包这些额外字符:

复制代码
"RK20260610-001",   
↑                ↑↑
左引号         右引号+逗号

所以额外需要 3 字节(" + " + ,),+16 是保守估算。

完整内存生命周期

复制代码
SQLite 回调(临时数据)
    │
    │ strdup → 复制到堆
    ▼
bd->list(永久保存,直到 TCP 发完)
    │
    │ snprintf → 拼到 JSON 缓冲区
    ▼
buf(malloc 临时缓冲区,发完即 free)
    │
    │ tcp_send → 发送到网络
    ▼
free(buf)                ← JSON 容器释放
db_free_str_list(list)   ← 数据释放

七、字符常量 '"' 的语法

C 语言的规则

复制代码
'A'   // 字符 A(ASCII 65)
'1'   // 字符 1(ASCII 49)
','   // 字符 逗号(ASCII 44)
'"'   // 字符 双引号(ASCII 34) ← 外层单引号是语法标记

外层两个 ' 是 C 语法标记 ,表示"这是一个字符常量"。内层的 " 才是真正的值。

在 JSON 解析中的应用

复制代码
for(i = 0; p[i] && p[i] != '"'; i++)
    out[i] = p[i];
//                        ↑
//            遇到双引号字符就停

在 JSON 里匹配的是值的结束引号

复制代码
...batch_no":"RK20260610-001","warehouse"...
                                   ↑
                            p[i] == '"' → 循环结束

八、JSON 解析方式

get_json_str 解析流程

复制代码
static void get_json_str(const char *json, const char *key, char *out, int out_size)
{
    char search[64];
    snprintf(search, sizeof(search), "\"%s\":\"", key);
    // search = "batch_no":"

    const char *p = strstr(json, search);
    // 在 JSON 里找 "batch_no":"

    p += strlen(search);
    // 跳过 "batch_no":",p 指向值开头

    int i;
    for(i = 0; i < out_size - 1 && p[i] && p[i] != '"'; i++)
        out[i] = p[i];
    // 逐字复制直到遇到 "
    out[i] = '\0';
}

为什么不直接用 cJSON

嵌入式设备上引入一个 JSON 库会增加代码体积和内存开销。用 strstr 做简单解析足够满足当前需求(只解析字符串值,不涉及嵌套对象)。

局限

  • 只能解析字符串值(键值对中的 "value"
  • JSON 格式不能有额外空格
  • 遇到 \" 转义会出错
  • 但对我们当前够用

九、SQL 整数 vs 字符串

sql

复制代码
CREATE TABLE inbound_records (
    total_count INTEGER    -- 整数类型
);

-- 插入时:
INSERT INTO inbound_records (..., total_count) VALUES ('...', 98);
--                                                      ↑
--                                              整数,不带引号

-- 错误的写法:
INSERT INTO inbound_records (..., total_count) VALUES ('...', '98');
--                                                          ↑
--                                                  带引号变成了字符串

在 C 代码中对应的格式化:

复制代码
sqlite3_mprintf(
    "VALUES ('%q', '%q', '%q', '%q', '%q', %d);",
    batch_no, warehouse, shelf, goods_name, product, count);
    // 字符串用 '%q'                              ↑
    // 整数用 %d(没有引号)

十、哈希表清空

为什么每次扫描前要 destroy

复制代码
int scan_Until_stable(...) {
    destroy_hash(head);  // 清空上次扫描的数据
    
    // 开始新扫描
    BeginInv(hDev);
    // ...
}

不清空的后果

复制代码
第1次扫描 → 哈希表有 98 个标签
用户忘了点"确认入库"
第2次扫描 → hash 不清 → 98 + 50 = 148 个
用户点"确认入库" → 保存了 148 个,实际只扫了 50 个 → 数据错乱

每次扫描都是独立的,从零开始。


十一、栈数组大小

复制代码
// handle_inbound_confirm 中:
char epc_list[1024][64];   // 1024 × 64 = 65536 字节 = 64KB
char tid_list[1024][64];   // 另一个 64KB
// 合计 128KB,在栈上

嵌入式设备的栈大小

i.MX6ULL 默认栈 8MB,128KB 占 1.5%,安全。

如果不够怎么办

malloc 按需分配:

复制代码
int total = HASH_COUNT(scan_tag_hash_list);
char (*epc_list)[64] = malloc(total * sizeof(*epc_list));
// 用完 free(epc_list);
方式 内存位置 大小
栈数组 1024 128KB 固定
malloc 按需 实际标签数 × 128 字节

十二、为什么不在回调里直接发 TCP

三个原因

问题 说明
回调每行调一次 如果发 TCP,QT 会收到 N 条小消息,QT 端要自己拼成完整列表
TCP 阻塞回调 send() 可能阻塞,导致 SQLite 查询也被阻塞
回调后数据失效 回调结束后 col_values 就没了

正确做法

复制代码
// 回调里只收集
static int collect_batch_callback(void *data, ...) {
    BatchListData *bd = data;
    bd->list[bd->count] = strdup(col_values[0]);
    bd->count++;
    return 0;
}

// 回调结束后统一发送
char **list = db_get_batch_list(&count);
tcp_send_batch_list(list, count);  // 拼成一条完整 JSON 发送
db_free_str_list(list, count);     // 释放

总结

复制代码
SQLite 操作
    │
    ├─ exec(回调) → collect → strdup + realloc → 收集完整
    │
    └─ prepare + bind + step → finalize → 安全释放
                │
                ▼
        TCP 发送(malloc JSON → send → free)

核心原则:回调只收集,不发送。需要持久化的数据必须 strdup。指针数组 realloc 扩大,字符串 strdup 存数据。JSON 发送时临时 malloc 缓冲区用完即释。

相关推荐
pigs20181 小时前
mysql8.0 access denied for user root localhost account is locked
数据库·adb
阿里云瑶池数据库1 小时前
从开源插件到生产级引擎:PolarDB PostgreSQL的向量能力新范式
数据库·阿里云·postgresql
tomcoding2 小时前
遇到一个ORA-01017错误,解决方法
数据库·oracle
ejinxian3 小时前
PolarDB ,MongoDB ,MySQL ,PostgreSQL ,Redis, OceanBase, Sql Server等数据库
数据库·mysql·mongodb
折哥的程序人生 · 物流技术专研10 小时前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
AOwhisky10 小时前
Redis 学习笔记(第三期):持久化与主从复制
运维·数据库·redis·笔记·学习·云计算
李白的天不白10 小时前
数据库连接报错问题
数据库
一条泥憨鱼10 小时前
【Redis】数据类型和常用命令
java·数据库·redis·后端·缓存
爱喝水的鱼丶11 小时前
SAP-ABAP:SAP视图开发入门:四类标准视图的适用场景与创建步骤详解
服务器·数据库·性能优化·sap·abap