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

🔥小叶-duck:个人主页

❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》

《Linux系统从入门到实践》《Linux网络从入门到实践》

《Qt 方寸极境》 《MySQL》

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


目录

前言

一、模块整体需求

[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 数据库表设计

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

  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 会话结构体

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

  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 构造函数、析构函数

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

分析:

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

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

分析:

  1. 使用CREATE TABLE IF NOT EXISTS,重复初始化不会报错,适配程序多次启动。
  2. 表字段设计:sessions存储会话元信息;messages通过外键绑定会话 ID,ON DELETE CASCADE实现会话删除时自动清理消息。
  3. 调用前面封装的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;
}

分析:

  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 插入会话

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;
}
  1. 使用预编译语句,?占位符,sqlite3_bind_xxx绑定参数,杜绝 SQL 注入。
  2. SQLITE_TRANSIENT代表 SQLite 内部拷贝一份字符串,函数结束后外部字符串销毁也不会野指针。
  3. sqlite3_step返回SQLITE_DONE代表插入完成;无论成功失败,必须调用sqlite3_finalize释放 stmt 资源。
  4. 只写入会话元数据,消息单独通过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;
}
  1. 先查询sessions拿到会话元信息;sqlite3_step返回SQLITE_ROW代表查到一行记录。
  2. 重点:消息加载调用私有不加锁接口getSessionMessagesInternal,避免同一个锁重复加锁,防止死锁。
  3. 组装完整 Session 对象,把消息列表填充进去,上层拿到的 Session 是完整可直接使用的会话数据。
  4. 查询不到会话直接返回 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;
}
  1. 核心用途:新增一条消息后,同步刷新会话的update_time,会话列表按更新时间倒序展示,保证最新会话排在最前面。
  2. 只更新时间字段,轻量操作;在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;
}
  1. 只删除sessions表记录,消息清理依靠外键 ON DELETE CASCADE,不需要手动调用删除消息接口。
  2. 前提:数据库打开时已经执行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;
}
  1. ORDER BY update_time DESC,最新交互的会话排在前面,适配前端会话列表展示。
  2. 返回std::optional,区分:查询失败(nullopt) 和 查询成功但是没有会话(空 vector)。
  3. 循环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;
}
  1. 先写入消息记录;写入成功之后,立刻调用updateSessionTimestamp刷新会话更新时间,保证会话列表排序是最新交互优先。
  2. 消息和会话时间更新是两次独立 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 核心原理与易错点小结

  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 函数

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

相关推荐
Evand J14 分钟前
【MATLAB例程,PMSM19】永磁同步电机(PMSM)基于多速率无迹卡尔曼滤波(UKF)的转速与转子位置估计。测量缺失、状态预测与恢复性能分析
开发语言·matlab·电机·ekf·卡尔曼·电机滤波
喵了几个咪23 分钟前
GoWind Flow 拆解:一个 Go 工作流引擎与它的双模式设计器
开发语言·人工智能·后端·golang
꧁细听勿语情꧂1 小时前
二叉树余题,排序、直接插入排序、希尔排序
c语言·开发语言·数据结构·算法
是main不是漫1 小时前
【C++——类和对象(上)】筑基篇:以封装奠定面向对象之基
c语言·开发语言·c++
唐老板1 小时前
uuidv7 来了:为什么别再拿 uuidv4 当主键了
数据库
apcipot_rain5 小时前
C++转Java日记(06)
java·开发语言
小七在进步12 小时前
string的使用(1)
c++·visual studio
SomeOtherTime12 小时前
Rust所有权(AI回答)
开发语言·后端·rust
Easyhood12 小时前
Android 开发入门实践(十四):Compose 副作用入门与四个常用 API
android·开发语言·kotlin·android jetpack·compose