Jython数据库编程全解析
在数据库编程领域,Jython为开发者提供了丰富的工具和方法来与各种数据库进行交互。本文将详细介绍Jython在数据库编程中的应用,包括DBM文件、序列化、数据库管理系统、JDBC以及zxJDBC等方面的内容。
1. DBM文件
DBM文件是一种哈希数据库文件,其行为类似于字典对象。不过,这种相似性存在一定限制,因为当前的实现并未使用所有的字典方法,并且所有的DBM键和值都必须是字符串。
在Jython中,虽然有多种DBM工具可供使用,但目前只有dumbdbm与Jython捆绑在一起。尽管它的名字不太吸引人,实现也简单且速度较慢,但它是一个相当有效的DBM克隆。未来,可能会有更多的DBM模块可供Jython使用。
以下是一个使用dumbdbm模块的简单交互式示例:
>>> import dumbdbm
>>> dbm = dumbdbm.open("dbmtest")
>>> dbm['Value 1'] = 'A'
>>> dbm['Value 2'] = 'B'
>>> dbm['Value 3'] = 'B'
>>> for key in dbm.keys():
... print key, ":", dbm[key]
...
Value 3 : B
Value 2 : B
Value 1 : A
>>>
>>>dir(dbm.__class__)
['__delitem__', '__doc__', '__getitem__', '__init__', '__len__',
'__module__', '__setitem__', '_addkey', '_addval', '_commit', '_setval',
'_update', 'close', 'has_key', 'keys']
>>>
>>> dbm.close()
通过 dir(dbm.__class__) 方法可以看出,dbm对象与字典有所不同。在字典方法中,只有 keys 和 has_key 被实现。不过,DBM对象具有一个明显的优势,即它是持久的。重新打开 dbmtest 目录可以看到之前输入的值:
>>> import dumbdbm
>>> dbm = dumbdbm.open("dbmtest")
>>> for key in dbm.keys():
... print key, ":", dbm[key]
...
Value 3 : B
Value 2 : B
Value 1 : A
>>> dbm.close()
打开一个新的dumbdbm目录实际上会创建两个文件:一个扩展名为 .dir ,另一个扩展名为 .dat 。在 open 函数中不需要指定扩展名,只需要指定目录名即可。其他DBM实现允许在 open 函数中使用标志和模式参数,但dumbdbm会忽略这些参数。
2. 序列化
序列化是将对象转换为适合传输或存储的流的过程。存储部分是将这个主题与数据库联系起来的原因,因为序列化对象通常存储在数据库中。
要序列化Jython对象,可以使用 marshal 、 pickle 或 cPickle 模块。 marshal 模块仅适用于内置类型,而 pickle 模块适用于内置对象、模块全局类和函数以及大多数实例对象。 cPickle 模块是 pickle 模块的高性能Java版本,它的名称借鉴自CPython的 cPickle 模块,但并不意味着它是用C语言编写的。 shelve 模块将数据库与 pickle 结合起来,创建持久的对象存储。Jython的 PythonObjectInputStream 是一个 ObjectInputStream ,它在反序列化继承自Java类的Jython对象时有助于解析类。
2.1 marshal模块
marshal 模块用于序列化代码对象和内置数据对象。它通常会被 cPickle 模块所取代,但在某些项目中仍会出现。需要注意的是, marshal 模块对CPython的一个吸引力在于它能够编译代码对象,但对于Jython对象目前并非如此,因此 marshal 模块的使用较少。
marshal 模块有四个方法: dump 、 dumps 、 load 和 loads 。 dump 和 load 方法用于将对象序列化到文件和从文件中恢复对象。因此, dump 的参数是要序列化的对象和文件对象,而 load 的参数只是文件对象。传递给 dump 和 load 函数的文件对象必须以二进制模式打开。 dumps 和 loads 方法用于将对象序列化到字符串和从字符串中恢复对象。
以下是一个使用 marshal 模块处理列表对象的交互式示例:
>>> import marshal
>>> file = open("myMarshalledData.dat", "w+b") # Must be Binary mode
>>> L = range(30, 50, 7)
>>> marshal.dump(L, file) # serialize object L to 'file'
>>>
>>> file.flush() # flush or close after the dump to ensure integrity
>>> file.seek(0) # put file back at beginning for load()
>>>
>>> restoredL = marshal.load(file)
>>> print L
[30, 37, 44]
>>> print restoredL
[30, 37, 44]
2.2 pickle和cPickle模块
pickle 和 cPickle 模块是序列化Jython对象的首选方法。 pickle 和 cPickle 之间的区别仅在于实现和性能,使用上没有区别。除了Java对象、代码对象以及那些具有复杂 __getattr__ 和 __setattr__ 方法的Jython对象外,大多数对象都可以使用 pickle 或 cPickle 进行序列化。
pickle 模块定义了四个函数,如下表所示:
| 方法 | 描述 |
| ---- | ---- |
| Dump(object, file[, bin]) | 将内置数据对象序列化为之前打开的文件对象。如果对象无法序列化,则会引发 PicklingError 异常。 pickle 可以使用文本或二进制格式。第三个参数为零或缺失表示文本格式,非零表示二进制格式。 |
| Load(file) | 从之前打开的文件对象中读取并反序列化数据。如果腌制的数据是以二进制模式写入文件的,那么传递给 load 的文件对象也应该以二进制模式打开。 |
| dumps(object[, bin]) | 将对象序列化为字符串对象而不是文件。如果对象无法序列化,则会引发 PicklingError 异常。第二个参数为零或缺失表示文本格式,非零表示二进制格式。 |
| loads(string) | 将字符串反序列化为数据对象。 |
以下是一个使用 pickle 存储 product 类实例的简单交互式示例:
>>> import cPickle
>>> class product:
... def __init__(self, productCode, data):
... self.__dict__.update(data)
... self.productCode = productCode
...
>>> data = {'name':'widget', 'price':'112.95', 'inventory':1000}
>>> widget = product('1123', data)
>>> f = open(widget.productCode, 'wb')
>>> cPickle.dump(widget, f, 1)
>>> f.close()
现在, widget 产品存储在一个名为 1123 的文件中。恢复这个实例使用 cPickle.load 函数,但为了正确重新创建实例,其类必须存在于反序列化的命名空间中。
>>> import cPickle
>>> class product:
... def __init__(self, productCode, data):
... self.__dict__.update(data)
... self.productCode = productCode
...
>>> f = open("1123", "rb")
>>> widget = cPickle.load(f)
>>> f.close()
>>> vars(widget)
{'name': 'widget', 'productCode': '1123', 'price': '112.95', 'inventory': 1000}
对象可以通过定义特殊方法 __getstate__ 和 __setstate__ 来控制其腌制和反腌制过程。如果定义了 __getstate__ ,则在腌制对象时序列化其返回值。如果定义了 __setstate__ ,则在重建对象时使用反序列化的数据(即 __getstate__ 返回的数据)调用它。 __getstate__ 和 __setstate__ 方法是互补的,但并非都需要定义。如果不定义 __setstate__ ,则 __getstate__ 必须返回一个字典。如果不定义 __getstate__ ,则在加载时 __setstate__ 将接收一个字典(即实例 __dict__ )。
你也可以使用 pickler 和 unpickler 对象。要创建这些对象,将适当的文件对象提供给 pickle.Pickler 或 pickle.Unpickler 类。
>>> import cPickle
>>> f = open("picklertest", "w+b")
>>> p = cPickle.Pickler(f)
>>> u = cPickle.Unpickler(f)
>>>
>>> L = range(10)
>>> p.dump(L) # use pickler object to serialize
>>> f.seek(0)
>>> print u.load()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
pickle 可以处理递归数据结构、包含自身引用的对象以及嵌套数据结构,如包含列表的字典,列表又包含元组等。 Pickler 对象会存储所有之前腌制对象的引用,确保对同一对象的后续引用不会再次进行序列化,无论这些额外的引用是来自递归还是嵌套。重用 Pickler 对象允许共享对象只被腌制一次,并且 Pickler.dump() 只对之前腌制过的对象写入一个短引用。
2.3 Shelves
Jython的 shelve 模块将DBM目录的便利性与 pickle 的序列化功能相结合,创建持久的对象存储。 shelve 的行为类似于字典,与 dumbdbm 模块类似,但不同之处在于 shelve 允许任何可腌制的对象作为值,尽管键仍然必须是字符串。
以下是一个使用 shelve 模块的交互式示例:
>>> import shelve, dumbdbm
>>> dbm = dumbdbm.open("dbmdata")
>>> shelf = shelve.Shelf(dbm)
>>> shelf['a list'] = range(10)
>>> shelf['a dictionary'] = {1:1, 2:2, 3:3}
>>> print shelf.keys()
['a list', 'a dictionary']
>>> shelf.close()
你也可以直接使用 shelve 的 open 方法,跳过 dumbdbm.open(...) 步骤:
>>> import shelve
>>> shelf = shelve.open("dbmdata")
2.4 PythonObjectInputStream
从Java内部序列化Jython对象的过程与序列化Java对象的过程相同,但有一个例外: org.python.util.PythonObjectInputStream 。通常,Jython对象可以从Java正常序列化和反序列化,但 PythonObjectInputStream 在反序列化继承自Java类的Jython对象时有助于解析类。这并不意味着它只适用于继承自Java对象的子类,它适用于任何Jython对象,但在反序列化具有Java超类的Jython对象时最为有用。
以下是从Java序列化Jython对象的伪代码示例:
// Import required classes
import java.io.*;
import org.python.core.*;
// Identify the resource
String file = "someFileName";
// To serialize, make an OutputStream (FileOutputStream in this case)
// and then an ObjectOutputStream.
FileOutputStream oStream = new FileOutputStream(file);
ObjectOutputStream objWriter = new ObjectOutputStream(oStream);
// Write a simple Jython object
objWriter.writeObject(new PyString("some string"));
// clean up
objWriter.flush();
oStream.close();
从上述示例中反序列化对象可以使用 ObjectInputStream 或 PythonObjectInputStream 。以下是所需步骤的伪代码:
// Import required classes
import java.io.*;
import org.python.core.*;
import org.python.util.PythonObjectInputStream;
// Identify the resource
String file = "someFileName;
// Open an InputStrea (FileInputStream in this case)
FileInputStream iStream = new FileInputStream(file);
// Use the input stream to open an ObjectInputStream or a
// PythonObjectInputStream.
PythonObjectInputStream objReader =
new PythonObjectInputStream(iStream);
// It could be this for objects without java superclasses
// ObjectInputStream objReader = new ObjectInputStream(iStream);
// Read the object
PyObject po = (PyObject)objReader.readObject();
// clean up
iStream.close();
3. 数据库管理系统
本节详细介绍了如何使用Jython与MySQL和PostgreSQL数据库管理系统(DBMS)进行交互。Jython可以与任何具有Java驱动程序的数据库一起使用,但选择这两个数据库是因为它们非常流行、免费、稳定、常用且有大量文档。
3.1 MySQL
MySQL是一个SQL数据库服务器,可在大多数类UNIX平台和Windows上运行。要运行本章中特定于MySQL的示例,你必须下载并安装MySQL及其相关的JDBC驱动程序,或者修改示例以适用于你选择的任何数据库系统。
MySQL可从 http://www.mysql.com/ 的“downloads”部分下载。MySQL JDBC驱动程序位于该网站的同一下载部分,但最新的驱动程序和版本信息可在 http://mmmysql.sourceforge.net/ 找到。本章使用的JDBC驱动程序版本是 mm.mysql-2.0.4-bin.jar ,它是目前最常用且最好的MySQL JDBC驱动程序。
安装说明很简单,因为MySQL以Windows安装程序或*nix包格式提供。只需根据需要运行安装程序或包管理器即可。MySQL JDBC驱动程序不需要安装,只需将其添加到类路径中。
安装MySQL后,你必须添加一个用户和一个数据库。本章的示例将使用用户 jyuser 、密码 beans 和数据库 test ,除非另有说明。要启动服务器,请根据你的平台使用以下命令:
- *nix: /pathToMysql/bin/safe_mysqld &
- Windows: \pathToMysql\bin\winmysqladmin.exe
本章使用的 test 数据库在安装MySQL时应自动创建,但如果由于某种原因该数据库不存在,则需要创建它。要创建 test 数据库,请从命令 shell 使用MySQL客户端程序 mysql 连接到服务器,然后输入 CREATE DATABASE 命令。客户端程序 mysql 位于MySQL安装目录的 bin 目录中。你需要以具有适当权限的用户身份运行此程序。注意,SQL命令以 ; 、 \g 或 \G 结尾。
C:\mysql\bin>mysql
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3 to server version: 3.23.39
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.
mysql> CREATE DATABASE test;
Query OK, 1 row affected (0.33 sec)
你可以使用 SHOW DATABASES 语句确认数据库是否存在:
mysql> SHOW DATABASES\g
+----------+
| Database |
+----------+
| mysql |
| test |
+----------+
2 rows in set (0.00 sec)mysql>
接下来,添加用户 jyuser 。该用户需要对 test 数据库拥有所有权限。为此,使用 GRANT 语句。 GRANT 语句有四个部分,分别对应于授予的权限、应用权限的数据库和表、用户以及认证方式。以下是 GRANT 语句的语法总结:
GRANT what ON where TO who IDENTIFIED BY how
在这个例子中,我们将授予用户 jyuser 所有权限,应用于 test 数据库。用户 jyuser 的连接源假设为本地主机。 how 是认证方式,即密码,这里使用 beans 。以下是满足本章示例要求的 GRANT 语句示例:
mysql> USE test;
mysql> GRANT ALL ON test TO jyuser@localhost IDENTIFIED BY "beans";
Query OK, 0 rows affected (0.05 sec)
3.2 PostgreSQL
PostgreSQL是一个非常先进且功能齐全的对象关系数据库管理系统(ORDBMS),可在大多数类UNIX平台和Windows 2000/NT上运行。本章中出现的PostgreSQL示例需要使用PostgreSQL服务器和PostgreSQL JDBC驱动程序,这些可以从 http://www.postgresql.org/ 获取。
本章假设在类UNIX系统上进行安装。由于Windows缺乏许多类UNIX功能,在Windows上安装PostgreSQL需要额外的软件包,通常会更麻烦。要在类UNIX系统上安装,请下载适合你系统的包格式并运行包管理器。要在Windows 2000/NT系统上安装,首先需要下载Cygwin和cygipc软件包,以在Windows操作系统上模拟UNIX环境。Cygwin软件包可在 http://sources.redhat.com/cygwin/ 获取,cygipc软件包可在 http://www.neuro.gatech.edu/users/cwilson/cygutils/V1.1/cygipc/ 获取。然后按照PostgreSQL发行版中的 doc/FAQ_MSWIN 文件中的说明或 http://www.ca.postgresql.org/users-lounge/docs/7.1/admin/installwin32.html 上免费提供的PostgreSQL管理员指南第2章中的说明进行操作。
安装服务器后,你需要初始化一个数据库集群。这应该以管理PostgreSQL数据库的用户身份进行,通常是 postgres 。你不应该以root或管理员身份对数据库执行管理操作。初始化数据库集群的命令如下:
initdb -D /path/to/datadirectory
initdb 文件随PostgreSQL一起提供。数据目录通常是 /usr/local/pgsql/data ,因此命令如下:
initdb -D /usr/local/pgsql/data
在上述命令创建的数据目录中,有一个 pg_hba.conf 文件,该文件控制哪些主机可以连接到数据库。在使用JDBC驱动程序连接到数据库之前,必须在 pg_hba.conf 文件中为你要连接的机器添加一个主机条目。主机条目需要包含单词 host 、该主机允许连接的数据库名称、连接机器的IP地址、指示IP地址中有效位的位掩码以及认证类型。如果你从本地机器连接,主机条目将如下所示:
host test 127.0.0.1 255.255.255.255 crypt
认证类型可以是以下任何一种:
- Trust:无需认证。
- Password:使用密码匹配用户名。
- Crypt:与 password 相同,但密码在网络传输时加密。
- Ident:使用ident服务器进行认证。
- Krb4:使用Kerberos V4认证。
- Krb5:使用Kerberos V5认证。
- Reject:拒绝来自指定IP的连接。
接下来,你需要创建用户 jyuser 。PostgreSQL附带了一个名为 createuser 的实用程序,你可以使用它来创建新用户。命令如下:
[shell prompt]$ createuser -U postgres -d -P jyuser
Enter password for user "jyuser":
Enter it again:
Shall the new user be allowed to create more new users? (y/n) n
CREATE USER
在密码提示处输入密码 beans ,但密码不会回显到终端。输出 CREATE USER 确认操作成功。创建新用户后,你现在可以减少以 postgres 用户身份执行的任务。以 jyuser 身份执行后续任务更安全,例如创建 test 数据库。
要创建 test 数据库,使用PostgreSQL实用程序 createdb 。使用 createdb 程序创建 test 数据库的命令如下:
[shell prompt]$ createdb -U jyuser test
CREATE DATABASE
输出 CREATE DATABASE 确认创建成功。
现在你可以启动服务器。你应该以管理服务器的用户身份启动服务器,通常是 postgres 。要启动服务器,请使用以下命令(将数据目录路径替换为你为安装选择的路径):
postmaster -i -D /usr/local/pgsql/data &
你必须使用 -i 选项,因为这是PostgreSQL接受网络套接字连接(使用JDBC连接所需)的唯一方式。
4. JDBC
Java使用JDBC和 java.sql 包与SQL数据库进行交互。这意味着Jython也可以使用JDBC和 java.sql 包。使用JDBC API和 java.sql 包与SQL数据库进行交互只需要使用的数据库的适当JDBC驱动程序(当然还有数据库)。本章的示例依赖于前面“数据库管理系统”部分中描述的MySQL和PostgreSQL驱动程序。实际的交互步骤与Java中的步骤相同,只是使用Jython的语法。交互需要一个连接、一个语句、一个结果集,并且始终需要处理错误和警告。这些是基本要素,本节将介绍如何从Jython使用这些基本要素。此外,还会介绍事务、存储过程等高级功能。
4.1 连接到数据库
数据库连接需要一个JDBC URL,并且很可能需要用户名和密码。在继续连接过程之前,你应该了解JDBC URL。
4.2 JDBC URL
URL是一个字符串,用于唯一标识数据库连接。其语法如下:
jdbc:<subprotocol>:<subname>
URL以 jdbc: 开头,后面跟着子协议,该子协议通常与数据库供应商的产品名称一致(如Oracle、MySQL、PostgreSQL)。除此之外,URL是特定于驱动程序的,这意味着子名称取决于子协议。以下是MySQL数据库和PostgreSQL数据库的JDBC URL语法:
- MySQL: jdbc:mysql://[hostname][:port]/databaseName[parameter=value]
- PostgreSQL: jdbc:postgresql://[hostname][:port]/databaseName[parameter=value]
这两个URL只有一个单词的区别,即子协议(产品)名称。要连接到本地机器上的 test 数据库,请使用与你要连接的数据库系统匹配的协议的以下命令:
- MySQL: jdbc:mysql://localhost:3306/test
- PostgreSQL: jdbc:postgresql://localhost:5432/test
默认主机是 localhost ,因此在URL中实际上不需要指定。MySQL和PostgreSQL的默认端口分别是 3306 和 5432 ,因此如果使用默认值,这些数字也不需要指定。这将本地主机到 test 数据库的连接缩短为以下形式:
- MySQL: jdbc:mysql:///test
- PostgreSQL: jdbc:postgresql:///test
注意,斜杠的数量保持不变,但当未指定端口时,分隔主机和端口号的冒号会被省略。
以下是其他数据库驱动程序的一些JDBC URL示例,假设数据库名称为 test :
| 驱动程序 | URL |
| ---- | ---- |
| COM.ibm.db2.jdbc.net.DB2Driver | jdbc:db2//localhost/test |
| oracle.jdbc.driver.OracleDriver | jdbc:oracle:thin:@localhost:ORCL |
| sun.jdbc.odbc.JdbcOdbcDriver | JDBC:ODBC:test |
| informix.api.jdbc.JDBCDriver | jdbc:informix-sqli://localhost/test |
在JDBC URL的末尾,你可以指定连接参数。参数可以是数据库驱动程序允许的任意数量的参数,用 & 字符分隔。下表显示了每个参数、其含义、默认值以及哪个数据库(MySQL或PostgreSQL)允许使用该参数。 java.sql 包还包含 DriverPropertyInfo 类,这是另一种探索和设置连接属性的方法。
| 参数 | 含义 | 默认值 | 数据库 |
| ---- | ---- | ---- | ---- |
| User | 你的数据库用户名 | 无 | 两者 |
| password | 你的密码 | 无 | 两者 |
| autoReconnect | 如果连接中断,是否尝试重新连接?( true | false ) | false | MySQL |
| maxReconnects | 驱动程序应尝试重新连接多少次?(假设 autoReconnect 为 true ) | 3 | MySQL |
| initialTimeout | 在重新连接之前等待多少秒?(假设 autoReconnct 为 true ) | 2 | MySQL |
| maxRows | 要返回的最大行数(0 = 所有行) | 0 | MySQL |
| useUnicode | 是否使用Unicode?( true 或 false ) | false | MySQL |
| characterEncoding | 如果 useUnicode 为 true ,使用哪种Unicode字符编码 | 无 | MySQL |
以下是一个指定本地机器上 test 数据库的URL示例,并且指定对字符串使用Unicode字符编码。端口缺失,但只有在不是默认端口时才需要指定:
- MySQL: jdbc:mysql://localhost/test?useUnicode=true
- PostgreSQL: jdbc:postgresql://localhost/test?useUnicode=true
如果再次排除主机名,因为 localhost 已经是默认值,那么这些URL变为:
- MySQL: jdbc:mysql:///test?useUnicode=true
- PostgreSQL: jdbc:postgresql:///test?useUnicode=true
指定IP地址为 192.168.1.10 、端口为 8060 、用户名为 bob 、密码为 letmein 的机器上的 products 数据库的URL如下:
- MySQL: jdbc:mysql://192.168.1.10:8060/products?user=bob&password=letmein
- PostgreSQL: jdbc:postgresql://192.168.1.10:8060/products?user=bob&password=letmein
实际上,用户名和密码很少作为参数指定,因为实际的连接方法已经接受这些参数。参数在URL字符串中可能会很繁琐。
4.3 JDBC连接
使用Java的数据库连接性建立数据库连接的步骤如下:
1. 将适当的驱动程序包含在类路径中。
2. 向JDBC DriverManager注册驱动程序。
3. 将JDBC URL、用户名和密码提供给 java.sql.DriverManager.getConnection 方法。
你必须确保适当的数据库驱动程序存在于类路径中,可以通过环境变量或在Jython启动脚本中使用Java的 -classpath 选项来实现。本章使用的MySQL和PostgreSQL JDBC驱动程序的jar文件名为 mm.mysql-2_0_4-bin.jar 和 jdbc7.1-1.2.jar 。将这些jar文件添加到类路径需要以下shell命令:
- MySQL: set CLASSPATH=\path\to\mm_mysql-2_0_4-bin.jar;%CLASSPATH%
- PostgreSQL: set CLASSPATH=\path\to\jdbc7.1-1.2.jar;%CLASSPATH%
一旦驱动程序存在于类路径中,使用JDBC还需要将驱动程序加载或注册到DriverManager中。加载驱动程序有两种基本方法。你可以使用 -D 开关在命令行上将 jdbc.drivers 属性设置为适当的驱动程序,或者你可以使用 java.lang.Class.forName(classname) 方法。
使用 -D 选项注册驱动程序需要创建一个新的批处理或脚本文件来启动Jython。以下是两个命令,一个用于注册MySQL驱动程序,另一个用于注册PostgreSQL驱动程序:
dos>java -Djdbc.drivers=org.gjt.mm.mysql.Driver \
-Dpython.home=c:\jython-2.1 \
-classpath c:\jython-2.1\jython.jar;\path\to\mm.mysql-2_0_4-bin.jar \
org.python.util.jython
dos>java -Djdbc.drivers=org.postgresql.Driver \
-Dpython.home=c:\jython-2.1 \
-classpath c:\jython-2.1\jython.jar;\path\to\jdbc7.1-1.2.jar \
org.python.util.jython
你也可以使用Java的动态 Class.forName(classname) 语法来加载驱动程序。请记住, java.lang 包中的类在Jython中不像在Java中那样自动可用,因此在调用此方法之前,你必须导入 java.lang.Class 或其父包之一。以下示例使用 Class.forName 加载MySQL和PostgreSQL驱动程序:
import java
try:
java.lang.Class.forName("org.gjt.mm.mysql.Driver")
java.lang.Class.forName("org.postgresql.Driver")
except java.lang.ClassNotFoundException:
print "No appropriate database driver found"
raise SystemExit
与在Java中一样,上述示例捕获Java的 ClassNotFoundException 。
驱动程序在类路径中并已向DriverManager注册后,我们就可以连接到数据库了。为此,使用 java.sql.DriverManager 的 getConnection 方法。 getConnection 方法有三种签名:
public static Connection getConnection(String url) throws SQLException
public static Connection getConnection(String url, Properties info)
throws SQLException
public static Connection getConnection(
String url, String user, String password) throws SQLException
假设本地数据库 test 存在,并且你以 jyuser 身份使用密码 beans 连接到该数据库。以下是可能的连接语法:
from java.sql import DriverManager
from java.util import Properties
# Using the getConnection(URL) method
mysqlConn = DriverManager.getConnection(
"jdbc:mysql://localhost/test?user=jyuser&password=beans")
postgresqlConn = java.sql.DriverManager.getConnection(
"jdbc:postgresql://localhost/test?user=jyuser&password=beans")
# Using the getConnection(URL, Properties) method
params = Properties()
params.setProperty("user", "jyuser")
params.setProperty("password", "beans")
mysqlConn = DriverManager.getConnection("jdbc:mysql://localhost/test",
params)
postgresqlConn =
DriverManager.getConnection("jdbc:postgresql://localhost/test",
params)
# Using the getConnection(URL, String, String) method
mysqlConn =
DriverManager.getConnection("jdbc:mysql://localhost/test",
"jyuser", "beans")
postgresqlConn =
DriverManager.getConnection("jdbc:postgresql://localhost/test",
"jyuser", "beans")
每种方法都返回一个数据库连接,允许你与数据库系统进行交互。
4.4 连接脚本
Jython的交互式解释器是一个理想的数据库客户端工具。MySQL和PostgreSQL都有交互式的控制台客户端应用程序,因此使用Jython的交互式解释器与这些工具类似。然而,你还可以继承Jython的函数、类等,并且对于任何具有JDBC驱动程序的数据库都有一个一致的接口。
Jython要成为交互式客户端,只需要一个连接。每次需要使用数据库时在Jython中交互式输入数据库连接语句并不是一个好方法。或者,你可以使用Jython的 -i 命令行选项。使用 -i 选项运行时,Jython即使在脚本执行后也会继续处于交互式模式。这允许你在脚本中设置数据库连接,并在脚本执行后继续与脚本创建的对象进行交互。但是, -i 开关的问题是没有机制可以自动显式关闭数据库资源。在处理数据库时,最好始终显式关闭任何数据库资源,这意味着每次完成与数据库的工作后,在退出解释器之前都需要输入 close 方法,这仍然不是最好的方法。
将交互式控制台包装在所需的连接和关闭语句中,或者使用Java的 Runtime.addShutdownHook 可以确保连接被关闭。由于并非所有版本的Java都提供 addShutdownHook ,因此最好的方法似乎是将 InteractiveConsole 实例包装在所需的连接和关闭语句中。以下是一个示例脚本:
# File: mystart.py
from java.lang import Class
from java.sql import DriverManager
from org.python.util import InteractiveConsole
import sys
url = "jdbc:mysql://localhost/%s"
user, password = "jyuser", "beans"
# Register driver
Class.forName("org.gjt.mm.mysql.Driver")
# Allow database to be optionally set as a command-line parameter
if len(sys.argv) == 2:
url = url % sys.argv[1]
else:
url = url % "test"
# Get connection
dbconn = DriverManager.getConnection(url, user, password)
# Create statement
try:
stmt = dbconn.createStatement()
except:
dbconn.close()
raise SystemExit
# Create inner console
interp = InteractiveConsole({"dbconn":dbconn, "stmt":stmt}, "DB Shell")
try:
interp.interact("Jython DB client")
finally:
# Close resources
stmt.close()
dbconn.close()
print
要连接到 test 数据库,你只需要确保适当的驱动程序在类路径中,然后运行 jython mystart.py 。以下是一个使用 mystart.py 的示例交互式会话:
shell-prompt> jython mystart.py
Jython DB Client
>>> dbconn.catalog
'test'
>>> # Exit the interpreter on the next line
>>>
PostgreSQL的启动脚本与 Listing 11.1 中的脚本类似,只是JDBC URL中的数据库名称不同。注意,使用PostgreSQL时, dbconn.catalog 为空。
4.5 连接对话框
另一个常见的连接任务是使用对话框获取连接信息。需要数据库登录的客户端应用程序通常使用对话框窗口获取连接信息。这个任务很常见,因此值得给出一个示例。以下是一个对话框窗口,它建立数据库连接并将该连接返回给注册的客户端对象。客户端对象是任何可调用对象,它接受一个参数:连接。当成功获取连接后,对话框将该连接转发给注册的客户端对象。然后,客户端有责任关闭连接。
# File: DBLoginDialog.py
import sys
import java
from java import awt
from java import sql
import pawt
class DBLoginDialog(awt.Dialog):
'''DBLoginDialog prompts a user of database information and
establishes a database login. A connection receiver is registered
as client- for example:
def connectionClient(dbconnection):
# do something with database connection
dbl = DBLoginDialog(parentframe, message)
dbl.client = connectionClient'''
def __init__(self, parentFrame, message):
awt.Dialog.__init__(self, parentFrame)
self.client = None
self.background = pawt.colors.lightgrey
bag = pawt.GridBag(self, anchor='WEST', fill='NONE')
# Make required components
self.Hostname = HighlightTextField("localhost", 24)
self.DBMS = awt.Choice(itemStateChanged=self.updatePort)
self.Port = HighlightTextField("3306", 5)
self.Database = HighlightTextField("test",12)
self.User = HighlightTextField("jyuser", 10)
self.Password = HighlightTextField("", 10, echoChar='*')
# Fill the choice component with opions
self.DBMS.add("MySQL")
self.DBMS.add("PostgreSQL")
# add message
bag.addRow(awt.Label(message), anchor='CENTER')
# Put components in the bag
for x in ["Hostname", "DBMS", "Port", "Database",
"User", "Password"]:
bag.add(awt.Label(x + ":"))
bag.addRow(self.__dict__[x])
# Add action buttons
bag.add(awt.Button("Login", actionPerformed=self.login),
fill='HORIZONTAL')
bag.addRow(awt.Button("Cancel", actionPerformed= self.close),
anchor='CENTER')
self.pack()
bounds = parentFrame.bounds
self.bounds = (bounds.x + bounds.width/2 - self.width/2,
bounds.y + bounds.height/2 - self.height/2,
self.width, self.height)
def login(self, e):
db = self.DBMS.selectedItem
if db == "MySQL":
driver = "org.gjt.mm.mysql.Driver"
else:
driver = "org.postgresql.Driver"
try:
java.lang.Class.forName(driver)
except java.lang.ClassNotFoundException:
self.showError("Unable to load driver %s" % driver)
return
url = "jdbc:%s://%s:%s/%s" % (db.lower(), self.Hostname.text,
self.Port.text,
self.Database.text)
try:
dbcon = sql.DriverManager.getConnection(url,
self.User.text,
self.Password.text)
self.dispose()
self.client(dbcon)
except sql.SQLException:
self.showError("Unable to connect to database")
def updatePort(self, e):
if self.DBMS.selectedItem == 'MySQL':
port = '3306'
elif self.DBMS.selectedItem == 'PostgreSQL':
port = '5432'
self.Port.text= port
def setClient(client):
self.client = client
def showError(self, message):
d = awt.Dialog(self.parent, "Error")
panel = awt.Panel()
panel.add(awt.Label(message))
button = awt.Button("OK")
panel.add(button)
d.add(panel)
d.pack()
bounds = self.parent.bounds
d.bounds = (bounds.x + bounds.width/2 -d.width/2,
bounds.y + bounds.height/2 - d.height/2,
d.width, d.height)
d.windowClosing = lambda e, d=d: d.dispose()
button.actionPerformed = d.windowClosing
d.visible = 1
def close(self, e):
self.dispose()
class HighlightTextField(awt.TextField, awt.event.FocusListener):
def __init__(self, text, chars, **kw):
awt.TextField.__init__(self, text, chars)
self.addFocusListener(self)
for k in kw.keys():
exec("self." + k + "=kw[k]")
def focusGained(self, e):
e.source.selectAll()
def focusLost(self, e):
e.source.select(0, 0)
def dummyClient(connection):
if connection != None:
print "\nDatabase connection successfully received by client."
print "Connection=", connection
connection.close()
print "Database connection properly closed by client."
if __name__ == '__main__':
# make a dummy frame to parent the dialog window
f = awt.Frame("DB Login", windowClosing=lambda e: sys.exit())
screensize = f.toolkit.screenSize
f.bounds = (screensize.width/2 - f.width/2,
screensize.height/2 - f.height/2,
f.width, f.height)
# create and show the dialog window
dbi = DBLoginDialog(f, "Connection Information")
dbi.client = dummyClient
dbi.windowClosing = dbi.windowClosed = lambda e: sys.exit()
dbi.visible = 1
要执行 DBLoginDialog ,你必须确保适当的数据库驱动程序在类路径中,然后执行 jython DBLoginDialog.py 。你应该会看到如图11.1所示的登录窗口。
4.6 数据库元数据
连接到数据库后,你可以探索有关数据库和连接的信息,即元数据。Java连接对象( java.sql.Connection )有一个 getMetaData 方法,它返回一个 java.sql.DatabaseMetaData 对象。 DatabaseMetaData 对象提供了有关其相关数据库连接的详细信息。 DatabaseMetaData 对象有许多方法,你应该查阅其javadoc页面以获取完整详细信息。
元数据在学习新的数据库工具和尝试支持多个数据库时非常重要。你可以使用Jython交互式地探索元数据,以发现数据库的功能和属性。假设你需要了解MySQL的系统函数、数值函数以及对事务的支持。交互式发现这些信息的过程如下:
>>> import java
>>> from java.sql import DriverManager
>>>
>>> # Register driver
>>> java.lang.Class.forName("org.gjt.mm.mysql.Driver")
<jclass org.gjt.mm.mysql.Driver at 650329>
>>>
>>> # Get Connection
>>> url, user, password = "jdbc:mysql:///test", "jyuser", "beans"
>>> dbconn = DriverManager.getConnection(url, user, password)
>>>
>>> # Get the DatabaseMetaData object
>>> md = dbconn.getMetaData()
>>>
>>> # Use the metadata object to find info about MySQL
>>> print md.systemFunctions
DATABASE,USER,SYSTEM_USER,SESSION_USER,PASSWORD,ENCRYPT,LAST_INSERT_ID,
VERSION
>>> md.numericFunctions
'ABS,ACOS,ASIN,ATAN,ATAN2,BIT_COUNT,CEILING,COS,COT,DEGREES,EXP,FLOOR,LOG,
LOG10,MAX,MIN,MOD,PI,POW,POWER,RADIANS,RAND,ROUND,SIN,SQRT,TAN,TRUNCATE'
>>> md.supportsTransactions()
0
>>>
>>> # Don't forget to close connections
>>> dbconn.close()
DatabaseMetaData 对象中有大量的方法,但只有一些方法能够揭示MySQL和PostgreSQL数据库之间的差异。以下脚本使用突出显示差异的方法创建一个可视化报告,比较MySQL和PostgreSQL。许多差异有些微不足道,但其他差异,如事务、模式和联合,则并非如此。该脚本使用前面定义的 DBLoginDialog 来创建连接。因此, DBLoginDialog.py 文件必须在 sys.path 中,最好在当前工作目录中。运行该脚本时,你将看到两个数据库连接对话框。第一个必须选择MySQL数据库,第二个选择PostgreSQL数据库。然后,脚本通过遍历所需方法的列表生成报告。在这个循环中,Jython的 exec 语句执行构造的语句字符串。通常的语句:
value = metaData.supportsTransactions()
变为类似这样的语句:
cmd = "supportsTransactions"
exec("value = metaData.%s()" % cmd)
收集所有信息后,每个数据库的结果将显示在一个 TextArea 中。
# File: jdbcMetaData.py
from DBLoginDialog import DBLoginDialog
from java import awt
import sys
class MetaData(awt.Frame):
def __init__(self):
self.windowClosing=lambda e: sys.exit()
self.databases = ["MySQL", "PostgreSQL"]
self.panel = awt.Panel()
self.infoArea = awt.TextArea("", 40, 60,
awt.TextArea.SCROLLBARS_VERTICAL_ONLY,
font=("Monospaced", awt.Font.PLAIN, 12))
self.panel.add(self.infoArea)
self.add(self.panel)
self.pack()
screensize = self.toolkit.screenSize
self.bounds = (screensize.width/2 - self.width/2,
screensize.height/2 - self.height/2,
self.width, self.height)
self.data = {}
self.visible = 1
self.dialog = DBLoginDialog(self, "Select a Connection")
self.dialog.client = self.gatherMetaInfo
self.dialog.visible = 1
def showResults(self):
infoWidth = self.infoArea.columns
info = 'Method' + 'MySQL PostgreSQL\n'.rjust(infoWidth - 7)
info += '-' * (infoWidth - 1) + '\n'
keys = self.data.keys()
keys.sort()
for x in keys:
info += x
mysql = str(self.data[x]['MySQL'])
postgresql = str(self.data[x]['PostgreSQL'])
results = mysql.ljust(18 - len(postgresql)) + postgresql
info += results.rjust(self.infoArea.columns - len(x) - 2)
info += "\n"
self.infoArea.text = info
def nextDatabase(self):
if len(self.databases):
self.dialog.visible = 1
else:
self.showResults()
def gatherMetaInfo(self, dbconn):
if dbconn==None:
return
metaData = dbconn.getMetaData()
dbname = metaData.databaseProductName
for cmd in self.getCommands():
value = ""
try:
exec("value = metaData.%s()" % cmd)
except:
value = "Test failed"
if self.data.has_key(cmd):
self.data[cmd][dbname] = value
else:
self.data.update({cmd:{dbname:value}})
dbconn.close() # close the database!
self.databases.remove(dbname)
self.nextDatabase()
def getCommands(self):
return ['getCatalogTerm', 'getMaxBinaryLiteralLength',
'getMaxCatalogNameLength', 'getMaxCharLiteralLength',
'getMaxColumnsInGroupBy', 'getMaxColumnsInIndex',
'getMaxColumnsInOrderBy', 'getMaxColumnsInSelect',
'getMaxColumnsInTable', 'getMaxConnections',
'getMaxCursorNameLength', 'getMaxIndexLength',
'getMaxProcedureNameLength', 'getMaxRowSize',
'getMaxStatementLength', 'getMaxStatements',
'getMaxTablesInSelect', 'getMaxUserNameLength',
'supportsANSI92EntryLevelSQL', 'supportsBatchUpdates',
'supportsCatalogsInDataManipulation',
'supportsCoreSQLGrammar',
'supportsDataManipulationTransactionsOnly',
'supportsDifferentTableCorrelationNames',
'supportsFullOuterJoins', 'supportsGroupByUnrelated',
'supportsMixedCaseQuotedIdentifiers',
'supportsOpenStatementsAcrossCommit',
'supportsOpenStatementsAcrossRollback',
'supportsPositionedDelete', 'supportsUnion',
'supportsSubqueriesInComparisons',
'supportsTableCorrelationNames', 'supportsTransactions']
if __name__ == '__main__':
md = MetaData()
要执行上述脚本,确保两个数据库驱动程序都在类路径中,然后运行以下命令:
jython jdbcMetaData.py
你应该首先看到与 Listing 11.2 中相同的对话框窗口。在那里输入MySQL连接信息。对话框会再次出现,输入PostgreSQL连接信息。之后,你应该会看到两个数据库之间的元数据比较,类似于图11.2所示。
5. 语句
与MySQL和PostgreSQL进行交互的大部分工作涉及SQL语句。发出SQL语句需要一个语句对象。要创建语句对象,使用连接的 createStatement() 方法。
>>> import java
>>> java.lang.Class.forName("org.gjt.mm.mysql.Driver")
<jclass org.gjt.mm.mysql.Driver at 1876475>
>>> db, user, password = "test", "jyuser", "beans"
>>> url = "jdbc:mysql://localhost/%s" % db
>>> dbconn = java.sql.DriverManager.getConnection(url, user, password)
>>> stmt = dbconn.createStatement()
createStatement 方法还可以为结果集设置类型和并发参数。在结果集可以使用其 update* 方法之前,必须将语句的并发设置为可更新。以下示例通过设置类型和并发创建一个可更新的结果集:
>>> import java
>>> from java import sql
>>> java.lang.Class.forName("org.gjt.mm.mysql.Driver")
<jclass org.gjt.mm.mysql.Driver at 1876475>
>>> db, user, password = "test", "jyuser", "beans"
>>> url = "jdbc:mysql://localhost/%s" % db
>>> dbconn = sql.DriverManager.getConnection(url, user, password)
>>>
>>> type = sql.ResultSet.TYPE_SCROLL_SENSITIVE
>>> concurrency = sql.ResultSet.CONCUR_UPDATABLE
>>> stmt = dbconn.createStatement(type, concurrency)
语句对象允许你使用其 execute() 、 executeQuery() 和 executeUpdate() 方法执行SQL语句。这三个方法的返回值不同。 execute() 方法返回0或1,表示是否有结果集。实际上,它返回一个Java布尔值,Jython将其解释为1或0。 executeQuery() 方法总是返回一个结果集,而 executeUpdate() 方法返回一个整数,表示受影响的行数。
>>> query = "CREATE TABLE random ( number tinyint, letter char(1) )"
>>> stmt.execute(query)
0
在上述示例中,查询更新了数据库,这意味着没有结果集。你可以使用语句的 getResultSet() 方法或 resultSet 自动bean属性来确认这一点。
>>> print stmt.resultSet
None
现在表已经创建,你可以向表中填充数据。这将使用 executeUpdate() 方法。以下交互式示例继续前面的示例(语句对象已经存在),向 random 表中添加一些数据:
>>> import string, random
>>> for i in range(20):
... number = random.randint(0, 51)
... query = "INSERT INTO random (number, letter) VALUES (%i, '%s')"
... query = query % (number, string.letters[number - 1])
... stmt.executeUpdate(query)
1
...
你应该会看到许多1的输出,表示每次更新都改变了一行。注意查询字符串中 '%s' 周围使用了单引号。字母作为所有字符串,必须用引号括起来。使用内部单引号很方便,但如果字符串是单引号怎么办?字母为单引号会导致 SQLException 。在查询字符串中,单引号必须加倍,这意味着字符串 "It's a bird" 在查询字符串中应该是 "It''s a bird" 。即使这样,你还必须在Jython的语法中处理这个问题,并包含转义双引号以保持正确的SQL格式。以下是使用转义双引号允许单引号字符串的示例:
>>> query = "INSERT INTO random (number, letter) VALUES (%i, \"%s\")"
>>> query = query % (4, "''")
现在 random 表已经有了数据,你可以使用 executeQuery() 方法选择这些数据。继续上面的交互式会话,以下示例选择 random 表中的所有数据:
>>> rs = stmt.executeQuery("SELECT * FROM random")
现在我们需要使用 ResultSet 对象来检查 SELECT 语句的结果。
6. 结果集
当你使用 executeQuery 时,返回的对象是一个 ResultSet 。 java.sql.ResultSet 对象包含许多方法,用于遍历结果并将其作为Java原生类型检索。请记住,Jython会自动将Java原生类型转换为适当的Jython类型。使用 ResultSet 的 getByte() 方法返回的值将成为 PyInteger , getDouble() 方法返回的值将成为 PyFloat ,依此类推,所有这些都根据第2章“运算符、类型和内置函数”中列出的Java类型转换规则进行转换。
要查询前面创建的 random 表并遍历其所有条目,使用 ResultSet 的 next() 和 get* 方法。在以下示例中, number 字段使用 getInt() 方法检索, letter 字段使用 getString() 方法检索。这将创建Jython类型 PyInteger 和 PyString :
>>> import java
>>> java.lang.Class.forName("org.gjt.mm.mysql.Driver")
<jclass org.gjt.mm.mysql.Driver at 1876475>
>>> db, user, password = "test", "jyuser", "beans"
>>> url = "jdbc:mysql://localhost/%s" % db
>>> dbconn = java.sql.DriverManager.getConnection(url, user, password)
>>> stmt = dbconn.createStatement()
>>>
>>> rs = stmt.executeQuery("SELECT * FROM random")
>>> while rs.next():
... print rs.getString('letter'), ": ", rs.getInt('number')
...
...
U : 46
K : 36
h : 7
Q : 42
l : 11
u : 20
n : 13
z : 25
U : 46
Y : 50
j : 9
o : 14
i : 8
s : 18
d : 3
A : 26
K : 36
j :
### Jython数据库编程全解析
#### 7. 结果集导航与更新
结果集的导航涉及使用`ResultSet`的导航方法在记录间移动,这取决于所使用的JDBC版本和创建的语句类型。在JDBC 2.0之前,导航仅限于`next()`方法。当前的JDBC版本支持`next()`、`first()`、`last()`、`previous()`、`absolute()`、`relative()`、`afterLast()`、`beforeFirst()`、`moveToCurrentRow()`和`moveToInsertRow()`等方法。不过,`moveToInsertRow()`和`moveToCurrentRow()`在PostgreSQL驱动中未实现,在MySQL驱动中使用也有特殊条件。
以下是这些导航方法的说明:
| 方法 | 说明 |
| ---- | ---- |
| `relative(int)` | 从当前位置将光标移动指定的行数。 |
| `absolute(int)` | 将光标移动到结果集的指定行,无论当前位置如何。 |
| `moveToInsertRow()` | (目前仅MySQL支持)将光标移动到一个特殊行,用于构建要插入的行。 |
| `moveToCurrentRow()` | 在调用`moveToInsertRow()`后,返回之前所在的行。 |
除了导航,若驱动支持,结果集还能更新行。目前PostgreSQL不支持,而MySQL驱动支持,但有条件限制。在MySQL中,结果集可更新的条件为:查询仅包含一个表、不使用连接、查询选择表的主键。此外,所有JDBC驱动都要求将语句的并发类型设置为`ResultSet.CONCUR_UPDATABLE`。进行插入操作时,生成可更新结果集的查询必须包含所有无默认值的行和所有非空行。
以下示例展示了如何更新表以包含主键,获取可更新结果集,插入行,滚动并更新行:
```python
>>> import java
>>> from java import sql
>>> java.lang.Class.forName("org.gjt.mm.mysql.Driver")
<jclass org.gjt.mm.mysql.Driver at 1876475>
>>> user, password = "jyuser", "beans"
>>> url = "jdbc:mysql://localhost/test"
>>> dbconn = sql.DriverManager.getConnection(url, user, password)
>>>
>>> type = sql.ResultSet.TYPE_SCROLL_SENSITIVE
>>> concurrency = sql.ResultSet.CONCUR_UPDATABLE
>>> stmt = dbconn.createStatement(type, concurrency)
>>>
>>> # Update table to include primary key (excuse the wrap)
>>> query = "ALTER TABLE random ADD pkey INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY"
>>> stmt.execute(query)
0
>>>
>>> # Now get an updatable result set.
>>> rs = stmt.executeQuery("select * from random")
>>> rs.concurrency # 1008 is "updatable"
1008
>>>
>>> # Insert a row
>>> rs.moveToInsertRow()
>>> rs.updateInt('number', 7)
>>> rs.updateString('letter', 'g')
>>> rs.updateInt('pkey', 22)
>>> rs.insertRow() # This puts it in the database
>>> rs.moveToCurrentRow()
>>>
>>> rs.relative(5) # scroll 5 rows
1
>>> # Print current row data
>>> # Remember, data is random- odds are yours will differ
>>> print rs.getString('letter'), rs.getInt('number')
h 8
>>> rs.updateInt('number', 3)
>>> rs.updateString('letter', 'c')
>>> rs.updateRow() # this puts it in the database
>>> stmt.close()
>>> dbconn.close()
8. 预编译语句
预编译频繁使用的SQL语句通常有助于提高效率。 java.sql.PreparedStatement 允许创建这样的预编译语句。创建预编译语句时,使用连接对象的 prepareStatement 方法,而非 createStatement 。
以下是一个简单的预编译语句示例,用于更新 random 数据库中的一行:
>>> import java
>>> from java import sql
>>> java.lang.Class.forName("org.postgresql.Driver")
<jclass org.gjt.mm.mysql.Driver at 1876475>
>>> dbconn = sql.DriverManager.getConnection(url, user, password)
>>> query = "INSERT INTO random (letter, number) VALUES (?, ?)"
>>> preppedStmt = dbconn.prepareStatement(query)
注意查询中的问号(?),预编译语句允许为值留下占位符,在执行时填充。填充这些占位符需要根据其位置标识使用 set* 方法为每个占位符设置值。每个支持的Java类型都有对应的 set 方法。此外,在设置新参数并执行更新之前,应先清除参数:
>>> # continued from previous interactive example
>>> preppedStmt.clearParameters()
>>> preppedStmt.setString(1, "f")
>>> preppedStmt.setInt(2, 6)
>>> preppedStmt.executeUpdate()
1
>>> preppedStmt.close()
>>> dbconn.close()
9. 事务
事务是一组数据库操作,这些操作必须全部成功完成,否则整个操作将被撤销(回滚)。这里以PostgreSQL为例进行事务示例,因此需要在PostgreSQL的 test 数据库中创建 random 表。
以下是创建 random 表的交互式示例:
>>> import java
>>> from java import sql
>>> java.lang.Class.forName("org.postgresql.Driver")
<jclass org.postgresql.Driver at 5303656>
>>> db, user, password = "test", "jyuser", "beans"
>>> url = "jdbc:postgresql://localhost/%s" % db
>>> dbconn = sql.DriverManager.getConnection(url, user, password)
>>> stmt = dbconn.createStatement()
>>> query = "CREATE TABLE random ( number int, letter char(1), pkey INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY)"
>>> stmt.execute(query)
0
>>> import string, random
>>> for i in range(20):
... number = random.randint(0, 51)
... query = "INSERT INTO random (number, letter) VALUES (%i, '%s')"
... query = query % (number, string.letters[number - 1])
... stmt.executeUpdate(query)
1
1
...
>>> stmt.close()
>>> dbconn.close()
要在Jython中使用JDBC事务,必须将连接的 autoCommit 属性设置为0,开始执行一组语句,并在任何语句失败时调用连接的 rollback() 方法。以下是一个使用PostgreSQL和 random 表的简单事务示例:
>>> import java
>>> from java import sql
>>> java.lang.Class.forName("org.postgresql.Driver")
<jclass org.postgresql.Driver at 5711596>
>>> url = "jdbc:postgresql://localhost/test"
>>> con = sql.DriverManager.getConnection(url, "jyuser", "beans")
>>> stmt = con.createStatement()
>>> con.autoCommit = 0
>>> try:
... # Insert an easy-to-find character for letter
... query = "INSERT INTO random (letter, number) VALUES ('.', 0)"
... stmt.executeUpdate(query)
... print "First update successful."
... stmt.execute("Create an exception here")
... con.commit()
... except:
... print "Error encountered, rolling back."
... con.rollback()
...
1
First update successful.
Error encountered, rolling back.
>>>
>>> # now confirm that the first update statement was in fact undone
>>> rs = stmt.executeQuery("SELECT * FROM random WHERE letter='.'")
>>> rs.next()
0
最后一个0表示查询没有结果集,这证实了第一个SQL插入语句已被回滚。
10. zxJDBC
使用Jython进行JDBC编程无疑是有价值的,它允许在Jython中进行原型设计并利用JDBC技能。然而,众多特定于Java类型的方法表明它是一个Java API。Java和数据库(因此JDBC)具有丰富的类型,其缺点是特定于Java原生类型的方法似乎与Jython的高级、多态动态类型相悖。
相比之下,Python有一个称为Python DB API的数据库API,目前版本为2.0。Python的DB API 2.0是CPython与数据库交互的标准API,但CPython使用的数据库驱动程序由于底层C实现,对Jython通常无用。尽管Jython可以轻松利用Java的数据库连接性,但仍然需要一个Java实现的Python DB API。Brian Zimmer编写了zxJDBC来填补这一空白。实际上,zxJDBC不仅实现了DB API,还对该API进行了扩展。
10.1 连接到数据库
使用zxJDBC包时,在调用连接函数之前,只需确保 zxJDBC.jar 和所需的JDBC驱动程序存在于类路径中。实际加载驱动程序的操作在创建数据库连接时在幕后进行。使用zxJDBC建立数据库连接的两个步骤如下:
1. 将适当的驱动程序和 zxJDBC.jar 文件包含在类路径中。
2. 将JDBC URL、用户名、密码和数据库驱动程序类的名称提供给 zxJDBC.connect() 方法。
以下是使用zxJDBC时适当的类路径设置示例:
- MySQL: set CLASSPATH=mm_mysql-2_0_4-bin.jar;\path\to\zxJDBC.jar;%CLASSPATH%
- PostgreSQL: set CLASSPATH=\path\to\jdbc7.1-1.2.jar;\path\to\zxJDBC.jar;%CLASSPATH%
zxJDBC.connect 方法返回数据库连接,其语法如下:
zxJDBC.connect(URL, user, password, driver) -> connection
使用 zxJDBC.connect 方法获取连接的示例如下:
from com.ziclix.python.sql import zxJDBC
mysqlConn = zxJDBC.connect("jdbc:mysql://localhost/test",
"jyuser", "beans",
"org.gjt.mm.mysql.Driver")
postgresqlConn = zxJDBC.connect("jdbc:postgresql://localhost/test",
"jyuser", "beans",
"org.postgresql.Driver")
驱动程序所需的特殊参数可以作为关键字参数传递给 connect 函数。例如,在连接到MySQL数据库时将 autoReconnect 设置为 true :
url = "jdbc:mysql://localhost/test"
user = "jyuser"
password = "beans"
driver = "org.gjt.mm.mysql.Driver"
mysqlConn = zxJDBC.connect(url, user, password, driver,
autoReconnect="true")
连接错误会引发 DatabaseError 异常,因此处理连接尝试中的错误需要使用 except 语句:
url = "jdbc:mysql://localhost/test"
user = "jyuser"
password = "beans"
driver = "org.gjt.mm.mysql.Driver"
try:
mysqlConn = zxJDBC.connect(url, user, password, driver,
autoReconnect="true")
except zxJDBC.DatabaseError:
pass
#handle error here
如果你使用 javax.sql 包中的连接工厂,或实现 javax.sql.DataSource 或 javax.sql.ConnectionPoolDataSource 的类,可以使用 zxJDBC.connectx 方法进行连接。注意, javax.sql 包通常不在JDK安装中,除了企业版。MySQL JDBC驱动包含 MysqlDataSource 类,以下是使用 zxJDBC.connectx 方法的示例:
from com.ziclix.python.sql import zxJDBC
userInfo = {'user':'jyuser', 'password':'beans'}
con = zxJDBC.connectx("org.gjt.mm.mysql.MysqlDataSource",
serverName='localhost', databaseName='test',
port=3306, **userInfo)
也可以将bean属性名包含在包含用户名和密码信息的字典中:
from com.ziclix.python.sql import zxJDBC
userInfo = {'user':'jyuser', 'password':'beans',
'databaseName':'test', 'serverName':'localhost',
'port':3306}
con = zxJDBC.connectx("org.gjt.mm.mysql.MysqlDataSource" , **userInfo)
还可以使用 zxJDBC.lookup 方法通过JNDI查找获取连接,该方法只需要一个表示绑定到特定连接或 DataSource 的JNDI名称的字符串。关键字参数可以包含在内,并在关键字与 javax.jndi.Context 的静态字段名称匹配时转换为其静态字段值。
10.2 游标
zxJDBC游标是用于实际与数据库中的数据进行交互的对象。它实际上是JDBC Statement 和 ResultSet 对象的包装器。静态和动态游标类型的区别在于对结果集的处理方式。动态游标是惰性的,仅在需要时遍历结果集,这节省了内存并均匀分布处理时间。静态游标不是惰性的,它会立即遍历整个结果集,并因此产生内存开销。静态游标的优点是在执行语句后不久就能知道行数,而动态游标则无法做到这一点。
要获取游标,调用zxJDBC连接对象的 cursor() 方法。以下是连接到数据库并获取游标对象的示例:
from com.ziclix.python.sql import zxJDBC
url = "jdbc:mysql://localhost/test"
user = "jyuser"
password = "beans"
driver = "org.gjt.mm.mysql.Driver"
con = zxJDBC.connect(url, user, password, driver,
autoReconnect="true")
cursor = con.cursor() # Static cursor
# Alternatively, you can create a dynamic cursor
cursor = con.cursor(1) # Optional boolean arg for dynamic
游标对象的 execute 方法用于执行SQL语句。以下示例展示了如何执行一个选择 random 表中所有数据的SQL语句:
>>> from com.ziclix.python.sql import zxJDBC
>>> url = "jdbc:mysql://localhost/test"
>>> user, password, driver = "jyuser", "beans", "org.gjt.mm.mysql.Driver"
>>> con = zxJDBC.connection(url, user, password, driver)
>>> cursor = con.cursor()
>>> cursor.execute("SELECT * FROM random")
要遍历语句的结果,必须使用游标对象的 fetchone 、 fetchmany 和 fetchall 方法。 fetchone 和 fetchall 方法如其名称所示,分别获取一行结果集或获取所有行。 fetchmany 方法接受一个可选参数,指定要返回的行数。每次返回多行时,它们以序列的序列(元组列表)形式返回。以下是继续上述示例展示这三个方法的使用:
>>> cursor.fetchone()
(41, 'O', 1)
>>> cursor.fetchmany()
[(6, 'f', 2)]
>>> cursor.fetchmany(4)
[(49, 'W', 4), (35, 'I', 5), (43, 'Q', 6), (37, 'K', 3)]
>>> cursor.fetchall() # All remaining in this case
[(3, 'c', 7), (17, 'q', 8), (29, 'C', 9), (36, 'J', 10), (43, 'Q', 11), (23, 'w', 12), (49, 'W', 13), (25, 'y', 14), (40, 'N', 15), (50, 'X', 16), (46, 'T', 17), (51, 'Y', 18), (8, 'h', 19), (25, 'y', 20), (7, 'g', 21), (11, 'k', 22), (1, 'a', 23)]
查询执行后,可以使用游标对象的 description 属性查看结果集中行的信息。该属性是只读的,包含结果集中每一行的序列,每个序列包含列的名称、类型、显示大小、内部大小、精度、比例和可空性信息。前面查询的描述如下:
>>> cursor.description
[('number', -6, 4, None, None, None, 1), ('letter', 1, 1, None, None, None, 1), ('pkey', 4, 10, None, 10, 0, 0)]
以下是游标对象的方法和属性的完整列表:
| 方法/属性 | 描述 |
| ---- | ---- |
| description | 描述查询结果中每列的信息,是一个七元组,包含名称、类型代码、显示大小、内部大小、精度、比例和可空性。 |
| rowcount | 结果集中的行数,仅在游标是静态游标或使用动态游标完全遍历结果集后才有效。 |
| callproc(procedureName, [parameters]) | 调用存储过程,仅适用于实现了存储过程的数据库。 |
| close() | 关闭游标。 |
| execute(statement) | 执行语句。 |
| executemany(statement, parameterList) | 使用参数列表执行语句,可以在语句中使用问号表示值,并在 parameterList 中包含值的元组来替换问号。 |
| fetchone() | 检索查询结果的一行。 |
| fetchmany([size]) | 如果没有参数,返回 arraysize 行数;如果提供参数 arg ,则返回 arg 行结果。 |
| fetchall() | 检索所有剩余的结果行。 |
| nextset() | 继续处理下一个结果集,仅适用于支持多个结果集的数据库。 |
| arraysize | fetchmany() 在没有参数时应返回的行数。 |
10.3 zxJDBC和元数据
Python DB API不包含元数据规范,但zxJDBC通过一些连接和游标属性提供了一些连接元数据。这些属性与本章前面讨论的JDBC java.sql.DatabaseMetaData 对象中的bean属性相匹配。以下是zxJDBC游标字段和底层 DatabaseMetaData bean方法的对应关系:
| zxJDBC属性 | DatabaseMetaData访问器 |
| ---- | ---- |
| connection.dbname | getDatabaseProductName |
| connection.dbversion | getDatabaseProductVersion |
| cursor.tables(catalog, schemapattern, tablepattern, types) | getTables |
| cursor.columns(catalog, schemapattern, tablenamepattern, columnnamepattern) | getColumns |
| cursor.foreignkeys(primarycatalog, primaryschema, pimarytable, foreigncatalog, foreignschema, foreigntable) | getCrossReference |
| cursor.primarykeys(catalog, schema, table) | getPrimaryKeys |
| cursor.procedures(catalog, schemapattern, procedurepattern) | getProcedures |
| cursor.procedurecolumns(catalog, schemapattern, procedurepattern, columnpattern) | getProcedureColumns |
| cursor.statistics(catalog, schema, table, unique, approximation) | getIndexInfo |
以下是从前面创建的MySQL random 数据库中提取一些元数据的示例:
>>> from com.ziclix.python.sql import zxJDBC
>>> url = "jdbc:mysql://localhost/test"
>>> driver = "org.gjt.mm.mysql.Driver"
>>> dbconn = zxJDBC.connect(url, "jyuser", "beans", driver)
>>> dbconn.dbname
'MySQL'
>>> dbconn.dbversion
'3.23.32'
其余元数据可通过游标对象访问。当游标检索信息时,它会将其存储在内部,等待用户提取。要查看游标提供的元数据,必须调用每个元数据方法,然后使用游标检索数据:
>>> cursor.primarykeys(None, "%", "random")
>>> cursor.fetchall()
[('', '', 'random', 'pkey', 1, 'pkey')]
>>> cursor.tables(None, None, '%', None)
>>> cursor.fetchall()
[('', '', 'random', 'TABLE', '')]
>>> cursor.primarykeys('test', '%', 'random')
>>> cursor.fetchall()
[('test', '', 'random', 'pkey', 1, 'pkey')]
>>> cursor.statistics('test', '', 'random', 0, 1)
>>> cursor.fetchall()
[('test', '', 'random', 'false', '', 'PRIMARY', '3', 1, 'pkey', 'A', '23', None, '')]
10.4 预编译语句
zxJDBC游标对象的 executemany() 方法相当于Java的预编译语句。实际上,其他执行的语句也会被预编译,但 executemany() 方法允许在SQL语句中使用问号表示值。该方法的第二个参数是一个值的元组,用于替换SQL语句中的问号:
>>> sql = "INSERT INTO random (letter, number) VALUES (?, ?)"
>>> cur.executemany(sql, ('Z', 51))
>>>
>>> # view the row
>>> cur.execute("SELECT * from random where letter='Z'")
>>> cur.fetchall()
[('Z', 51, 24)]
10.5 错误和警告
zxJDBC中可能引发的异常如下:
- Error :通用异常。
- DatabaseError :针对特定数据库错误引发,连接对象和所有连接方法( connect 、 lookup 、 conntectx )可能引发此异常。
- ProgrammingError :针对编程错误(如缺少参数、错误的SQL语句)引发,游标对象和查找连接可能引发此异常。
- NotSupportedError :当方法未实现时引发。
这些异常都在 zxJDBC 包中,因此 except 子句应如下所示:
>>> try:
... pass #Assume a method that raises and error is here
... except zxJDBC.DatabaseError:
... pass # Handle DatabaseError
... except zxJDBC.ProgrammingError:
... pass # handle ProgrammingError
... except notSupportedError:
... pass # handle not supported error
... except zxJDBC.Error:
... pass # Handle the generic Error exception
还可以使用游标对象的 warnings 属性获取警告信息。如果没有警告,该属性为 None 。
10.6 dbexts
zxJDBC包中的另一个扩展是 dbexts ,它是一个Python模块,在Python DB API 2.0之上添加了另一层抽象。使用 DBexts ,可以在配置文件中指定连接信息,然后对定义的连接使用更高级的 dbexts 方法。要使用 dbexts ,必须将随zxJDBC提供的 dbexts.py 模块添加到 sys.path 中。
配置文件可以是任何文件,默认是与 dbexts.py 文件位于同一目录下的 dbexts.ini 文件。若要使用其他文件,可在 dbexts 构造函数中包含文件名。以下是一个配置文件示例,定义了与本章中使用的两个数据库的连接:
[default]
name=mysqltest
[jdbc]
name=mysqltest
url=jdbc:mysql://localhost/test
user=jyuser
pwd=beans
driver=org.gjt.mm.mysql.Driver
[jdbc]
name=pstest
url=jdbc:postgresql://localhost/test
user=jyuser
pwd=beans
driver=org.postgresql.Driver
配置文件定义好后,可以通过实例化 dbexts 类来连接到数据库。以下示例中, dbexts.ini 文件位于当前工作目录:
>>> from dbexts import dbexts
>>> mysqlcon = dbexts("mysqltest", "dbexts.ini")
>>> psgrscon = dbexts("pstest", "dbexts.ini")
>>>
>>> # execute raw sql and get a list of headers and results
>>> psgrscon.raw("SELECT * from random")
([('letter', 1, 1, None, None, None, 1), ('number', 4, 11, None, 10, 0, 1)], [('A', 4), ('f', 6), ('a', 1)])
>>>
>>> # execute interactive sql
>>> psgrscon.isql("select * from random")
LETTER | NUMBER
---------------
A | 4
f | 6
a | 1
3 rows affected
>>>
>>> # Display schema- this works with postgresql, not MySQL
>>> psgrscon.schema("random")
Table
random
Primary Keys
Imported (Foreign) Keys
Columns
letter bpchar(1), nullable
number int4(4), nullable
Indices
>>>
>>> # show tables in MySQL 'test' database
>>> mysqlcon.table()
TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS
-----------------------------------------------------------
| | random | TABLE |
1 row affected
以下是 dbexts 的主要方法列表:
| 方法 | 描述 |
| ---- | ---- |
| __init__(dbname, cfg, resultformatter, autocommit) | dbexts 构造函数,所有参数都有默认值,因此都是可选的。 dbname 是在 dbexts.ini 文件中为连接指定的名称。 cfg 是 dbexts.ini 文件的位置,如果它不在 dbexts.py 文件所在的目录中。 resultformatter 是一个可调用对象,接受一行列表作为一个参数,并可选地接受一个标题列表,用于显示数据。 autocommit 参数默认设置为1( true ),但在调用构造函数时可以设置为0。 |
| isql(sql, params, bindings, maxrows) | 交互式执行SQL语句,即执行语句后立即使用 resultformatter 显示结果,类似于MySQL和pSQL客户端程序。除了SQL语句外,所有参数都有默认值。 sql 是SQL语句本身。 params 是用于替换SQL语句中 ? 的值参数元组。 bindings 参数允许绑定数据处理程序。 maxrows 指定要返回的最大行数,0或 None 表示无限制。 |
| raw(sql, params, bindings) | 执行SQL语句并返回一个包含标题和结果的元组, params 和 bindings 参数与 isql 方法相同。 |
| schema(table, full, sort) | 显示表的索引、外键、主键和列信息。如果 full 参数非零,结果将包括表的引用键。 sort 值非零表示表的列名将被排序。 |
| table(table) | table 参数可选。没有表参数时,此方法显示所有表的列表;否则,显示指定表的列信息。 |
| proc(proc) | proc 参数可选。没有该参数时,列出所有存储过程;有该参数时,显示指定存储过程的参数。 |
| bcp(src, table, where='(1=1)') | 将指定数据库和表( src 和 table )中的数据复制到当前实例的数据库中。 |
| begin(self) | 创建一个新的游标。 |
| rollback(self) | 回滚自创建新游标以来执行的语句。 |
| commit(self, cursor=None, maxrows=None) | 提交自创建新游标以来执行的语句。 |
| display(self) | 使用当前格式化程序显示结果。 |
综上所述,Jython在数据库编程领域提供了丰富的功能和多样的工具。从DBM文件的使用到序列化技术,再到与MySQL、PostgreSQL等数据库管理系统的交互,以及JDBC和zxJDBC的应用,开发者可以根据具体需求选择合适的方法和工具来实现高效的数据库编程。无论是处理简单的数据存储,还是进行复杂的事务管理和元数据探索,Jython都能提供有力的支持。
超级会员免费看
869

被折叠的 条评论
为什么被折叠?



