
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[1.1 业务目标](#1.1 业务目标)
[1.2 数据库表设计](#1.2 数据库表设计)
[1.2.1 表关联约束](#1.2.1 表关联约束)
[1.3 并发线程安全要求](#1.3 并发线程安全要求)
[1.4 DataManager 模块职责](#1.4 DataManager 模块职责)
[二、公共数据结构体定义 common.h](#二、公共数据结构体定义 common.h)
[2.1 Session 会话结构体](#2.1 Session 会话结构体)
[三、DataManager 头文件 DataManager.h](#三、DataManager 头文件 DataManager.h)
[3.1 完整头文件代码](#3.1 完整头文件代码)
[3.2 接口业务逻辑要点](#3.2 接口业务逻辑要点)
[四、DataManager 设计规范要点](#四、DataManager 设计规范要点)
[五、DataManager.cpp 实现与代码分析](#五、DataManager.cpp 实现与代码分析)
[5.1 构造函数、析构函数](#5.1 构造函数、析构函数)
[5.2 数据库初始化 initDatabase](#5.2 数据库初始化 initDatabase)
[5.3 通用无结果 SQL 执行工具 executeSQL](#5.3 通用无结果 SQL 执行工具 executeSQL)
[5.4 Session 会话业务接口实现](#5.4 Session 会话业务接口实现)
[5.4.1 insertSession 插入会话](#5.4.1 insertSession 插入会话)
[5.4.2 getSession 根据 sessionId 获取会话](#5.4.2 getSession 根据 sessionId 获取会话)
[5.4.3 updateSessionTimestamp 更新会话时间戳](#5.4.3 updateSessionTimestamp 更新会话时间戳)
[5.4.4 deleteSession 删除会话(级联删除消息)](#5.4.4 deleteSession 删除会话(级联删除消息))
[5.4.5 getAllSessionIds 获取全部会话 ID(按更新时间倒序)](#5.4.5 getAllSessionIds 获取全部会话 ID(按更新时间倒序))
[5.4.6 getAllSessions 获取全部会话元数据](#5.4.6 getAllSessions 获取全部会话元数据)
[5.4.7 getSessionCount 获取会话总数](#5.4.7 getSessionCount 获取会话总数)
[5.5 Message 消息业务接口实现](#5.5 Message 消息业务接口实现)
[5.5.1 insertMessage 插入消息并更新会话时间](#5.5.1 insertMessage 插入消息并更新会话时间)
[5.5.2 getSessionMessages 查询会话全部消息(对外接口)](#5.5.2 getSessionMessages 查询会话全部消息(对外接口))
[5.5.3 getSessionMessagesInternal 私有内部查询(不加锁)](#5.5.3 getSessionMessagesInternal 私有内部查询(不加锁))
[5.5.4 deleteSessionMessages 手动删除会话消息](#5.5.4 deleteSessionMessages 手动删除会话消息)
[5.6 SQLite API 核心原理与易错点小结](#5.6 SQLite API 核心原理与易错点小结)
[六、DataManager 单元测试(TestLLM.cpp)](#六、DataManager 单元测试(TestLLM.cpp))
[6.1 测试入口 main 函数](#6.1 测试入口 main 函数)
[6.2 测试用例代码](#6.2 测试用例代码)
[6.3 测试用例整体说明](#6.3 测试用例整体说明)
[6.4 模块解耦与后续规划](#6.4 模块解耦与后续规划)
[6.5 CMake 构建配置](#6.5 CMake 构建配置)
前言
在前面章节我们完成了
SessionManager内存会话管理模块的开发,它可以在程序运行期间维护会话与消息,但是内存数据有明显短板:程序退出之后所有会话数据都会丢失,无法实现历史对话持久化。为了解决这个问题,本章我们引入 SQLite 轻量数据库,设计并实现DataManager数据持久化管理模块。本章会先明确模块的业务需求、数据表结构以及线程安全约束,定义会话与消息的公共结构体,再完成
DataManager头文件声明、接口实现,同时梳理 SQLite 的调用要点与常见坑点。最后基于 GTest 编写单元测试,单独验证数据库读写接口的正确性。需要重点说明的是,
DataManager和SessionManager遵循解耦设计:DataManager 只专注会话、消息的磁盘增删改查,不关心上层业务逻辑;测试时直接使用模拟的会话、消息数据,不依赖内存会话模块。在完成本模块的验证之后,我们再把内存会话管理和磁盘持久化模块进行联动,实现 "内存缓存 + 数据库落盘" 的完整会话能力,让我们的大模型 SDK 具备历史对话持久保存的能力。
一、模块整体需求
1.1 业务目标
我们前面实现的 SessionManager 只会在内存中维护会话与消息,程序退出后数据全部丢失。本模块的核心目标,就是为 AI 聊天 SDK 提供持久化能力,将会话Session、单条消息Message数据落地保存到 SQLite 嵌入式数据库中,实现对话记录永久存储。
1.2 数据库表设计
我们采用一对多的表结构,设计两张核心数据表:
- sessions 会话表 :存储会话的元信息,包含会话 ID、绑定模型、创建时间、更新时间等基础字段,
session_id作为本表主键。 - messages 消息表 :存储每一条聊天消息的内容、角色、时间戳,通过
session_id和会话表进行关联。
1.2.1 表关联约束
消息表的session_id作为外键,引用会话表的主键session_id。
开启外键级联删除 ON DELETE CASCADE:当删除某一条会话记录时,数据库自动把该会话下所有消息一并删除,保证两张表的数据一致性。
重要注意点: SQLite 默认是关闭外键约束的,打开数据库连接之后,必须手动执行
PRAGMA foreign_keys = ON;这条语句,否则级联删除不会生效,这是很容易遗漏的关键点。
1.3 并发线程安全要求
SDK 会存在多线程同时读写会话数据 的场景,并发访问数据库会产生竞争条件,引发数据错乱、脏读等问题。 我们使用std::mutex互斥锁保护所有数据库读写接口;锁变量使用mutable修饰 ,这样就算是const常量成员函数,内部也可以对互斥锁进行加锁、解锁操作,适配查询类只读接口。
1.4 DataManager 模块职责
DataManager 作为数据库层的统一管理类,封装了全部 SQLite 操作,对外屏蔽底层原生 API 细节:
- 管理 sqlite3 数据库连接的创建、销毁生命周期
- 数据库初始化、自动建表
- 会话 Session 的完整 CRUD
- 消息 Message 的完整 CRUD
- 封装通用 SQL 执行工具函数,统一处理 SQL 执行逻辑
二、公共数据结构体定义 common.h
会话和消息的结构体统一放在公共头文件common.h,方便上层业务、DataManager、SessionManager 共用。
2.1 Session 会话结构体
cpp
// 会话信息:保存一轮对话的上下文与历史消息
struct Session
{
std::string _session_id; // 会话ID
std::string _model_name; // 会话使用的模型名称
std::vector<Message> _messages; // 会话中的消息记录
std::time_t _created_at; // 会话创建时间
std::time_t _updated_at; // 会话更新时间
// 构造函数:绑定对应模型
Session(const std::string &model_name = "")
: _model_name(model_name)
{
}
};
结构体承载一个会话的全部信息,内置消息列表,上层拿到 Session 对象后可以直接读取全部对话历史。
三、DataManager 头文件 DataManager.h
3.1 完整头文件代码
cpp
#ifndef DATAMANAGER_H
#define DATAMANAGER_H
#include "common.h"
#include <iostream>
#include <memory>
#include <mutex>
#include <optional>
#include <sqlite3.h>
namespace ai_chat_sdk
{
class DataManager
{
public:
DataManager(const std::string &dbName);
~DataManager();
// -------------------- Session 相关操作 --------------------
// 插入会话:将会话元数据写入会话表
bool insertSession(const Session &session);
// 获取指定会话:根据sessionId查询会话,同时加载关联的全部消息
std::shared_ptr<Session> getSession(const std::string &sessionId) const;
// 删除指定会话:依赖外键级联,自动删除下属消息
bool deleteSession(const std::string &sessionId);
// 删除全部会话
bool deleteAllSessions();
// 更新会话的更新时间戳,用于会话列表排序
bool updateSessionTimestamp(const std::string &sessionId, std::time_t timestamp);
// 获取全部会话ID列表,按更新时间倒序
std::optional<std::vector<std::string>> getAllSessionIds() const;
// 获取全部会话完整信息(包含消息)
std::optional<std::vector<std::shared_ptr<Session>>> getAllSessions() const;
// 获取当前会话总数
int getSessionCount() const;
// -------------------- Message 相关操作 --------------------
// 向指定会话插入一条消息,同时自动更新会话的updated_at时间
bool insertMessage(const std::string &sessionId, const Message &message);
// 获取某个会话下全部消息,按时间升序排列
std::optional<std::vector<Message>> getSessionMessages(const std::string &sessionId) const;
// 删除指定会话下所有消息
bool deleteSessionMessages(const std::string &sessionId);
private:
// 初始化数据库:创建sessions、messages两张表,开启外键约束
bool initDatabase();
// 通用SQL执行函数,执行无返回结果的SQL语句
bool executeSQL(const std::string &sql);
// 内部私有查询接口:不加锁,供内部调用,规避递归加锁死锁
std::optional<std::vector<Message>> getSessionMessagesInternal(const std::string &sessionId) const;
private:
sqlite3* _db; // SQLite数据库连接句柄
std::string _dbName; // 数据库文件名
mutable std::mutex _dbMutex; // 数据库互斥锁,保护所有读写操作
};
} // namespace ai_chat_sdk
#endif
3.2 接口业务逻辑要点
- 接口分层设计 :对外接口全部加锁,保证线程安全;单独抽离不加锁的私有内部接口
getSessionMessagesInternal,给类内部其他函数调用,防止递归加锁引发死锁。 - 返回值使用 std::optional:区分 "执行失败" 和 "查询结果为空" 两种情况,上层更容易判断数据库操作状态。
- 资源生命周期:构造函数完成数据库打开、初始化建表;析构函数负责关闭 sqlite3 句柄,释放数据库资源。
- 职责边界清晰 :只负责数据持久化,不做会话业务逻辑,和上层 SessionManager 解耦。
四、DataManager 设计规范要点
在后续DataManager.cpp实现中,需要遵循下面这套设计规范:
- 插入消息自动刷新会话时间 :调用
insertMessage新增消息时,同步更新对应会话updated_at字段,保证会话列表排序是最新交互优先。 - 外键级联删除:依靠数据库外键约束,删除会话时自动清理下属消息,不需要在代码里手动遍历删除消息,简化逻辑、保证一致性。
- 会话排序规则 :查询会话列表统一按照
updated_at时间戳降序,用户优先看到最近交互过的会话。 - 消息时序规则:读取消息时按时间戳升序加载,保证聊天消息顺序符合对话逻辑。
- 完整对象组装:查询会话元数据之后,自动关联查询消息记录,组装成完整的 Session 对象返回给上层。
- 线程安全:所有对外读写接口都加互斥锁,多线程并发访问数据库不会出现竞争问题。
- 防 SQL 注入 :所有查询、新增、修改操作,全部使用
sqlite3_prepare_v2预编译 + 参数绑定,不直接拼接 SQL 字符串,规避注入风险。
五、DataManager.cpp 实现与代码分析
5.1 构造函数、析构函数
cpp
DataManager::DataManager(const std::string& dbName)
: _dbName(dbName), _db(nullptr)
{
// 打开或者创建sqlite数据库文件
int rc = sqlite3_open(_dbName.c_str(), &_db);
if (rc != SQLITE_OK)
{
ERR("数据库打开失败:%s", sqlite3_errmsg(_db));
_db = nullptr;
return;
}
// 开启外键约束,必须在打开连接后执行
executeSQL("PRAGMA foreign_keys = ON;");
// 初始化数据表
initDatabase();
INFO("数据库连接初始化成功,dbname: %s", _dbName.c_str());
}
DataManager::~DataManager()
{
if (_db != nullptr)
{
sqlite3_close(_db);
INFO("数据库连接关闭:%s", _dbName.c_str());
}
}
分析:
- 构造函数职责 :接收数据库文件名,调用
sqlite3_open打开数据库;文件不存在时 SQLite 会自动创建。打开成功后立刻执行PRAGMA foreign_keys = ON;,开启外键级联删除,这是容易遗漏的关键点。最后调用initDatabase自动建表。 - 析构函数职责 :判断句柄有效时调用
sqlite3_close关闭数据库连接,释放资源,避免句柄泄漏。 - 异常处理 :打开失败时置空
_db,后续所有接口先判断_db == nullptr,防止空指针访问。 - 注意:构造、析构只执行一次,不需要加锁;锁只用于并发读写业务接口。
5.2 数据库初始化 initDatabase
cpp
bool DataManager::initDatabase()
{
// 1. 创建会话表 sessions
std::string createSessionTable = R"(
CREATE TABLE IF NOT EXISTS sessions(
session_id TEXT PRIMARY KEY,
model_name TEXT NOT NULL,
create_time INTEGER NOT NULL,
update_time INTEGER NOT NULL
);
)";
// 2. 创建消息表 messages,外键关联sessions,开启级联删除
std::string createMessageTable = R"(
CREATE TABLE IF NOT EXISTS messages(
message_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp INTEGER NOT NULL,
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
);
)";
if (!executeSQL(createSessionTable))
{
ERR("创建sessions表失败");
return false;
}
if (!executeSQL(createMessageTable))
{
ERR("创建messages表失败");
return false;
}
return true;
}
分析:
- 使用
CREATE TABLE IF NOT EXISTS,重复初始化不会报错,适配程序多次启动。 - 表字段设计:
sessions存储会话元信息;messages通过外键绑定会话 ID,ON DELETE CASCADE实现会话删除时自动清理消息。 - 调用前面封装的
executeSQL执行建表语句,失败直接返回 false,终止初始化流程。
5.3 通用无结果 SQL 执行工具 executeSQL
cpp
bool DataManager::executeSQL(const std::string& sql)
{
if (_db == nullptr)
{
ERR("数据库未初始化");
return false;
}
char* errMsg = nullptr;
int rc = sqlite3_exec(_db, sql.c_str(), nullptr, nullptr, &errMsg);
if (rc != SQLITE_OK)
{
ERR("executeSQL执行失败:%s", errMsg);
sqlite3_free(errMsg);
return false;
}
return true;
}
分析:
- 适用场景:DDL 建表、
PRAGMA、无返回集的INSERT/UPDATE/DELETE,不需要读取查询结果的 SQL 语句。 sqlite3_exec回调传 nullptr,不处理行数据;出错时必须调用sqlite3_free释放 errMsg 内存,防止内存泄漏。- 这是基础工具函数,供初始化、简单 SQL 调用;查询类、带参数的语句我们统一使用
sqlite3_prepare_v2预编译方案,不用这个函数,规避 SQL 注入。
5.4 Session 会话业务接口实现
所有对外业务接口第一行使用
std::lock_guard<std::mutex> lock(_dbMutex)自动加锁,函数退出自动释放锁,保证线程安全。
5.4.1 insertSession 插入会话
cpp
bool DataManager::insertSession(const Session& session)
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string insertSQL = R"(
INSERT INTO sessions(session_id, model_name, create_time, update_time)
VALUES (?, ?, ?, ?);
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, insertSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("insertSession预编译失败:%s", sqlite3_errmsg(_db));
return false;
}
// 参数绑定
sqlite3_bind_text(stmt, 1, session._session_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, session._model_name.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 3, session._created_at);
sqlite3_bind_int64(stmt, 4, session._updated_at);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
{
ERR("insertSession执行失败:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
INFO("insertSession 插入会话成功,session_id=%s", session._session_id.c_str());
return true;
}
- 使用预编译语句,
?占位符,sqlite3_bind_xxx绑定参数,杜绝 SQL 注入。 SQLITE_TRANSIENT代表 SQLite 内部拷贝一份字符串,函数结束后外部字符串销毁也不会野指针。sqlite3_step返回SQLITE_DONE代表插入完成;无论成功失败,必须调用sqlite3_finalize释放 stmt 资源。- 只写入会话元数据,消息单独通过
insertMessage写入。
5.4.2 getSession 根据 sessionId 获取会话
cpp
// 获取指定会话:根据会话ID从会话表中查询会话信息
std::shared_ptr<Session> DataManager::getSession(const std::string &sessionId) const
{
// 加锁保护,确保在多线程环境下数据表操作是线程安全的,并且在函数退出时自动释放锁
std::lock_guard<std::mutex> lock(_dbMutex);
std::string selectSessionSQL = R"(
SELECT model_name, created_at, updated_at FROM sessions WHERE session_id = ?;
)";
// 编译SQL语句
sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(_db, selectSessionSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("getSession-编译SQL语句失败: {}", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return nullptr;
}
// 绑定参数
sqlite3_bind_text(stmt, 1, sessionId.c_str(), -1, SQLITE_TRANSIENT);
// 执行SQL语句
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW)
{
ERR("getSession-执行SQL语句失败: {}", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return nullptr;
}
// 创建会话对象
std::shared_ptr<Session> session = std::make_shared<Session>();
// 从结果集中提取数据
session->_model_name = (const char *)(sqlite3_column_text(stmt, 0));
session->_created_at = static_cast<std::time_t>(sqlite3_column_int64(stmt, 1));
session->_updated_at = static_cast<std::time_t>(sqlite3_column_int64(stmt, 2));
session->_session_id = sessionId;
// 从消息表中获取会话的所有消息(内部函数:防止死锁)
if (auto messages = getSessionMessagesInternal(session->_session_id))
{
session->_messages = messages.value();
}
// 清理语句
sqlite3_finalize(stmt);
INFO("getSession-会话获取成功: {}", sessionId);
return session;
}
- 先查询
sessions拿到会话元信息;sqlite3_step返回SQLITE_ROW代表查到一行记录。 - 重点:消息加载调用私有不加锁接口
getSessionMessagesInternal,避免同一个锁重复加锁,防止死锁。 - 组装完整 Session 对象,把消息列表填充进去,上层拿到的 Session 是完整可直接使用的会话数据。
- 查询不到会话直接返回 nullptr,上层可以判断会话是否存在。
5.4.3 updateSessionTimestamp 更新会话时间戳
cpp
bool DataManager::updateSessionTimestamp(const std::string& sessionId, std::time_t timestamp)
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string updateSQL = R"(
UPDATE sessions SET update_time = ? WHERE session_id = ?;
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, updateSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("updateSessionTimestamp预编译失败:%s", sqlite3_errmsg(_db));
return false;
}
sqlite3_bind_int64(stmt, 1, timestamp);
sqlite3_bind_text(stmt, 2, sessionId.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
{
ERR("updateSessionTimestamp执行失败:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
INFO("updateSessionTimestamp 更新会话时间成功,session_id=%s", sessionId.c_str());
return true;
}
- 核心用途:新增一条消息后,同步刷新会话的
update_time,会话列表按更新时间倒序展示,保证最新会话排在最前面。 - 只更新时间字段,轻量操作;在
insertMessage内部会调用这个接口。
5.4.4 deleteSession 删除会话(级联删除消息)
cpp
bool DataManager::deleteSession(const std::string& sessionId)
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string deleteSQL = R"(
DELETE FROM sessions WHERE session_id = ?;
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, deleteSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("deleteSession预编译失败:%s", sqlite3_errmsg(_db));
return false;
}
sqlite3_bind_text(stmt, 1, sessionId.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
{
ERR("deleteSession执行失败:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
INFO("deleteSession 删除会话成功,session_id=%s", sessionId.c_str());
return true;
}
- 只删除
sessions表记录,消息清理依靠外键 ON DELETE CASCADE,不需要手动调用删除消息接口。 - 前提:数据库打开时已经执行
PRAGMA foreign_keys = ON;,否则级联失效。
5.4.5 getAllSessionIds 获取全部会话 ID(按更新时间倒序)
cpp
std::optional<std::vector<std::string>> DataManager::getAllSessionIds() const
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string selectSQL = R"(
SELECT session_id FROM sessions ORDER BY update_time DESC;
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, selectSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("getAllSessionIds预编译失败:%s", sqlite3_errmsg(_db));
return std::nullopt;
}
std::vector<std::string> result;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
{
std::string sid = (const char*)sqlite3_column_text(stmt, 0);
result.push_back(sid);
}
if (rc != SQLITE_DONE)
{
ERR("getAllSessionIds执行异常:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return std::nullopt;
}
sqlite3_finalize(stmt);
return result;
}
ORDER BY update_time DESC,最新交互的会话排在前面,适配前端会话列表展示。- 返回
std::optional,区分:查询失败(nullopt) 和 查询成功但是没有会话(空 vector)。 - 循环
sqlite3_step逐行读取,直到SQLITE_DONE读取完毕。
5.4.6 getAllSessions 获取全部会话元数据
cpp
std::optional<std::vector<std::shared_ptr<Session>>> DataManager::getAllSessions() const
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string selectSQL = R"(
SELECT session_id, model_name, create_time, update_time
FROM sessions ORDER BY update_time DESC;
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, selectSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("getAllSessions预编译失败:%s", sqlite3_errmsg(_db));
return std::nullopt;
}
std::vector<std::shared_ptr<Session>> result;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
{
auto session = std::make_shared<Session>();
session->_session_id = (const char*)sqlite3_column_text(stmt, 0);
session->_model_name = (const char*)sqlite3_column_text(stmt, 1);
session->_created_at = sqlite3_column_int64(stmt, 2);
session->_updated_at = sqlite3_column_int64(stmt, 3);
// 内部无锁接口加载消息
auto msgOpt = getSessionMessagesInternal(session->_session_id);
if (msgOpt.has_value())
{
session->_messages = msgOpt.value();
}
result.push_back(session);
}
if (rc != SQLITE_DONE)
{
ERR("getAllSessions执行异常:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return std::nullopt;
}
sqlite3_finalize(stmt);
return result;
}
一次性拿到所有会话完整数据(包含消息),适合会话列表页面批量渲染;同样复用内部无锁消息查询接口规避死锁。
5.4.7 getSessionCount 获取会话总数
cpp
int DataManager::getSessionCount() const
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string selectSQL = R"(
SELECT COUNT(*) FROM sessions;
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, selectSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("getSessionCount预编译失败:%s", sqlite3_errmsg(_db));
return 0;
}
int count = 0;
if ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
{
count = sqlite3_column_int(stmt, 0);
}
sqlite3_finalize(stmt);
return count;
}
聚合查询,读取 COUNT 统计结果;用于统计会话数量,做分页、面板统计。
5.5 Message 消息业务接口实现
5.5.1 insertMessage 插入消息并更新会话时间
cpp
bool DataManager::insertMessage(const std::string& sessionId, const Message& message)
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string insertMsgSQL = R"(
INSERT INTO messages(message_id, session_id, role, content, timestamp)
VALUES (?, ?, ?, ?, ?);
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, insertMsgSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("insertMessage预编译失败:%s", sqlite3_errmsg(_db));
return false;
}
sqlite3_bind_text(stmt, 1, message._message_id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, sessionId.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, message._role.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, message._content.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 5, message._timestamp);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
{
ERR("insertMessage插入消息失败:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
// 插入消息成功,同步更新会话的update_time
auto now = message._timestamp;
bool ret = updateSessionTimestamp(sessionId, now);
if (!ret)
{
ERR("insertMessage 同步更新会话时间失败");
}
INFO("insertMessage 消息插入成功, session_id=%s", sessionId.c_str());
return true;
}
- 先写入消息记录;写入成功之后,立刻调用
updateSessionTimestamp刷新会话更新时间,保证会话列表排序是最新交互优先。 - 消息和会话时间更新是两次独立 SQL,在更高级版本可以用事务包裹,保证原子性。
5.5.2 getSessionMessages 查询会话全部消息(对外接口)
cpp
std::optional<std::vector<Message>> DataManager::getSessionMessages(const std::string& sessionId) const
{
std::lock_guard<std::mutex> lock(_dbMutex);
return getSessionMessagesInternal(sessionId);
}
对外加锁包装 ,内部调用不加锁的getSessionMessagesInternal;这样上层直接调用时自动加锁,而类内部其他加锁接口(getSession、getAllSessions)调用内部版本不会重复加锁,解决死锁。
5.5.3 getSessionMessagesInternal 私有内部查询(不加锁)
cpp
std::optional<std::vector<Message>> DataManager::getSessionMessagesInternal(const std::string& sessionId) const
{
std::string selectSQL = R"(
SELECT message_id, role, content, timestamp FROM messages
WHERE session_id = ? ORDER BY timestamp ASC;
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, selectSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("getSessionMessagesInternal预编译失败:%s", sqlite3_errmsg(_db));
return std::nullopt;
}
sqlite3_bind_text(stmt, 1, sessionId.c_str(), -1, SQLITE_TRANSIENT);
std::vector<Message> msgs;
while ((rc = sqlite3_step(stmt)) == SQLITE_ROW)
{
Message msg;
msg._message_id = (const char*)sqlite3_column_text(stmt, 0);
msg._role = (const char*)sqlite3_column_text(stmt, 1);
msg._content = (const char*)sqlite3_column_text(stmt, 2);
msg._timestamp = sqlite3_column_int64(stmt, 3);
msgs.push_back(msg);
}
if (rc != SQLITE_DONE)
{
ERR("getSessionMessagesInternal执行异常:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return std::nullopt;
}
sqlite3_finalize(stmt);
return msgs;
}
ORDER BY timestamp ASC:消息按时间升序,保证聊天顺序是从上到下的对话时序。- 不加锁 :约定只能由已经持有锁的成员函数调用,用来规避递归锁死锁,是本模块非常关键的防死锁设计。
5.5.4 deleteSessionMessages 手动删除会话消息
cpp
bool DataManager::deleteSessionMessages(const std::string& sessionId)
{
std::lock_guard<std::mutex> lock(_dbMutex);
std::string deleteSQL = R"(
DELETE FROM messages WHERE session_id = ?;
)";
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(_db, deleteSQL.c_str(), -1, &stmt, nullptr);
if (rc != SQLITE_OK)
{
ERR("deleteSessionMessages预编译失败:%s", sqlite3_errmsg(_db));
return false;
}
sqlite3_bind_text(stmt, 1, sessionId.c_str(), -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
if (rc != SQLITE_DONE)
{
ERR("deleteSessionMessages执行失败:%s", sqlite3_errmsg(_db));
sqlite3_finalize(stmt);
return false;
}
sqlite3_finalize(stmt);
INFO("deleteSessionMessages 清空会话消息成功,session_id=%s", sessionId.c_str());
return true;
}
这是备用接口。正常业务优先使用deleteSession依靠外键级联自动清理消息;这个接口用于只清空消息、保留会话元数据的场景。
5.6 SQLite API 核心原理与易错点小结
- sqlite3_prepare_v2:预编译 SQL 语句,生成 stmt 语句句柄,支持参数绑定,是防注入的核心。
- bind 系列函数 :绑定
?占位参数;SQLITE_TRANSIENT标记字符串,让 SQLite 拷贝数据。 - sqlite3_step :执行 SQL,返回
SQLITE_ROW代表读到一行数据,SQLITE_DONE代表语句执行完成。 - sqlite3_column_xxx:读取当前行指定列的数据。
- sqlite3_finalize:必须调用,释放 stmt 资源,否则内存泄漏。
- 锁的分层设计:对外接口加锁、内部查询接口不加锁,是这套 DataManager 防死锁的核心设计。
- 外键开关: SQLite 默认关闭外键,每次打开连接后都要执行
PRAGMA foreign_keys = ON;,否则级联删除失效。
六、DataManager 单元测试(TestLLM.cpp)
6.1 测试入口 main 函数
cpp
int main(int argc, char **argv)
{
// 初始化Google Test框架
testing::InitGoogleTest(&argc, argv);
// 初始化日志系统
Logger::logger_init("testLLM", "stdout", spdlog::level::info);
// 运行所有测试用例
return RUN_ALL_TESTS();
}
入口是 GTest 标准启动流程,先完成测试框架与日志组件初始化,再自动执行全部测试用例,日志输出到控制台,方便我们观察数据库操作过程。
6.2 测试用例代码
cpp
TEST(DataManagerTest, operDatabase)
{
// 1、创建DataManager对象
DataManager dataManager("test.db");
// 会话操作:
// 插入多个会话
Session session1("test_session1");
session1._session_id = "session1_12345678"; // 模拟会话ID
session1._created_at = std::time(nullptr);
session1._updated_at = std::time(nullptr);
int res = dataManager.insertSession(session1);
ASSERT_TRUE(res);
sleep(2);
Session session2("test_session2");
session2._session_id = "session2_87654321"; // 模拟会话ID
session2._created_at = std::time(nullptr);
session2._updated_at = std::time(nullptr);
res = dataManager.insertSession(session2);
ASSERT_TRUE(res);
sleep(2);
Session session3("test_session3");
session3._session_id = "session3_11223344"; // 模拟会话ID
session3._created_at = std::time(nullptr);
session3._updated_at = std::time(nullptr);
res = dataManager.insertSession(session3);
ASSERT_TRUE(res);
sleep(2);
// 获取会话
std::shared_ptr<Session> queriedSession = dataManager.getSession("session1_12345678");
ASSERT_TRUE(queriedSession != nullptr);
std::cout << "session_id: " << queriedSession->_session_id << std::endl;
std::cout << "model_name: " << queriedSession->_model_name << std::endl;
std::cout << "created_at: " << queriedSession->_created_at << std::endl;
std::cout << "updated_at: " << queriedSession->_updated_at << std::endl;
//获取所有会话ID列表(按创建时间戳降序排序)
std::cout << "-----------------获取所有会话ID列表(按创建时间戳降序排序)-----------------" << std::endl;
auto sessions_id1 = dataManager.getAllSessionIds();
for (auto &session_id : sessions_id1.value())
{
std::cout << session_id << std::endl;
}
// 更新会话时间戳
sleep(5);
dataManager.updateSessionTimestamp("session2_87654321", std::time(nullptr));
// 获取所有会话ID列表(按更新时间戳降序排序)
std::cout << "-----------------更新会话时间戳后, 获取所有会话ID列表(按更新时间戳降序排序)-----------------" << std::endl;
auto sessions_id2 = dataManager.getAllSessionIds();
for (auto &session_id : sessions_id2.value())
{
std::cout << session_id << std::endl;
}
// 删除指定会话
std::cout << "-----------------删除指定会话-----------------" << std::endl;
res = dataManager.deleteSession("session3_11223344");
ASSERT_TRUE(res);
// 检查会话总数是否减少
std::cout << "sessionsCount: " << dataManager.getSessionCount() << std::endl;
// 消息操作:
// 插入多个消息
Message message1("user", "你好");
message1._timestamp = std::time(nullptr);
message1._message_id = "message1_12345678"; // 模拟消息ID
res = dataManager.insertMessage("session1_12345678", message1);
ASSERT_TRUE(res);
sleep(2);
Message message2("assistant", "你好, 我是AI助手");
message2._timestamp = std::time(nullptr);
message2._message_id = "message2_12345678"; // 模拟消息ID
res = dataManager.insertMessage("session1_12345678", message2);
ASSERT_TRUE(res);
sleep(2);
Message message3("user", "简单介绍自己");
message3._timestamp = std::time(nullptr);
message3._message_id = "message3_12345678"; // 模拟消息ID
res = dataManager.insertMessage("session1_12345678", message3);
ASSERT_TRUE(res);
sleep(2);
// 获取会话的所有消息(按时间戳升序排序)
std::cout << "-----------------获取会话的所有消息(按时间戳升序排序)-----------------" << std::endl;
auto messages = dataManager.getSessionMessages("session1_12345678");
for (auto &message : messages.value())
{
std::cout << "role: " << message._role << std::endl;
std::cout << "content: " << message._content << std::endl;
std::cout << "timestamp: " << message._timestamp << std::endl;
std::cout << "-----------------" << std::endl;
}
}
6.3 测试用例整体说明
本次测试用例operDatabase用来验证 DataManager 模块的数据库持久化接口能力
DataManager 只负责会话、消息数据的磁盘增删改查,它不关心上层业务逻辑,因此测试中所有会话 ID、消息 ID 都是手动构造的模拟数据,和前面的 SessionManager 相互解耦,保证单元测试可以独立运行,不依赖内存会话管理模块。
测试流程:创建test.db数据库实例,依次执行会话插入、按 ID 查询会话、获取会话 ID 列表(按时间降序)、更新会话时间戳、删除会话、统计会话数量;接着向指定会话插入多条消息,查询该会话下全部消息并打印。用ASSERT_TRUE断言接口返回结果,同时打印字段信息,用来核对排序、数据读写是否符合预期。代码中加入 sleep 制造时间差,用来验证基于时间戳的排序逻辑。
6.4 模块解耦与后续规划
DataManager 仅承担磁盘持久化职责,输入为结构体、字符串 ID,和内存侧 SessionManager 职责分离。
后续业务层会将二者结合:SessionManager 负责内存会话的调度管理,新增、修改、删除会话与消息时,同步调用 DataManager 完成 SQLite 持久化;程序启动时,也会从数据库加载历史会话至内存,实现内存缓存 + 磁盘持久化的完整会话能力。
本用例属于基础冒烟测试,只覆盖正常流程,后续还可以补充异常场景测试,例如查询不存在 ID、重复插入、非法参数等场景。
6.5 CMake 构建配置
bash
# 设置Cmake最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目名称
project(testLLM)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 要求必须使用C++17标准
# 设置构建类型为Debug
set(CMAKE_BUILD_TYPE Debug)
# 添加可执行文件
add_executable(testLLM TestLLM.cpp
../SDK/src/DeepSeekProvider.cpp
../SDK/src/util/MyLog.cpp
../SDK/src/ChatGPTProvider.cpp
../SDK/src/GeminiProvider.cpp
../SDK/src/OllamaLLMProvider.cpp
../SDK/src/LLMManager.cpp
../SDK/src/SessionManager.cpp
../SDK/src/DataManager.cpp)
#设置生成可执行文件的输出目录
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
# ${CMAKE_BINARY_DIR} 表示生成可执行文件的所在目录,即cmake指令执行所在目录
# 添加头文件搜索路径
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../SDK/include)
# ${CMAKE_CURRENT_SOURCE_DIR} 表示当前CMakeLists.txt所在目录
# ${CMAKE_SOURCE_DIR} 表示项目根目录,即所有CMakeLists.txt所在路径中最外层目录
# 查找第三方库:确保在使用前已经通过find_package命令找到并配置了这些库
find_package(GTest REQUIRED)
find_package(fmt REQUIRED)
find_package(spdlog REQUIRED)
find_package(jsoncpp REQUIRED)
find_package(OpenSSL REQUIRED)
# 链接第三方库
target_link_libraries(testLLM gtest fmt spdlog jsoncpp pthread OpenSSL::SSL OpenSSL::Crypto sqlite3)
运行结果展示:

结束语
本章我们完成了
DataManager持久化模块从需求设计、表结构定义、接口编码到单元测试的全部开发工作,借助 SQLite 数据库补齐了 SDK 会话数据持久化的短板。整个模块严格遵循单一职责原则,只负责磁盘侧会话、消息数据的增删改查,和之前的SessionManager内存会话管理保持解耦,两个模块各自独立可测试、可维护。在实现过程中,我们也梳理了 SQLite 开发的常见易错点,同时通过 GTest 冒烟测试验证了全部接口的读写、排序、级联删除等基础能力。
到这里,持久化底层能力已经就绪,后续我们就可以把
SessionManager和DataManager进行整合联动,将内存会话变更同步落盘到数据库,程序启动时自动加载历史会话,真正实现会话数据断电不丢失,完善整套大模型 SDK 的会话链路。