与 psycopg2 的区别
Psycopg 3 使用的是同许多其他数据库适配器一样的 DBAPI 结构,并尝试尽可能在行为上接近psycopg2
。然而,有一些差异需要注意。
大多数情况下,此处建议的解决方法会同时适用于 Psycopg 2 和 3,如果您要移植或编写同时适用于 Psycopg 2 和 3 的程序,这可能会很有用。
Psycopg 3 将查询和参数分别发送到服务端,而不是在客户端合并它们。服务端绑定适用于普通SELECT
语句和数据操作语句(INSERT
、UPDATE
、DELETE
),但不适用于许多其他语句。例如,它不适用于SET
或NOTIFY
语句:
>>> conn.execute("SET TimeZone TO %s", ["UTC"])
Traceback (most recent call last):
...
psycopg.errors.SyntaxError: syntax error at or near "$1"
LINE 1: SET TimeZone TO $1
^
>>> conn.execute("NOTIFY %s, %s", ["chan", 42])
Traceback (most recent call last):
...
psycopg.errors.SyntaxError: syntax error at or near "$1"
LINE 1: NOTIFY $1, $2
^
也不适用于任何数据定义语句:
>>> conn.execute("CREATE TABLE foo (id int DEFAULT %s)", [42])
Traceback (most recent call last):
...
psycopg.errors.UndefinedParameter: there is no parameter $1
LINE 1: CREATE TABLE foo (id int DEFAULT $1)
^
有时,PostgreSQL 提供了替代方案:例如,可以使用 set_config() 函数代替SET
语句,可以使用 pg_notify() 函数代替NOTIFY
语句:
>>> conn.execute("SELECT set_config('TimeZone', %s, false)", ["UTC"])
>>> conn.execute("SELECT pg_notify(%s, %s)", ["chan", "42"])
如果无法使用服务端绑定,您必须在客户端合并查询和参数。您可以使用psycopg.sql
对象来执行此操作:
>>> from psycopg import sql
>>> cur.execute(sql.SQL("CREATE TABLE foo (id int DEFAULT {})").format(42))
或创建客户端绑定游标,例如ClientCursor
:
>>> cur = ClientCursor(conn)
>>> cur.execute("CREATE TABLE foo (id int DEFAULT %s)", [42])
如果您经常需要ClientCursor
,可以设置Connection.cursor_factory
默认调用Connection.cursor()
创建它们。这样,Psycopg 3 的行为方式与 Psycopg 2 基本相同。
请注意,无论是服务端还是客户端,您都只能指定值作为参数(即用单引号括起来的字符串)。如果需要参数化语句的不同部分(例如表名),则必须使用该psycopg.sql
模块:
>>> from psycopg import sql
# This will quote the user and the password using the right quotes
# e.g.: ALTER USER "foo" SET PASSWORD 'bar'
>>> conn.execute(
... sql.SQL("ALTER USER {} SET PASSWORD {}")
... .format(sql.Identifier(username), password))
由于使用服务端绑定的关系,当使用参数时,不可能在同一个execute()
调用中执行多个语句,并用分号分隔它们:
>>> conn.execute(
... "INSERT INTO foo VALUES (%s); INSERT INTO foo VALUES (%s)",
... (10, 20))
Traceback (most recent call last):
...
psycopg.errors.SyntaxError: cannot insert multiple commands into a prepared statement
解决该问题的一种明显方法是使用多个execute()
调用。
如果不使用参数则没有这样的限制。因此,您可以在客户端编写多个查询,使用psycopg.sql
对象在同一个execute()
调用中运行它们:
>>> from psycopg import sql
>>> conn.execute(
... sql.SQL("INSERT INTO foo VALUES ({}); INSERT INTO foo values ({})"
... .format(10, 20))
或者使用客户端绑定游标:
>>> cur = psycopg.ClientCursor(conn)
>>> cur.execute(
... "INSERT INTO foo VALUES (%s); INSERT INTO foo VALUES (%s)",
... (10, 20))
如果一条语句必须在事务外执行(例如
CREATE DATABASE
),则它不能与其他语句一起批量执行,即使连接处于自动提交模式:>>> conn.autocommit = True >>> conn.execute("CREATE DATABASE foo; SELECT 1") Traceback (most recent call last): ... psycopg.errors.ActiveSqlTransaction: CREATE DATABASE cannot run inside a transaction block
发生这种情况是因为 PostgreSQL 本身会将多个语句包装在一个事务中。请注意,您会在 psql 中体验到不同的行为(psql 会按分号分割查询并将它们单独发送到服务端)。
这不是 Psycopg 3 新引入的行为:同样的限制也存在于
psycopg2
。
如果在 psycopg2 中执行多个返回结果的语句,则仅返回最后一条语句的结果:
>>> cur_pg2.execute("SELECT 1; SELECT 2")
>>> cur_pg2.fetchone()
(2,)
而在 Psycopg 3 中,所有的结果都可以获取。运行查询后,第一个结果将在游标中随时可用,并且可以使用常用的fetch*()
方法获取。为了访问后面的结果,您可以使用Cursor.nextset()
方法:
>>> cur_pg3.execute("SELECT 1; SELECT 2")
>>> cur_pg3.fetchone()
(1,)
>>> cur_pg3.nextset()
True
>>> cur_pg3.fetchone()
(2,)
>>> cur_pg3.nextset()
None # no more results
请记住,如果您向查询传递参数,则不能使用服务端绑定在同一查询中执行多个语句。
在极少数情况下,尤其是在可变参数的函数方面,PostgreSQL 可能无法找到给定数据类型的候选函数:
>>> conn.execute("SELECT json_build_array(%s, %s)", ["foo", "bar"])
Traceback (most recent call last):
...
psycopg.errors.IndeterminateDatatype: could not determine data type of parameter $1
这可以通过强制转换显式指定参数类型来解决:
>>> conn.execute("SELECT json_build_array(%s::text, %s::text)", ["foo", "bar"])
IN
不能与用作单个参数的元组一起使用,而psycopg2
是可以的:
>>> conn.execute("SELECT * FROM foo WHERE id IN %s", [(10,20,30)])
Traceback (most recent call last):
...
psycopg.errors.SyntaxError: syntax error at or near "$1"
LINE 1: SELECT * FROM foo WHERE id IN $1
^
您可以做的是使用 = ANY() 形式的表达式,以列表而不是元组传递候选值,这样可以适配到 PostgreSQL 数组:
>>> conn.execute("SELECT * FROM foo WHERE id = ANY(%s)", [[10,20,30]])
请注意,ANY()
也可以在psycopg2
中使用,并且具有接受空值列表作为参数的优点,而IN
运算符不支持空值列表作为参数。
您不能使用IS %s
或IS NOT %s
:
>>> conn.execute("SELECT * FROM foo WHERE field IS %s", [None])
Traceback (most recent call last):
...
psycopg.errors.SyntaxError: syntax error at or near "$1"
LINE 1: SELECT * FROM foo WHERE field IS $1
^
这可能是由于在 PostgreSQL 中 IS
不是二元运算符所致;相反,IS NULL
和IS NOT NULL
是一元运算符,您不能在IS
的右侧放置其他任何内容一起使用。在 psql 中测试:
=# SELECT 10 IS 10;
ERROR: syntax error at or near "10"
LINE 1: select 10 is 10;
^
您可以做的是使用 IS DISTINCT FROM 运算符,它可以接受占位符:
>>> conn.execute("SELECT * FROM foo WHERE field IS NOT DISTINCT FROM %s", [None])
类似地,您可以将IS DISTINCT FROM %s
用作IS NOT %s
的参数版本。
为了解决服务端的参数适配,同时也考虑性能、灵活性、易于定制性,Psycopg 3 的适配系统已经完全重写。
内置数据的默认行为应该是您所期望的。如果您自定义了适配数据的方式,或者如果您正在管理自己的扩展类型,则应该查看新的适配系统。
- 适配基础 Python 类型以实现基本行为。
- 数据适配配置以供更高级的使用。
psycopg2
公开了与 PostgreSQL COPY
交互的一些复制方法。它们基于文件的接口并不容易将动态生成的数据加载到数据库中。
现在有一个copy()
方法,类似于psycopg2
copy_expert()
,接受自由格式的COPY
命令,并返回一个对象,按块或按记录读取/写入数据。不同的COPY
使用模式还可以用于异步交互。
有关详细信息,请参阅使用 COPY TO 和 COPY FROM。
在psycopg2
中,使用 with 连接 语法,仅关闭事务,而不关闭连接。对于习惯了其他几个包装资源(例如文件)的 Python 类的人来说,这种行为是令人惊讶的。
在 Psycopg 3 中,使用 with 连接 将在with
块末尾关闭连接,从而使处理连接资源更加熟悉。
为了将事务作为代码块进行管理,您可以使用Connection.transaction()
方法,该方法允许更精细的控制,例如使用嵌套事务。
有关详细信息,请参阅事务上下文。
cursor.callproc()
没有实现。该方法具有简单的语义,不考虑 PostgreSQL 位置参数、过程、返回集合的函数等等。可使用SELECT function_name(...)
或CALL procedure_name(...)
语句调用普通的 execute()
方法代替。
Psycopg 自动使用数据库客户端编码将数据解码为 Unicode 字符串。如果您需要读取编码,请使用ConnectionInfo.encoding
。您可以使用client_encoding
连接参数在连接时选择编码,并且可以通过运行SET client_encoding
语句来更改连接的编码。但是为什么要这样做呢?
PostgreSQL 可以表示比 Python 范围更大的日期和时间戳。Python 日期仅限于 1 到 9999 之间的年份(由datetime.date.min
和max
等常量表示),而 PostgreSQL 日期则扩展到 BC 日期和 10K 以上的年份。此外,PostgreSQL 还可以在两个方向上表示符号日期 “infinity”。
在 psycopg2 中,默认情况下,无限日期和时间戳映射到 ‘date.max’ 和类似的常量。这存在创建非对称映射的问题(两个 Postgres 日期,无穷大和 9999-12-31,都映射到相同的 Python 日期)。还有一个反常现象是,有效的 Postgres 日期(大于 Python 的date.max
但小于无穷大)仍然会溢出。
在 Psycopg 3 中,每个大于 9999 年的日期都会溢出,包括无穷大。如果您想自定义此映射(例如,将 10000 年后的每个日期按date.max
展平),您可以子类化并调整适当的加载器:查看此示例以了解如何操作。