Qt C++ 集成 SQLite 实现本地数据持久化:从原理到宠物投喂器实战
1. 项目概述与核心需求解析最近在做一个宠物智能投喂器的数据管理后台核心需求是把设备上传的各种运行数据持久化存储起来方便后续分析和查看。设备会上传投喂间隔时间、水温、剩余重量这几个关键参数我需要一个轻量、可靠且易于集成的本地数据库方案。经过一番对比最终选择了SQLite并用Qt的C框架来实现整个数据层的增删改查功能。这个组合对于嵌入式设备或桌面应用来说非常合适不需要额外部署数据库服务一个文件搞定所有数据存储。如果你也在做类似的需要本地数据存储的C项目比如物联网设备数据记录、小型桌面应用的用户数据管理或者只是想学习如何在Qt里操作数据库那这篇文章应该能给你提供一套可以直接拿来用的解决方案。我会从为什么选SQLite和Qt开始讲起然后一步步拆解数据库连接、表设计、每一类操作的代码实现最后还会分享几个我实际开发中踩过的坑和调试技巧。整个方案代码量不大但功能完整你可以直接复制代码块到你的项目里修改使用。2. 技术选型为什么是SQLite与Qt2.1 SQLite的核心优势与适用场景在项目初期我评估过几种本地存储方案比如纯文件存储、XML/JSON序列化甚至是更重量级的MySQL嵌入式版本。最后锁定SQLite主要是看中了它以下几个不可替代的优点这些优点完美契合了宠物投喂器这类项目的需求。首先就是零配置和轻量级。SQLite整个数据库就是一个普通的磁盘文件不需要像MySQL或PostgreSQL那样先安装、配置服务、设置用户权限。对于投喂器这种可能跑在资源有限的嵌入式Linux板子或者直接是Windows/Linux桌面端的应用来说部署复杂度直接降为零。它的库文件很小编译进程序后增加的开销几乎可以忽略不计这对于追求启动速度和内存占用的场景至关重要。其次是完整的SQL支持与ACID事务。虽然是个轻量级数据库但SQLite支持绝大部分标准的SQL-92语法我们熟悉的CREATE,INSERT,SELECT,UPDATE,DELETE语句都能用。更重要的是它支持ACID原子性、一致性、隔离性、持久性事务。这意味着即使在向数据库写入投喂记录时程序突然崩溃数据也不会处于半截写入的损坏状态这对于确保设备运行数据的完整性非常关键。你可以用BEGIN TRANSACTION和COMMIT把多条插入语句包起来要么全部成功要么全部回滚。再者是出色的可移植性和广泛的语言绑定。SQLite的数据库文件格式是跨平台的你在Windows上创建的.db文件可以直接复制到Linux或macOS上用同样的代码打开访问。而且几乎所有的编程语言都有成熟的SQLite驱动我用C/Qt只是其中一种选择。这种可移植性为未来可能的设备迁移或数据备份分析提供了极大的便利。最后单用户访问模式恰恰是优点。很多资料会提到SQLite不支持高并发写但这对于我们的宠物投喂器项目来说根本不是问题。数据写入方通常只有设备上传服务这一个进程读取方可能是GUI界面读写频率都很低。这种单文件、单进程优先的访问模型反而避免了配置网络和用户权限的麻烦简化了架构。2.2 Qt SQL模块的简洁与高效选定了SQLite作为存储引擎接下来就是操作它的工具。Qt框架内置的QtSql模块让数据库操作变得异常简单。它提供了一套统一的、面向对象的API来操作不同的数据库SQLite, MySQL, PostgreSQL等你不需要去记各种数据库原生C API的晦涩函数。QSqlDatabase类负责管理数据库连接QSqlQuery类用来执行任何SQL语句并处理结果QSqlTableModel或QSqlQueryModel还能方便地将数据库表和Qt的QTableView等视图组件绑定实现GUI的快速开发。这种抽象层次既屏蔽了底层差异又保留了足够的灵活性。对于我们的增删改查需求主要用到前两个类就足够了。更重要的是Qt的这套API错误处理很清晰。每个可能失败的操作如打开数据库、执行查询都会有一个关联的QSqlError对象你可以很方便地获取错误描述这对于调试和构建健壮的程序非常重要。后面在代码实现部分你会看到我如何利用这一点。3. 项目环境搭建与数据库设计3.1 Qt项目配置与SQL模块引入在开始写代码之前首先要确保你的Qt项目已经正确配置能够使用SQL模块。无论你是用Qt Creator新建项目还是在已有的项目里添加数据库功能步骤都很简单。打开你的Qt项目配置文件通常是.pro文件在里面添加一行QT sql core这里的core是默认包含的写上是为了清晰。这行配置告诉Qt的构建系统qmake或CMake你的项目需要链接QtSql模块。保存后重新执行qmake在Qt Creator里通常是“构建”-“执行qmake”并重新构建项目。接下来在你需要操作数据库的C源文件头部包含必要的头文件#include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug // 用于调试输出QSqlDatabase用于建立连接QSqlQuery用于执行SQL命令QSqlError用于获取错误信息QDebug是我们用来在控制台打印日志的在实际产品中你可能会换成更正式的日志系统。3.2 数据库表结构设计与考量宠物投喂器每次上传的数据包我设计用一张表来存储。表结构的设计直接影响到后续查询的效率和便利性。这是我的petfeeder表结构CREATE TABLE IF NOT EXISTS petfeeder ( id INTEGER PRIMARY KEY AUTOINCREMENT, interval INTEGER NOT NULL, temperature REAL NOT NULL, weight REAL NOT NULL, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP );我来解释一下每个字段的设计考虑id (INTEGER PRIMARY KEY AUTOINCREMENT): 这是主键。AUTOINCREMENT关键字保证每条新记录都会自动获得一个唯一且递增的ID。这不仅是良好的数据库实践便于定位单条记录比如根据id删除或更新而且在后续如果需要关联其他表时也很有用。即使当前只有一张表加上它也是推荐做法。interval (INTEGER NOT NULL): 投喂间隔时间单位是秒。定义为INTEGER类型NOT NULL约束确保每条记录这个字段必须有值避免了数据不完整。temperature (REAL NOT NULL): 水温单位是摄氏度。SQLite的REAL类型对应C的double或float适合存储带小数的温度值。weight (REAL NOT NULL): 剩余重量单位是千克。同样用REAL类型。upload_time (DATETIME DEFAULT CURRENT_TIMESTAMP): 这是一个我强烈建议添加的字段。它记录了数据插入数据库的服务器时间即运行此程序的电脑的时间。DEFAULT CURRENT_TIMESTAMP是SQLite的魔法它会在你执行INSERT语句时自动将当前时间戳填入这个字段你不需要在代码里手动传值。这个时间戳对于数据分析至关重要比如你可以查询“今天上午10点到12点之间的所有投喂记录”或者按小时、按天聚合数据。没有时间戳数据就失去了时序性价值大打折扣。注意这里有一个关键点需要理解。设备上传的数据里可能自带一个“设备时间戳”但那个时间可能不准设备时钟未校准或电池耗尽后重置。而upload_time记录的是数据到达并存入我们数据库的可靠时间。在分析时你可以根据业务需求决定使用哪个时间。我选择优先相信服务器时间。4. 核心功能实现数据库连接与基本操作4.1 建立与关闭数据库连接所有数据库操作的第一步都是建立连接。在Qt中我们使用QSqlDatabase类来管理连接。我习惯将创建连接的逻辑封装成一个函数这样结构清晰也便于在程序启动时调用。bool createConnection() { // 1. 添加一个SQLite类型的数据库连接连接名可以自定义这里用默认连接。 QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); // 2. 设置数据库文件路径。这里使用相对路径数据库文件会生成在程序运行目录下。 db.setDatabaseName(petfeeder.db); // 3. 尝试打开数据库。如果文件不存在SQLite会自动创建一个新的空数据库文件。 if (!db.open()) { qDebug() Failed to connect database: db.lastError().text(); return false; } qDebug() Database connected successfully!; return true; }关键点解析QSqlDatabase::addDatabase(QSQLITE)这里的QSQLITE是驱动名称告诉Qt我们要用SQLite。Qt还支持QMYSQL,QPSQL等。setDatabaseName()参数可以是绝对路径如C:/data/petfeeder.db或相对路径。使用相对路径更便于程序移植。如果文件不存在SQLite会新建它如果存在则打开它。db.open()这是可能失败的操作比如磁盘写保护、路径无权限等。所以必须检查返回值。db.lastError().text()如果打开失败通过这个方法可以获取到可读的错误描述对于调试至关重要。当程序退出或者确定一段时间不再需要访问数据库时应该主动关闭连接。虽然程序结束时连接会自动关闭但显式关闭是好习惯。void closeConnection() { // 获取我们之前建立的默认数据库连接 QSqlDatabase db QSqlDatabase::database(); if (db.isOpen()) { db.close(); qDebug() Database connection closed.; } }4.2 创建数据表的稳健策略连接建立后第一件事就是确保我们需要的表存在。我们不能假设数据库文件是全新的也可能是一个已有的、甚至包含旧版本表的文件。因此创建表的SQL语句应该使用IF NOT EXISTS子句。bool createTable() { QSqlQuery query; QString sql CREATE TABLE IF NOT EXISTS petfeeder ( id INTEGER PRIMARY KEY AUTOINCREMENT, interval INTEGER NOT NULL, temperature REAL NOT NULL, weight REAL NOT NULL, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP); if (!query.exec(sql)) { qDebug() Failed to create table: query.lastError().text(); return false; } qDebug() Table checked/created successfully.; return true; }注意事项SQL字符串的拼接在C中拼接长SQL字符串使用QString的arg()方法或者直接换行拼接都是可以的。确保字符串最终是合法的SQL语法。注意字段定义后面的逗号不要漏掉或多余。错误检查QSqlQuery::exec()执行任何SQL语句都可能失败比如语法错误、表已存在但结构冲突等。务必检查其返回值并通过query.lastError()获取详细信息。表结构变更如果项目迭代中需要为已有的表增加字段比如后期想加一个food_type字段CREATE TABLE IF NOT EXISTS不会修改现有表。你需要额外处理数据库迁移Migration比如写脚本检查表结构版本然后用ALTER TABLE ADD COLUMN语句。对于小型项目也可以在程序启动时尝试添加列并忽略“列已存在”的错误。5. 数据操作CRUD的详细实现与优化5.1 数据插入Create参数化查询防注入插入数据是最常用的操作。设备每上传一次数据我们就执行一次插入。这里有一个极其重要的安全和性能最佳实践使用参数化查询Prepared Statement而不是手动拼接SQL字符串。错误示范存在SQL注入风险且效率低// 危险不要这样做 void insertDataBad(int interval, double temperature, double weight) { QSqlQuery query; QString sql QString(INSERT INTO petfeeder (interval, temperature, weight) VALUES (%1, %2, %3)).arg(interval).arg(temperature).arg(weight); query.exec(sql); // 如果参数中包含SQL特殊字符可能导致注入或语法错误。 }正确做法参数化查询bool insertData(int interval, double temperature, double weight) { QSqlQuery query; // 使用占位符 ? 来表示参数 QString sql INSERT INTO petfeeder (interval, temperature, weight) VALUES (?, ?, ?); query.prepare(sql); // 准备查询语句 // 按照占位符的顺序绑定实际的值 query.addBindValue(interval); query.addBindValue(temperature); query.addBindValue(weight); if (!query.exec()) { qDebug() Failed to insert data: query.lastError().text(); return false; } qDebug() Data inserted, ID: query.lastInsertId().toInt(); // 获取自增ID return true; }为什么必须用参数化查询安全这是最主要的原因。如果interval、temperature等参数来自不可信的用户输入比如网络请求恶意用户可能输入类似0); DROP TABLE petfeeder; --的内容。如果直接拼接最终SQL会变成INSERT ... VALUES (0); DROP TABLE petfeeder; -- ...)导致数据被删除。参数化查询将数据和指令分离数据库引擎会确保传入的值只被当作数据处理永远不会被解释为SQL指令从根本上杜绝了SQL注入攻击。性能对于需要反复执行相同结构SQL语句的操作比如批量插入参数化查询只需编译一次SQL语句然后每次执行只需绑定新参数即可数据库引擎可以复用执行计划显著提升性能。正确性自动处理了数据类型转换和特殊字符转义。比如如果temperature是一个字符串OBrien直接拼接会导致SQL语法错误而参数化查询会正确处理。5.2 数据查询Read灵活筛选与结果遍历查询功能是我们从数据库读取数据的途径。基础查询是获取所有记录但更常见的是根据条件筛选。基础查询获取所有记录void queryAllData() { QSqlQuery query(SELECT id, interval, temperature, weight, upload_time FROM petfeeder ORDER BY upload_time DESC); if (!query.isActive()) { // 检查查询是否成功执行 qDebug() Query failed: query.lastError().text(); return; } while (query.next()) { int id query.value(id).toInt(); int interval query.value(interval).toInt(); double temp query.value(temperature).toDouble(); double weight query.value(weight).toDouble(); QString uploadTime query.value(upload_time).toString(); // SQLite存储为字符串 qDebug() QString(ID:%1, 间隔:%2秒, 水温:%3°C, 重量:%4kg, 时间:%5) .arg(id).arg(interval).arg(temp).arg(weight).arg(uploadTime); } }query.next()用于遍历结果集的每一行。在首次调用前查询结果指针位于第一行之前。每次调用next()指针移动到下一行如果还有数据则返回true否则返回false。query.value()获取当前行指定列的值。参数可以是列的索引从0开始也可以是列的名字字符串如id。我推荐使用列名因为代码可读性更好即使表结构改变列顺序调整代码也不容易出错。ORDER BY upload_time DESC按上传时间降序排列这样最新的记录会显示在最前面符合大多数查看习惯。条件查询带参数化 假设我们需要查询水温高于某个阈值并且剩余重量低于某个阈值的记录用于预警。void queryDataWithCondition(double tempThreshold, double weightThreshold) { QSqlQuery query; query.prepare(SELECT * FROM petfeeder WHERE temperature ? AND weight ? ORDER BY id); query.addBindValue(tempThreshold); query.addBindValue(weightThreshold); if (!query.exec()) { qDebug() Conditional query failed: query.lastError().text(); return; } while (query.next()) { // ... 遍历结果同上 } }5.3 数据更新Update与删除Delete更新和删除操作通常需要精确定位到某一条或某一批记录WHERE子句是关键。同样为了安全和清晰务必使用参数化查询。更新指定ID的记录bool updateData(int id, int newInterval, double newTemperature, double newWeight) { QSqlQuery query; query.prepare(UPDATE petfeeder SET interval?, temperature?, weight? WHERE id?); query.addBindValue(newInterval); query.addBindValue(newTemperature); query.addBindValue(newWeight); query.addBindValue(id); // WHERE条件的值 if (!query.exec()) { qDebug() Failed to update data ID id : query.lastError().text(); return false; } // 检查是否真的有行被更新 if (query.numRowsAffected() 0) { qDebug() Data updated successfully for ID: id; return true; } else { qDebug() No data found with ID: id . Nothing updated.; return false; // 或者根据业务逻辑这可能不算错误 } }query.numRowsAffected()返回受上一次UPDATE、DELETE或INSERT操作影响的行数。这对于判断操作是否真的生效非常有用。比如如果你传了一个不存在的id这个值会是0。删除指定ID的记录bool deleteData(int id) { QSqlQuery query; query.prepare(DELETE FROM petfeeder WHERE id?); query.addBindValue(id); if (!query.exec()) { qDebug() Failed to delete data ID id : query.lastError().text(); return false; } if (query.numRowsAffected() 0) { qDebug() Data deleted successfully for ID: id; return true; } else { qDebug() No data found with ID: id . Nothing deleted.; return false; } }关于批量删除如果需要根据条件批量删除例如删除所有3天前的记录可以这样写query.prepare(DELETE FROM petfeeder WHERE upload_time datetime(now, -3 days));这里用到了SQLite的日期时间函数datetimenow表示当前时间-3 days表示减去3天。这比在C里计算时间再传参更简洁。6. 高级话题与性能优化实践6.1 使用事务提升批量操作性能当需要一次性插入大量数据时比如设备离线一段时间后重新连接上传历史数据逐条执行INSERT语句会非常慢因为每次插入都涉及磁盘I/O和事务日志写入。这时应该使用事务Transaction。事务将一系列数据库操作打包成一个原子单元。在事务内所有操作要么全部成功要么全部失败回滚。对于批量插入使用事务可以带来数十倍甚至上百倍的性能提升。bool insertBatchData(const QListFeedingRecord records) { QSqlDatabase db QSqlDatabase::database(); if (!db.transaction()) { // 开始事务 qDebug() Failed to start transaction: db.lastError().text(); return false; } QSqlQuery query; query.prepare(INSERT INTO petfeeder (interval, temperature, weight) VALUES (?, ?, ?)); foreach (const FeedingRecord record, records) { query.addBindValue(record.interval); query.addBindValue(record.temperature); query.addBindValue(record.weight); if (!query.exec()) { qDebug() Batch insert failed, rolling back: query.lastError().text(); db.rollback(); // 任何一条失败回滚整个事务 return false; } query.finish(); // 为下一次绑定参数准备查询对象 } if (!db.commit()) { // 提交事务所有更改永久生效 qDebug() Failed to commit transaction: db.lastError().text(); db.rollback(); return false; } qDebug() Batch insert of records.size() records successful.; return true; }关键点db.transaction()开始一个事务。在循环内执行query.exec()但错误处理中调用db.rollback()确保一条失败全部撤销。query.finish()在QSqlQuery准备prepare后执行exec前绑定值addBindValue。执行后如果想用同一个query对象准备新的语句需要先调用finish()来重置其状态。在循环中复用同一个query对象比每次都新建一个更高效。db.commit()所有操作成功提交事务。提交后数据才真正写入磁盘。6.2 数据库连接管理策略在稍微复杂的应用中你可能需要在多个类或线程中访问数据库。Qt的数据库连接是可以通过名称来区分的。使用命名连接// 在主线程创建默认连接 QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); db.setDatabaseName(petfeeder.db); // 在另一个模块或线程中如果你想使用不同的连接不推荐多线程共享连接见下文 QSqlDatabase dbWorker QSqlDatabase::addDatabase(QSQLITE, worker_connection); dbWorker.setDatabaseName(petfeeder.db);之后你可以通过QSqlDatabase::database(worker_connection)来获取这个特定名称的连接。关于多线程SQLite本身在默认配置下不支持多线程同时写入。Qt的SQLite驱动可以在多个线程中打开连接但每个连接应该只被创建它的线程使用。最佳实践是在主线程创建和打开数据库连接。如果其他线程需要访问数据库不要直接共享QSqlDatabase或QSqlQuery对象。它们不是线程安全的。应该在线程内部创建自己的数据库连接使用不同的连接名或者通过线程安全的队列将数据库操作请求发送给主线程的一个专门的管理器来执行。6.3 数据库文件维护与备份SQLite数据库是一个单独的文件维护起来相对简单但也有需要注意的地方。文件位置与权限确保应用程序对数据库文件所在目录有读写权限。在Linux嵌入式设备上这点尤其要注意。建议将数据库文件放在用户数据目录如QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)下。备份备份SQLite数据库最简单的方式就是直接复制.db文件。但要注意在复制时数据库可能正处于写入状态。为了获得一个一致性的备份可以在复制前执行以下步骤在程序中执行BEGIN IMMEDIATE TRANSACTION通过QSqlQuery。复制文件。执行ROLLBACK或COMMIT。 或者更简单的方法是使用SQLite的.backup命令需要通过QSqlQuery执行VACUUM INTO backup.db或使用SQLite的C API。对于宠物投喂器这种小应用可以在程序关闭或不进行写操作时例如深夜进行备份。数据库损坏与修复虽然罕见但电源故障或磁盘错误可能导致数据库文件损坏。SQLite提供了命令行工具sqlite3来进行修复尝试sqlite3 corrupted.db .recover | sqlite3 new.db在程序中可以定期比如每月一次执行PRAGMA integrity_check;语句来检查数据库完整性。如果检查失败应提示用户并从备份中恢复。7. 实战中遇到的典型问题与排查技巧7.1 常见错误与解决方案速查表错误现象可能原因解决方案Failed to connect database: unable to open database file1. 数据库文件路径错误或不存在父目录。2. 程序对目标目录没有写权限。3. 数据库文件被其他进程独占锁定如另一个未关闭的SQLite连接。1. 使用绝对路径或确保程序运行目录正确。用QDir::currentPath()打印当前目录检查。2. 检查并修改目录权限。在Linux下可能需要chmod或chown。3. 关闭所有可能占用该文件的程序。检查代码中是否在操作后未调用close()。Failed to create table/insert/update...: no such table: petfeeder1. 创建表的SQL执行失败但被忽略表实际不存在。2. 表名拼写错误SQLite大小写不敏感但需一致。1. 检查创建表的SQL语法确保CREATE TABLE语句成功执行检查返回值并打印错误。2. 使用QSqlQuery执行.tables命令query.exec(SELECT name FROM sqlite_master WHERE typetable;)列出所有表确认表是否存在。Failed to insert data: UNIQUE constraint failed: ...试图插入一条违反唯一性约束的记录例如向定义为PRIMARY KEY或UNIQUE的字段插入了重复值。检查插入的数据确保主键或唯一键字段的值不重复。如果是自增主键不要在INSERT语句中指定其值。查询结果为空但数据应该存在1.WHERE条件太严格或写错。2. 字符串比较时大小写或空格问题。3. 查询语句执行失败但未检查错误。1. 简化WHERE条件先尝试SELECT * FROM table看所有数据。2. 使用SQLite的UPPER()、TRIM()函数处理字符串或使用参数化查询避免拼接错误。3. 在query.exec()后立即检查query.lastError()。程序运行一段时间后变慢1. 未使用事务进行批量插入/更新。2. 数据库文件因频繁增删产生碎片。3. 未对常用查询条件建立索引。1. 对批量操作使用事务。2. 定期如每插入1000次后在空闲时执行VACUUM;命令重建数据库整理碎片注意VACUUM会暂时占用大量磁盘空间。3. 为WHERE或ORDER BY子句中频繁使用的字段创建索引如CREATE INDEX idx_time ON petfeeder(upload_time);。注意索引会加快查询但减慢插入。7.2 调试与日志记录心得在开发数据库相关功能时清晰的日志是快速定位问题的关键。我习惯在每一个数据库操作函数里都加入详细的日志。启用SQLite的调试输出你可以在打开数据库连接后执行以下SQL命令让SQLite将其所有操作输出到控制台仅调试时使用QSqlQuery query; query.exec(PRAGMA vdbe_trace ON;); // 需要SQLite编译时启用调试支持不一定可用更通用的方法是利用Qt的信号槽机制。QSqlDatabase有一个void committed()信号但更细粒度的日志需要自己封装。一个简单有效的方法是创建一个包装函数bool executeQueryWithLog(QSqlQuery query, const QString sql, const QVariantList params QVariantList()) { query.prepare(sql); for (const QVariant param : params) { query.addBindValue(param); } bool success query.exec(); if (!success) { qCritical() [SQL ERROR] Failed to execute query: sql; qCritical() [SQL ERROR] Parameters: params; qCritical() [SQL ERROR] Reason: query.lastError().text(); } else { qDebug() [SQL OK] Executed: sql; // 生产环境可关闭此日志 } return success; }这样所有SQL错误都会被集中、清晰地记录下来。检查数据库文件本身当代码逻辑查不出问题时不妨直接用图形化工具如DB Browser for SQLite打开生成的.db文件直观地查看表结构、数据内容甚至直接在里面执行SQL语句进行测试。这能帮你快速区分是代码逻辑问题还是数据本身的问题。7.3 关于时间戳处理的陷阱前面提到我添加了upload_time DATETIME DEFAULT CURRENT_TIMESTAMP字段。这里有一个细节SQLite的DATETIME类型实际上是以TEXT格式为YYYY-MM-DD HH:MM:SS、REAL儒略日或INTEGERUnix时间戳存储的。CURRENT_TIMESTAMP生成的是UTC时间格式为YYYY-MM-DD HH:MM:SS。如果你需要存储本地时间或者在查询时按本地时间过滤需要小心处理。例如想查询“今天”的记录// 错误这比较的是UTC时间的‘今天’ query.prepare(SELECT * FROM petfeeder WHERE date(upload_time) date(now)); // 更准确的做法如果需要考虑本地时区可以使用本地时间函数但SQLite内置函数有限 // 一种常见做法是在存入时用C代码生成本地时间字符串存进去 QString currentTime QDateTime::currentDateTime().toString(yyyy-MM-dd HH:mm:ss); query.prepare(INSERT INTO petfeeder (..., upload_time) VALUES (..., ?)); query.addBindValue(currentTime); // 然后查询时也基于这个字符串日期 QString today QDate::currentDate().toString(yyyy-MM-dd); query.prepare(SELECT * FROM petfeeder WHERE upload_time LIKE ? || %); query.addBindValue(today);对于时间处理要求高的应用我建议统一用**Unix时间戳整数**存储。使用QDateTime::currentSecsSinceEpoch()获取秒级时间戳存入INTEGER字段。查询和显示时再转换回QDateTime。这样计算和比较都非常简单且与时区无关。8. 一个完整的示例程序框架最后我将上面提到的关键点整合成一个更健壮、更贴近实际项目的示例main.cpp框架。这个框架包含了错误处理、资源清理和基本的使用流程。#include QCoreApplication // 如果是GUI程序则用QApplication #include QSqlDatabase #include QSqlQuery #include QSqlError #include QDebug #include QDateTime // 定义一个结构体来表示一条投喂记录 struct FeedingRecord { int id; int interval; double temperature; double weight; QString uploadTime; }; bool initDatabase() { QSqlDatabase db QSqlDatabase::addDatabase(QSQLITE); // 将数据库放在用户数据目录更规范 QString dbPath QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); QDir dir(dbPath); if (!dir.exists()) dir.mkpath(.); db.setDatabaseName(dir.filePath(petfeeder.db)); if (!db.open()) { qCritical() Cannot open database: db.lastError().text(); return false; } // 启用外键约束如果未来有多表关联 QSqlQuery query; if (!query.exec(PRAGMA foreign_keys ON;)) { qWarning() Failed to enable foreign keys: query.lastError().text(); } // 创建表 QString createTableSQL CREATE TABLE IF NOT EXISTS petfeeder ( id INTEGER PRIMARY KEY AUTOINCREMENT, interval INTEGER NOT NULL, temperature REAL NOT NULL, weight REAL NOT NULL, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP); if (!query.exec(createTableSQL)) { qCritical() Failed to create table: query.lastError().text(); return false; } return true; } bool insertFeedingRecord(int interval, double temperature, double weight) { QSqlQuery query; query.prepare(INSERT INTO petfeeder (interval, temperature, weight) VALUES (?, ?, ?)); query.addBindValue(interval); query.addBindValue(temperature); query.addBindValue(weight); if (!query.exec()) { qCritical() Insert failed: query.lastError().text(); return false; } qDebug() Record inserted, ID: query.lastInsertId().toInt(); return true; } QListFeedingRecord queryRecentRecords(int limit 10) { QListFeedingRecord records; QSqlQuery query; query.prepare(SELECT * FROM petfeeder ORDER BY upload_time DESC LIMIT ?); query.addBindValue(limit); if (!query.exec()) { qCritical() Query failed: query.lastError().text(); return records; } while (query.next()) { FeedingRecord record; record.id query.value(id).toInt(); record.interval query.value(interval).toInt(); record.temperature query.value(temperature).toDouble(); record.weight query.value(weight).toDouble(); record.uploadTime query.value(upload_time).toString(); records.append(record); } return records; } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); // GUI程序改为QApplication // 1. 初始化数据库 if (!initDatabase()) { qCritical() Database initialization failed. Exiting.; return -1; } // 2. 模拟插入一些数据 insertFeedingRecord(5, 26.5, 0.8); insertFeedingRecord(3, 25.0, 0.6); insertFeedingRecord(4, 27.0, 0.5); // 3. 查询并打印最近记录 qDebug() --- Recent Feeding Records ---; QListFeedingRecord recent queryRecentRecords(5); for (const FeedingRecord record : recent) { qDebug() QString(ID:%1 | 间隔:%2秒 | 水温:%3°C | 重量:%4kg | 时间:%5) .arg(record.id).arg(record.interval) .arg(record.temperature, 0, f, 1) // 格式化浮点数保留1位小数 .arg(record.weight, 0, f, 2) .arg(record.uploadTime); } // 4. 程序退出前Qt会自动关闭数据库连接但显式关闭是好习惯 QSqlDatabase::database().close(); // 如果是GUI程序这里应该是 return app.exec(); return 0; }这个框架展示了从初始化、插入、查询到关闭的完整生命周期。你可以以此为基础扩展出更复杂的业务逻辑比如定时清理旧数据、数据导出、图表展示等。记住良好的错误处理、资源管理和日志记录是构建稳定可靠的数据持久层的基础。