Psycopg 3 项目的目标之一是简化从 Psycopg 2 开发的代码的移植。因此,Django 后端(您在设置中指定为数据库引擎的模块)的创建是一个具有双重目标的项目:A Django 驱动程序是一种从一开始就使 Psycopg 3 有用的方法,可以将其透明地放入项目中,并在需要时提供新功能(例如卓越的 COPYsupport)。在 Django 代码库中引入 Psycopg 3 的难度以及所需更改的类型表明了在移植其他项目时可能会发现的问题类型。 ……完成了!几天前,新的 Psycopg 3 Django 后端可以通过整个 Django 测试套件! Django后端的实现其实在几个月前就开始了,但是从上面的测试进度可以看出,它的开发已经暂停了几个月。在第一次尝试中,问题是太多的 Django 代码需要修改:这表明使用新适配器所需的更改太具有侵入性,并且每个尝试的人都会遇到相同类型的困难将 Psycopg 2 替换为 Psycopg 3。然后回到设计板,但希望最终的适配器将按照您的预期运行,并且不会强迫用户更改其程序中的每个查询(这对于大多数非平凡的项目来说都是一个交易破坏者) .后端不能与当前的 Django 版本一起使用:需要对 Django 代码库进行一些修改才能使用它。这些更改将提交给 Django 项目:如果 Django 维护者接受它们,则驱动程序应该可以从下一个 Django 版本之一开始使用。本文的目的是查看其中的一些修改,以了解 Psycopg 3 的行为与其众所周知的前身有何不同,以及如何解决这些差异。
许多这些更改是使用服务器端绑定查询参数(使用 libpq PQexecParams() 函数)的结果,而不是将参数合并到客户端的查询并使用更简单的 PQexec() 函数。在 PQexec() 的情况下,Postgres 查询解析器可以访问使用它们的上下文中的文字值,并且看起来它能够以我们不喜欢的方式使用这些信息......直到我们失去它们。您认为文本是将 Python 字符串转换为最佳的 PostgreSQL 数据类型吗?我希望它如此简单。下面是 psycopg.pq 对象的实验,这是 Psycopg 3 公开的低级 libpq 包装器:>>> from psycopg.postgres 导入类型 >>> conn 。 execute ( "create table testjson (id serial primary key, data jsonb)" ) # <psycopg.Cursor [COMMAND_OK] [INTRANS] (database=piro) at 0x7f92d43d7b80># 注意:$1, $2...是低级Postgres占位符。# 在普通的 Psycopg 查询中,您将使用经典的“%s”。 >>> 连接。 pgconn 。 exec_params ( ... b "insert into testjson (data) values ($1)" , ... [ b " {} " ], [ types [ "text" ] . oid ]) # <psycopg_c.pq.PGresult [FATAL_ERROR ] at 0x7f92cec70a90> >>> print ( _ . error_message . decode ( "utf8" )) # ERROR: column "data" is of type jsonb but expression is of type text# LINE 1: insert into testjson (data) values ($1 )# ^# 提示:您需要重写或转换表达式。指定文本 Postgres 类型是一种非常严格的类型指示:在大多数情况下,Postgres 将无法自动将值转换为所需的类型。当我们在查询中使用文字“{}”时,我们指定了一个无类型文字。 Postgres 文档说我们可以使用 0 作为参数的类型 OID 来做同样的事情(参见 paramTypes[] 描述)。但似乎情况并非总是如此。例如: >>> conn 。 execue ( "select concat( %s , %s )" , [ "foo" , "bar" ]) # ...变成... >>> conn . pgconn 。 exec_params ( ... b "select concat($1, $2)" , ... [ b "foo" , b "bar" ], [ 0 , 0 ]) # <psycopg_c.pq.PGresult [FATAL_ERROR] at 0x7f92d43db4d0> >>> print ( _ . error_message . decode ( "utf8" )) # 错误:无法确定参数 $1 的数据类型 这个问题不会发生在每个函数中:它似乎只是“可变”函数的问题,例如作为 concat() 或 json_build_object()。尽管是零星的,但似乎没有一种普遍正确的方法将 Python 类型映射到 PostgreSQL 类型的 OID:我们可以在大多数情况下尝试正确处理(因此,默认情况下,Psycopg 3 使用 OID 0 转储 Python 字符串),但是确实存在一些不正确的地方……当然,它们存在于 Django 中。
有两种不同的方法可以解决这个问题,这两种方法各有千秋,而且在不同的上下文中,一种可能比另一种更容易使用。向占位符添加强制转换:在您的查询中指定 %s::text (或其他类型),可以消除“未知”不起作用的类型的歧义: >>> conn 。 execue ( "select concat( %s ::text, %s ::text)" , [ "foo", "bar" ]) # ...变成... >>> conn . pgconn 。 exec_params ( ... b "select concat($1::text, $2::text)" , ... [ b "foo" , b "bar" ], [ 0 , 0 ]) # <psycopg_c.pq.PGresult [TUPLES_OK] 在 0x7f92cebfb630> >>> _ 。 get_value ( 0 , 0 ) # b'foobar' 在 Django 中需要这样做的一个地方是在数组比较中,因为它们遵循比基类型比较更严格的规则,并且可能需要显式转换才能工作。另一种选择是使用与 Python str 不同的类型并使用不同的转储程序处理它。 >>> 连接。 execue ( "select concat( %s , %s )" , [ Text ( "foo" ), Text ( "bar" )]) # ...变成... >>> conn . pgconn 。 exec_params ( ... b "select concat($1, $2)" , ... [ b "foo" , b "bar" ], [ types [ "text" ] . oid , types [ "text" ] . oid ] ) # <psycopg_c.pq.PGresult [TUPLES_OK] at 0x7f92cebfbbd0> >>> _ . get_value ( 0 , 0 ) # b'foobar' 请注意,这两种解决方案都使查询与 Psycopg 2 和 3 兼容:%s::text 转换在 psycopg2 查询中没有问题,并且 psycopg2 足够聪明,可以注意到 Text 是一个 str子类并应用与香草字符串相同的转换规则。
服务器上的参数绑定仅适用于选择和修改数据的查询,但不适用于数据定义语言。例如: >>> conn 。 execute ( "create table test (id int default %s )" , [ 42 ]) # Traceback (最近调用 last):# ...# psycopg.errors.UndefinedParameter: 没有参数 $1# LINE 1: create table test (id int default $1)# ^ 此类问题的解决方法是使用psycopg.sql模块在客户端显式生成一个查询并发送到服务器,不带参数:psycopg2中也有类似的模块,所以它是易于编写适用于两个版本的代码:它只是需要更改的导入语句。另一个意外问题表现为测试失败并显示“列名必须出现在 GROUP BY 子句中或用于聚合函数”之类的消息。当聚合键包含参数时,这种类型的错误出现在利用 Django ORMaggregation 功能的测试中。例如,如果您想通过按姓名的前两个字母对人数进行分组来计算人数,您可以使用如下查询: SELECT left(name, 2) AS prefix, count(*) AS numberFROM peopleGROUP BY left(name , 2)ORDER BY left(name, 2)
如果“2”实际上是一个参数,Django 最终会编写和执行这样的查询: cursor 。执行 ( """ SELECT left(name, %s ) AS prefix, count(*) AS number FROM people GROUP BY left(name, %s) ORDER BY left(name, %s) """ , [ 2 , 2 , 2 ]) 如果由客户端组成,则此查询没有问题,因为 serverquery 解析器可以清楚地看到输出列和组/顺序谓词中的相同表达式。然而,转向使用服务器端参数,查询将转换为: SELECT left(name, $1) AS prefix, count(*) AS numberFROM peopleGROUP BY left(name, $2)ORDER BY left(name, $3) 这个查询仅当三个参数相同时才能执行,但在解析时服务器无法确定是否会出现这种情况,并因上述错误而失败。如果此查询在某人的控制之下,则可以使用命名参数而不是位置参数轻松地重写它。我个人会写: cursor 。执行 ( """ SELECT left(name, %(len)s ) AS prefix, count(*) AS number FROM people GROUP BY left(name, %(len)s ) ORDER BY left(name, %(len)s) ) """ , { "len" : 2 })
它在查询中转换为一个 $1 占位符,使用了三次,并成功解析。不幸的是,Django 只在其整个代码库中使用位置占位符,切换到参数映射将是一个非常侵入性的更改。更本地化的更改是使用列别名:相同的查询可以重写为:其中“1”指的是第一个输出列。它不是一个特别受欢迎的语法,但在这里很有用。可能不是每个数据库都支持这种语法,因此,在提议的 Djangochangeset 中,新功能参数可用于表示特定数据库接受该功能,目前仅对 PostgreSQL 启用。理解这一点就不那么神秘了。 psycopg2 包有一些混乱的组织,有几个厨房水槽模块(扩展和额外)包含一些东西:游标子类、额外的数据类型、实用函数、符号常量......从 Psycopg 3 开始,这个包是有组织的以更合理的方式分离在不同的模块和子包中。从 Django 使用 Psycopg 3 所需的大部分更改并不是假设将使用 psycopg2 完成与 PostgreSQL 的对话,而是根据所使用的驱动程序的版本导入和使用对象。请注意,通过这些修改,Django 可以在同一个项目中同时使用 Psycopg 2 和 3。虽然这是可能的,但它可能不是您项目中的一般用例:通常您只对从 Psycopg 2 升级到 3 感兴趣。在更简单的更新情况下,您只需要更改导入语句,假设无条件安装了 psycopg 包(没有 psycopg3 包:相同的模块名称也适用于以下主要版本),然后就告别 psycopg2 迄今为止的辉煌主力。 👋 在 Psycopg 3 的设计中,我们做了很大的努力,以允许有经验和代码库的用户顺利采用 psycopg2。 Django后端移植的经验表明,大部分需要的调整都属于以下几类:
希望现在您知道如何解决这些问题,以防您考虑在下一个或当前项目中使用 Psycopg 3!