libpqxx数据类型处理详解:从基础到高级应用

【免费下载链接】libpqxx The official C++ client API for PostgreSQL. 【免费下载链接】libpqxx 项目地址: https://gitcode.com/gh_mirrors/li/libpqxx

libpqxx作为PostgreSQL官方C++客户端API,提供了强大的数据类型转换系统,让开发者能够轻松处理PostgreSQL与C++之间的数据交互。本文将从基础概念到高级应用,全面解析libpqxx的数据类型处理机制,帮助开发者掌握类型转换的核心技术。

数据类型转换基础

在libpqxx中,数据库通信主要通过文本格式进行。当你在查询中包含整数值时,要么使用to_string将其转换为文本格式,要么将其作为参数传递,由libpqxx在底层完成转换。从查询结果中获取"浮点型"字段时,libpqxx会将文本格式转换为浮点类型。这些转换在libpqxx中无处不在。

转换系统支持许多内置类型,但也具有可扩展性。你可以在自己的应用程序范围内"教导"libpqxx将其他类型的值与PostgreSQL的字符串格式相互转换。这非常有用,但需要注意的是,执行此操作的API可能会在libpqxx的任何主要版本中发生变化

类型转换规则

在应用程序中,转换完全由你指定的C++类型驱动。数据库端的值的SQL类型与此无关,字符串中也没有标识其类型的内容。你的代码指定"转换为此类型",libpqxx就会执行相应的转换。

例如,如果你从数据库中选择了一个64位整数,并尝试将其转换为C++的short,会发生两种情况之一:要么数字足够小以适应short,转换成功;要么抛出转换异常。同样,如果你尝试将32位SQL int读取为C++ 32位unsigned int,通常可以正常工作,除非值为负数,此时转换将抛出conversion_error

或者,你的数据库表可能有一个文本列,但某个字段可能包含看起来像数字的字符串。你可以将该值转换为整数类型或浮点类型。对转换而言,重要的是实际值和代码指定的类型。

转换为字符串时,可以从传递的参数中判断类型:

auto x = to_string(99);

在相反方向的转换中,需要显式实例化函数模板:

auto y = from_string<int>("99");

支持新类型的步骤

假设你有其他SQL类型需要存储到数据库或从数据库中检索,需要完成哪些工作呢?

有时你不需要完整的支持。你可能只需要转换字符串,或者只需要从字符串转换某个类型。在编译时编写转换,所以不必担心不完整。如果你遗漏了其中一个步骤,不会在运行时崩溃或弄乱数据。最坏的情况是代码无法构建。

1. 定义你的类型

你需要一个尚未定义转换的C++类型,因为C++类型决定了正确的转换。一种类型对应一对转换(与SQL字符串之间的转换)。

该类型不一定是你创建的。转换逻辑的设计允许围绕任何类型构建。因此,你可以轻松地为其他地方定义的类型构建转换。类型本身不需要包含任何特殊方法或其他成员。这也是libpqxx能够转换int等内置类型的原因。

库还为std::optional<T>std::shared_ptr<T>std::unique_ptr<T>(对于任何给定的T)提供转换。如果你有T的转换,也会自动获得这些类型的转换。

2. 特化'name_type()'

当转换过程中发生错误时,libpqxx将组成一条错误消息,应用程序可以向用户显示或写入日志。有时此消息需要提及正在转换的类型的名称。

默认情况下,这在某些编译器上可能工作正常,但在其他编译器上名称可能会显得有些奇怪,有些编译器会使名称更难识别。因此,自己定义名称会有所帮助。

要告诉libpqxx类型T的人类可读名称,有一个名为pqxx::name_type()的函数模板。对于任何给定的类型T,此函数都应存在:

// T是你的类型。
namespace pqxx
{
template<> inline constexpr std::string_view
name_type<T>() noexcept { return "My T type's name"; };
}

(是的,这意味着你需要在pqxx命名空间内定义某些内容。)

在可能导致libpqxx需要名称的任何代码之前,在翻译单元中尽早定义它。这样,需要知道类型名称的libpqxx代码才能看到你的定义。

如果名称不是简单的编译时常量,而是需要代码来计算,则可能需要将其类型设为std::stringstring_view不维护它包含的文本的存储空间。但是,某些代码分析工具在初始化时可能会报告误报。

3. 特化'nullness'

结构体模板pqxx::nullness定义你的类型是否具有内置的"空值"。例如,std::optional实例化具有一个值,可以很好地映射到SQL null:未初始化状态。

如果你的类型具有这样的值,其pqxx::nullness特化还提供用于生成和识别空值的成员函数。

最简单的情况也是最常见的:大多数类型没有内置的空值。C++中没有"空int"。在这种情况下,只需从pqxx::no_null派生你的空值特征作为简写:这告诉libpqxx你的类型没有自己的空值。

// T是你的类型。
namespace pqxx
{
template<> struct nullness<T> : pqxx::no_null<T> {};
}

(这里你再次在pqxx命名空间中定义它。)

空值不是string_traits的一部分,因为就字符串转换系统而言,空值不是值!你不能将其与字符串相互转换。考虑如果你尝试将空值转换为SQL字符串会发生什么:你会得到一个包含值NULL的字符串,而不是空值。空值的处理发生在更高的级别,当向SQL语句传递参数时,或者将值引用和转义以包含在SQL语句中作为文字值时。

如果你的类型具有自然的空值,nullness的定义会比上面的复杂一些:

namespace pqxx
{
// T是你的类型。
template<> struct nullness<T>
{
  // T是否具有应转换为SQL null的值?
  static constexpr bool has_null{true};

  // 此C++类型是否_始终_表示SQL null,如nullptr_t?
  static constexpr bool always_null{false};

  // `value`是否为空?
  static bool is_null(T const &value)
  {
    return ...;
  }

  // 返回一个空值。
  [[nodiscard]] static T null()
  {
    return ...;
  }
};
}

(在编写应用程序时,如果你有像int这样没有空值但需要传递或解析可能为空的SQL值的类型,可以使用std::optional<int>代替。没有值的optional是null。)

你可能想知道为什么有一个生成空值的函数,还有一个检查值是否为空的函数。为什么不将值与null()的结果进行比较?因为两个空值可能不相等。可能就像在SQL中,NULL不等于任何东西,包括NULL。或者可能有几个不同的空值。或者T可能覆盖比较运算符以以某种不寻常的方式运行。

第三种情况是,你的类型可能始终表示空值。std::nullptr_tstd::nullopt_t就是这种情况。在这种情况下,你将nullness<TYPE>::always_null设置为true(当然还有has_null),并且不需要定义任何实际的转换。

4. 特化'string_traits'

这部分是最麻烦的。对于始终为空的类型,你可以跳过它,但当然这些类型非常罕见。

执行此操作的API旨在尽可能避免在自由存储(也称为"堆")上分配内存的需要。换句话说,API尽量减少使用new/delete,甚至隐藏在std::stringstd::vector等内部的那些。转换API允许你使用std::string以方便使用,或使用固定内存缓冲区以获得最大速度。

首先特化pqxx::string_traits模板。它具有双向转换函数:从C++值到SQL字符串,以及从SQL字符串到C++值。你不必绝对实现两者。通常,如果你需要的代码能够编译,那么暂时就可以了。但请记住,未来的libpqxx版本可能会更改API——或者它在内部使用API的方式。事实上,libpqxx 8.0与7.x相比有很多变化。

截至8.0,string_traits的完整特化如下所示:

namespace pqxx
{
// T是你的类型。
template<> struct string_traits<T>
{
  // 如果你支持转换_到_ SQL字符串:

  // 将`value`表示为字符串,必要时使用`buf`进行存储。
  // (但结果可能位于缓冲区外部,或者位于缓冲区内部但不完全从缓冲区的开头开始。它甚至可以是对`value`本身的引用。)
  //
  // 我们将在下面进一步解释上下文`c`。
  static [[nodiscard]] std::string_view to_buf(
    std::span<char> buf, T const &value, ctx c = {});

  // 将值转换为字符串可能最多需要这么多缓冲区空间。
  static [[nodiscard]] std::size_t size_buffer(T const &value) noexcept;

  // 如果你支持转换_从_ SQL字符串:

  // 将`text`解析为T值。
  static [[nodiscard]] T from_string(std::string_view text, ctx c = {});
};
}

顺便说一下,在你自己的代码中,通常不会自己调用这些函数。相反,你会调用全局包装器:pqxx::to_string()pqxx::to_buf()pqxx::into_buf()(它有细微的不同)和pqxx::size_buffer()

ctx

注意那些ctx参数,默认值为{}?这些包含一些额外的"上下文"信息用于转换。它没有明确定义的角色,只是保存你的转换可能需要或可能不需要的额外信息。以这种方式传递它使libpqxx更容易在未来扩展转换API而不会破坏兼容性。

名称ctxpqxx::conversion_context const t&的简写。其中的每个项目都有合理的默认值,因此调用者可以省略它是可以接受的。

截至libpqxx 8.0,conversion_context包含:

  • pqxx::encoding_group enc。这告诉转换足够的文本编码信息来完成其工作。它没有确切说明文本是UTF-8、Latin-15还是EUC-KR等。但这足以让代码弄清楚每个字符的开始和结束位置,这是它真正需要的所有理解。
  • std::source_location loc。我们将该类型缩写为pqxx::sl,因为它在很多地方出现。如果转换抛出异常,异常可以在其消息中包含此源位置作为调试提示。

未来可能会有更多字段。

编码组默认为一个特殊的组unknown。这足以处理纯ASCII文本,这是大多数转换所需要的。如果你的转换确实需要知道文本的编码,则由调用转换的代码负责传递该信息。

源位置默认为直接调用代码的位置。但是如果该调用者也在libpqxx中,这通常对尝试调试错误的应用程序作者不是很有帮助。因此,我们尝试传递控制最后从应用程序转移到libpqxx的位置。

from_string

现在我们开始实现函数。我们从简单的开始:from_string将字符串解析为SQL值,并将其转换为类型T(你的类型)的C++值。

字符串是std::string_view,这意味着它不一定以终止零结尾。在许多情况下,字符串后面仍然会有一个零,因此请确保你的测试涵盖字符串后面有非零字节的情况!

字符串实际上可能不表示T值,这总是可能的。错误会发生。可能会有极端情况。也许某个值超出了你可以合理支持的范围,例如当你尝试将SQL integer读入unsigned int并且它恰好是负数时。当你遇到这种情况时,抛出pqxx::conversion_error

(当然,也可能遇到其他错误,因此抛出不同的异常也是可以的。但是当确定"这不是T的正确格式"时,抛出conversion_error。)

to_buf

在这个函数中,你将T的值转换为postgres服务器能够理解的字符串。

调用者将为你提供一个缓冲区,你可以在其中写入字符串(如果需要)。但是对于这个函数,你将字符串存储在哪里并不重要:缓冲区内部的某个地方,或者作为字符串常量,甚至通过引用输入值。缓冲区只是在你需要时存在。通常你需要它。

不能做的是分配一些内存来存储你的SQL文本。如果buf不够大,无法存储你的输出,抛出pqxx::conversion_overrun。这不必完全准确:你可以有点悲观,需要比实际需要多一点的空间。稍后我们将讨论你需要多少缓冲区空间的协商。如果有任何缓冲区溢出的风险,只需抛出异常。

转换时要注意区域设置。如果你使用像sprintf这样的标准库功能,它们可能会遵循代码运行的系统上当前设置的区域设置。这意味着像1000000这样的简单整数在你的系统上可能显示为"1000000",但在我的系统上显示为"1,000,000",或者对于其他人显示为"1.000.000",在印度系统上可能是"1,00,000"。不要让这种情况发生,否则会造成混淆。仅使用非区域敏感的库函数。从数据库来或到数据库的值应该采用固定的、非本地化的格式。

如果你的类型包含libpqxx已经支持的类型的字段,你可以对这些类型使用现有的转换函数:pqxx::from_stringpqxx::to_stringpqxx::to_buf等。它们反过来将调用这些类型的string_traits特化。例如,如果你想转换日期,你可以对年、月和日期数字使用int转换。

size_buffer

在这里,你估计将T转换为SQL字符串可能需要多少缓冲区空间。如果可以,请精确,但如果必须,请悲观。浪费几个字节的空间通常比花费大量时间计算你需要的确切缓冲区空间更好。由于预算不足而导致转换失败是最糟糕的。

如果可以,将size_buffer设为constexpr函数。它有时允许调用者在堆栈上分配缓冲区,大小在编译时已知。

可选:特化'is_unquoted_safe'

当将数组或复合值转换为字符串时,libpqxx可能需要引用值并转义任何特殊字符。这需要时间。

但是,某些类型(如整数或浮点类型)在其字符串表示中永远不会有任何特殊字符,如引号、逗号或反斜杠。在这种情况下,在SQL数组或复合类型中不需要引用或转义此类值。

如果你的类型是这样的,你可以通过定义以下内容告诉libpqxx:

namespace pqxx
{
// T是你的类型。
template<> inline constexpr bool is_unquoted_safe<T>{true};
}

然后,将这种类型的字段转换为数组或复合类型中的字符串的代码可以使用更简单、更高效的代码变体。省略它总是安全的;这只是当你完全确定它安全时的优化。

确保在调用可能调用你的转换的libpqxx代码的任何地方都可见此定义。

如果你的类型的字符串表示可能包含逗号、分号、括号、大括号、引号、反斜杠、换行符或任何其他可能需要转义的字符,请不要这样做。

可选:特化'param_format'

通常你不需要担心这个。如果你正在编写表示原始二进制数据的类型,或者你正在编写其中某些特化可能包含原始二进制数据的模板,请继续阅读。

当你调用参数化语句或带有参数的预备语句时,libpqxx需要将你的参数传递给libpq(底层C级PostgreSQL客户端库)。

有两种格式可以做到这一点:文本二进制。首先,我们将所有值表示为PostgreSQL文本格式的字符串,然后服务器将它们转换为自己的内部二进制表示。这就是上面的那些字符串转换的全部内容,我们几乎对所有类型的参数都这样做。

但是当参数是原始字节的连续系列并且相应的SQL类型是BYTEA时,我们会以不同的方式处理。这些有文本格式,但我们为了效率而绕过它。在这种情况下,服务器可以以你提供的确切形式使用二进制数据,无需任何转换或额外处理。BYTEA的二进制数据在传输过程中也紧凑 twice。

(人们有时会问为什么我们不能将所有类型都视为二进制。然而,一般情况并非如此明确。二进制格式没有文档记录,不能保证它们是平台无关的,或者它们在postgres版本之间保持稳定,并且没有真正可靠的方法来检测我们何时可能弄错格式。最重要的是,转换不一定像听起来那样简单和高效。因此,对于一般情况,libpqxx坚持使用文本格式。仅原始二进制数据明显是一个胜利。)

长话短说,传递参数的机制需要知道:这个参数是二进制字符串还是不是?在正常情况下,它可以假设"否",事实也确实如此。文本格式始终是安全的选择;我们只是尝试在更快且安全的地方使用二进制格式。

函数模板param_format驱动决策。我们为可能是二进制字符串的类型特化它,并对所有其他类型使用默认值。

"可能是二进制的类型"?你可能认为我们知道类型是否是二进制类型。但是泛型类型有一些并发症。

std::shared_ptrstd::optional等模板就像另一种类型的"包装器"。如果T是二进制的,则std::optional<T>是二进制的。否则,它不是。std::variant可能包含完全不同的类型,因此决策甚至可以基于单个对象而不仅仅是类型。如果你正在构建对此类模板的支持,你可能希望为其实现param_format

容器是另一个难题。我们应该以二进制方式传递std::vector<T>吗?即使T是二进制类型,我们目前也没有任何方法以二进制格式传递数组,因此我们总是以文本方式传递它。

总结

libpqxx的数据类型处理系统为C++与PostgreSQL之间的数据交互提供了灵活而强大的支持。通过理解和利用类型转换机制,开发者可以轻松地在应用程序中集成PostgreSQL数据库,处理各种复杂的数据类型。无论是内置类型还是自定义类型,libpqxx都提供了清晰的扩展途径,使数据转换变得简单而高效。

要深入了解libpqxx的数据类型处理,建议参考官方文档include/pqxx/doc/datatypes.md,其中包含更详细的技术细节和示例代码。

【免费下载链接】libpqxx The official C++ client API for PostgreSQL. 【免费下载链接】libpqxx 项目地址: https://gitcode.com/gh_mirrors/li/libpqxx

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐