ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

libpqxx 7.7.3 快速上手:用 C++ 连接 PostgreSQL 的三大核心类型与实战模式

libpqxx 7.7.3 快速上手:用 C++ 连接 PostgreSQL 的三大核心类型与实战模式 libpqxx 7.7.3 快速上手用 C 连接 PostgreSQL 的三大核心类型与实战模式【免费下载链接】ZeroTierOneA Smart Ethernet Switch for Earth项目地址: https://gitcode.com/GitHub_Trending/ze/ZeroTierOnelibpqxx 是 PostgreSQL 官方 C 客户端库 libpq 之上的现代 C 封装本仓库在ext/libpqxx-7.7.3/目录下完整携带了其 7.7.3 版本源码、头文件与文档。本指南以官方 getting-started.md 为骨架带你从零掌握connection连接、transaction事务、result结果集三大核心类型并延伸到结果访问、字符串转义、参数化查询、流式读写与类型转换扩展等实战能力。读完本文你将能写出安全、高效、可直接编译运行的 libpqxx 客户端程序并能在源码中定位每个接口的底层实现。libpqxx 的三个最基本类型libpqxx 官方文档指出入门阶段你只需要认识三个类型它们是整个库的基石pqxx::connection—— 数据库连接。通过创建该对象即完成与 PostgreSQL 服务器的连接connections 相关文档。pqxx::transaction—— 事务。所有 SQL 操作都必须在事务对象上进行最常用的是其变体pqxx::work。pqxx::result—— 查询结果集。它是行pqxx::row的标准容器而行又是字段pqxx::field的标准容器。这三个类型的配合关系如下创建pqxx::connection对象连接数据库在该连接上创建事务对象通常用pqxx::work事务对象被销毁时未提交的工作会自动回滚调用事务的exec、query_value、stream及其变体执行 SQL 语句语句本身以普通字符串传入大多数exec函数返回pqxx::result它充当行的标准容器每一行又是字段的容器每个字段的数据在内部以 PostgreSQL 定义的文本格式存储可通过字段或行的as()/to()成员函数转换为 C 原生类型事务关闭后连接即可自由地开启下一个事务。第一个示例连接、查询、提交与结果读取下面是最基本的完整示例来自官方 getting-started 文档连接默认数据库、执行一个极简查询、把结果转成int并打印同时带有基础错误处理。#include iostream #include pqxx/pqxx int main() { try { // Connect to the database. In practice we may have to pass some // arguments to say where the database server is, and so on. // The constructor parses options exactly like libpqs // PQconnectdb/PQconnect, see: // https://www.postgresql.org/docs/10/static/libpq-connect.html pqxx::connection c; // Start a transaction. In libpqxx, you always work in one. pqxx::work w(c); // work::exec1() executes a query returning a single row of data. // Well just ask the database to return the number 1 to us. pqxx::row r w.exec1(SELECT 1); // Commit your transaction. If an exception occurred before this // point, execution will have left the block, and the transaction will // have been destroyed along the way. In that case, the failed // transaction would implicitly abort instead of getting to this point. w.commit(); // Look at the first and only field in the row, parse it as an integer, // and print it. // // r[0] returns the first field, which has an as...() member // function template to convert its contents from their string format // to a type of your choice. std::cout r[0].asint() std::endl; } catch (std::exception const e) { std::cerr e.what() std::endl; return 1; } }程序输出数字1。需要特别注意的是结果对象可以在事务甚至连接关闭之后继续存活。大多数情况下这是完全安全的个别情况下不能这样做例如你自定义了从数据库接收错误消息的回调此时必须保持连接对象存活否则你就可以“fire-and-forget”地丢弃连接、从容处理数据。从源码角度看w.exec1(SELECT 1)正是transaction_base中exec_n系列的封装其内部会先执行查询再校验返回的行数恰好为 1见 transaction_base.hxx 中exec1/exec_n的实现。一行代码把整行结果转换为 C 类型除了逐字段调用asT()你还可以一次性把整行转换成一组 C 类型借助 C17 的结构化绑定直接拿到各个值pqxx::connection c; pqxx::work w(c); pqxx::row r w.exec1(SELECT 1, 2, Hello); auto [one, two, hello] r.asint, int, std::string(); std::cout (one two) std::strlen(hello) std::endl;这里r.asint, int, std::string()返回一个std::tuple解构后one为 1、two为 2、hello为 Hello。类型转换完全由你指定的 C 类型驱动与数据库列声明的 SQL 类型无关——只要实际值能转换成目标类型即可详见后文“类型转换系统”。第二个示例安全地把外部输入嵌入 SQL把变量直接拼进 SQL 字符串是危险的如果变量含有引号就可能产生 SQL 注入漏洞或难以排查的 Bug例如名字恰好是 dArcy。libpqxx 提供了一系列转义与引用函数其中最常用的是事务的quote#include iostream #include stdexcept #include pqxx/pqxx int main(int argc, char *argv[]) { try { if (!argv[1]) throw std::runtime_error(Give me a string!); pqxx::connection c; pqxx::work w(c); // work::exec() returns a full result set, which can consist of any // number of rows. pqxx::result r w.exec(SELECT w.quote(argv[1])); // End our transaction here. We can still use the result afterwards. w.commit(); // Print the first field of the first row. Read it as a C string, // just like std::string::c_str() does. std::cout r[0][0].c_str() std::endl; } catch (std::exception const e) { std::cerr e.what() std::endl; return 1; } }这个示例展示了work::exec与exec1的区别exec返回完整的结果集可以有任意多行随后用r[0][0]双重下标取第一行第一列并通过c_str()以 C 风格字符串读出。关键的w.quote(argv[1])会把用户输入正确转义并用引号包裹使其只能作为字符串字面量出现无法“逃逸”出引号改变 SQL 语义。深入SQL 注入原理与转义机制为什么必须转义看一个攻击场景源自 escaping.mdTX.exec( SELECT number,amount FROM accounts WHERE allowed_to_see( userid , password ));如果攻击者输入密码x) OR (x x拼装后的 SQL 变成SELECT number,amount FROM accounts WHERE allowed_to_see(user,x) OR (x x)OR (x x)恒为真allowed_to_see()的鉴权被完全绕过攻击者可以看到全部账户。而使用TX.esc(userid)/TX.esc(password)之后SQL 中的单引号会被成对复制转义SQL 标准中单引号用两个连续单引号转义SELECT number,amount FROM accounts WHERE allowed_to_see(user, x) OR (x x)结果只是产生一个外观奇怪的密码字符串SQL 语句本身没有被改变。更进一步的推荐做法是使用参数化查询$1、$2占位符连字符串拼接都省去见下文。读取结果集result / row / field 的访问方式exec系列函数返回的result是“全有或全无”的exec会等待全部结果数据到达后才返回result。结果集的访问方式详见 accessing-results.md非常灵活方式一基于范围的 for 循环推荐pqxx::result r tx.exec(SELECT * FROM mytable); for (auto const row: r) { for (auto const field: row) std::cout field.c_str() \t; std::cout \n; }方式二数组式下标访问std::size_t const num_rows std::size(r); std::size_t const num_cols r.columns(); // 每行列数相同只需查一次 for (std::size_t rownum0u; rownum num_rows; rownum) { pqxx::row const row r[rownum]; for (std::size_t colnum0u; colnum num_cols; colnum) { pqxx::field const field row[colnum]; std::cout field.c_str() \t; } std::cout \n; }方式三按字段名访问不追求速度时使用std::cout row[salary] \n;注意按列名查找需要额外耗时若循环体内反复使用最好在循环前先把列名解析成列索引循环内一律用数字下标。若使用 C23 或更高版本还可直接用二维下标运算符result[rownum, colnum]。迭代器细节结果集不可变immutable因此所有迭代器实际都是const_iterator另有反向迭代器。libpqxx 的迭代器有一个普通 C 迭代器没有的便利引用透明referential transparency——你不需要解引用就能使用它所指的对象例如可以写row.end()代替row-end()、field.c_str()代替field-c_str()甚至直接row[0]而不必写丑陋的(*row)[0]。流式读取stream 与 stream_fromexec会把全部结果收齐后才交给应用如果查询返回的数据量很大更高效的做法是流式处理详见 streams.md。事务基类提供了stream与for_each便捷函数见 transaction_base.hxx底层用 PostgreSQL 的COPY命令实现for (auto [id, name, x, y] : tx.streamint, std::string_view, float, float( SELECT id, name, x, y FROM point)) process(id 1, point- name, x * 10.0, y * 10.0);类型转换内建在流中你甚至看不到row、field、迭代器或转换方法。使用流式读取有三个注意点部分失败风险第一条数据到达时就开始处理若传输中途断网应用可能已处理部分数据才发现剩下的数据没来查询类型受限stream()会把查询包进COPY命令只支持SELECT、VALUES以及带RETURNING子句的INSERT/UPDATE/DELETE视图类型生命周期若把字段转成std::string_view之类的“视图”类型其指向的底层数据只在下一次迭代或循环结束前有效需要长期使用必须自行拷贝。更底层的pqxx::stream_from::query可以显式逐行读取auto stream pqxx::stream_from::query( tx, SELECT name, points FROM score); std::tuplestd::string, int row; while (stream row) process(row); stream.complete();空值NULL的处理空值的处理取决于目标 C 类型。char const *这类自带“空”概念的类型nullptr会被转换为 SQL NULL而对int这类没有内建空值的类型应包一层std::optionalint也可用std::shared_ptr/std::unique_ptr但智能指针通常在堆上分配、效率低于optional。需要说明的是这种包装支持并非泛化的——libpqxx 只显式支持shared_ptr和unique_ptr两类智能指针。流式写入stream_to批量插入大量数据时逐行INSERT的开销可观。pqxx::stream_to直接向数据表写数据避免了每次插入的开销pqxx::stream_to stream{ tx, score, std::vectorstd::string{name, points}}; for (auto const entry: scores) stream entry; stream.complete();对stream_to来说complete()极其重要——它类似于事务结束时的 commit/abort即使你省略它析构函数也会自动完成但析构函数无法抛出异常此阶段的失败将不可见。因此务必显式调用complete()来正常收尾。参数化查询$1、$2 占位符与动态参数与其手动拼接并转义字符串更稳妥的是使用语句参数见 parameters.md。查询文本中可以写$1、$2等占位符执行时依次用传入的参数值替换// pqxx::connection::exec_params 直接执行参数化语句 pqxx::result r conn.exec_params( SELECT name FROM Employee WHERE salary $1 AND name $2, 50000, Alice);事务基类对应的封装为exec_params、exec_params0、exec_params1见 transaction_base.hxx。参数以安全格式直接跨网络传给数据库省去了手动 quote/escape当参数是二进制数据SQLBYTEA类型时libpqxx 会直接以二进制形式发送比把二进制数据转成文本再拼进 SQL 更省 CPU、也更紧凑。动态参数列表当参数个数在编译期无法确定时可用pqxx::params在运行时组合参数列表甚至可以一次性加入一整个参数区间。假设调用时传常规参数a、一个含参数b的params、常规参数c实际传递的参数就是a, b, c若params为空则为a, c若params含x, y则为a, x, y, c。静态与动态参数可以自由混用。占位符生成器当 SQL 文本复杂到难以跟踪“哪个值对应哪个$N”时可用pqxx::placeholders计数器辅助生成pqxx::params values; pqxx::placeholders name; ... if (extra_clause) { query AND x name.get(); // 例如 AND x $3 values.append(my_x); name.next(); // 前进到下一个占位符 }预编译语句prepare 与 exec_prepared预编译语句见 prepared-statement.md是把 SQL 定义一次、反复调用通常带不同参数的机制。对需要高频执行的语句数据库后端可以免去反复解析与生成执行计划的开销同时参数无需转义不少企业的编码规范甚至强制要求所有 SQL 参数都以这种方式传递以杜绝注入风险。在连接上准备语句标识符只能包含 ASCII 字母、数字与下划线且须以字母开头、区分大小写void prepare_find(pqxx::connection c) { // 准备名为 find 的语句找指定姓名$1且薪水高于给定值$2的员工 c.prepare( find, SELECT * FROM Employee WHERE name $1 AND salary $2); }之后可在同一连接的任何事务中调用它pqxx::result execute_find( pqxx::transaction_base t, std::string name, int min_salary) { return t.exec_prepared(find, name, min_salary); }事务基类的exec_prepared/exec_prepared0/exec_prepared1位于 transaction_base.hxx。特殊情形存在“无名预编译语句”名字为空字符串它可以随时被重新定义而无需先取消准备。性能提示不要默认预编译语句一定更快当后端能看到语句的实际参数值时往往能生成更好的执行计划。例如查询“inactive 用户中邮箱属于某域名 X 的用户”若 X 是大厂商域名最优计划可能是先筛 inactive 用户再过滤邮箱反之则可能先匹配邮箱再筛状态。预编译语句必须针对两种情形折中规划而直接查询可基于表统计信息、部分索引等做出针对性优化。零字节警告传给参数的任何字符串都会在第一个值为 0 的字节处截断。需要包含零字节的数据本质上是二进制字符串应使用 SQL 的BYTEA类型或大对象blob在 libpqxx 中二进制数据用一段连续内存的std::byte表示如std::basic_stringstd::byte、std::basic_string_viewstd::byte或std::vectorstd::byte以便向底层 C 库传递指针。类型转换系统与扩展自定义类型libpqxx 与数据库的通信基本以文本格式进行把int拼进查询用to_string读取结果字段用asT()/from_string。这套转换系统支持大量内建类型且可扩展——你可以在自己的应用范围内“教会” libpqxx 转换更多类型见 datatypes.md。需要留意的是该扩展 API 可能随 libpqxx 大版本升级而变动。类型转换完全由 C 类型驱动与 SQL 类型无关SELECT 一个 64 位整数若转成short且数值放得下则直接成功否则抛转换异常一个看起来像数字的文本列也能自由转成整数或浮点类型。部分场景模板可推导类型auto x to_string(99);另一些则需显式实例化auto y from_stringint(99);。扩展一个新类型的完整步骤准备 C 类型不要求是你自己写的类型也不要求类型内定义任何特殊成员枚举类型最简单直接在全局命名空间调用宏PQXX_DECLARE_ENUM_CONVERSION(ENUM)即可见 strconv.hxx特化pqxx::type_name提供类型的人类可读名称用于错误消息等场合namespace pqxx { template std::string const type_nameT{T}; }特化pqxx::nullness说明类型是否自带空值。多数类型没有内建空值直接继承pqxx::no_nullT若自带空值则提供has_null、is_null()、null()等成员两个空值未必相等所以既要能“产生”也要能“判断”std::nullptr_t、std::nullopt_t这类总是为空的类型则置always_null true特化pqxx::string_traits定义真正的转换逻辑通常实现四个成员from_string解析字符串为T格式不对时抛pqxx::conversion_error注意输入可能不是以零结尾的字符串to_buf把T写成半开区间[begin, end)内的字符串缓冲区不够时抛pqxx::conversion_overrun返回保证末尾有合法零字节的pqxx::zview转数字时务必避开 locale不同地区数字分隔符不同数据库应使用非本地化格式into_bufto_buf的严格版必须恰好从begin开始写返回结尾零字节之后的位置size_buffer预估所需缓冲区字节数包含结尾零字节尽量写成constexpr以便调用方在栈上按编译期大小分配宁可高估也不要低估可选特化pqxx::is_unquoted_safe若类型转成字符串后绝不含逗号、引号、反斜杠、括号、分号等需要转义的字符如整型、浮点型可置为true让数组/复合类型转换走更快的路径——漏掉它永远安全它只是纯优化可选特化pqxx::param_format默认所有参数走文本格式对可能表示原生二进制数据的类型如BYTEA特化它走二进制格式。由于std::optionalT、std::shared_ptrT等包装模板的二进制性取决于被包装类型且std::variant取决于具体对象、std::vector目前一律按文本传递这类泛型判断需谨慎实现。线程安全、错误处理与配套资料线程安全见 thread-safety.mdlibpqxx 内部不包含任何锁保护多线程程序中必须由你来保证不发生冲突的并发操作。核心原则把连接及其所有相关对象视为一个独立的“世界”在对其中任何对象做非 const 操作时其他线程不得访问同一个“世界”——例如不要在开启子事务的同时在别处发查询、不要一边提交一边访问游标。游标尤其危险容易无意触发非 const 操作跨线程共享游标时要非常保守地加锁。好消息是结果集不可变可以放心在线程间共享。运行时可用pqxx::describe_thread_safety()查询当前构建的线程安全模型。错误处理示例中的try/catch (std::exception const e)能捕获大多数异常。若需更精细地处理 libpqxx 抛出的异常例如打印失败查询的 SQL 文本可参考 exception 相关文档 中列出的资料索引libpqxx 的异常体系以pqxx::sql_error含错误 SQL 信息等类型为核心均派生自std::exception。进一步阅读本仓库ext/libpqxx-7.7.3/include/pqxx/doc/下还提供了 访问结果、字符串转义、参数化查询、流式读写、预编译语句、数据类型扩展 与 线程安全 等专题文档接口声明可深入 connection.hxx、transaction_base.hxx 与 strconv.hxx 查看完整签名。编译时注意构建 libpqxx 需要系统已安装 PostgreSQL 及其 C 头文件libpq但客户端程序本身只需链接 libpqxx无需 libpq 头文件。【免费下载链接】ZeroTierOneA Smart Ethernet Switch for Earth项目地址: https://gitcode.com/GitHub_Trending/ze/ZeroTierOne创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进