49、STL与数据库:数据库连接池、SQL语句构建、结果集处理
说实话,很多C++程序员一提到数据库操作,第一反应就是去用ORM框架。但在我多年的项目经验里,真正的高性能服务,底层往往还是直接跟SQL打交道。STL在这中间扮演的角色,比你想象的要重要得多。
今天我们就来聊聊,怎么用STL这把瑞士军刀,把数据库操作的三板斧——连接池、SQL构建、结果集处理——打磨得又快又稳。
一、数据库连接池:用STL容器管理稀缺资源
数据库连接是个昂贵的东西。每次创建和销毁TCP连接,开销都不小。我见过不少项目,就是因为频繁创建连接,导致数据库服务器CPU飙升。
连接池的核心思想很简单:把用过的连接放回去,下次复用。用STL来实现,最直观的就是用std::queue或std::deque。
核心要点:连接池 = 容器 + 互斥锁 + 超时机制
1. 基础连接池实现
#include <queue>
#include <mutex>
#include <memory>
#include <chrono>
#include <condition_variable>
class ConnectionPool {
private:
std::queue<std::shared_ptr<DatabaseConnection>> pool_;
std::mutex mtx_;
std::condition_variable cv_;
size_t max_size_;
size_t current_size_;
std::chrono::seconds timeout_;
public:
ConnectionPool(size_t max_size, std::chrono::seconds timeout)
: max_size_(max_size), current_size_(0), timeout_(timeout) {}
std::shared_ptr<DatabaseConnection> acquire() {
std::unique_lock<std::mutex> lock(mtx_);
// 等待直到有可用连接或超时
if (!cv_.wait_for(lock, timeout_, [this]() {
return !pool_.empty() || current_size_ < max_size_;
})) {
throw std::runtime_error("获取连接超时");
}
if (!pool_.empty()) {
auto conn = pool_.front();
pool_.pop();
return conn;
}
// 创建新连接
++current_size_;
return std::make_shared<DatabaseConnection>();
}
void release(std::shared_ptr<DatabaseConnection> conn) {
std::lock_guard<std::mutex> lock(mtx_);
pool_.push(conn);
cv_.notify_one();
}
};
嗯,这里有个细节要注意:std::condition_variable的wait_for配合lambda表达式,比手动轮询优雅得多。我曾经在一个老项目里看到有人用while(true)加sleep来实现等待,那CPU占用率简直不忍直视。
我的习惯:连接池的max_size_不要设太大。一般跟数据库的最大连接数挂钩,留20%的余量就够了。设太大反而容易把数据库拖垮。
2. 连接池的RAII封装
手动调用acquire和release容易漏掉。我建议用RAII思想封装一下:
class ConnectionGuard {
private:
ConnectionPool& pool_;
std::shared_ptr<DatabaseConnection> conn_;
public:
ConnectionGuard(ConnectionPool& pool) : pool_(pool) {
conn_ = pool_.acquire();
}
~ConnectionGuard() {
if (conn_) {
pool_.release(conn_);
}
}
DatabaseConnection* operator->() { return conn_.get(); }
DatabaseConnection& operator*() { return *conn_; }
};
这样用起来就安全多了:
ConnectionPool pool(10, std::chrono::seconds(5));
{
ConnectionGuard guard(pool);
guard->execute("SELECT ...");
} // 自动归还连接
避坑指南:我曾经在析构函数里忘记检查conn_是否为空,结果程序退出时double free了。记住:RAII包装器一定要处理空指针和移动语义。
二、SQL语句构建:用STL算法组装查询
拼SQL字符串是件很烦人的事。尤其是条件查询,要处理各种AND、OR、IN子句。用STL的std::string和算法,可以写得既清晰又安全。
1. 动态WHERE子句构建
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
class SQLBuilder {
private:
std::ostringstream sql_;
std::vector<std::string> conditions_;
std::string table_;
public:
SQLBuilder(const std::string& table) : table_(table) {}
SQLBuilder& addCondition(const std::string& column,
const std::string& op,
const std::string& value) {
std::ostringstream cond;
cond << column << " " << op << " '" << value << "'";
conditions_.push_back(cond.str());
return *this;
}
std::string build() {
sql_.str("");
sql_ << "SELECT * FROM " << table_;
if (!conditions_.empty()) {
sql_ << " WHERE ";
// 用STL算法拼接条件
auto it = conditions_.begin();
sql_ << *it;
std::for_each(++it, conditions_.end(), [this](const std::string& cond) {
sql_ << " AND " << cond;
});
}
return sql_.str();
}
};
你看,用std::for_each来拼接条件,比手写循环要简洁得多。而且std::ostringstream比直接+=拼接字符串效率高不少——我测试过,在拼接大量小字符串时,性能差距能有3-5倍。
2. 处理IN子句
IN子句在SQL里很常见。用STL的std::vector和std::accumulate可以优雅地生成:
#include <numeric>
std::string buildInClause(const std::vector<int>& ids) {
if (ids.empty()) return "IN ()";
std::string result = "IN (";
result += std::accumulate(
std::next(ids.begin()), ids.end(),
std::to_string(ids.front()),
[](const std::string& a, int b) {
return a + ", " + std::to_string(b);
}
);
result += ")";
return result;
}
注意:这里用std::next(ids.begin())跳过了第一个元素,因为std::accumulate的初始值已经包含了它。这个小技巧能避免在循环里判断是不是第一个元素。
三、结果集处理:用STL容器装数据
数据库查回来的数据,怎么存?怎么用?STL容器是最好的选择。
1. 行数据存储
我一般用std::unordered_map<std::string, std::string>来表示一行数据。键是列名,值是字段值。整个结果集就是std::vector套这个map:
using Row = std::unordered_map<std::string, std::string>;
using ResultSet = std::vector<Row>;
ResultSet fetchAll(DatabaseConnection* conn, const std::string& sql) {
ResultSet results;
auto* stmt = conn->prepare(sql);
while (stmt->next()) {
Row row;
int col_count = stmt->getColumnCount();
for (int i = 0; i < col_count; ++i) {
std::string col_name = stmt->getColumnName(i);
std::string col_value = stmt->getString(i);
row[col_name] = col_value;
}
results.push_back(std::move(row));
}
return results;
}
用std::move把row移进vector,避免了拷贝。数据量大的时候,这个优化能省不少内存操作。
2. 结果集排序与过滤
有时候数据库查回来的数据,还需要在应用层做二次处理。STL的算法就派上用场了:
// 按年龄排序
std::sort(results.begin(), results.end(),
[](const Row& a, const Row& b) {
return std::stoi(a.at("age")) < std::stoi(b.at("age"));
});
// 过滤出活跃用户
std::vector<Row> active_users;
std::copy_if(results.begin(), results.end(),
std::back_inserter(active_users),
[](const Row& row) {
return row.at("status") == "active";
});
我的经验:能用数据库做的过滤和排序,尽量在SQL里做。应用层处理只适合那些数据库函数不好表达的逻辑,比如复杂的字符串匹配或业务规则判断。
四、知识体系总览
下面这张图,把STL在数据库操作中的角色梳理清楚了:
五、实战中的坑与建议
说了这么多,最后分享几个我踩过的坑:
- 连接泄漏:用RAII包装器之前,我至少遇到过三次连接泄漏。每次都是因为异常路径没释放连接。现在强制团队用
ConnectionGuard,再没出过问题。 - SQL注入:用字符串拼接SQL时,一定要做转义。我建议用参数化查询,STL的
std::string配合std::replace可以快速转义单引号。 - 大数据量:结果集如果可能很大(比如上万行),不要一次性全拉进
std::vector。用游标分批读取,STL的std::queue可以做个简单的缓冲区。 - 线程安全:连接池的
std::queue操作一定要加锁。我习惯用std::lock_guard,简单又安全。
总结一下:STL不是ORM的替代品,而是ORM底层的最佳实现工具。理解STL容器和算法在数据库操作中的角色,能让你写出更高效、更健壮的代码。说白了,STL就是C++程序员的「数据库工具箱」。
好了,这一章的内容就到这里。希望这些实战经验对你有帮助。下次遇到数据库操作,不妨想想:这个场景,STL能帮我做什么?
公众号:蓝海资料掘金营,微信deep3321