加载中...
sqlite-orm-迅速上手.下
第1节:libfswatch-文件变动监控
第2节:libiconv-字符集编码转换
第3节:CLI11-命令行参数解析
第4节:nlohmann/json-自然的JSON库
第5节:libb64-理解并玩转base64编码
第6节:libSnappy-快速压缩工具
第7节:spdlog-首选的C++日志库
第8节:libUSB-脱掉USB的外套
第9节:libxlsxwriter-让数据说话
第10节:sqlite-orm-迅速上手.上
第11节:sqlite-orm-迅速上手.下
第12节:libExiv2-读写图像元数据
第13节:webview-让浏览器颤抖的力量
第14节:pystring-躺平的C++字符串工具库
第15节:cpr-自制Deepseek心理陪聊师
第16节:ExprTk-计算的力量
课文封面

以家庭行为记录小机器为例,学习——

  1. 通过聚合操作挖掘数据信息(count/sum/max/min…)
  2. 通过连接操作,实现跨表查询(join-on)
  3. “预处理” 在sqlite-orm 中作用与性能影响
  4. 索引、外键等更多建表、建库配置以及实体库应用

1. 分组与聚合

聚合(aggregate):对一组数据进行计算、 合并或汇总,最终得到一个单一结果的过程。在 SQLite 中,常用聚合函数有:

  • count :计数(典型如:统计符合条件的记录有几条)
  • sum : 总和:忽略统计目标字段为空值(NULL)的记录,因此返回结果也有可能为NULL(比如,查无符合条件的待统计记录)
  • total : 求和:将目标字段为空值(NULL)的记录数据,视为浮点数:0.0 参与统计,因此返回结果不可能为NULL
  • avg : 求平均值,结果可为NULL
  • max/min : 求一组记录中指定数据中最大的最小的值(典型如:求时间的最早或最晚)
  • group_concat : 将一组记录的指定字段的值(通常为字符串类型)拼成更长的字符串

聚合操作通常基于按字段值分组后的数据。

2. 联合查询

联合(JOIN)查询也称为连接查询,其连接方式又包括:内连接(INNER JOIN)、左外连接(LEFT OUTER JOIN)、右外连接(RIGHT OUTER JOIN)、交叉连接(CROSS JOIN)以及 sqlite 不直接支持的全连接(FULL JOIN)。

sqlite join分类

正如名字所表达,联合查询允许我们跨多张表查询,即结果数据包含来自多张表的字段。

3. 预处理 / prepare

想要查询名为 “Tom” 的用户所输入的密码(“1234”)是否正确(通常用于登录操作),SQL语句内容如下:

SELECT id, nick FROM users WHERE nick='Tom' AND pwd = '1234';

如果 Tom 的在系统中的密码不是 “1234”,则上述查询将查无结果。但入侵者可以输入密码内容为 'OR '1'='1,如果开发者在组织上述 SQL 语句时,仅是简单的拼接(比如使用 + 相加),将得到如下 SQL 语句:

SELECT id, nick FROM users WHERE nick='Tom' AND pwd = '' OR '1'='1'

查询条件变成:昵称是 Tom 并且密码是空串 ** 或者 ** 1=1 —— 这将永远成立!

这种入侵手段称为 “SQL 注入”, prepare 是用于解决这一安全问题的关键方法。在 sqlite-orm 中,prepare 也可以帮助我们实现对同一 SQL 语句进行一次 prepare(预备),然后多次 execute (执行),从而提升程序性能。

4. 建库配置

关于建库(表、字段),之前我们已经学习过:

  1. 设定某字段为主键且自增:primary_key().autoincrement();
  2. 设定某字段的值,必须全表唯一: unique();
  3. 设定某字段的值,可为NULL,或不可为NULL:not_null() / null();
  4. 设定某字段在库中的默认值:default_value(默认值)。

本小节我们还将学习:① 如何创建索引(含 index 和 unique_index)、② 如何创建外键(foreign_key)、③ 从内存表切换为实体文件表。

创建索引及外键时,需注意两个和代码次序有关的“坑”——

  • 在 make_storage() 中 ,要先写创建索引的代码,再写创建字段的代码;
  • 在 make_table () 中,要先写完所有创建列的代码,再写创建外键的代码。

5. 完整代码

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.15) project(helloSQLiteORM) SET(CMAKE_CXX_STANDARD 17) # 请将路径更换为你的路径 FIND_LIBRARY(LIBSQLite3 sqlite3 PATHS "C:/msys64/ucrt64/lib") ADD_EXECUTABLE(${PROJECT_NAME}_2 main2.cpp) TARGET_LINK_LIBRARIES(${PROJECT_NAME}_2 ${LIBSQLite3}) TARGET_INCLUDE_DIRECTORIES(${PROJECT_NAME}_2 PRIVATE "C:/msys64/ucrt64/include")\
  • main2.cpp
#include <iostream> #include <iomanip> #include <sstream> #include <optional> #include <string> #include <exception> #include <vector> #include <sqlite_orm/sqlite_orm.h> void line(int len = 80, char c = '-') { std::cout << '\n' << std::string(len, c) << std::endl; } // 家庭成员 struct FamilyMember { int id = -1; // 主键,自增长 std::string role; // 角色昵称,不可重复 std::optional<std::string> tel; // 手机,NULL,可以重复 std::string birthday; // 出生日期,不得为空 std::string avatar = "0.png"; // 头像,NOT NULL,默认为 "0.png" bool is_admin = false; // 是否管理员,默认 false }; void print(FamilyMember const& m) { std::cout << "#" << m.id << ' ' << std::setw(10) << std::left << m.role; std::cout << std::setw(16) << std::left << (m.is_admin ? "\t管理员" : "\t普通用户") << "\t"; std::cout << m.birthday << "\t" << std::setw(11) << (m.tel? *m.tel : "~") << "\t" << m.avatar; line(); } // 行为名称 std::string const ActionNames[12] = { "未知", "早起", "早睡", "大解", "小解", "洗澡", "正餐", "零食", "健身", "服药", "晚睡", "熬夜" }; std::string const& GetActionName(int i) { static std::string const unknow = "!非法!"; if (i < 0 || i >= std::size(ActionNames)) { return unknow; } return ActionNames[i]; } // 家庭家庭成员活动记录 struct FamilyAction { int id = -1; int member_id = 0; // 成员ID std::string role; // 角色,可重复 int action = 0; // 活动类型 int rating = 0; // 行为评分(分正负) std::string datetime; // 活动的日期时间 }; void print(FamilyAction const& a) { auto role_with_member_id = a.role + "(" + std::to_string(a.member_id) + ")"; std::cout << "#" << a.id << " " << std::setw(10) << std::left << role_with_member_id << "\t" << GetActionName(a.action) << '\t' << a.rating << '\t' << a.datetime; line(); } namespace orm = sqlite_orm; auto create_storage(std::string db_file) { return orm::make_storage(std::move(db_file) // “坑”:创建索引的代码,要写在(创建表)的代码前面 :( // 因为:sync_schema 里的代码被执行的次序,是从后往前 , orm::make_unique_index("idx_family_members_role", &FamilyMember::role) , orm::make_index("idx_family_member_birthday", &FamilyMember::birthday) , orm::make_table("family_members" , orm::make_column("id", &FamilyMember::id, orm::primary_key().autoincrement()) , orm::make_column("role", &FamilyMember::role, orm::unique()) , orm::make_column("tel", &FamilyMember::tel) , orm::make_column("birthday", &FamilyMember::birthday, orm::not_null()) , orm::make_column("avatar", &FamilyMember::avatar, orm::default_value("0.png")) , orm::make_column("is_admin", &FamilyMember::is_admin, orm::default_value(false)) ) // end talbe: family_members , orm::make_index("idx_family_actions_member_id", &FamilyAction::member_id) , orm::make_index("idx_family_actions_role", &FamilyAction::role) , orm::make_index("idx_family_actions_create_time", &FamilyAction::datetime) , orm::make_table("family_actions" , orm::make_column("id", &FamilyAction::id, orm::primary_key().autoincrement()) , orm::make_column("mid", &FamilyAction::member_id) // 对应的成员ID , orm::make_column("role", &FamilyAction::role, orm::not_null()) , orm::make_column("action", &FamilyAction::action, orm::default_value(0)) , orm::make_column("rating", &FamilyAction::rating, orm::default_value(0)) , orm::make_column("create_time", &FamilyAction::datetime, orm::default_value("")) // 创建外键 (必须在所有列创建语句之后写) , orm::foreign_key(&FamilyAction::member_id).references(&FamilyMember::id) .on_delete.cascade() ) ); } using Storage = decltype(create_storage("")); #define i_am_here std::cout << "\n[" << __FUNCTION__ << "]\n" // 准备数据 void prepare_data(Storage& storage) { FamilyMember members [] = { {.role="爸爸", .tel = "13900001234", .birthday = "1980-04-11"} , {.role="妈妈", .tel = "13411118888", .birthday = "1984-11-21", .is_admin = true} , {.role="大宝", .tel = "19901111111", .birthday = "2011-05-20", .avatar = "rabbit.png"} , {.role="二宝", .birthday = "2020-04-07", .avatar = "baby.png"} , {.role="二大爷", .birthday = "2025-09-01", .avatar = "robot.jpg"} }; for (auto& m : members) { m.id = storage.insert(m); } auto getMemberID = [&](std::string const& role) -> int { auto it = std::find_if(std::cbegin(members), std::cend(members), [&](auto const& m) { return m.role == role; }); return (it != std::cend(members)? it->id : 0); }; FamilyAction actions [] = { {.role="爸爸", .action=1, .rating=1, .datetime="2025-09-10 06:30:56"}, // 早起 {.role="爸爸", .action=3, .rating=-1, .datetime="2025-09-10 06:55:08"}, // 大解 {.role="妈妈", .action=1, .rating=1, .datetime="2025-09-10 06:45:12"}, // 早起 {.role="大宝", .action=1, .rating=1, .datetime="2025-09-10 07:15:44"}, // 早起 {.role="大宝", .action=6, .rating=0, .datetime="2025-09-10 07:50:11"}, // 正餐(早餐) {.role="妈妈", .action=4, .rating=0, .datetime="2025-09-10 08:15:00"}, // 小解 {.role="爸爸", .action=6, .rating=1, .datetime="2025-09-10 08:00:00"}, // 正餐(早餐) {.role="妈妈", .action=6, .rating=0, .datetime="2025-09-10 08:15:00"}, // 正餐(早餐) {.role="二宝", .action=3, .rating=0, .datetime="2025-09-10 08:30:40"}, // 大解 {.role="二宝", .action=6, .rating=1, .datetime="2025-09-10 08:45:00"}, // 正餐(早餐) {.role="爸爸", .action=8, .rating=1, .datetime="2025-09-10 09:30:00"}, // 健身 {.role="妈妈", .action=8, .rating=2, .datetime="2025-09-10 10:00:00"}, // 健身 {.role="大宝", .action=7, .rating=-1, .datetime="2025-09-10 10:30:00"}, // 零食 {.role="爸爸", .action=6, .rating=0, .datetime="2025-09-10 12:00:00"}, // 正餐(午餐) {.role="妈妈", .action=6, .rating=1, .datetime="2025-09-10 12:15:00"}, // 正餐(午餐) {.role="二宝", .action=6, .rating=1, .datetime="2025-09-10 12:45:00"}, // 正餐(午餐) {.role="二大爷", .action=6, .rating=0, .datetime="2025-09-10 13:00:00"}, // 正餐(充电) {.role="二宝", .action=7, .rating=-2, .datetime="2025-09-10 15:00:00"}, // 零食 {.role="爸爸", .action=5, .rating=0, .datetime="2025-09-10 18:00:00"}, // 洗澡 {.role="妈妈", .action=5, .rating=0, .datetime="2025-09-10 18:30:00"}, // 洗澡 {.role="大宝", .action=5, .rating=1, .datetime="2025-09-10 19:00:00"}, // 洗澡 {.role="二宝", .action=5, .rating=2, .datetime="2025-09-10 19:30:00"}, // 洗澡 {.role="爸爸", .action=6, .rating=0, .datetime="2025-09-10 19:00:00"}, // 正餐(晚餐) {.role="妈妈", .action=6, .rating=1, .datetime="2025-09-10 19:15:00"}, // 正餐(晚餐) {.role="大宝", .action=6, .rating=1, .datetime="2025-09-10 19:30:00"}, // 正餐(晚餐) {.role="二宝", .action=6, .rating=1, .datetime="2025-09-10 19:45:00"}, // 正餐(晚餐) {.role="爸爸", .action=9, .rating=-1, .datetime="2025-09-10 22:00:00"}, // 服药 {.role="爸爸", .action=2, .rating=2, .datetime="2025-09-10 22:00:00"}, // 早睡 {.role="妈妈", .action=2, .rating=1, .datetime="2025-09-10 22:15:00"}, // 早睡 {.role="二宝", .action=2, .rating=0, .datetime="2025-09-10 21:00:00"}, // 早睡 {.role="大宝", .action=11, .rating=-3, .datetime="2025-09-11 00:25:30"}, // 熬夜 }; for(auto& a : actions) { a.member_id = getMemberID(a.role); a.id = storage.insert(a); } for (auto const& a : actions) { std::cout << storage.dump(a) << std::endl; } } // 查询全家活动记录中,最早和最晚的两条 void min_max_time_actions(Storage& storage) { i_am_here; auto first = storage.min(&FamilyAction::datetime); auto last = storage.max(&FamilyAction::datetime); if (first && last) { std::cout << "家庭行为最早发生于:" << *first << ",最晚结束于:" << *last << std::endl; } } // 查询总记录数,以及爸爸的活动次数 void count_on_daddy_actions(Storage& storage) { i_am_here; int all = storage.count<FamilyAction>(); // select count(*) // select count(id) from family_actions where role = "爸爸" int c = storage.count(&FamilyAction::id , orm::where(orm::c(&FamilyAction::role) == "爸爸")); std::cout << "在 " << all << " 条记录中,爸爸的活动记录有 " << c << " 条" << std::endl; } // 查询每个角色的活动记录数 void select_count_on_memebers_actions(Storage& storage) { i_am_here; auto rows = storage.select(orm::columns(&FamilyAction::role, orm::count(&FamilyAction::id)) , orm::group_by(&FamilyAction::role)); std::cout << "角色\t活动记录数"; line(); for (auto const& row : rows) { std::cout << std::get<0>(row) << '\t' << std::get<1>(row); line(); } } // 得分排行榜:累计每个角色的得分,并且从高到低排列 void rates_ranking(Storage& storage) { i_am_here; auto rating_sum = orm::sum(&FamilyAction::rating); auto rows = storage.select(orm::columns(&FamilyAction::role, rating_sum) , orm::group_by(&FamilyAction::role) , orm::order_by(rating_sum).desc()); std::cout << "名次\t角色\t得分"; line(); int o = 1; for (auto const& row : rows) { std::cout << o << '\t' << std::get<0>(row) << '\t' << *std::get<1>(row); line(); ++o; } } // 连接成员与活动表,通过 role 得到带头像的活动信息记录 void join_action_on_role_with_avatar(Storage& storage) { i_am_here; auto rows = storage.select( orm::columns( &FamilyMember::id, &FamilyMember::role, &FamilyMember::tel, &FamilyMember::avatar // 0,1,2,3 , &FamilyAction::id, &FamilyAction::action, &FamilyAction::rating) //4,5,6 , orm::join<FamilyAction>(orm::on(orm::c(&FamilyMember::role) == &FamilyAction::role)) , orm::where(orm::c(&FamilyAction::rating) > 0) ); std::cout << "AID\tMID\t角色\t活动\t评分\t联系电话\t头像"; // 4,0,1,5,6,2,3 line(); for (auto r : rows) { std::cout << std::get<4>(r) << '\t' << std::get<0>(r) << '\t' << std::get<1>(r); // 4,0,1 std::cout << '\t' << GetActionName(std::get<5>(r)) << '\t' << std::get<6>(r) << '\t'; // 5, 6 std::cout << std::setw(11) << std::get<2>(r).value_or("~") << '\t' << std::get<3>(r); line(); } } // prepare & execute 语句 void prepare_and_execute(Storage& storage) { // 使用 ORM 方式,取得家庭活动记录中,create_time 的时间部分: // substr : 从指定字符位置开始,取指定长度的片段,注意:下标从1开始 // -8 : HH:MM:SS (正向取的话,相当从 12 开始取) auto times = storage.select(orm::substr(&FamilyAction::datetime, -8) , orm::from<FamilyAction>(), orm::limit(4)); for (auto const& t : times) { std::cout << t << '\n'; } line(80, '='); // 使用预备(预处理)语句,取指定时间(小时)以后,到次日 0 点前的活动记录 auto selectStatement = storage.prepare(orm::select( orm::columns(&FamilyAction::id, &FamilyAction::role, &FamilyAction::datetime), // columns orm::where(orm::substr(&FamilyAction::datetime, -8) >= "12:00:00") ) // select ); // prepare std::cout << "\nORM 自行组装的 SQL 语句:\n"; std::cout << storage.dump(selectStatement) << "\n"; std::cout << "\n底层数据库 sqlite 生成的 SQL 语句:\n"; std::cout << selectStatement.sql() << "\n"; std::cout << "\nPreare 语句存在的参数:\n"; std::cout << orm::get<0>(selectStatement) << "\n"; std::cout << orm::get<1>(selectStatement) << "\n"; auto rows = storage.execute(selectStatement); std::cout << "\n12点之后的活动:\n"; for (auto const& row : rows) { std::cout << std::get<0>(row) << '\t' << std::get<1>(row) << '\t' << std::get<2>(row); line(); } std::cout << "\n已经执行:\n"; std::cout << selectStatement.expanded_sql() << std::endl; orm::get<1>(selectStatement) = "18:00:00"; rows = storage.execute(selectStatement); std::cout << "\n16点之后的活动:\n"; for (auto const& row : rows) { std::cout << std::get<0>(row) << '\t' << std::get<1>(row) << '\t' << std::get<2>(row); line(); } } // 级联打印 void cascade_print(Storage& storage) { std::vector<std::string> roles_in_members; // 还在 family_members 中的角色 std::cout << "\n存在关联的成员活动记录:\n"; for (auto m : storage.iterate<FamilyMember>()) { roles_in_members.push_back(m.role); print(m); for (auto a : storage.iterate<FamilyAction>(orm::where(orm::c(&FamilyAction::member_id) == m.id))) { std::cout << '\t'; print(a); } } std::cout << "\n失去角色关联的活动记录:\n"; int lost = 0; for (auto a : storage.iterate<FamilyAction>(orm::where(not orm::in(&FamilyAction::role, roles_in_members)))) { ++lost; print(a); } if (lost == 0) { std::cout << "无\n"; } } // 级联删除 void cascade_delete(Storage& storage) { std::cout << "当前库中数据:\n"; cascade_print(storage); std::string role_would_delete; std::cout << "你希望删除哪位成员(角色): "; std::cin >> role_would_delete; std::cin.ignore(); // 吃掉回车 std::cout << "确认要删除 " << role_would_delete << " 及其所有活动记录吗?本操作不可恢复!(N/y) :"; int r = std::cin.get(); if (r != 'Y' && r != 'y') { return; } storage.remove_all<FamilyMember>(orm::where(orm::c(&FamilyMember::role) == role_would_delete)); std::cout << "删除指定角色后,库中数据:\n"; cascade_print(storage); } int main() { auto storage = create_storage("we_are_family.db"); try { storage.drop_table_if_exists("family_members"); storage.drop_table_if_exists("family_actions"); storage.sync_schema(); prepare_data(storage); // 示例 15 :最早最晚的活动时间 min_max_time_actions(storage); // 示例 16: 爸爸的活动记录数信息 count_on_daddy_actions(storage); // 示例 17: 展现每个角色的活动记录数 select_count_on_memebers_actions(storage); // 示例 18 : 家庭活动得分排行榜 rates_ranking(storage); // 示例 19: 列出得正分的活动记录,附电话和头像 join_action_on_role_with_avatar(storage); // 示例 20: prepare 语句(顺带:偷窥 orm 和 db 各自生成的 SQL 字符串) prepare_and_execute(storage); // 示例 21: 以树形方式,分组打印各个成员的活动记录 cascade_print(storage); // 示例 22: 级联删除 cascade_delete(storage); } catch(const std::exception& e) { std::cerr << e.what() << '\n'; } }