第37章 数据库连接操作
第37章 数据库连接操作
- 使用SQLite3进行轻量级本地数据存储
- 通过MySQL Connector/C++连接远程数据库服务器
- 编写安全的SQL语句并处理查询结果集
你的C++程序需要记住用户信息、商品库存或者游戏存档,这些数据在程序关闭后不能消失。把数据存进文件是一种方式,但当数据关系复杂、查询频繁时,文件操作会变得笨重且低效。这时,你需要一个专业的“数据仓库管理员”——数据库。
数据库连接是什么?
生活类比: 就像你的手机通过Wi-Fi路由器连接互联网,C++程序需要通过一个“数据库驱动”连接到数据库服务器,建立一条传输SQL指令和接收数据的通道。技术定义: 数据库连接是应用程序与数据库管理系统(DBMS)之间建立的一个会话(Session),通过这个会话,程序可以执行SQL命令并获取结果。
在C++中,没有像Java的JDBC那样的标准数据库接口。我们主要通过与数据库厂商提供的原生C语言库(如SQLite)或通用接口封装库(如MySQL Connector/C++、ODBC)进行交互。本章将聚焦于两个最典型的场景:嵌入式数据库SQLite和客户端-服务器数据库MySQL。
37.1 连接方案选型:嵌入式 vs 客户端-服务器
选择哪种数据库,取决于你的应用场景。我在一个桌面工具项目中,因为用户环境无法保证安装MySQL服务,果断选择了SQLite,省去了大量的部署麻烦。
| 特性 | SQLite (嵌入式) | MySQL (客户端-服务器) |
|---|---|---|
| 架构 | 库直接链接到程序中,数据库是单个文件(如 .db) | 程序通过网络协议(如TCP/IP)连接独立的数据库服务进程 |
| 部署复杂度 | 零配置,无需安装独立服务,随程序分发 | 需安装并配置数据库服务器,管理用户权限 |
| 典型应用 | 桌面应用、移动应用、嵌入式设备、本地缓存 | Web后端、企业级应用、多用户并发访问的系统 |
| 性能特点 | 本地文件IO,无网络延迟,轻量级事务 | 支持高并发连接、复杂查询优化、主从复制 |
| C++集成方式 | 直接包含 `sqlite3.h`,链接 `sqlite3.lib` 或 `-lsqlite3` | 使用 `mysqlx` 或 `mysql.h` (C API),需链接 `mysqlcppconn` 等库 |
关键决策点
如果你的应用是单机版、用户数据独立存储(如每个用户一个配置文件),选SQLite。如果是中心化的服务端程序,需要多个客户端同时访问和修改同一份数据,必须选MySQL或同类数据库。实际开发中,很多团队在开发测试阶段用SQLite,上线部署再用MySQL,但要注意两者SQL方言的细微差别。
37.2 实战SQLite3:从连接到查询
让我们从一个具体任务开始:创建一个用户表,并插入一条数据。SQLite3是一个C语言库,在C++中可以直接使用。首先需要获取库文件,可以从官网下载 sqlite-amalgamation-3410200.zip(包含源码)和预编译的DLL或SO文件。
连接SQLite数据库的本质是打开一个文件句柄。核心对象是 `sqlite3*`,它代表一个数据库连接。
// 示例:打开或创建一个数据库文件
#include <iostream>
#include "sqlite3.h" // 包含头文件
int main() {
sqlite3 *db = nullptr;
const char *db_file = "myapp.db";
// 打开数据库连接。如果文件不存在,SQLite会创建它。
int rc = sqlite3_open(db_file, &db);
if(rc != SQLITE_OK) {
std::cerr << "无法打开数据库: " << sqlite3_errmsg(db) << std::endl;
return 1;
}
std::cout << "数据库连接成功!" << std::endl;
// ... 执行SQL操作
sqlite3_close(db); // 必须关闭连接,释放资源
return 0;
}
编译这个程序需要链接SQLite3库。在Linux/macOS上通常是 `g++ -o app app.cpp -lsqlite3`,在Windows的Visual Studio中,需要在项目属性中添加 `sqlite3.lib` 的附加依赖项。
⚠️ 常见误区
- 忘记检查返回值: `sqlite3_open` 返回 `SQLITE_OK` (值为0) 才表示成功。我踩过这个坑,没检查返回值,后续所有操作都失败,却花了半天查别的原因。
- 内存泄漏: 每个 `sqlite3_open` 必须对应一个 `sqlite3_close`。在复杂函数中,如果提前返回,务必确保关闭了连接。
- 路径问题: 传入的相对路径 `"./data.db"` 是相对于程序运行时的当前工作目录,不是源代码目录。最好使用绝对路径或明确管理工作目录。
37.3 执行SQL与处理结果
打开连接后,就可以发送SQL命令了。最简单的是执行不返回数据的DDL(创建表)或DML(插入、更新、删除)语句,使用 `sqlite3_exec` 函数。
// 创建用户表
const char *sql_create = "CREATE TABLE IF NOT EXISTS users ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"name TEXT NOT NULL, "
"age INTEGER);";
char *err_msg = nullptr;
rc = sqlite3_exec(db, sql_create, nullptr, nullptr, &err_msg);
if(rc != SQLITE_OK) {
std::cerr << "SQL错误: " << err_msg << std::endl;
sqlite3_free(err_msg); // 错误信息内存需要手动释放!
}
当需要执行查询(SELECT)并处理返回的多行数据时,`sqlite3_exec` 就不够灵活了。你需要使用 `sqlite3_prepare_v2`, `sqlite3_step`, `sqlite3_column_*` 这一套组合拳。
// 查询所有用户
const char *sql_select = "SELECT id, name, age FROM users;";
sqlite3_stmt *stmt = nullptr; // 预处理语句句柄
rc = sqlite3_prepare_v2(db, sql_select, -1, &stmt, nullptr);
if(rc != SQLITE_OK) { /* 处理错误 */ }
while((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
// 按列索引(从0开始)或列名获取数据
int id = sqlite3_column_int(stmt, 0);
const unsigned char *name = sqlite3_column_text(stmt, 1); // TEXT类型
int age = sqlite3_column_int(stmt, 2);
std::cout << "ID: " << id << ", Name: " << name << ", Age: " << age << std::endl;
}
if(rc != SQLITE_DONE) { /* 处理非正常结束的错误 */ }
sqlite3_finalize(stmt); // 释放预处理语句资源
编者提示: 处理从数据库获取的文本数据(`sqlite3_column_text`)时,其返回的是指向SQLite内部缓冲区的指针,该数据仅在当前 `sqlite3_step` 调用期间有效。如果你需要持久化这个字符串,请立即复制它(例如使用 `std::string`)。这与你在第22章学习的 `std::string` 的内存管理逻辑不同,务必注意。
37.4 连接MySQL:使用Connector/C++
当你的数据需要被网络上的多个客户端共享时,就需要像MySQL这样的数据库服务器。Oracle官方提供了MySQL Connector/C++ 8.0,这是一个纯C++的驱动库,比老的C API更现代、更安全。
首先,你需要安装:1) MySQL服务器(如MySQL Community Server 8.0.36);2) Connector/C++库。在Ubuntu上可以 `apt install libmysqlcppconn8-2`,在Windows上需要从官网下载安装包并配置头文件和库路径。
预处理语句(Prepared Statement)
生活类比: 就像填空题模板,“查找名叫____的人”,你只需要填入不同的名字,模板结构不变。这比每次手写整个句子(SQL语句)更安全高效。技术定义: 预处理语句是预先编译好的SQL模板,其中参数用占位符表示。它可以防止SQL注入,并且对于重复执行(仅参数不同)的语句,性能更高。
使用Connector/C++连接MySQL的代码风格更面向对象。核心类是 `sql::mysql::MySQL_Driver`, `sql::Connection`, `sql::Statement`, `sql::PreparedStatement` 和 `sql::ResultSet`。
#include <iostream>
#include <mysqlx/xdevapi.h> // Connector/C++ 8.0 的主要头文件
// 注意:新版本推荐使用X DevAPI,老版本是 `#include <cppconn/driver.h>`
int main() {
try {
// 1. 创建会话(连接)
mysqlx::Session sess("localhost", 3306, "your_username", "your_password", "your_database");
std::cout << "连接到MySQL服务器成功!" << std::endl;
// 2. 获取数据库模式(Schema)
mysqlx::Schema db = sess.getSchema("your_database");
// 3. 执行一个简单查询(不推荐,仅演示)
mysqlx::SqlResult result = sess.sql("SHOW TABLES;").execute();
auto rows = result.fetchAll();
for(auto row : rows) {
std::cout << row[0] << std::endl;
}
// 4. 使用预处理语句安全地插入数据(推荐)
mysqlx::TableInsert insert_op = db.getTable("users").insert("name", "age");
insert_op.values("Alice", 25).values("Bob", 30);
insert_op.execute();
std::cout << "数据插入成功。" << std::endl;
sess.close();
} catch (const mysqlx::Error &err) {
std::cerr << "MySQL错误: " << err.what() << std::endl;
return 1;
} catch (const std::exception &ex) {
std::cerr << "标准异常: " << ex.what() << std::endl;
return 1;
}
return 0;
}
编译时需要链接 `mysqlcppconn8` 库。例如:`g++ -o mysql_app mysql_app.cpp -lmysqlcppconn8`。如果你看到链接错误“undefined reference to `mysqlx::Session::Session(...)`”,大概率是库路径没设置对或者库文件名不对。
简单脚本/工具
- 选择: SQLite
- 理由: 无需额外服务,单文件部署
- 注意: 文件锁,不支持高并发写
桌面应用程序
- 选择: SQLite
- 理由: 用户数据本地存储,安装简单
- 注意: 数据库文件可能被用户误删
游戏服务器后端
- 选择: MySQL/PostgreSQL
- 理由: 需要处理数百上千个并发连接,数据强一致
- 注意: 需设计连接池,优化查询
数据采集与缓存
- 选择: SQLite + 定期同步到MySQL
- 理由: 本地快速写入,批量上传
- 注意: 处理同步冲突和网络异常
37.5 错误处理与资源管理
数据库操作充满了不确定性:网络可能断开,磁盘可能写满,SQL可能有语法错误。健壮的程序必须处理这些异常。
对于SQLite,每个函数调用后检查返回值是必须的。对于MySQL Connector/C++,它使用C++异常机制,所以必须用 `try-catch` 块包裹可能出错的代码。一个重要的实践是使用RAII(Resource Acquisition Is Initialization)思想来管理数据库资源,确保在任何情况下(包括异常抛出)连接都能被正确关闭。
// 一个简单的RAII包装器示例,用于SQLite连接
class SQLiteDB {
public:
SQLiteDB(const std::string &filename) {
if(sqlite3_open(filename.c_str(), &db_) != SQLITE_OK) {
throw std::runtime_error(sqlite3_errmsg(db_));
}
}
~SQLiteDB() {
if(db_) sqlite3_close(db_);
}
// 删除拷贝构造和赋值,防止重复关闭
SQLiteDB(const SQLiteDB&) = delete;
SQLiteDB& operator=(const SQLiteDB&) = delete;
operator sqlite3*() { return db_; } // 提供获取原始指针的方法
private:
sqlite3 *db_ = nullptr;
};
// 使用:当sqlite_db对象离开作用域时,析构函数会自动关闭连接
try {
SQLiteDB sqlite_db("data.db");
// 使用 sqlite_db 进行操作...
} catch(const std::exception &e) {
std::cerr << e.what() << std::endl;
}
对于MySQL,Connector/C++的 `Session` 对象本身已经实现了RAII,在其析构时会自动关闭连接。但你还是应该主动在 `try-catch` 块中调用 `close()`,以便更早地释放服务器资源并捕获可能的关闭错误。
动手试一试
1. 搭建SQLite环境: 从 sqlite.org 下载“Precompiled Binaries for Windows”的 `sqlite-tools-win32-*.zip` 和“Source Code”的 `sqlite-amalgamation-*.zip`。解压后者得到 `sqlite3.h` 和 `sqlite3.c`。创建一个控制台项目,将 `sqlite3.c` 加入源文件,包含 `sqlite3.h`,编写代码创建一个 `students.db` 数据库,并包含 `id`(主键)、`name`、`score` 三个字段的表格。
2. 实现CRUD: 在上面项目的基础上,编写函数实现插入3个学生记录、根据ID查询一个学生、更新某个学生的分数、删除某个学生的功能。将所有数据库操作封装在一个类中。
3. (选做)连接MySQL: 如果你有可用的MySQL服务器(可以安装XAMPP或WAMP套件快速获得),尝试使用MySQL Connector/C++ 8.0连接它,并执行一个简单的 `SELECT NOW();` 查询,将服务器时间打印到控制台。
检验你的理解
- SQLite的数据库文件后缀通常是什么?一个SQLite连接(`sqlite3*`)对应一个文件还是多个文件?
- 为什么使用预处理语句(如 `sqlite3_prepare_v2`)比直接拼接SQL字符串(如 `sqlite3_exec`)更安全?请从“SQL注入攻击”的角度解释。
- 在MySQL Connector/C++的代码中,如果连接字符串中的密码错误,程序会在哪一行抛出异常?是 `Session` 构造时,还是执行 `sql()` 时?
- 以下代码片段有什么问题?
sqlite3 *db; sqlite3_open("test.db", &db); sqlite3_exec(db, "INSERT INTO logs(msg) VALUES('hello');", nullptr, nullptr, nullptr); // 程序在此处崩溃或异常退出
本章小结
- 连接方案二选一: 单机、嵌入式场景用SQLite(文件数据库);多用户、网络化服务端用MySQL(客户端-服务器数据库)。
- SQLite核心流程: `open` -> `prepare`/`exec` -> `step` (循环) -> `column` (取数据) -> `finalize`/`close`,每一步都必须检查返回值。
- MySQL现代用法: 使用Connector/C++ 8.0的X DevAPI,通过 `Session` 对象连接,优先使用预处理语句或Table API进行安全的数据操作。
- 安全与健壮性铁律: 永远使用预处理语句来防止SQL注入;使用RAII或严格的 `try-catch` 确保数据库连接等资源被正确释放。
- 排错线索: 连接失败首先检查网络、地址、端口、用户名密码;SQL错误仔细阅读驱动返回的错误信息;链接错误检查库文件和编译命令。
微信扫码关注
抖音扫码关注