小叶-duck头像
《C++ 项目:手写大模型接入 SDK》DataManager数据管理模块:SQLite持久化存储封面图

《C++ 项目:手写大模型接入 SDK》DataManager数据管理模块:SQLite持久化存储

 

🔥小叶-duck:个人主页

 ❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
《Qt 方寸极境》 《MySQL》

✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

前言

一、模块整体需求

  1.1 业务目标

  1.2 数据库表设计

    1.2.1 表关联约束

  1.3 并发线程安全要求

  1.4 DataManager 模块职责

二、公共数据结构体定义 common.h

  2.1 Session 会话结构体

三、DataManager 头文件 DataManager.h

  3.1 完整头文件代码

  3.2 接口业务逻辑要点

四、DataManager 设计规范要点

五、DataManager.cpp 实现与代码分析

  5.1 构造函数、析构函数

  5.2 数据库初始化 initDatabase

  5.3 通用无结果 SQL 执行工具 executeSQL

  5.4 Session 会话业务接口实现

    5.4.1 insertSession 插入会话

    5.4.2 getSession 根据 sessionId 获取会话

    5.4.3 updateSessionTimestamp 更新会话时间戳

    5.4.4 deleteSession 删除会话(级联删除消息)

    5.4.5 getAllSessionIds 获取全部会话 ID(按更新时间倒序)

    5.4.6 getAllSessions 获取全部会话元数据

    5.4.7 getSessionCount 获取会话总数

  5.5 Message 消息业务接口实现

    5.5.1 insertMessage 插入消息并更新会话时间

    5.5.2 getSessionMessages 查询会话全部消息(对外接口)

    5.5.3 getSessionMessagesInternal 私有内部查询(不加锁)

    5.5.4 deleteSessionMessages 手动删除会话消息

  5.6 SQLite API 核心原理与易错点小结

六、DataManager 单元测试(TestLLM.cpp)

  6.1 测试入口 main 函数

  6.2 测试用例代码

  6.3 测试用例整体说明

  6.4 模块解耦与后续规划

  6.5 CMake 构建配置

结束语


前言

      在前面章节我们完成了SessionManager内存会话管理模块的开发,它可以在程序运行期间维护会话与消息,但是内存数据有明显短板:程序退出之后所有会话数据都会丢失,无法实现历史对话持久化。为了解决这个问题,本章我们引入 SQLite 轻量数据库,设计并实现DataManager数据持久化管理模块。

      本章会先明确模块的业务需求、数据表结构以及线程安全约束,定义会话与消息的公共结构体,再完成DataManager头文件声明、接口实现,同时梳理 SQLite 的调用要点与常见坑点。最后基于 GTest 编写单元测试,单独验证数据库读写接口的正确性。

      需要重点说明的是,DataManager和SessionManager遵循解耦设计:DataManager 只专注会话、消息的磁盘增删改查,不关心上层业务逻辑;测试时直接使用模拟的会话、消息数据,不依赖内存会话模块。在完成本模块的验证之后,我们再把内存会话管理和磁盘持久化模块进行联动,实现 “内存缓存 + 数据库落盘” 的完整会话能力,让我们的大模型 SDK 具备历史对话持久保存的能力。

一、模块整体需求

  1.1 业务目标

      我们前面实现的 SessionManager 只会在内存中维护会话与消息,程序退出后数据全部丢失。本模块的核心目标,就是为 AI 聊天 SDK 提供持久化能力,将会话Session、单条消息Message数据落地保存到 SQLite 嵌入式数据库中,实现对话记录永久存储。

  1.2 数据库表设计

我们采用一对多的表结构,设计两张核心数据表:

  1. sessions 会话表:存储会话的元信息,包含会话 ID、绑定模型、创建时间、更新时间等基础字段,session_id作为本表主键。
  2. 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 会话结构体

// 会话信息:保存一轮对话的上下文与历史消息
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 完整头文件代码

#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 接口业务逻辑要点

  1. 接口分层设计:对外接口全部加锁,保证线程安全;单独抽离不加锁的私有内部接口getSessionMessagesInternal,给类内部其他函数调用,防止递归加锁引发死锁。
  2. 返回值使用 std::optional:区分 “执行失败” 和 “查询结果为空” 两种情况,上层更容易判断数据库操作状态。
  3. 资源生命周期:构造函数完成数据库打开、初始化建表;析构函数负责关闭 sqlite3 句柄,释放数据库资源。
  4. 职责边界清晰:只负责数据持久化,不做会话业务逻辑,和上层 SessionManager 解耦。

四、DataManager 设计规范要点

在后续DataManager.cpp实现中,需要遵循下面这套设计规范:

  1. 插入消息自动刷新会话时间:调用insertMessage新增消息时,同步更新对应会话updated_at字段,保证会话列表排序是最新交互优先。
  2. 外键级联删除:依靠数据库外键约束,删除会话时自动清理下属消息,不需要在代码里手动遍历删除消息,简化逻辑、保证一致性。
  3. 会话排序规则:查询会话列表统一按照updated_at时间戳降序,用户优先看到最近交互过的会话。
  4. 消息时序规则:读取消息时按时间戳升序加载,保证聊天消息顺序符合对话逻辑。
  5. 完整对象组装:查询会话元数据之后,自动关联查询消息记录,组装成完整的 Session 对象返回给上层。
  6. 线程安全:所有对外读写接口都加互斥锁,多线程并发访问数据库不会出现竞争问题。
  7. 防 SQL 注入:所有查询、新增、修改操作,全部使用sqlite3_prepare_v2预编译 + 参数绑定,不直接拼接 SQL 字符串,规避注入风险。

五、DataManager.cpp 实现与代码分析

  5.1 构造函数、析构函数

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());
    }
}

分析:

  1. 构造函数职责:接收数据库文件名,调用sqlite3_open打开数据库;文件不存在时 SQLite 会自动创建。打开成功后立刻执行PRAGMA foreign_keys = ON;,开启外键级联删除,这是容易遗漏的关键点。最后调用initDatabase自动建表。
  2. 析构函数职责:判断句柄有效时调用sqlite3_close关闭数据库连接,释放资源,避免句柄泄漏。
  3. 异常处理:打开失败时置空_db,后续所有接口先判断_db == nullptr,防止空指针访问。
  4. 注意:构造、析构只执行一次,不需要加锁;锁只用于并发读写业务接口。

  5.2 数据库初始化 initDatabase

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;
}

分析:

  1. 使用CREATE TABLE IF NOT EXISTS,重复初始化不会报错,适配程序多次启动。
  2. 表字段设计:sessions存储会话元信息;messages通过外键绑定会话 ID,ON DELETE CASCADE实现会话删除时自动清理消息。
  3. 调用前面封装的executeSQL执行建表语句,失败直接返回 false,终止初始化流程。

  5.3 通用无结果 SQL 执行工具 executeSQL

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;
}

分析:

  1. 适用场景:DDL 建表、PRAGMA、无返回集的INSERT/UPDATE/DELETE,不需要读取查询结果的 SQL 语句。
  2. sqlite3_exec回调传 nullptr,不处理行数据;出错时必须调用sqlite3_free释放 errMsg 内存,防止内存泄漏。
  3. 这是基础工具函数,供初始化、简单 SQL 调用;查询类、带参数的语句我们统一使用sqlite3_prepare_v2预编译方案,不用这个函数,规避 SQL 注入。

  5.4 Session 会话业务接口实现

      所有对外业务接口第一行使用std::lock_guard<std::mutex> lock(_dbMutex)自动加锁,函数退出自动释放锁,保证线程安全。

    5.4.1 insertSession 插入会话

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;
}
  1. 使用预编译语句,?占位符,sqlite3_bind_xxx绑定参数,杜绝 SQL 注入。
  2. SQLITE_TRANSIENT代表 SQLite 内部拷贝一份字符串,函数结束后外部字符串销毁也不会野指针。
  3. sqlite3_step返回SQLITE_DONE代表插入完成;无论成功失败,必须调用sqlite3_finalize释放 stmt 资源。
  4. 只写入会话元数据,消息单独通过insertMessage写入。

    5.4.2 getSession 根据 sessionId 获取会话

// 获取指定会话:根据会话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;
}
  1. 先查询sessions拿到会话元信息;sqlite3_step返回SQLITE_ROW代表查到一行记录。
  2. 重点:消息加载调用私有不加锁接口getSessionMessagesInternal,避免同一个锁重复加锁,防止死锁。
  3. 组装完整 Session 对象,把消息列表填充进去,上层拿到的 Session 是完整可直接使用的会话数据。
  4. 查询不到会话直接返回 nullptr,上层可以判断会话是否存在。

    5.4.3 updateSessionTimestamp 更新会话时间戳

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;
}
  1. 核心用途:新增一条消息后,同步刷新会话的update_time,会话列表按更新时间倒序展示,保证最新会话排在最前面。
  2. 只更新时间字段,轻量操作;在insertMessage内部会调用这个接口。

    5.4.4 deleteSession 删除会话(级联删除消息)

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;
}
  1. 只删除sessions表记录,消息清理依靠外键 ON DELETE CASCADE,不需要手动调用删除消息接口。
  2. 前提:数据库打开时已经执行PRAGMA foreign_keys = ON;,否则级联失效。

    5.4.5 getAllSessionIds 获取全部会话 ID(按更新时间倒序)

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;
}
  1. ORDER BY update_time DESC,最新交互的会话排在前面,适配前端会话列表展示。
  2. 返回std::optional,区分:查询失败(nullopt) 和 查询成功但是没有会话(空 vector)。
  3. 循环sqlite3_step逐行读取,直到SQLITE_DONE读取完毕。

    5.4.6 getAllSessions 获取全部会话元数据

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 获取会话总数

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 插入消息并更新会话时间

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;
}
  1. 先写入消息记录;写入成功之后,立刻调用updateSessionTimestamp刷新会话更新时间,保证会话列表排序是最新交互优先。
  2. 消息和会话时间更新是两次独立 SQL,在更高级版本可以用事务包裹,保证原子性。

    5.5.2 getSessionMessages 查询会话全部消息(对外接口)

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 私有内部查询(不加锁)

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 手动删除会话消息

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 核心原理与易错点小结

  1. sqlite3_prepare_v2:预编译 SQL 语句,生成 stmt 语句句柄,支持参数绑定,是防注入的核心。
  2. bind 系列函数:绑定?占位参数;SQLITE_TRANSIENT标记字符串,让 SQLite 拷贝数据。
  3. sqlite3_step:执行 SQL,返回SQLITE_ROW代表读到一行数据,SQLITE_DONE代表语句执行完成。
  4. sqlite3_column_xxx:读取当前行指定列的数据。
  5. sqlite3_finalize:必须调用,释放 stmt 资源,否则内存泄漏。
  6. 锁的分层设计:对外接口加锁、内部查询接口不加锁,是这套 DataManager 防死锁的核心设计。
  7. 外键开关:SQLite 默认关闭外键,每次打开连接后都要执行PRAGMA foreign_keys = ON;,否则级联删除失效。

六、DataManager 单元测试(TestLLM.cpp)

  6.1 测试入口 main 函数

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 测试用例代码

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 构建配置

# 设置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 的会话链路。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2501_91275995/article/details/170652824

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--