如前所述,数据库的创建是特定于 DBMS 的。为有助于了解范例,这里对遵 守 JDBC 标准的基本过程规则进行了例外处理。只需设置传递到驱动程序的数据 库连接的 URL 属性,就可以在 Cloudscape 中创建数据库。该属性的设置是: create=true. 在 DBMS 的默认目录中,就创建了已命名的数据库, 此处是 jGuru。对于“Cloudscape 安装和 设置”中介绍的 J2EE 下载,它将为 J2EE_HOME/Cloudscape。 如果数据库已经存在,Cloudscape 会创建“连接”,但另一方面会给出 SQLWarning。
尽管多数情况下,本代码会创建额外的对象,但用于确定是否创建了对象实例 并在没有创建时创建新对象所要求的代码,通常超出新建的成本。好在废料收集 器最后会清理未引用的对象,而 DriverManager 不会将驱动程序 注册两次。
java -Djdbc.drivers=DriverClassName AJavaApp
本课程中使用的连接到 Cloudscape 的特定 DriverClassName 的建议创建方 式是:
COM.cloudscape.core.RmiJdbcDriver
连接数据源。
驱动程序提供创建“连接”的方法,但要求使用 jdbc 协议的特定的 URL 类 型。通用的表单是 jdbc:<subprotocol>:<subname>。更 多信息,请查看“ JDBC API 基础知识”中的“ 常规使用和 JDBC URL”。
人们经常认为理当如此的一个明显的观点是:使用 URL 意味着 JDBC 应用程 序会自动
jdbc:cloudscape:rmi:jGuru;create=true
在使用 DriverManager 类时,如果请求连接已传递 URL 的“连 接”,DriverManager 就会选择适当的驱动程序;这里,仅仅装载 了 Cloudscape 驱动程序。“连接”请求的标准形式如下:
Connection con = DriverManager.getConnection(
URL,
Username,
Password );
本形式具有最佳的可移植性,即使“用户名”和“密码”由于数据库默认或 ODBC 数据源的文本文件无法使用此类属性而为空字符串(""), 也是如此。
就 Cloudscape 驱动程序而言,这是因‘create = true’URL 属性(随后连 接将去掉)创建数据库的实际所在。
虽然“连接”类有许多性能,但是为了使用 DDL 或“数据操作语言”(Data Manipulation Language,DML)SQL 语句,仍要求“语句”对象。所以,下一步 是向“连接”要求“语句”对象:
Statement stmt = con.createStatement();
此时,程序可以开始起一点实际作用了。为了存储数据,范例在 jGuru 数据库中 创建了命名为
JJJJData 的表。下面是该 SQL 语句,包括每个数 据项需要的列。为清晰起见,范例中的 SQL 关键字都是大写,但这根据程序员的 爱好而定,并非必需如此。
CREATE TABLE JJJJData (
Entry INTEGER NOT NULL,
Customer VARCHAR (20) NOT NULL,
DOW VARCHAR (3) NOT NULL,
Cups INTEGER NOT NULL,
Type VARCHAR (10) NOT NULL,
PRIMARY KEY( Entry )
)
其程序代码是:
stmt.executeUpdate( "CREATE TABLE JJJJData (" +
"Entry INTEGER NOT NULL, " +
"Customer VARCHAR (20) NOT NULL, " +
"DOW VARCHAR (3) NOT NULL, " +
"Cups INTEGER NOT NULL, " +
"Type VARCHAR (10) NOT NULL," +
"PRIMARY KEY( Entry )" +
")" );
注意,实际的 SQL语句没有终结符。不同的数据库使用不同的终结符,代码列表 中为了可移植性没有使用终结符。相反,将插入适当终结符的任务交给了驱动程 序。
该代码还对数据库表明,没有“零值”列,这主要是为了避免给 SQL 新手带 来困扰。为了识别每行,代码定义了主键。
表创建后,就可以使用 SQL 的 INSERT 语句添加数据了:
INSERT INTO JJJJData VALUES ( 1, 'John', 'Mon', 1, 'JustJoe' )
INSERT INTO JJJJData VALUES ( 2, 'JS', 'Mon', 1, 'Cappuccino' )
INSERT INTO JJJJData VALUES ( 3, 'Marie', 'Mon', 2, 'CaffeMocha' )
...
在程序范例中,命名为
SQLData的阵列包含有实测值,每个元素的 形式类似于:
"(1, 'John', 'Mon', 1, 'JustJoe')"
The program code corresponding to the
INSERT statements above is:
stmt.executeUpdate(
"INSERT INTO JJJJData VALUES " + SQLData[i] );
简短地复习一下迄今为止学习的内容:首先,任何 JDBC 程序会装载 JDBC 驱 动程序,并使用 jdbc 协议(包括此处创建数据库的属性)创建 URL 。此时,程序可以连接到数据库。其次,向返回的“连接”对象请求“语句”。 本节专门范例将使用传递给驱动程序的 SQL 语句创建并填充 JJJJData表。
本节的练习包括创建 JJJJData 表和插入要求行的完整应用程序的源代码。
练习
- 创建并填充表
要从数据库检索信息,可借助于 Statement.executeQuery 方法向数据库发送 SQL SELECT 语句,该语句以数据行的形式返回 ResultSet 对象中的要求信息。默认的 ResultSet 会逐行使用 ResultSet.next() (定位到下一行)和 ResultSet.getXXX() 进行检查, 以获得各列的数据。
例如,如何获得 4J Cafe 顾客一天内消费的最大咖啡数量(杯)。根据 SQL, 获取最大值的方法之一是使用“订购者”从句按“杯”列对表进行降序排序。返 回的 ResultSet 中的首行就是最大数量(杯)。全部列都会被选 中,以便程序按预期报告和检验添加到表中的数据。SQL 语句如下:
SELECT Entry, Customer, DOW, Cups, Type
FROM JJJJData
ORDER BY Cups DESC
在程序中,用下列方式执行 SQL 语句:
ResultSet result = stmt.executeQuery(
"SELECT Entry, Customer, DOW, Cups, Type " +
"FROM JJJJData " +
"ORDER BY Cups DESC");
如果存在下一行,ResultSet.next() 会返回布尔值:true;否 则返回 false(表示已经到达数据/集合的末尾)。 原则上,获得 ResultSet 时,指针或光标刚好定位在第一行之前。调用 next() 会移到第一 行,然后是第二行等等。为了获取第一行,即杯数最大的行,可进行一些特别处 理:
if( result.next() )
if 语句收集数据。然后,采用
while(result.next())
循环,以允许程序持续到数据的末尾。
数据提取一旦定位在行上,应用程序就可以使用适当的 ResultSet.getXXX 方法逐列获取数据。下面是范例中用于收集数据的方法,以及为每行汇总杯列的 代码。
iEntry = result.getInt("Entry");
Customer = result.getString("Customer");
DOW = result.getString("DOW");
Cups = result.getInt("Cups");
TotalCups += Cups; // increment total
Type = result.getString("Type");
程序对 System.out.println() 使用报告标准。
如果运行顺利,会显示下列结果:
JS consumed the most coffee, 9 Espressos on Friday!
The total cups of coffee consumed was 48.
The row by row output is:
| 12 |
JS |
Fri |
9 |
Espresso |
| 4 |
Anne |
Tue |
8 |
Cappuccino |
| 11 |
jDuke |
Thu |
4 |
JustJoe |
| 8 |
JS |
Wed |
4 |
Latte |
| 7 |
Marie |
Wed |
4 |
Espresso |
| 13 |
John |
Fri |
3 |
Cappuccino |
| 9 |
Alex |
Thu |
3 |
Cappuccino |
| 6 |
jDuke |
Tue |
3 |
Cappuccino |
| 14 |
Beth |
Fri |
2 |
Cappuccino |
| 5 |
Holley |
Tue |
2 |
MoJava |
| 3 |
Marie |
Mon |
2 |
CaffeMocha |
| 15 |
jDuke |
Fri |
1 |
Latte |
| 10 |
James |
Thu |
1 |
Cappuccino |
| 2 |
JS |
Mon |
1 |
Cappuccino |
| 1 |
John |
Mon |
1 |
JustJoe |
注意, ResultSet 只按“杯”的次序排列。因此,很难保 证杯数相同项目的次序。例如,都是3杯的 John、Alex 和 jDuke 项目可能以 任何次序出现。这 3 个项目将在 4 杯或 4 杯以上项目之后,在 2 杯或 2 杯以 下的项目之后(记住,是按降序排列),实际上,也只能做到这些。
本节的练习包括检查 JJJJData表和生成报告的 完整应用程序的源代码。
练习
- 数据检索
本节结束时,要记住:
JDBC 具有可移植性。
为简单起见,此处的驱动程序名称、URL、用户和密码已经进行了固定编码。 用变量替代此信息后,这些程序将以任何 JDBC Compliant 驱动程序运行。
本节所有代码和材料适用于并运行于带有适当驱动程序的 JDK 1.1 和 JDBC 1.2。
不过,在这一点上,本课程假定可以使用 JDK 1.3 和 JDBC 2.0(但大多数 材料在 JDK 1.2 之下也可以顺利运行)。
“ 连接”对象代表并控制到数据库的连接。“连接数据 库”中已经介绍了连接的基本知识;本节将澄清几个要点,介绍“连接”控制 的不同区域,并给出两个练习来示范提供顺利连接所需信息的一般方法。
虽然 JDBC 中的一切都取决于数据库和 JDBC 驱动程序的功能,但一般说来, 可以有多个方法连接到相同的数据库和/或“连接”到多个数据库。DriverManager 类处理驱动程序注册,并提供获取“连接”的方法。注意,全部 DriverManager 方法都是静态的;此处不举例说明。
获取“连接”的第一步常常最难:即,如何创建@_#_$!!!@_#_ database URL? 如上所述,在用<subprotocol>:识别机器或服务器以及<subname>基 本上用来识别数据库时,基本代码jdbc:<subprotocol>:<subname> 显得非常精炼。实际上,其内容取决于专用驱动程序,并会因为产生“没有合适 的驱动程序”的错误等类途径问题使人不知所措,倍受困扰。以上述的范例中使 用的 Cloudscape URL 为例:
jdbc:cloudscape:rmi:jGuru
上述代码会解译成
jdbc: <subprotocol>: <subname>
jdbc: cloudscape:rmi: jGuru
这个相当简单,主要是因为客户程序且服务器都在相同的机器上运行。在低于第 四种类型的驱动程序中常常见到类似的 URL,因为要涉及到另外的设置,且定位 服务器要求的信息要从设置信息处获得。
即使在这里,情况并不总是令人满意。支持远程(并且甚至当地)连接的大多 数 DBMS 引擎,都使用TCP / IP(传输控制协议/ Internet协议)端口来做到 这一点。事实上,即使 Cloudscape 也在启动后使用cloudscape:rmi: subprotocol; run netstat,并可以在 1099 端口上看到。像任何 其他的套接字程序一样,DBMS 引擎可以随意确定要使用的端口。除了通常使用 的 TCP/IP 标准外,也可以使用其他的通信协议。例如, DB2 就可以在几个平 台上使用 APPC (Advanced Program to Program Communication,先进的程序 间通讯)。
在应用程序试着连接网络或 Internet 服务器时,必须提供识别/存储单元信 息。常见的 JDBC 方法是使用//host:port/subsubname,这里的主 机是 IP 地址或 DNS ( Domain Name Service,域名服务)或其他的可定位名 称。在驱动程序/数据库文档里查找默认端口,且记住系统管理员有权决定使用 不同的端口。这里的数据库就是 subsubname,而驱动程序的编写者可以随意在 他们自己的句法中添加其他属性。再次使用 Cloudscape 为例,该代码用来创建 数据库:
jdbc:cloudscape:rmi:jGuru;create=true
;create=true部分是使用 Cloudscape 句法的属性。含义是:在文档 中,检查出驱动程序和数据库。
“连接”在收集无用数据时自动关闭,但审慎的程序员会始终明确地关闭“连 接”以直接地确保节省资源。注意,虽然 API 会明确地表示关闭“连接”以“立 即释放数据库和 JDBC 资源”,但 JDBC 会建议关闭“连接”和“语句”。
和其他 JDBC API 的重要区域一样,“连接”也是界面。很多的程序员想 知道对象的起源处,因为界面不能用具体事例来予以说明。扼要的答案是: JDBC 驱动程序实现了界面并在请求时返回实际对象。 这同时说明了为什么应用程序编 译时十分完整,运行时却问题成堆:因为代码是参照标准接口编译的,只有装载 并运行程序和驱动程序时才能得到实际的效果。
前面的大多数章节都介绍了 DriverManager 的 getConnection() 方法的设置。“连接”本身负责的区域包括:
创建“语句”、 PreparedStatement 和 CallableStatement (和存储过程一起使用)实例。
获取 DatabaseMetadata 对象。
通过 commit() 和 rollback() 方法控制事务。
设置事务涉及的隔离级别。
在给定数据库的本地非标准语言中,甚至有获取任何 SQL 语句的方法,该方 法的适当名称是 nativeSQL()。本课程随后的部分会介绍这些区 域。
在继续介绍前,先讲解“ JDBC 2.0 可选包”引进的新 DataSource 类。该规范说明建议将 DataSource 用作获取“连接”的方法,并 实际上提到了反对当前的 DriverManager/Connection 方法。虽 然 JDBC 程序员应该知道这点,甚至可能也在大多数 J2EE 环境中使用---但令 人惊奇的是 DriverManager 方法不久就被废弃了。
从上面获取“连接”对象所需信息的介绍中可知,对信息进行固定编码不是个 令人满意的决定。以下练习给出了在两个通用的编程方案中获取该信息的两种方 法---即使用 ResourceBundle 和/或直接从最终用户处获取。
您可能想知道,练习中给登录名和密码设置的“sa”和“admin”是 Cloudscape 的默认值还是胡乱设置的。答案是,在逻辑框以外,没有启用 Cloudscape 的验证/安全。必须亲自对它进行设置。否则,它就会忽视这些无效 参数和属性。从一开始,就已经引入了这些有效的虚拟名称,以介绍 JDBC 标准 “连接”参数。这再次强调了检查驱动程序和数据库文档的重要性。第二个答案 是,如同在很多其他区域中一样,编程时可能有镜像,但没有什么魔法。
练习
- 连接信息概述---批处理
- 连接信息概述---交互e
“语 句”对象是发送/执行(常规) SQL语句,并通过相关“连接”检索结果的容 件或传输机制。“连接界面控制的区域”中曾经介 绍,语句类型有三种,包括 Prepared Statements 和 Callable Statements,两者都是“语句”的子界面。 如前所述,无需创建新的“语句”实例,而是请求相关的“连接”进行创建:
Statement stmt = con.createStatement();
execute 系列是“语句”方法最常使用的:
executeQuery() 用来执行返回单个 ResultSet 的 SQL 语句。
executeUpdate()用来执行修改表或表中列值的 SQL 语句,并返回 修改过的行数(在 DDL 语句中为零)。
execute() 可用于执行任何类型的 SQL语句,但更针对那些可以返 回多个结果或值的 SQL语句。本课程不深入介绍该语句。
为了更灵活地使用不同的数据库和数据源, JDBC 没有限制语句可以发送的 SQL 语句种类。实际上,只要数据源可以识别(这是程序员的责任了),语句甚 至无需是 SQL 语句,这就带来了一些令人感兴趣的可能性。不过,标明为 JDBC Compliant 的驱动程序必须至少支持 ANSI SQL-92 Entry Level 的性能。
在“连接”收集无用数据时,语句将自动地关闭,但在不再需要时应该亲自关 闭。JDBC 建议始终明确地关闭该“语句”。
更新对程序员来说有特别的含义,甚至对 SQL 也是如此。所以,对于用来执 行 DML(INSERT、UPDATE 和 DELETE) 语句、DDL 语句(如 CREATE TABLE、 DROP TABLE 和 ALTER TABLE 语句)的方法来说,executeUpdate() 大概不太受人欢迎。无论如何,它用于所有这些方面;实际上,根据经验,它应 用于不返回 ResultSet 的任何语句。
JDBC 对类型进行定义以匹配 SQL 数据类型。这些定义必须适合于数据,以避 免出现技术问题、意外结果,并能促进工作效率。可用和合适类型的更详尽信息, 请查看“Java- SQL 类型等效性”。
executeUpdate() 返回 int 值,它包含 INSERT、 UPDATE 或 DELETE 语句作用过的行数,或不返回任何东西的 SQL 语句(如 DDL 语句)的零。
练习
- 使用 executeUpdate()
executeQuery() 应用于返回 ResultSet 的“语句”,基本上是 SELECT 语句。
executeQuery()返回的默认 ResultSet 对象拥 有只向前移动的光标,该光标使用 next() 方法。应该注意, executeQuery()始终返回非空的 ResultSet。新手 常常比较 ResultSet 和零值,以确定是否已返回行。如果没有驱 动程序错误,就决不会出现这类情况。 next() 返回布尔值,在 另一行生效时是 true;在 ResultSet 耗尽时为 false。如果只希望返回单行,就可以使用 if 语 句。否则,通常使用 while 循环:
int iCount = 0;
while( myResultSet.next() )
{
// retrieve column data
// do something with it
iCount++;
}
if( iCount == 0 )
{
System.out.println(
"myResultSet returned no data.");
}
else
if( bNoErrorsOrExceptionsOrEarlyTerminations )
{
System.out.println(
"All rows from myResultSet were processed.");
}
应该按从左到右的语句(次序和 SELECT 中的相同)次序进行读 取列,并可以按列名称或索引获取列。尽管列名可能更易于理解,但使用索引更 有效率(按 1,2,3 而非 0,1,2,3...的索引顺序)。数据库和驱动程序都可能变 化,如果没有可移植性,在默认的 ResultSet 中可能一次仅仅只 能获取一行甚至是该行中的一列。
ResultSet的 getXXX() 方法用来检索列数据。JDBC 对类型进行定义,以和 SQL数据类型匹配,每个数据类型都有 getXXX() 方法。可用类型和合适类型的更详尽信息,请查看“Java-SQL类 型等效性”。
“语句”一次仅仅打开一个 ResultSet,常常对新建数据重复 使用相同的 ResultSet。应该确保从 ResultSet 中 获取全部所需数据,再通过相关的“语句”执行另一个查询。在重复执行和 Statement.close() 时,“语句”应该自动地关闭 ResultSet, 但在不再需要数据时可以亲自关闭 ResultSet。审慎的程序员可能 始终明确地关闭 ResultSet。
ResultSet 也可以返回元数据,它是有关 ResultSet 本身和所包含数据的信息。在“ ResultSet 元数据” 将更进一步地对此进行介绍。
练习
- 选择数据和显示信息
PreparedStatement 是“语句”的子界面,有以下好处:
已包含的 SQL 会被发送到数据库,并预先进行编译或筹备。由此开始发送已 筹备的 SQL,本步骤会被省略。更有活力的“语句”对每个执行都要求本步骤。 根据 DB 引擎的不同,SQL 可能会进行高速缓存并重复使用。即使对不同的 PreparedStatement 也是如此,并且大部分工作由 DB 引擎而不是 驱动程序完成。
PreparedStatement 可以处理列值的 IN 参数, 该参数的作用非常类似于方法的参数。
例如,PreparedStatement 能处理直接运行很易于出错的数据 变换,这些数据变换是迅速地在 SQL 上创建的;并以一种对开发者来说透明的方 式处理引用和日期。
注意: SQL3 类型通常假设使用 DML 的筹备语句。
这里,有两个安装和获取筹备语句的范例:
pstmtU = con.prepareStatement(
"UPDATE myTable SET myStringColumn = ? " +
"WHERE myIntColumn = ?" );
pstmtQ = con.prepareStatement(
"SELECT myStringColumn FROM myTable " +
"WHERE myIntColumn = ? ");
问号,也被称作参数标志,是在语句执行前待设置的值。从 1 开始,按从左 到右的数字顺序对它们进行引用。PreparedStatement 的 setXXX() 方法用来设置 IN 参数,在进行更改前会保持所作设置。可用类型 和合适类型的更详尽信息,请查看“Java- SQL 类型等效性”。 下面是在上述语句中设置参数的范例:
pstmtU.setString( 1, "myString" );
pstmtU.setInt( 2, 1024 );
pstmtU.executeUpdate();
pstmtQ.setInt( 1, 1024 );
pstmtQ.executeQuery();
也可以筹备没有参数的语句。注意, PreparedStatement 拥有 自己的 execute 方法系列版本,它由于要设置参数所以没有自变 数。记住, PreparedStatement 是从“语句”继承下来的,并包 括了“语句”所有的功能。一般而言,在查询运行了多次仅有相同列的值发生变 化或重复运行同一查询时,请考虑使用筹备语句。
练习
- 使用筹备"语句"
JDBC 的" 类型"定义为转换成标准 Java 类型,提供了属类的 SQL 类型。通常是直接 确定所需的类型和方法。以下两个表显示用于获取每个数据类型的常规 ResultSet 方法。典型 setxxx() 方法的格式相同。
通用的 SQL 类型---标准检索方法
| SQL 类型 | Java方法 |
| BIGINT |
getLong() |
| BINARY |
getBytes() |
| BIT |
getBoolean() |
| CHAR |
getString() |
| DATE |
getDate() |
| DECIMAL |
getBigDecimal() |
| DOUBLE |
getDouble() |
| FLOAT |
getDouble() |
| INTEGER |
getInt() |
| LONGVARBINARY |
getBytes() |
| LONGVARCHAR |
getString() |
| NUMERIC |
getBigDecimal() |
| OTHER |
getObject() |
| REAL |
getFloat() |
| SMALLINT |
getShort() |
| TIME |
getTime() |
| TIMESTAMP |
getTimestamp() |
| TINYINT |
getByte() |
| VARBINARY |
getBytes() |
| VARCHAR |
getString() |
为了显示, ResultSet.getString() 也可以应用于上述类型, 可能对 OTHER 例外。
SQL3 类型---检索方法
| SQL 类型 | Java 方法 |
| ARRAY |
getArray() |
| BLOB |
getBlob() |
| CLOB |
getClob() |
| DISTINCT |
getUnderlyingType() |
| REF |
getRef() |
| STRUCT |
(castToStruct)getObject() |
| JAVA_OBJECT |
(castToObjectType)getObject() |
ResultSet.getObject() 也可以用于两个表中列出的任何类型。
这些看起来非常清晰明了,难度也不大,但专业程序设计员应该花些时间阅读 映射 Java 的 SQL 数据类型和映射 SQL 及 Java "类型"。尤其要通过" ResultSet.getXXX() 方法"检查"转换"表,以查看可用选项的种类。
对于应用定位程序的 SQL3 类型,因为文档上令 人遗憾的缺陷,人们常常对它发出这样的疑问:"开始时,该如何将类型输入数据 库呢?"最好的答案是,检查它们对应的类(例如, BLOB 的 Blob 类),并根据 getXXX() 方法找出 setXXX() 方法应用的具体化数据,通常带有 PreparedStatement。就 Blob 而言是 getBinaryStream() 和 getBytes(),因此对应地就 有了 setBinaryStream() 和 setBytes()。更多的 信息和范例代码,请查看 LOB 和本部分相关的练习。
"我再不愿意想这件事情。"---一般而言,实话实说的开发者对问到例外/错误 处理时的反应很可能就是这句话。它说明了很难正确地进行处理,还常常会吃力 不讨好。这对于编制优质的应用程序也很关键。
本课程中的练习突出强调专门的 JDBC 区域,未对生产质量提出要求。同时, 从第一个练习开始介绍了异常处理的标准。不过,标准并不完整,因此介绍了三 种 SQLExceptions 以作弥补。
注意,加入 JDBC 2.0 中的第四种类型,BatchUpdateException, 在"批更新装置"中介绍。
java.sql包中的许多方法都会形成 SQLException,它和任何其他"例外"一样要求尝试/捕捉语句块。其 目的是描述数据库或驱动程序错误(例如,SQL 语法)。除了从 Throwable 继承下来的标准 getMessage(),SQLException 还 有两种提供详细资料的方法:一种方法是获取(或链接)其他的例外,一种方法 是设置其他的例外。
getSQLState()返回基于 X/ Open SQL 规范说明的 SQLState 标识符。这些会在 DBMS 手册中列出,或在"资源"中 寻找 SQLStates 的信息。
getErrorCode() 用来检索特定于供应商的错误代码。
getNextException() 检索下一个 SQLException, 或在没有 SQLException时为零值。程序和数据库间很多的情况都 可能出错。本方法允许跟踪全部发生的问题。
setNextException() 允许程序员给链添加 SQLException。
这些方法应该相当直接了当。典型的捕捉代码看起来如下所示:
try
{
// some DB work
} // end try
catch ( SQLException SQLe)
{
while( SQLe != null)
{
// do handling
SQLe = SQLe.getNextException();
}
} // end catch
提示: 程序员常常被语法错误弄得迷惑不解,这些语法错误似乎参 考一些无形的操作,如"ungrok found at line 1, position 14."不断地报告异 常处理程序中Connection.nativeSQL(yourQueryString)的输出量将会澄清事 实。