5.1.1 JDBC

5.1.1 JDBC 
5.1.1.1 连接到数据库 
如何打开一个数据库连接 
必须在连接数据库之前完成驱动程序的加载 
Class.forName加载驱动 
要实现在java类路径中放入驱动程序jar包 
其他数据库的驱动名称 
协议 
5.1.1.2 向数据库系统中传递SQL语句 
通过数据库连接创建Statement对象 
`executeQuery`方法执行查询语句 
`executeUpdate`方法执行费非查询语句 更新 插入删除 建表等 
5.1.1.3 获取查询结果 
executeQuery方法返回结果集 
结果集的next方法 
结果集的get方法 
结果集get方法的参数 
java成宿结束后一定要关闭数据库连接 
5.1.1.4 预备语句 
使用Connection类的prepareStatement方法创建PreparedStatement对象 
优先使用预备语句 
预备语句更加高效 
预备语句可检查用户输入 
预备语句可防止SQL注入 
一次执行多条SQL语句的情况 
5.1.1.5 可调用语句 
5.1.1.6 元数据特性 
结果集元数据 ResultsetMetaData 
如何获取ResultsetMetaData对象 `Resultset.getMetaData`方法 
数据库元数据 DatabaseMetaData 
如何获取`DatabaseMetaData` `Connection实例.getMetaData`方法 
其他获取数据库本省信息的方法 
模板 
获取数据库其他信息 
元数据信息可以是代码更通用 
5.1.1.7 其他特性 
打开或关闭自动提交 
关闭自动提交 开启事务 
开始自动提交 
处理大对象数据 
读取数据库中的大对象数据 
将大对象数据写入数据库中 
行集row set 

5.1.1 JDBC

JDBC标准定义了Java程序连接数据库服务器的应用程序接口(Application Program Interface,API),JDBC原来是Java数据库连接(Java Database Connectivity)的缩写,但其全称现在已经不用了)。
Java程序必须引用java.sql.*,它包含了JDBC所提供功能的接口定义。

5.1.1.1 连接到数据库

要在Java程序中访问数据库,首先要打开一个数据库连接。这一步需要选择要使用哪个数据库,例如,可以是你的机器上的一个Oracle实例,也可以是运行在另一台机器上的一个PostgreSQL数据库。只有在打开数据库连接以后,Java程序才能执行SQL语句。

如何打开一个数据库连接

可以通过调用java.sql包中的DriverManager类的getConnection方法来打开一个数据库连接。该方法有三个参数:

  1. URL:getConnection方法的第一个参数是以字符串类型表示的URL,指明服务器所在的主机名称以及可能包含的其他信息,例如:
    • 与数据库通信所用的协议,
    • 数据库系统用来通信的端口号,
    • 还有服务器端使用的特定数据库。

注意JDBC只是指定API而不指定通信协议。一个JDBC驱动器可能支持多种协议,我们必须指定一个数据库和驱动器都支持的协议。协议的详细内容是由提供商设定的。
2. 数据库用户:getConnection方法的第二个参数用于指定一个数据库用户标识,它为字符串类型
3. 密码:getConnection方法的第三个参数是密码,它也是字符串类型。(注意,把密码直接写在JDBC代码中会增加安全性隐患,因为你的代码有可能会被某些未被授权的用户所访问。)

必须在连接数据库之前完成驱动程序的加载

每个支持JDBC的数据库产品都会提供一个JDBC驱动程序(JDBC driver),该驱动程序必须被动态加载才能实现Java对数据库的访问。事实上,必须在连接数据库之前完成驱动程序的加载

Class.forName加载驱动

调用Class.forName方法完成驱动程序的加载,在调用时需要通过参数来指定一个实现了java.sql.Driver接口的实体类java.sql.Driver接口的功能是为了实现不同层面的操作之间的转换,一边是与产品类型无关的JDBC操作,另一边是与产品相关的、在所使用的特定数据库管理系统中完成的操作。

要实现在java类路径中放入驱动程序jar包

Oracle的驱动程序:oracle.jdbe.driver.OracleDriver。该驱动程序包含在一个.jar文件里,可以从提供商的网站下载,然后放在Java的类路径( classpath)里,用于Java编译器访问。

其他数据库的驱动名称

数据库 对应的驱动名称
IBM DB2数据库驱动 com.ibm.db2.jdbe.app.DB2Driver;
Microsoft SQL Server数据库驱动 com.microsoft.sqlserver.jdbc.SQLServerDriver;
Postgre SQL数据库驱动 org.postgresqL.Driver;
MySQL数据库驱动 com.mysql.jdbc.Driver;

Sun公司还提供了一种”桥接驱动器“,可以把JDBC调用转换成ODBC。该驱动仅是用于那些支持ODBC但不支持JDBC的厂商。

协议

用来与数据库交换信息的具体协议并没有在JDBC标准中定义,而是由所使用的驱动程序决定的。有些驱动程序支持多种协议,使用哪一种更合适取决于所连接的数据库支持什么协议。
我们的例子里在打开一个数据库连接时,字符串jdbe:oracle:thin指定了Oracle支持的一个特定协议

5.1.1.2 向数据库系统中传递SQL语句

通过数据库连接创建Statement对象

旦打开了一个数据库连接,程序就可以利用该连接来向数据库发送SQL语句用于执行。这是通过Statement类的一个实例来完成的。一个Statement对象并不代表SQL语句本身,而是实现了可以被Java程序调用的一些方法,通过参数来传递SQL语句并被数据库系统所执行。我们的例子在连接变量conn创建了一个Statement句柄(stmt)。
使用

executeQuery方法执行查询语句

executeUpdate方法来执行查询语句,返回一个结果集。

executeUpdate方法执行费非查询语句 更新 插入删除 建表等

executeUpdate方法可以执行像更新(update)、插入(insert)、删除(delete)、创建表(create table)等这样的非查询性语句

  • 对于更新插入或删除executeUpdate方法它返回一个整数,表示被插入、更新或者删除的元组个数。
  • 对于DDL语句,executeUpdate方法返回值是0

5.1.1.3 获取查询结果

executeQuery方法返回结果集

示例程序用stmt.executeQuery来执行一次查询。它可以把结果中的元组集合提取到Resultset对象变量rset中并每次取出一个进行处理。

结果集的next方法

结果集的next方法用来查看在集合中是否还存在至少一个尚未取回的元组,如果存在的话就取出。next方法的返回值是一个布尔变量,表示是否从结果集中取回了个元组。

结果集的get方法

可以通过一系列的名字以get为前缀的方法来得到所获取元组的各个属性。方法setstring可以返回所有的基本SQL数据类型的属性(被转换成Java中的String类型的值),当然也可以使用像getFloat那样一些约束性更强的方法。

结果集get方法的参数

这些不同的get方法的参数既可以是一个字符串类型的属性名称,又可以是一个整数,用来表示所需获取的属性在元组中的位置。图5-1给出了两种在元组中提取属性值的办法:
利用属性名提取( dept_name)或者利用属性位置提取(2,代表第二个属性)。

java成宿结束后一定要关闭数据库连接

Java程序结束的时候语句连接都将被关闭。注意关闭连接是很重要的,因为数据库连接的个数是有限制的;未关闭的连接可能导致超过这一限制。如果发生这种情况,应用将不能再打开任何数据库连接。

5.1.1.4 预备语句

我们也可以通过以”?“来代表以后再给出的实际值,而创建一个预备语句。
数据库系统在准备查询语句的时候对它进行编译。在每次执行该语句时(用新值替换”?”),数据库可以重用预先编译的查询的形式,应用新值进行查询。
图5-2的代码框架给出了如何使用预备语句的示例。

使用Connection类的prepareStatement方法创建PreparedStatement对象

可以使用Connection类的prepareStatement方法来提交SQL语句用于编译。它返回一个PreparedStatement类的对象。此时还没有执行SQL语句。执行需要PreparedStatement类的两个方法executeQueryexecuteUpdate
但是在它们被调用之前,我们必须使用PreparedStatement类的方法来为”?“参数设定具体的值。

setString方法以及诸如setInt等用于其他的SQL基本类型的其他类似的方法使我们能够为参数指定值。

  • set第一个参数用来确定我们为哪个”?“设定值,
    • 如果给第1个问号设置值,则set方法的第一个参数设置为1
  • set方法的第二个参数是我们要设定的值。

优先使用预备语句

预备语句更加高效

在同一查询编译一次然后设置不同的参数值执行多次的情况下,预备语句使得执行更加高效

预备语句可检查用户输入

然而,预备语句有一个更加重要的优势,预备语句可以用户输入,
即使是只运行一次,预备语句都是执行SQL查询的首选方法。假设我们读取了一个用户输入的值,然后使用Java的字符串操作来构造SQL语句。如果用户输入了某些特殊字符,例如一个单引号,除非我们采取额外工作对用户输入进行检查,否则生成的SQL语句会出现语法错误。
setString方法为我们自动完成检查,并插入需要的转义字符,以确保语法的正确性

预备语句可防止SQL注入

一种叫做SQL注入(SQL injecton)的技术可以被恶意黑客用来窃取数据或损坏数据库。
假设一个Java程序输入一个字符串name,并且构建下面的查询:

1
"select * from instructor where name='"+name+"';" 

如果用户没有输人一个名字,而是输入:
x' or 'y'='y'
这样,产生的查询语句就变成:

1
select * from instructor where name='x' or 'y'='y';

在生成的查询中, where子句总是真,所以查询结果返回整个instructor关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select * from instructor where name='x' or 'y'='y';
+-------+------------+------------+-----------+
| ID | name | dept_name | salary |
+-------+------------+------------+-----------+
| 10101 | Srinivasan | Comp. Sci. | 65000.00 |
| 12121 | Wu | Finance | 90000.00 |
| 15151 | Mozart | Music | 40000.00 |
| 22222 | Einstein | Physics | 95000.00 |
| 25566 | Brown | Biology | 100000.00 |
| 30765 | Green | Music | NULL |
| 32343 | El Said | History | 60000.00 |
| 33456 | Gold | Physics | 87000.00 |
| 45565 | Katz | Comp. Sci. | 75000.00 |
| 58583 | Califieri | History | 62000.00 |
| 76543 | Singh | Finance | 80000.00 |
| 76766 | Crick | Biology | 72000.00 |
| 83821 | Brandt | Comp. Sci. | 92000.00 |
| 98345 | Kim | Elec. Eng. | 80000.00 |
+-------+------------+------------+-----------+
14 rows in set (0.05 sec)

更诡计多端的恶意用户甚至可以编写输入值以输出更多的数据。使用预备语句就可以防止这类问题,因为输入的字符串setString方法将被插入转义字符,因此最后的查询变为:

1
select * from instructor where name ='x\' or \'Y\'=\'Y';

这是无害的查询语句,返回结果为空集。

1
2
mysql> select * from instructor where name ='x\' or \'Y\'=\'Y';
Empty set

一次执行多条SQL语句的情况

比较老的系统允许多个由分号隔开的语句在一次调用里被执行
此功能正逐渐被淘汰,因为恶意的黑客会利用SQL注入技术插入整个SQL语句。由于这些语句在Java程序所有者的权限上运行,像删除表(drop table)这样毁灭性的SQL语句会被执行。**SQL应用程序开发者必须警惕这种潜在的安全漏洞**。

5.1.1.5 可调用语句

JDBC还提供了CallableStatement接口来允许调用SQL的存储过程和函数(稍后在5.2节描述)。此接口对函数和过程所扮演的角色跟prepareStatement对查询所扮演的角色一样。
函数返回值和过程的对外参数的数据类型必须先用方法registerOutParameter()注册,它们可以用与结果集用的方法类似的get方法获取。请参看JDBC手册以获得更细节的信息。

5.1.1.6 元数据特性

正如我们此前提到的,一个Java应用程序不包含数据库中存储的数据的声明。这些声明是SQL数据定义语言(DDL)的一部分。因此,使用JDBCJava程序必须

  • 要么将关于数据库模式的假设硬编码到程序中,
  • 要么直接在运行时从数据库系统中得到那些信息。

后一种方法更可取,因为它使得应用程序可以更健壮地处理数据库模式的变化。

结果集元数据 ResultsetMetaData

回想一下,当我们提交一个使用executeQuery方法的查询时,查询结果被封装在一个Resultset对象中。

如何获取ResultsetMetaData对象 Resultset.getMetaData方法

接口Resultset有一个getMetaData()方法,它返回一个包含结果集元数据ResultsetMetaData对象。 ResultSetMetaData进一步又包含查找结果集元数据的方法,例如结果的列数、某个特定列的名称,或者某个特定列的数据类型。这样,即使不知道结果的模式,我们也可以方便地执行查询。

实例省略

ResultSetMetaData对象的getColumnCount方法返回结果关系的属性个数(元数)。
这使得我们能够遍历每个属性(请注意,和JDBC的惯例一致,我们从1开始)。对于每一个属性,我们采用getColumnNamegetColumnTypeName两个方法分别得到属性的名称和属性的数据类型。

数据库元数据 DatabaseMetaData

DatabaseMetaData接口提供了查找数据库元数据的机制。

如何获取DatabaseMetaData Connection实例.getMetaData方法

接口Connection包含一个getMetaData方法用于返回一个DatabaseMetaData对象。接口DatabaseMetaData进一步又含有大量的方法可以用于获取程序所连接的数据库数据库系统的元数据。
例如,有些方法可以返回数据库系统的产品名称和版本号。另外一些方法可以用来查询数据库系统所支持的特性

其他获取数据库本省信息的方法

还有其他可以返回数据库本身信息的方法,下面的代码显示了如何找出数据库中的关系的列(属性)信息。变量com假定存储了一个已经打开的数据库连接。方法getColumns有四个参数:

  • 一个目录名称(null表示目录名称被忽略)、
  • 一个模式名称模板、
  • 一个表名称模板,
  • 以及一个列名称模板。

模板

模式名称表名称列名称模板可以用于指定一个名字或一个模板。

模板可以使用SQL字符串匹配特殊字符如”%“和”_“;例如模板”%“匹配所有的名字。
只有满足特定名称或模板的模式中的表的列才会被检索到

结果集中的每行包含一个列的信息
结果集中的行包括若干个列,如目录名称、模式、表和列、列的类型,等等。

1
2
3
4
5
6
DatabaseMetaData dbmd = conn.getMetaData();
ResultSet rs = dbmd.getColumns(null, "univdb", "department", "%");
while (rs.next()) {
System.out.println(rs.getString("COLUMN_NAME"));
System.out.println(rs.getString("TYPE_NAME"));
}

获取数据库其他信息

DatabaseMetaData还提供了获取数据库信息的一些其他方法,比如,可以用来获取关系(get Tables()),外码参照( getCrossReference())、授权数据库限制如最大连接数等的元数据。
元数据接口可以用于许多不同的任务。例如,它们可以用于编写数据库的浏览器,该浏览器允许用户找出一个数据库中的关系表,检查它们的模式,检查表中的行,用选择操作查看想要的行,等等。

元数据信息可以是代码更通用

元数据信息可以用于使这些任务的代码更通用;

  • 例如,用来显示一个关系中的行的代码可以用这样的方法编写使得它能够在所有可能的关系上工作,无论这些关系的模式是什么。
  • 类似地,可以编写这样的代码,它获得一个査询字符串,执行查询,然后把结果打印成一个格式化的表;无论提交了的实际查询是什么,这段代码都可以工作。

5.1.1.7 其他特性

JDBC提供了一些其他特性,如可更新的结果集(updatable result sets)。它可以从一个在数据库关系上执行选择和/或投影操作的查询中创建一个可更新的结果集。然后,一个对结果集中的元组的更新将引起对数据库关系中相应元组的更新
回想一下4.3节,事务把多个操作封装成一个可以被提交或者回滚的原子单元
默认情况下,每个SQL语句都被作为一个自动提交的独立的事务来对待。

打开或关闭自动提交

JDBCConnection接口中的方法setAutoCommit()允许打开或关闭这种行为。

关闭自动提交 开启事务

因此,如果con是一个打开的连接,则conn.setAutoCommit(false)将关闭自动提交。然后事务必须用conn.commit()显式提交或用conn.rollback()回滚。

开始自动提交

自动提交可以用conn.setAutoCommit(true)来打开。

处理大对象数据

读取数据库中的大对象数据

JDBC提供处理大对象的接口而不要求在内存中创建整个大对象。为了获取大对象, Resultset接口提供方法getBlob()getClob(),它们与getString()方法相似,但是分别返回类型为BlobClob的对象。这些对象并不存储整个大对象,而是存储这些大对象的定位器,即指向数据库中实际大对象的逻辑指针。从这些大对象中获取数据与从文件或者输入流中获取数据非常相似,可以采用像getBytesgetSubString这样的方法来实现。

将大对象数据写入数据库中

反向操作时,为了向数据库里存储大对象,可以用PreparedStatement类的方法setBlob(int parameterIndex, InputStream inputStream)把一个类型为二进制大对象(blob)的数据库列与一个输入流关联起来。
当预备语句被执行时,数据从输入流被读取,然后被写入数据库的二进制大对象中。与此相似,使用方法setClob可以设置字符大对象(clob)列,该方法的参数包括该列的序号和一个字符串流。

行集row set

JDBC还提供了行集( row set)特性**,允许把结果集打包起来发送给其他应用程序行集既可以向后又可以向前扫描,并且可被修改。行集一旦被下载下来就不再是数据库本身的内容了**,所以我们在这里并不对其做详细介绍。