Python cx_Oracle 5.0 新特性概述
用于访问 Oracle 数据库的标准 Python 扩展 cx_Oralce 的新特性概述
作者:Anthony Tuininga
2009 年 4 月发布
cx_Oracle 是一个 Python 扩展模块,通过使用所有数据库访问模块通用的数据库 API 来实现 Oracle 数据库的查询和更新。为使用一些专为 Oracle 设计的特性,还加入了多个通用数据库 API 的扩展。
cx_Oracle 的开发历时十多年,涵盖了大多数需要在 Python 中访问 Oracle 的客户的需求。2008 年 12 月,一个新的主要版本解决了早期版本的众多限制,并增添了对 Python 3.0 和 Oracle 新推出的一些特性的支持。本文旨在深入介绍这些新特性,并解释您为什么希望在您的代码中使用这些特性的原因。
对 Unicode 和 Python 3.0 的支持
在 cx_Oracle 的早期版本中,没有对那些希望在其代码中使用 Unicode 字符串的用户提供帮助。代码将不得不编写成清单 1 中所示的那样。
清单 1:cx_Oracle 4.x 中的老式 Unicode 处理方法import cx_Oracle import os os.environ["NLS_LANG"] = ".AL32UTF8" START_VALUE = u"Unicode \u3042 3" END_VALUE = u"Unicode \u3042 6" connection = cx_Oracle.Connection("cx_Oracle/dev@t11g") cursor = connection.cursor() cursor.execute(""" select UnicodeCol from TestUnicodes where UnicodeCol > :startValue and UnicodeCol < :endValue""", startValue = START_VALUE.encode(connection.nencoding), endValue = END_VALUE.encode(connection.nencoding)) for rawValue, in cursor: actualValue = rawValue.decode(connection.nencoding) print "Actual Value:", actualValue注意,将数据传递到 Oracle 数据库后,需要将其编码到客户端字符集中;从数据库中检索到数据后,需要从客户端字符集对数据进行解码。虽然可以编写代码对 Unicode 字符串进行相应处理,但这样做很繁琐且容易出错。Python 3.0 的所有字符串都是 Unicode 字符串,所以随着 Python 3.0 的出现,cx_Oracle 也需要进行相应调整。显然,Python 3.x C API 的改变使得同时实现对 Python 3.x 和 Unicode 字符串的支持变得困难。因此,我们首先在 Python 2.x 中以 Unicode 模式 的形式添加了对 Unicode 字符串的支持,然后才添加了对 Python 3.x 的支持。
在 Unicode 模式下,所有传入 Oracle 的字符串都应是 Unicode 格式,并且所有返回的字符串也都是 Unicode 格式。与 Python 3.x 中一样,这不仅包含绑定变量和来自查询的结果集,还包括语句文本、连接参数以及属性值。Unicode 模式是编译时选项,在 Python 2.x 中默认为禁用,但可以在构建模块前通过设定环境变量 WITH_UNICODE 来启用。
无论在哪种模式下,使用国家字符集表示的所有数据(例如,NCLOB 和 NVARCHAR2 列)现在将以 Unicode 格式返回,并且也应以 Unicode 格式绑定。这意味着以通过删除所有编码和解码操作简化清单 1 中的示例代码。清单 2 将对此进行演示。
清单 2:cx_Oracle 5.0 中简化后的 Unicode 处理方法import cx_Oracle START_VALUE = u"Unicode \u3042 3" END_VALUE = u"Unicode \u3042 6" connection = cx_Oracle.Connection("cx_Oracle/dev@t11g") cursor = connection.cursor() cursor.execute(""" select UnicodeCol from TestUnicodes where UnicodeCol > :startValue and UnicodeCol < :endValue""", startValue = START_VALUE, endValue = END_VALUE) for actualValue, in cursor: print "Actual Value:", actualValue但是,未使用 Unicode 模式时,所有以数据库字符集表示的数据仍像在 cx_Oracle 4.x 中一样以 Python 字符串表示。幸运的是,本文要介绍的下一个增强将解释如何避免此类情况(如果需要),无需使用 Unicode 模式或升级到 Python 3.x。
输入/输出类型处理程序
cx_Oracle 5.x 的另一个主要特性是引入了输入和输出类型处理程序。利用这些处理程序,可以实现很高的灵活性和可自定义性。例如,如果需要将所有字符串以 Unicode 格式返回,可以使用清单 3 中的代码。 清单 3:如何将所有字符串返回为 Unicode 字符串
import cx_Oracle def OutputTypeHandler(cursor, name, defaultType, size, precision, scale): if defaultType in (cx_Oracle.STRING, cx_Oracle.FIXED_CHAR): return cursor.var(unicode, size, cursor.arraysize) connection = cx_Oracle.Connection("cx_Oracle/dev@t11g") connection.outputtypehandler = OutputTypeHandler cursor = connection.cursor() cursor.execute(""" select * from TestStrings where StringCol > :startValue and StringCol < :endValue""", startValue = u"String 3", endValue = u"String 6") for row in cursor: print "Row:", row输入和输出类型处理程序可以在游标级别设定(只对该游标生效),也可以在连接级别设定(将对所有使用该连接创建且没有自己的输入或输出类型处理程序的游标生效)。输入或输出类型处理程序中的 None 值意味着处理将使用 cx_Oracle 的默认算法进行。
当需要创建绑定变量以从 Python 向 Oracle 传递数据时,将调用输入类型处理程序方法。需要创建的游标(已绑定值)和元素数量将传递到该方法。返回值应该是一个变量对象或 None(意味着将执行默认处理)。创建的变量对象应该能够绑定 Python 值。为解决这一问题,现在可以使用输入转换器创建变量对象。这些方法使用绑定的 Python 值调用,并应返回一个 cx_Oracle 已经知道如何直接绑定的 Python 值。最后一点:调用 PL/SQL 过程和函数或者匿名 PL/SQL 块将有可能引入输入/输出,甚至是仅用于输出的绑定变量。即使数据可以从 Oracle 返回到 Python,这些操作仍将涉及到输入类型处理程序。
当需要创建绑定变量以从 Oracle 向 Python 返回数据时(执行查询后),将调用输出类型处理程序方法。在这种情况下,游标、返回列的名称、默认变量类型以及返回列的大小、精度和范围将传递到该方法。与使用输入类型处理程序时一样,返回值应该是一个变量对象或者 None(意味着将执行默认处理)。如果希望对返回 Python 的数据进行更多操作,可在变量上设置一个输出转换器。这些方法使用原本可能返回到 Python 的值调用,并返回将实际返回到 Python 的值。
下面几个示例将向您解释此增强在控制 Python 和 Oracle 间的接口方面是多么强大。清单 4 演示了如何将任意对象和游标绑定。清单 5 演示如何将数字返回为 decimal.Decimal 对象或字符串,以便管理远大于浮动精度数字所能处理的数字。
假设您需要将几个二进制大对象 (BLOB) 上载到数据库并将它们保存到 blob_tab 表,您可能已经在自定义数据库模式中创建了该表,如下所示:
清单 4:将任意 Python 对象与游标绑定import cx_Oracle class ArbitraryObject(object): def __init__(self, intValue, someOtherData): self.intValue = intValue self.someOtherData = someOtherData def BindValue(self): return self.intValue def InputTypeHandler(cursor, value, numElements): if isinstance(value, ArbitraryObject): return cursor.var(int, arraysize = numElements, inconverter = ArbitraryObject.BindValue) connection = cx_Oracle.Connection("cx_Oracle/dev@t11g") connection.inputtypehandler = InputTypeHandler cursor = connection.cursor() cursor.execute(""" select * from TestNumbers where IntCol = :obj""", obj = ArbitraryObject(1, "arbitrary")) for row in cursor: print "Row:", row
清单 5:将数字返回为字符串或 decimal.Decimal 对象
import cx_Oracle import decimal def NumbersAsDecimal(cursor, name, defaultType, size, precision, scale): if defaultType == cx_Oracle.NUMBER: return cursor.var(str, 100, cursor.arraysize, outconverter = decimal.Decimal) def NumbersAsString(cursor, name, defaultType, size, precision, scale): if defaultType == cx_Oracle.NUMBER: return cursor.var(str, 100, cursor.arraysize) connection = cx_Oracle.Connection("cx_Oracle/dev@t11g") connection.outputtypehandler = NumbersAsDecimal cursor = connection.cursor() cursor.execute(""" select * from TestNumbers where IntCol < 3""") for row in cursor: print "Row (as decimal):", row cursor = connection.cursor() cursor.outputtypehandler = NumbersAsString cursor.execute(""" select * from TestNumbers where IntCol between 6 and 8""") for row in cursor: print "Row (as string):", row
数据库驻留连接池 (DRCP) 支持
在 Oracle 数据库 11g 第 1 版中,Oracle 引入了数据库驻留连接池(DRCP,请参见技术白皮书)。与会话池(其中的连接在客户端中汇集,并且仅在单线程进程中有效)不同,DRCP 将服务器上的连接汇集到一起,实现了跨多个客户端进程的汇集。该增强在使用经常设置为每次点击都产生一个进程的 Web 服务器(如 Apache)时尤为有用。在这种情况下,使用 DRCP 可以显著提升性能和降低资源需求。对该特性的支持已在 2008 年添加到 PHP 中;在版本 5 中,cx_Oracle 也将对该特性的支持添加到 Python 中。
在数据库上启动 DRCP 池并在 tnsnames.ora 条目中添加了 SERVER=POOLED 后,在 Python 中使用该特性将变得非常简单。创建普通的连接和使用 DRCP 创建连接的唯一不同之处在于参数 cclass 的使用。该参数是一个用于指定连接类(服务器逻辑分组)的字符串。使用同一个连接类生成的连接可以共享,使用不同类生成的连接则不能共享。连接构造器中另一个影响池中现有连接是否可用的参数是 purity 参数。如果该值设置为 cx_Oracle.ATTR_PURITY_SELF(默认值),则将使用池中的现有会话(如果可用)。但是,如果该值设置为 cx_Oracle.ATTR_PURITY_NEW,则将创建一个新的会话。
创建连接后,可以像使用其他连接一样使用该连接。当连接关闭后(或已对该连接对象进行垃圾回收操作),会话将返回到连接池以供重用。清单 6 展示了一个使用 DRCP 创建连接的简单示例。
清单 6:使用 DRCP 创建连接import cx_Oracle o connection = cx_Oracle.Connection("cx_Oracle/dev@t11g_drcp", cclass = "TEST1.CLASS") connection2 = cx_Oracle.Connection("cx_Oracle/dev@t11g_drcp", cclass = "TEST2.CLASS", purity = cx_Oracle.ATTR_PURITY_NEW)
数据库更改通知
Oracle 数据库的另一个特性是数据库更改通知,最初在 Oracle 数据库 10g 第 2 版中推出,并在 Oracle 数据库 11g 第 1 版中得到增强。通过该特性,客户端应用程序可以在数据库中注册查询,并在提交事务更改时接收查询基础表中数据操作语言 (DML) 或数据定义语言 (DDL) 的更改通知。通知可以通过电子邮件、HTTP URL PL/SQL 过程或回调的方式进行。cx_Oracle 目前仅支持回调方法,但是其他方法将在后续版本中提供支持。
要启动数据库更改通知,必须创建一个订阅对象。该对象描述通知发生的条件和满足该条件时所进行的操作。通过调用连接上的订阅方法来创建订阅对象。该方法的参数控制订阅的行为,并且在订阅创建后不能进行更改。可以创建任何数量的订阅对象,但是必须具有不同的特性。为了方便起见,已创建订阅对象上这些特性的值为只读属性。
我们所需要的唯一一个参数是 callback 参数。它将指定一个接受单个参数的可回调对象:在回调期间生成的消息对象,描述触发通知的事件(在后文详述)。所有其他参数均为可选项,其默认值在最常用的合理范围内。
要筛选发送通知的操作,可使用根据下表中标志的按位或构建的操作参数。下表描述了各个选项及其含义。这些值可以通过“或”操作组合在一起,从而同时涵盖多个操作。
参数值 | 描述 |
cx_Oracle.OPCODE_ALTER | 该表已更改。 |
cx_Oracle.OPCODE_DELETE | 行已从表中删除。 |
cx_Oracle.OPCODE_DROP | 该表已删除。 |
cx_Oracle.OPCODE_INSERT | 行已插入到表中。 |
cx_Oracle.OPCODE_UPDATE | 行已在表中更新。 |
要缩短通知发送的时间,可使用超时参数。默认值 0 表示在订阅对象已删除或超出范围时才会发送通知。其他值表示通知发送历经的秒数。
最后一个可以具有一个有意义值的参数是 rowid,它是一个布尔值,指定受影响行的行标识是否应包含在发送的通知消息中。保留其他两个参数(namespace 和 protocol)以供将来进行扩展,并且应保留其默认值。
创建订阅对象后,最后一步是注册查询,每个查询会触发针对每个更改的查询基础表发送通知。针对每个更改已注册表的事务发送一条消息。通过调用 registerquery 方法注册查询,与在游标上执行方法类似。它接受查询或者参数列表的 SQL。在当前版本中,参数不带有已发送的消息。在未来版本中,cx_Oracle 将添加对查询更改通知的支持(将在 Oracle 数据库 11g 第 1 版中提供),参数将变得很重要。
在订阅对象上注册查询后,已注册表上每次执行所请求的操作时都会进行回调。这将在一个单独的线程中发生,因此应注意避免同时在两个线程中使用同一个连接。传递到回调的消息对象有三个属性,用于描述已发生的事件。第一个名为 dbname,提供生成通知的数据库名称。第二个是 type,描述发生事件的类型,但是在此版本中其值将始终为:cx_Oracle.EVENT_OBJCHANGE。第三个参数名为 tables,是一个消息表对象列表。
消息表对象具有以下属性:name(受影响表的名称)、operation(在表上发生的操作)以及 rows。创建订阅对象时,如果 rowid 参数的值为 true,则 rows 属性将是一个消息行对象列表。消息行对象具有两个属性:operation(发生在该行的操作)和 rowid(包括受影响行的行标识)。
清单 7 是在 cx_Oracle 中使用数据库更改通知的一个示例。代码运行时,在表 cx_Oracle.TestExecuteMany 中插入、更新或删除行以查看通知的生成。
清单 7:通知演示import cx_Oracle def OperationToString(operation): operations = [] if operation & cx_Oracle.OPCODE_INSERT: operations.append("insert") if operation & cx_Oracle.OPCODE_DELETE: operations.append("delete") if operation & cx_Oracle.OPCODE_UPDATE: operations.append("update") if operation & cx_Oracle.OPCODE_ALTER: operations.append("alter") if operation & cx_Oracle.OPCODE_DROP: operations.append("drop") if operation & cx_Oracle.OPCODE_ALLOPS: operations.append("all operations") return ", ".join(operations) def OnChanges(message): print "Message received" print " Database Name:", message.dbname print " Tables:" for table in message.tables: print " Name:", table.name, print " Operations:", print OperationToString(table.operation) if table.rows is None \ or table.operation & cx_Oracle.OPCODE_ALLROWS: print " Rows: all rows" else: print " Rows:" for row in table.rows: print " Rowid:", row.rowid print " Operation:", print OperationToString(row.operation) connection = cx_Oracle.Connection("cx_Oracle/dev@t11g", events = True) sql = "select * from TestExecuteMany" subscriptionAll = connection.subscribe(callback = OnChanges) subscriptionAll.registerquery(sql) subscriptionInsertUpdate = \ connection.subscribe(callback = OnChanges, operations = cx_Oracle.OPCODE_INSERT | \ cx_Oracle.OPCODE_UPDATE, rowids = True) subscriptionInsertUpdate.registerquery(sql) raw_input("Hit enter to terminate...\n")
结论
在版本 5 中,cx_Oracle 与 Python 齐头并进,实现了更高的灵活性和所需的自定义程序,并且增加了对 Oracle 数据库中大量令人激动的新特性的支持。