驱动程序开发
背景
本页面介绍为各种主机语言基于 libpq 开发 PostgreSQL 驱动程序的目标和指导原则。
注意:目前,这是基于我(杰夫·戴维斯)在开发 ruby-pg 方面的经验,但我希望其他人也能就此发表评论。
项目
开发驱动程序是一个不只是涉及代码的项目
- 测试(应该能够在 GNU/Linux、Mac 和 Windows 上全面测试)
- 构建系统
- 项目页面
- 邮件列表
- 版本
- 某种形式的错误报告(即使只是邮件列表)
- 与语言和框架社区合作以确保支持驱动程序。
- 善待这些社区的人们,让他们放心根据 PostgreSQL 开发。
目标
- 提供 libpq 的所有功能,包括参数化查询、非阻塞调用、COPY 支持等。
- 提供稳定可靠的界面
- 便于移植(到多个平台、多个主机语言版本和多个 PostgreSQL 版本)
- 处理具有明确解决方案的低级问题,例如文本编码
- 以有用的方式将错误转换为主机语言(例如将其转换为异常)
- 在合理范围内匹配语言风格
非目标
- 并非专为数据库不可知而设定
- 不适用于非 libpq PostgreSQL 驱动程序(如 JDBC 驱动程序)
- 并非专为 ORM 而设定
- 不应具有创造性、聪明才智或预见到“大多数用户想要做什么”
上述问题都很重要,但应通过另一层(例如 ActiveRecord)或驱动程序提供的、完成 libpq 功能并不必需的可选函数来解决。
指南
受支持版本
对于驱动程序,理想情况下,应支持主机语言和 PostgreSQL 的多个版本。但是,多年来 libpq 已发生很大变化,因此构建系统、测试、代码复杂度和条件编译代码都很快成为一项重大负担。坚持PostgreSQL 发布支持政策似乎最为合理。如果有人需要连接到旧服务器,他们可以下载驱动程序的旧版本。
要支持哪些主机语言版本更多取决于语言。根据可用开发人员资源选择合理支持的版本。
包装 libpq
坚持libpq API 很重要,并在驱动程序的 API 中公开所有 libpq 功能。试图以创造性方式对其进行主机语言适配很有吸引力,但这可能会导致两个问题
- 遗漏有用的 libpq 功能。
- 创造力没有尽头,因此 API 将会有无穷无尽的调整和重新审视。
创造力很重要,但应在驱动程序级别之上完成(例如 SQLAlchemy、ActiveRecord 等)。
话虽如此,有些小便利可以使 API 调用更顺畅地融入语言中,这是一个好主意。应该可以轻松传递参数,以便可以使用 PQexecParams(和其他函数),从而避免 SQL 注入风险。重要的是在 libpq 和驱动程序的 API 之间保持近乎一一的映射,并提供所有 libpq 的功能。
例如,在 ruby-pg 中,可以在简单的情况下轻松传递参数
conn.exec("INSERT INTO foo VALUES($1, $2)", ["Jeff", "Davis"])
并且在复杂的情况下也可以
# insert some BYTEA data using binary format and explicit type oid conn.exec("INSERT INTO bar VALUES($1, $2)", ["Jeff", {:value => filedata, :format => 1, :type => 17}])
我相信这在充分发挥 libpq 功能的同时,可以在合理保持 ruby 风格并为典型情况提供便利之间取得了很好的平衡。
如果某些 libpq 功能特别晦涩难懂(例如 PQprint()),则可以将其省略。但是,应该明确遗漏的内容,以便新开发人员在需要时可以实现该功能。
抽象层
许多语言有自己的标准化抽象层,例如 perl 的 DBI。强烈建议支持这些抽象层。但是,不应将此类抽象层视为完全支持所有 libpq 功能(这是很重要的)的替代品。
类型转换
一般来说,类型转换应留给更高级别的语言(例如 ActiveRecord)或驱动程序提供的可选便利函数(这在某些特殊情况下对于性能可能很重要)。类型转换必须在某个时候进行,但是开发人员需要能够在出错的情况下轻松控制此类转换。
以受控的方式从 postgresql 类型转换为宿主语言中的类型(反之亦然)。
- 避免“魔法”。
- 记住,结果可能以二进制模式返回,这使得翻译成宿主语言中的类型不太可行(但对于效率原因可能也非常有用)。
- 与 libpq 函数(如 PQgetvalue())最直接类似的函数应返回原始表示(取决于模式是文本还是二进制)。任何类型转换都应作为独立的便捷例程的一部分完成。
- 请注意,转换几乎总是不完美的,即使对于看似相似的类型也是如此。
- 请注意,你永远无法为所有类型提供转换,因为可能存在用户定义类型或没有相应类型(或至少没有完全定义的类型)的类型宿主语言中。
- 考虑转换过程中的错误。例如,Ruby 中的数组可能同时包含文本和整数,并且无法将其转换为等效的 PostgreSQL 数组。
- 如果处理用户定义类型的转换(包括 contrib 中的转换,例如 citext 或 hstore;以及来自 PostGIS 的几何形状等第三方类型),请注意,类型的 OID(或其存在)可能会随时更改。
- 类型转换很可能需要不断调整和更新,这可能会影响 API 的稳定性。
- 由于这些类型转换的性质不完善,我认为永远不会有“标准”的方式进行类型转换。只会有一些大部分时间都能正常工作的便捷函数。
对返回结果使用“神奇”对象进行包装可能是一个好方法,只要
- “神奇”对象始终允许你检索最初返回的值(文本或二进制格式)。
- 这些包装器对象的 API 足够稳定。
文本编码
- 如果结果集为二进制格式
- 什么都不做。用户明确规避了编码之类的问题。任何返回的值都应为字节序列,而不是字符序列。
- 如果宿主语言中的字符串是字节序列(如 C、Ruby 1.8 或 Python 2.X)
- 大多数编码问题都可以忽略。你只需将字节传送到 postgresql,如果它拒绝它们,那就是应用程序没有遵守 client_encoding 的设置(或者出于某种原因 postgresql 无法在 client_encoding 和 server_encoding 之间进行转码)。
- 如果宿主语言中的字符串是字符序列(如 Python 3.X)
- 将数据发送到 postgresql 时,使用 client_encoding 对字符串进行编码。从 postgresql 接收数据时,使用结果对象返回时的client_encoding 值解码字符串。
- 如果主机语言中的字符串可以进行任何编码(比如 Ruby 1.9)
- 将数据发送到 Postgresql 时,将字符串从其编码转到 client_encoding 中。从 Postgresql 接收数据时,将字符串与 client_encoding 的值相关联(在结果对象的返回时间)并且(可选)将其转到其他一些编码中。
原始二进制表示
类型转换 的理念可以合理地扩展到文本编码中——换句话说,总是返回字节序列而不是字符串。必须以某种方式提供字节序列,以防开发人员做一些不寻常的事情(例如对两个编码之间进行有损转换,但两个编码间有无损转换)。
但是,处理已编码字符串应该是与驱动程序的 API 交互最简单的方式,并且仅仅需要处理字节序列(不一定简单)。自动处理字符串编码不是“魔法”的原因是:有一个执行转换的标准无损方法,并且几乎总是需要这样做。
结果对象和 client_encoding
在结果对象返回后,client_encoding 可能会更改,但结果对象中的字节显然不会更改。因此,必须在从 libpq 返回结果对象后立即将 client_encoding 值附加到该结果对象。然后,从结果对象中读取数据时使用该编码,而不是使用 client_encoding 的当前值。
client_encoding 在不恰当的时间更改似乎有些牵强,但实际上这是完全合理的。考虑使用 COPY 在其他编码中使用文本文档。
编码名称
libpq 提供了 PQclientEncoding() 函数,该函数返回一个编码 ID(显然它匹配代码页数字),以及 pg_encoding_to_char(),它从编码 ID 中返回一个编码名称。这些可以作为从驱动程序代码中选择正确编码的可靠方式。
实用程序功能
解决编码的最佳方法是定义两个实用程序功能(驱动程序内部的)
- pg_str_encode(string object) -> bytes
- 将主机语言中的字符串对象编码成 client_encoding。务必在发送到 PostgreSQL 之前直接使用,确保 client_encoding 未事先更改。
- pg_str_decode(bytes, result object) -> string object
- 使用存储在结果对象(而不是当前 client_encoding)中的 client_encoding 来解码从 postgresql 中接收的字符串。这也可以是污染对象的一个便利位置,如果语言支持它(注意:其他位置也需要污染;例如如果结果集采用二进制格式)。
使用这两个函数,可以在以下进行集中化
- 条件编译代码(支持多个语言版本可能需要这样)
- 代码在主机语言和 PostgreSQL(如果需要)之间正确匹配编码名称。
污染
对于支持“污染”变量的语言,切记污染所有容纳 PostgreSQL 返回数据的变量。
非阻塞 libpq 调用
不要使用阻塞 libpq 接口函数。改用这些函数的非阻塞版本,并在函数中使用主机语言首选的阻塞方法进行阻塞。例如,在 Ruby 中,可以使用 rb_thread_select(),该函数允许 ruby 线程实现执行其他任务,同时等待 postgresql 完成其任务。
这完全不会限制驱动程序的功能,并且主机语言中的 API 仍应与 libpq 一一匹配。不同之处在于主机语言中的阻塞调用在内部将使用 libpq 的非阻塞调用。
请注意,您需要做好准备,以防因服务器已忙而导致命令失败。
另外,请注意,这与线程安全问题无关。使用 libpq 的阻塞调用或非阻塞调用都可以实现线程安全(或不安全)。
错误处理
PostgreSQL 在发生错误时会提供许多有用的信息,并且这些信息应该由驱动程序公开。然而,在支持异常的主机语言中,当处理错误时,连接和结果对象可能已超出作用域。应将对连接和结果对象(如果适用)的引用与错误(通常是异常对象)一起保留,从而允许完全访问有关错误的信息。
连接和结果对象
支持在连接对象上调用 PQfinish() 以立即断开连接非常重要,而不会等待垃圾回收器。主机语言的垃圾回收器只应负责释放内存,我们肯定不希望鼓励保持空闲连接打开状态直到等待它们被垃圾回收。
类似地,建议允许显式清除结果对象,因为它可能是大量的内存。
其中任何一项都引入了拥有“僵尸”连接(或结果)对象的可能性,并且对这些对象的大多数操作都应引发有用的错误。然而,如上所述,这些对象仍可能包含有用的错误信息。