JDBC数据库访问技术


软件的本质就是处理数据,而在商业应用中,数据往往需要专门的数据库来存放。所以在软件开发
过程中,几乎所有的项目都要使用到数据库。那么在 Java 项目中是如何对数据库进行操作的呢?这就需
要用到 Java 数据库应用程序。在这一章中,将学习如下内容:
  JDBC 技术和驱动程序;
  java.sql 包下的常用类;
  JDBC 操作数据库实例。
23.1   JDBC 技术概述
JDBC (Java Database Connectivity)是一种用于执行 SQL 语句的 Java API。它由一组用 Java 编程语
言编写的类和接口组成,为 Java 应用程序与各种不同数据库之间进行对话提供了一种便捷的方法,使得
开发人员能够用纯 Java API 来编写具有跨平台性的数据库应用程序。
23.1.1   JDBC 技术介绍
应用程序可以通过数据库厂商提供的 API 以及 SQL 语句对数据库进行操作,如图 23.1 所示。
图 23.1  应用程序访问数据库 
在此模式下,看到不同的数据库有不同的 API 操作界面。这样的话,对于实现同样功能的应用程序,
针对不同的数据库,开发人员需要编写不同的代码,这样对 Java 设计人员是很不方便的。于是 JDBC 应
运而生。
JDBC 的出现使 Java 程序对各种数据库的访问能力大大增强。通过使用 JDBC,开发人员可以很方
便地将 SQL 语句传送给几乎任何一种数据库。
JDBC 的工作模式如下面的图 23.2 所示。从图中可以看到,应用程序通过调用 JDBC 来操作数据库
的过程,其实是由数据库厂商提供的 JDBC 驱动程序来负责的。Java 提供统一的 JDBC 接口 API,各个
数据库厂商负责开发实现这些接口的 API。那么,如果数据库访问程序要更换数据库的话,只要更换新
的数据库驱动程序,并在 JDBC 中载入新的驱动程序来源,即可完成数据库系统的变更。换句话说,使
用 JDBC 后就没有必要写一个程序访问 Oracle 数据库,同时再写另一个程序访问 SQL  Server 数据库,
再写一个程序访问 DB2 了。 
2
图 23.2   JDBC 工作模式
JDBC 的主要功能如下。
  建立与数据库或者其他数据源的连接;
  向数据库发送 SQL 命令;
  处理数据库的返回结果。
23.1.2   JDBC  的 4 种驱动程序
为了与某个数据库连接,必须要具有适合该数据库的驱动程序。JDBC 驱动程序主要有以下 4 种基
本类型:
1. JDBC-ODBC 桥加 ODBC 驱动程序
JDBC-ODBC 桥产品经由 ODBC 驱动程序供 JDBC 访问数据库。ODBC(开放式数据库连接)的出
现要比 JDBC 早,广泛地应用于连接各种环境中的数据库。 JDBC-ODBC 桥加 ODBC 驱动程序实际是把
所有 JDBC 的调用传递给 ODBC,再由 ODBC 调用本地数据库驱动代码。在本书中,将采用此驱动程序
+Access 数据库进行演示。这种驱动程序的主要优点有 3 个:
  它提供了连接几乎所有平台上的所有数据库的能力。
  它可能是访问低端桌面数据库(例如 Access)和应用的程序的唯一方式。
  操作简单,适应初学才练习。
这种驱动程序的缺点如下。
  ODBC 驱动程序需要安装并加载到目标机器上。
  JDBC 和 ODBC 之间的转换将在很大程度上影响系统的性能。
2.  本地 API 和部分 Java 编写的驱动程序
这种类型的驱动程序是把客户机 API 上的 JDBC 调用转换为对数据库的调用。也就是说,这种类型
的驱动程序是通过调用本地 API 来实现与数据库的通信。这种类型的驱动程序要比采用类型 1 方式的速
度快很多,但它仍然存在着一些缺点:
  它仍然需要在目标机器上安装本地代码。
  JDBC 所依赖的本地接口在不同的 Java 虚拟机供应商以及不同的操作系统上是不同的。
3. JDBC 网络纯 Java 驱动程序
这种驱动程序根据三层结构建立:网络协议驱动、中间件服务器和数据库服务。该类型的驱动程序
由中间件服务器提供,这样客户端程序可以使用数据库无关的协议和中间件服务器进行通信,中间件服
务器再将客户端的 JDBC 调用转发给数据库进行处理。这种方式的驱动程序有以下优点:
  不需要客户机上有任何本地代码。
  不需要客户安装任何程序。
  大部分功能实现都在 server 端,所以这种驱动可以设计的很小,可以非常快速的加载到内存中。    
·3·
缺点为:中间件层仍然需要有配置其他数据库驱动程序,并且由于多了一个中间层传递数据,它的
执行效率还不是最好。
4.  本地协议纯 Java 驱动程序
该类型的驱动程序中包含了特定数据库的访问协议,使得客户端可以直接和数据库进行通信。这种
方式的驱动程序有以下优点:
  效率高,速度快。
  驱动程序可以动态的被下载。
缺点为:对于不同的数据库需要下载不同的驱动程序。
这四种驱动程序的属性如表 23.1 所示
表 23.1  驱动程序的类型及其属性
驱动程序种类  纯Java  网络协议
JDBC-ODBC桥加ODBC  非  直接
本地API和部分Java  非  直接
JDBC网络纯Java  是  要求连接器
本地协议纯Java  是  直接
23.1.3   如何获得 JDBC 驱动程序
要想得到某个数据库的驱动程序,可以从该数据库厂商的网站上进行下载,例如 SQL  Server  2005 
JDBC  Driver 的下载地址为:http://www.microsoft.com 。也可以访问 Oracle 公司的网站(网址是
http://www.oracle.com)下载 Oracle 数据库的 JDBC 驱动数据或 MySQL 数据库的驱动程序。
如 果 本 机 安 装 了 Oracle 数 据 库 的 话 , 可 以 到 本 机 Oracle 安 装 目 录 下 找 。 例 如 ,
E:\app\Administrator\product\11.2.0\dbhome_1\jdbc\lib\ojdbc6.jar。(jdk 1.6 及以上使用 ojdbc6.jar,jdk 1.5
使用 ojdbc5.jar)
23.1.4   设置环境变量使得驱动程序可被访问
获得驱动程序以后,需要将驱动程序加入 Classpath 的设定之中。例如作者本人的 Oracle 11g 的驱动
程序路径为“ E:\app\Administrator\product\11.2.0\dbhome_1\jdbc\lib\ojdbc6.jar ”, 则必须在环境变量
CLASSPATH 中添加路径“E:\app\Administrator\product\11.2.0\dbhome_1\jdbc\lib\ojdbc6.jar”。
说明:如果是使用 IDE 的话(如 NetBeans 或 Eclipse 等),可以不用设置环境变量。IDE 中支持将
数据库驱动程序库添加到工程构建路径上。
23.2   java.sql 包
在 JDBC 介绍中可以看到,JDBC 是采用接口和实现分离的思想设计了 Java 数据库编程的框架。接
口由 Java 定义并提供,实现交给各个数据库厂商。
JDBC 的接口包含在 java.sql 及 javax.sql 包中。在 java.sql 包中定义了很多接口和类,其中几个常用
的接口和类如下。 
4
1.驱动程序接口:Driver
在 java.sql 包中定义的 Driver 接口是所有 JDBC 驱动程序必须实现的接口。 在程序中要连接数据库,
必须先加载特定厂商提供的数据库驱动程序。几种主要的数据库驱动程序加载代码如下所示:
Class.forName("oracle.jdbc.driver.OracleDriver");        //加载 Oracle 数据库驱动程序
Class.forName("com.mysql.jdbc.Driver ");           //加载 MySQL 数据库驱动程序
Class.forName("org.postgresql.Driver ");            //加载 PostgreSQL 数据库驱动程序
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver ");  //加载 SQL Server2005 数据库驱动程序
2.  管理驱动程序类:DriverManager
在 java.sql 包中定义的 DriverManager 类是 JDBC 的管理类,作用于用户和驱动程序之间。它跟踪可
用的驱动程序,并在数据库和驱动程序之间建立连接。
当 DriverManager 调用 getConnection()方法时,DriverManager 类首先从它已加载的驱动池中找到一
个可以接受该数据 URL 的驱动程序,然后请求该驱动程序使用相关的数据库 URL 连接到数据库中。于
是,getConnection()方法建立了与数据库的连接。
3.  数据库连接接口:Connection
在 java.sql 包中定义的 Connecton 类的对象代表与数据库的连接,也就是在已经加载的 Driver 和数
据库之间建立连接。必须创建一个 Connection 类的实例,其中包括数据库的信息:连接 URL、数据库
账户和登录密码。
几种主要的数据库的连接 URL 如下:
String ORACLE_CON_URL = "jdbc:oracle:thin:@localhost:1521/orcl";    //Oracle 数据库连接 URL
String MYSQL_CON_URL = "jdbc:mysql://localhost:3306/booklibrary";    //MySQL 数据库连接 URL
String POSTGRESQL_CON_URL = "jdbc:postgresql://localhost:5432/testdb";  //PostgreSQL 数据库连接 URL
String SQLSERVER2005_CON_URL = "jdbc:sqlserver://localhost:1433;DatabaseName=booklibrary";
调用 DriverManager 的 getConnection()方法,并向其传递建立数据库连接的信息,以建立并返回和
数据库的连接。代码如下。
Connection con = DriverManager.getConnection(url, login,password);
其中参数 url 即要连接数据库的连接 URL,参数 login 为连接数据库的合法账户,参数 password 为
连接数据库的密码。
4. SQL 声明接口:Statement
在 java.sql 包中定义的 Statement 类代表要在数据库连接上执行的 SQL 语句,并且提供执行方法。
Connection 接口提供了生成 Statement 对象的方法。在一般情况下,我们通过 connection.createStatement()
方法就可以得到 Statement 的实例。
有三种 Statement 对象,它们都作为在给定连接上执行 SQL 语句的包容器: Statement 、
PreparedStatement (它从 Statement 继承而来)和 CallableStatement (它从 PreparedStatement 继承而来)。
该接口提供了可以被执行的基本的 SQL 语句,作为提高性能的一项措施, PreparedStatement 对象提
供了可以与查询信息一起预编译的一种语句类型。CallableStatement 对象用来封装数据库中存储过程的
执行。
Statement 提供了许多方法,最常用的方法如下。
  execute()  运行语句,返回是否有结果集。
    executeQuery()  运行查询语句,返回 ReaultSet 对象。
    executeUpdate()  运行更新操作,返回更新的行数。
    addBatch()  增加批处理语句。
    executeBatch()  执行批处理语句。 
·5·
    clearBatch()  清除批处理语句。
Statement 对象用于执行不带参数的简单 SQL 语句,它的典型使用如下所示。

Connection conn = DatabaseConnection.getConnection();    //获得数据库连接对象
Statement stmt = conn.createStatement();            //创建 SQL 语句对象
ResultSet rst = stmt.executeQuery(“select * from sometable”);  //执行查询,获得查询结果集

PreparedStatement 对象用于执行带或不带 IN 参数的预编译 SQL 语句,它的典型使用如下所示。

Connection conn = DatabaseConnection.getConnection();  //获得数据库连接对象
//创建预编译语句对象
PrepareStatement pstmt = conn.prepareStatement(“insert into user_info values(?,?,?,?,?,?,?)”;
pstmt.setString(1.”hellking”);              //设置预编译 SQL 语句中的第一个参数
pstmt.setInt(2,20);                  //设置预编译 SQL 语句中的第二个参数
pstmt.setBoolean(3,true);                //设置预编译 SQL 语句中的第三个参数
...
pstmt.executeUpdate();                //执行 SQL 语句
...
CallableStatement 对象用于执行对数据库已存储过程的调用,它的典型使用如下所示。

Connection conn = DatabaseConnection.getConnection();  //获得数据库连接对象
String strSQL = “{call demo_account(?,?,?,?,?,?,?)}”;
java.sql.CallableStatement.sqlStmt = conn.prepareCall(strSQL);
sqlStmt.setString(1,”userid_3”);            //设置调用存储过程 SQL 语句的第一个参数
sqlStmt.setString(2,”userid_1”);            //设置调用存储过程 SQL 语句的第二个参数
...
int I = sqlStmt.executeUpdate();            //执行存储过程

5.  查询结果接口:ResultSet
在 Statement 对象执行 SQL 语句时,有时会返回 ResultSet 结果集。 ResultSet 往往包含的是查询的结
果集。此接口抽象了运行 select 语句的结果,提供了逐行访问结果的方法,通过它访问结果的不同字段。
结果集通常是一张表,其中有查询所返回的列标题及相应的值。
通过 ResultSet.next()方法把当前的指针向下移动一位。最初它位于第一行之前,因此第一次调用 next
将把指针置于第一行上,使它成为当前行。随着每次调用 next()导致指针向下移动,按照从上至下的次
序获取 ResultSet 行。通常情况下,可使用如下的代码遍历 ResultSet 对象内的每一行数据:
...
try{
Connection conn = DatabaseConnection.getConnection();
Statement stmt = conn.createStatement(); 
ResultSet rst = stmt.executeQuery(“select * from User_Info”);
while(rs.next()){
//显示结果集
...
}
}
读取 ResultSet 对象中每一行数据的方法是调用它的 getXXX()方法,XXX 表示 Java 的数据类型。
在使用该方法读取数据时,应该为它指定要读取的列。指定读取的列有两种方法:一种方法是使用列的
6
序号,另一种方法是使用列名。例如,结果集的第 2 列的列名为“StuName”,则下面的两行代码是等
价的。

String name = rs.getString(2);
String name = rs.getString(“StuName”);

如果 XXX 所代表的数据类型与结果集中对应列的数据类型不匹配,将会发生类型转换。例如,结
果集中“StuID”列的数据类型为 int,那么调用 getString(“StuID”)读取 StuID 一列的信息时,将会把
结果集中的 int 类型转化为 String 类型。
23.3   访问数据库
下面通过一个完整示例来演示 Java 数据库应用程序访问数据库的全过程。在此例中采用数据库使用
Oracle 11g 数据库,驱动程序使用 Oracle 数据库的 ojdbc2.jar 驱动程序。
23.3.1   准备工作
在正式开始编写代码之前,先做好开发的准备工作。包括:
1)  下载相应数据库的 JDBC 驱动
本教程使用 Oracle 驱动 ojdbc6.jar。在已经安装 Oracle 数据库的情况下,ojdbc6.jar 已经安装在本机
上了。路径为:E:\app\Administrator\product\11.2.0\dbhome_1\jdbc\lib\ojdbc6.jar。如果是使用记事本开发
JDBC 应用程序,还需要将驱动包配置到环境变量 CLASSPATH;如果是使用 IDE,则不需要配置环境
变量。
2)  选择合适的 IDE
本教程使用 Oracle 的开源 IDE 工具 NetBeans 7.3.1。请先安装 NetBeans 7.3。(其他常用的开源 IDE
工具还包括 Eclipse,可自行选择)
3)  创建示例数据表并插入样本数据
首先创建一个新的账户,在该账户下创建示例数据表。启动 SQL Developer 或 SQL*Plus,创建一个
新的账户 xlw,密码也为 xlw。
create user xlw identifiey by xlw;        --创建新账户并设置密码
grant create session, resource to xlw;      --授予该账户连接数据库并使用资源的权限
本示例程序实现一个咖啡连锁店的数据的管理。其中包括:
suppliers 表:保存每个咖啡供应商的信息。
create table SUPPLIERS(
SUP_ID      number(8) NOT NULL,
SUP_NAME    varchar2(80) NOT NULL,
STREET      varchar2(80) NOT NULL,
CITY      varchar2(40) NOT NULL,
STATE    char(2) NOT NULL,
ZIP      char(5),
PRIMARY KEY (SUP_ID) 
·7·
);
coffees 表:保存在“咖啡休闲屋”连锁店中可用于销售的咖啡信息。
create table COFFEES(
COF_NAME    varchar2(64) NOT NULL,
SUP_ID      number(8) NOT NULL,
PRICE      number(10,2) NOT NULL,
SALES      number(8) NOT NULL,
TOTAL      number(8) NOT NULL,
PRIMARY KEY (COF_NAME),
FOREIGN KEY (SUP_ID) REFERENCES SUPPLIERS (SUP_ID)
);
cof_descriptions 表:保存对每种咖啡的详细描述。
create table COFFEE_DESCRIPTIONS(
COF_NAME    varchar2(64) NOT NULL,
COF_DESC    clob,
COF_PIC    blob,
PRIMARY KEY (COF_NAME),
FOREIGN KEY (COF_NAME) REFERENCES COFFEES (COF_NAME)
);
cof_inventory 表:保存每个仓库库存咖啡数量的信息。
create table COF_INVENTORY(
WAREHOUSE_ID    number(8) NOT NULL,
COF_NAME      varchar2(64) NOT NULL,
SUP_ID        number(8) NOT NULL,
QUAN        number(8) NOT NULL,
DATE_VAL      date,
FOREIGN KEY (COF_NAME) REFERENCES COFFEES (COF_NAME),
FOREIGN KEY (SUP_ID) REFERENCES SUPPLIERS (SUP_ID)
);
merch_inventory 表:保存仓库中非咖啡商品数量的信息。
create table MERCH_INVENTORY(
ITEM_ID        number(8) NOT NULL,
ITEM_NAME      varchar2(40),
SUP_ID        number(8),
QUAN        number(8),
DATE_VAL      date,
PRIMARY KEY (ITEM_ID),
FOREIGN KEY (SUP_ID) REFERENCES SUPPLIERS (SUP_ID)
);
coffee_houses 表:保存咖啡店的位置信息(所在州、城市、咖啡销量、非咖啡商品销量、总销量等)。
create table COFFEE_HOUSES(
STORE_ID      number(8) NOT NULL,
CITY        varchar2(64),
COFFEE        number(8) NOT NULL,
MERCH        number(8) NOT NULL,
TOTAL        number(8) NOT NULL,
PRIMARY KEY (STORE_ID)
);
data_repository 表:保存一些 URL。这些 URL 指向对“咖啡休闲屋”连锁店感兴趣的参考文档和其
8
他数据。
create table DATA_REPOSITORY(
DOCUMENT_NAME varchar2(100),
URL          varchar2(400)
);
执行以上创建数据表的脚本,并向表中插入以下样本数据:
insert into SUPPLIERS values(49,   'Superior Coffee', '1 Party Place', 'Mendocino', 'CA', '95460');
insert into SUPPLIERS values(101, 'Acme, Inc.', '99 Market Street', 'Groundsville', 'CA', '95199');
insert into SUPPLIERS values(150, 'The High Ground', '100 Coffee Lane', 'Meadows', 'CA', '93966');
insert into SUPPLIERS values(456, 'Restaurant Supplies, Inc.', '200 Magnolia Street', 'Meadows', 'CA', '9396 6');
insert into SUPPLIERS values(927, 'Professional Kitchen', '300 Daisy Avenue', 'Groundsville', 'CA', '95199');
insert into COFFEES values('Colombian',           101, 7.99, 0, 0);
insert into COFFEES values('French_Roast',        49,   8.99, 0, 0);
insert into COFFEES values('Espresso',            150, 9.99, 0, 0);
insert into COFFEES values('Colombian_Decaf',     101, 8.99, 0, 0);
insert into COFFEES values('French_Roast_Decaf', 049, 9.99, 0, 0);
insert into COF_INVENTORY values(1234, 'Colombian',        10 1, 0, to_date('2006-04-01','yyyy-MM-dd'));
insert into COF_INVENTORY values(1234, 'French_Roast',     49,   0, to_date('2006 -04-01','yyyy-MM-dd'));
insert into COF_INVENTORY values(1234, 'Espresso',         150, 0, to_date('2006 -04-01','yyyy-MM-dd'));
insert into COF_INVENTORY values(1234, 'Colombian_Decaf', 101, 0, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00001234, 'Cup_Large', 456, 28, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00001235, 'Cup_Small ', 456, 36, to_date('2006-04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00001236, 'Saucer', 456, 64, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00001287, 'Carafe', 456, 12, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00006931, 'Carafe', 927, 3, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00006935, 'PotHolder', 927, 88, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00006977, 'Napkin', 92 7, 108, to_date('2006-04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00006979, 'Towel', 927, 24, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00004488, 'CofMaker', 456, 5, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00004490, 'CofGrinder', 456, 9, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00004495, 'EspMaker', 456, 4, to_date('2006 -04-01','yyyy-MM-dd'));
insert into MERCH_INVENTORY values(00006914, 'Cookbook', 927, 12, to_date('2006-04-01','yyyy-MM-dd'));
insert into COFFEE_HOUSES values(10023, 'Mendocino', 3450, 2005, 5455);
insert into COFFEE_HOUSES values(33002, 'Seattle', 4699, 3109, 7808);
insert into COFFEE_HOUSES values(10040, 'SF', 5386, 2841, 8227);
insert into COFFEE_HOUSES values(32001, 'Portland', 3147, 3579, 6726);
insert into COFFEE_HOUSES values(10042, 'SF', 2863, 1874, 4710);
insert into COFFEE_HOUSES values(10024, 'Sacramento', 1987, 2341, 4328);
insert into COFFEE_HOUSES values(10039, 'Carmel', 2691, 1121, 3812);
insert into COFFEE_HOUSES values(10041, 'LA', 1533, 1007, 2540);
insert into COFFEE_HOUSES values(33005, 'Olympia', 2733, 1550, 4283);
insert into COFFEE_HOUSES values(33010, 'Seattle', 3210, 2177, 5387);
insert into COFFEE_HOUSES values(10035, 'SF', 1922, 1056, 2978);
insert into COFFEE_HOUSES values(10037, 'LA', 2143, 1876, 4019);
insert into COFFEE_HOUSES values(10034, 'San_Jose', 1234, 1032, 2266);
insert into COFFEE_HOUSES values(32004, 'Eugene', 1356, 1112, 2468); 
·9·
23.3.2   使用 JDBC 驱动程序连接 Oracle 数据库
使用 JDBC 连接一个 Oracle 数据库的代码片段如下:
Class.forName(“oracle.jdbc.driver.OracleDriver”);
Connection con = null;
con = DriverManager.getConnection(“jdbc:oracle:thin:@hostname:port/dbname”,”username”,”password”);
con.close();
DriverManager 的 getConnection 方法,还有一个重载的形式,接收一个属性对象,在属性对象中封
装了连接数据库的账号和密码。代码如下所示:
Properties connectionProps = new Properties();    //创建一个属性对象
connectionProps.setProperty("user", userName);    //将数据库连接账号放入到属性对象中
connectionProps.setProperty("password", password);  //将数据库连接密码放入到属性对象中
con = DriverManager.getConnection(ORACLE_CON_URL, connectionProps);  //重载的方法
根据类的单一职责原则, 我们将连接数据库的方法封装到一个单独的类 JDBCUtilities.java 中。 完整
的示例程序如下:
package com.citymango.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class JDBCUtilities {
private static final String ORACLE_DB_DRIVER = "oracle.jdbc.driver.OracleDriver";    
private static final String ORACLE_CON_URL = "jdbc:oracle:thin:@localhost:1521/orcl";
private static String USER = "xlw";
private static String PWD = "xlw";
static{      //jdbc4.0 之前,需要手工加载驱动程序
try{
System.out.println("正在加载 Oracle   JDBC 驱动程序......");
Class.forName(ORACLE_DB_DRIVER);
System.out.println("加载 Oracle   JDBC 驱动程序成功!");
} catch (ClassNotFoundException ex) {
System.out.println("加载 Oracle   JDBC 驱动程序失败。");
ex.printStackTrace();            
}
}
//获得数据库连接的方法
public static Connection getConnection(){
Connection con = null;
try {
System.out.println("正在连接数据库......");
con = DriverManager.getConnection(ORACLE_URL, USER, PWD);
System.out.println("已经和数据库建立连接。");
} catch (SQLException ex) { 
10
System.out.println("无法和数据库建立连接。");
ex.printStackTrace();
}
return con;
}
}
将 JDBC 驱动程序(ojdbc6.jar 包)添加到 classpath 路径下。运行 JDBC Oracle 程序需要 JDBC 驱动
程序。 需要注意的是,在 Java SE 7 中,任何在 class path 中找到的 JDBC 4.0 驱动程序都能自动地被加载。
即如果使用的是 jdk 7 的话,可以省略掉加载驱动程序的代码。不过,如果使用 JDBC4.0 之前的版本,
则必须得使用方法 Class.forName()方法手工加载任何驱动。
编写一个测试程序,代码如下所示:
package com.citymango.test;
import java.sql.Connection;
import java.sql.SQLException;
public class AppTest {
public void testDBConnection(){
Connection con = null;
try {
System.out.println("正在连接数据库......");
con = JDBCUtilities.getConnection();            
} catch (SQLException ex) {
System.out.println("无法连接数据库!");
ex.printStackTrace();
return;
}
if(con != null){
System.out.println("已成功连接上数据库!");
}else{
System.out.println("数据库连接失败!");
}
}
public static void main(String[] args){
AppTest appTest = new AppTest();
appTest.testDBConnection();
}
}
然后编译并运行 AppTest.java 程序。输出结果如下:
--------------Oracle JDBC 连接测试--------------正在加载 Oracle JDBC 驱动程序......
加载 Oracle JDBC 驱动程序成功! 
·11·
正在连接数据库……
已成功连接上数据库!
23.3.3   对数据库进行查询
和数据库(数据源)建立连接以后,就可以对数据库进行各种操作了。对数据库最经常进行的操作,
就是数据查询。在咖啡连锁店管理系统中,我们需要经常对咖啡信息进行查询(查询 coffees 表)。接
下来,实现对 coffees 表的查询操作。
根据类的职责单一原则,我们实现一个数据访问对象类 CoffeesDAO.java,由其提供对数据表 coffees
的查询方法。实现代码如下:
package com.citymango.dao;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class CoffeesDAO{
//查看 coffees 表中的记录数据
public void viewTable(){
String query = "select cof_name,sup_id,price,sales,total from coffees";
try(Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(query) ){
while(rs.next()){
String coffeeName = rs.getString("cof_name");
int supplierID = rs.getInt("sup_id");
float price = rs.getFloat("price");
int sales = rs.getInt("sales");
int total = rs.getInt("total");
System.out.println(coffeeName + "\t\t" + supplierID + "\t" + price +
"\t" + sales + "\t" + total);
}
}catch(SQLException e){
System.out.println("查询 coffees 表失败!");
e.printStackTrace();
}
}
}
在上面的代码中,Statement 对象的 executeQuery 方法会执行参数所代表的 SQL 语句,并返回一个
ResultSet 对象。ResultSet 对象是一个结果集,可由任何实现了 Statement 接口的对象来创建,包括
PreparedStatement,CallableStatement 和 RowSet。
对 ResultSet 中的数据的访问,需要通过一个 cursor。它是一个指向 ResultSet 中一行数据的一个指
针。一开始时,这个指针指向第一行之前。通过重复地调用 ResultSet.next 方法,可以迭代 ResultSet 中
12
所有的数据。rs.getXXXX()方法可以用来获取各字段的值。该方法的参数可以是列名、列的索引或列的
别名。列的索引更有效率,从 1 开始。为了获得最大的兼容性,结果应该从左向右的顺序读取,并且每
列只能读取一次。
有的时候,我们需要根据指定的商品名称查询该商品的详细信息。比如,根据咖啡的名称查询其详
细信息。我们可以在 CoffeesDAO 类中实现一个 findByName 方法,它根据参数指定的咖啡名称来查找
相应的商品信息,并返回这些信息。实现代码如下所示:
package com.citymango.dao;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class CoffeesDAO{
//查找某个指定名称咖啡的信息
public CoffeeBean findByName(String coffeeName) {
CoffeeBean coffee = null;    //一个 JavaBean 形式的值对象,用来封装查找到的咖啡记录信息
String queryStr = "select cof_name,sup_id,price,sales,total from coffees where cof_name='" 
+ coffeeName + "'";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(queryStr) //ResultSet:结果集,保存返回的查询记录集合
) {
if (rs.next()) {
coffee = new CoffeeBean();
coffee.setCoffeeName(rs.getString("cof_name"));
coffee.setSupplierId(rs.getInt("sup_id"));
coffee.setPrice(rs.getFloat("price"));
coffee.setSales(rs.getInt("sales"));
coffee.setTotal(rs.getInt("total"));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
return coffee;
}
//查看 coffees 表中的记录数据
public void viewTable(){
//……
}
}
在上面的代码中,使用了一个 JavaBean 形式的值对象 CoffeeBean,用它来封装查询到的咖啡信息,
·13·
并将其返回。CoffeeBean.java 的代码实现如下:
package com.citymango.beans;
public class CoffeeBean {
private String coffeeName;
private Integer supplierId;
private Float price;
private Integer sales;
private Integer total;
public CoffeeBean(){}
public CoffeeBean(String coffeeName, Integer supplierId, Float price, Integer sales, Integer total) {
this.coffeeName = coffeeName;
this.supplierId = supplierId;
this.price = price;
this.sales = sales;
this.total = total;
}
public String getCoffeeName() {
return coffeeName;
}
public void setCoffeeName(String coffeeName) {
this.coffeeName = coffeeName;
}
public Integer getSupplierId() {
return supplierId;
}
public void setSupplierId(Integer supplierId) {
this.supplierId = supplierId;
}
public Float getPrice() {
return price;
}
public void setPrice(Float price) {
this.price = price;
}
public Integer getSales() {
return sales;
}
public void setSales(Integer sales) {
this.sales = sales; 
14
}
public Integer getTotal() {
return total;
}
public void setTotal(Integer total) {
this.total = total;
}    
}
如果想要得到所有(或满足条件的)多条咖啡记录信息,那么则需要另外实现一个方法,查询并返
回一个对象集合。这里我们使用动态数组来保存查询到的记录的集合。实现代码如下所示:
package com.citymango.dao;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class CoffeesDAO{
//查找所有的咖啡信息
public List<CoffeeBean> findCoffees(){
List<CoffeeBean> coffees = new ArrayList<>();    //jdk 7 中的菱形语法-泛型推断
//List<CoffeeBean> coffees = new ArrayList<CoffeeBean>();    //jdk 6 中使用的语法
String queryStr = "select cof_name,sup_id,price,sales,total from coffees";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(queryStr) //ResultSet:结果集,保存返回的查询记录集合
) {
while (rs.next()) {
CoffeeBean coffee = new CoffeeBean();
coffee.setCoffeeName(rs.getString("cof_name"));
coffee.setSupplierId(rs.getInt("sup_id"));
coffee.setPrice(rs.getFloat("price"));
coffee.setSales(rs.getInt("sales"));
coffee.setTotal(rs.getInt("total"));
coffees.add(coffee);
}
} catch (SQLException ex) {
ex.printStackTrace();
}
return coffees;
}
·15·
//查找某个指定名称咖啡的信息
public CoffeeBean findByName(String coffeeName) {
CoffeeBean coffee = null;    //一个 JavaBean 形式的值对象,用来封装查找到的咖啡记录信息
//……
return coffee;
}
//查看 coffees 表中的记录数据
public void viewTable(){
//……
}
}
编写一个测试程序,测试上述 DAO 类中实现的方法。测试程序主方法中的代码部分如下所示:
public static void main(String[] args) {
CoffeesDAO coffeesDAO = new CoffeesDAO();
CoffeeBean coffee = coffeesDAO.findByName("Colombian");  //测试 findByName 方法
if(coffee == null){
System.out.println("没有找到你需要的信息");
}else{
System.out.println(coffee.getCoffeeName() + "\t" 
+ coffee.getSupplierId() + "\t"
+ coffee.getPrice() + "\t" 
+ coffee.getSales() + "\t" 
+ coffee.getTotal());
}
List<CoffeeBean> coffees = coffeesDAO.findCoffees();    //测试 findCoffees 方法
if(coffees.size() == 0){
System.out.println("对不起,暂时没有咖啡信息");
}else{
for(CoffeeBean coffee : coffees){
System.out.println(coffee.getCoffeeName() + "\t" 
+ coffee.getSupplierId() + "\t"
+ coffee.getPrice() + "\t" 
+ coffee.getSales() + "\t" 
+ coffee.getTotal());
}
}
}
23.3.4   向数据表中添加新记录
想要对数据库进行增加数据的操作,可以通过 Statement 的 executeUpdate() 方法,如果在
executeUpdate()方法中的 sql 语句为 insert 语句,即可向数据库中增加数据。下面在 CoffeesDAO 类中增
加一个向 Coffees 表添加新记录的方法:
public void add(String coffeeName, int supplierId, float price, int sales, int total) { 
16
String updateStr = "insert into coffees values('" + coffeeName + "',"
+ supplierId + ","
+ price + ","
+ sales + ","
+ total + ")";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement()) {
int result = stmt.executeUpdate(updateStr);    //注意,这里是执行 executeUpdate 方法
if (result == 1) {
System.out.println("新咖啡信息添加成功");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
其中 Statement 对象的 executeUpdate 方法,返回的是执行 SQL 语句后在数据表中影响的行数。我
们也可以使用上一小节定义的值对象 CoffeeBean,将要添加的新记录的数据封 CoffeeBean 中,然后作为
参数传递给 add 方法。下面是重载后的 add 方法,与上面的 add 方法完成同样的添加新记录的功能。
public void add(CoffeeBean coffee) {
String updateStr = "insert into coffees values('" + coffee.getCoffeeName() + "',"
+ coffee.getSupplierId() + ","
+ coffee.getPrice() + ","
+ coffee.getSales() + ","
+ coffee.getTotal() + ")";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement()) {
int result = stmt.executeUpdate(updateStr);    //注意,这里是执行 executeUpdate 方法
if (result == 1) {
System.out.println("新咖啡信息添加成功");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
编写一个测试程序,测试上面的方法。测试的部分代码如下:
CoffeesDAO coffeesDAO = new CoffeesDAO();
System.out.println("添加新记录前:");
coffeesDAO.viewTable();
coffeesDAO.add("China Tea",49,88.80f,0,0);  //添加新记录
System.out.println("添加新记录后:");
coffeesDAO.viewTable();      
CoffeeBean coffee = new CoffeeBean("China Tea2",49,58.80f,0,0);
coffeesDAO.add(coffee);          //添加新记录
System.out.println("再次添加新记录后:"); 
·17·
coffeesDAO.viewTable();    
23.3.5   修改数据表中指定记录的信息
想要对数据库进行修改数据的操作,可以通过 Statement 的 executeUpdate() 方法,如果在
executeUpdate()方法中的 sql 语句为 update 语句,即可修改数据库中指定的数据。
下面在 CoffeesDAO 类中增加一个修改 Coffees 表中指定记录的方法:
//修改咖啡表中某个咖啡的信息
public void update(CoffeeBean coffee) {
String updateStr = "update coffees set sup_id=" + coffee.getSupplierId()
+ ",price=" + coffee.getPrice()
+ ",sales=" + coffee.getSales()
+ ",total=" + coffee.getTotal()
+ " where cof_name='" + coffee.getCoffeeName() + "'";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement()) {
int result = stmt.executeUpdate(updateStr);
if (result > 0) {
System.out.println("更新成功");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
编写一个测试程序,测试上面的方法。测试的部分代码如下:
CoffeesDAO coffeesDAO = new CoffeesDAO();
System.out.println("修改记录前:");
coffeesDAO.viewTable();
CoffeeBean newCoffee = new CoffeeBean("China Tea2",101,98.90f,0,0);   //代表要修改的新的信息
coffeesDAO.update(newCoffee);
System.out.println("修改记录后:");
coffeesDAO.viewTable();
23.3.6   删除数据表中指定的记录
想要对数据库进行增加、修改或删除数据的操作,可以通过 Statement 的 executeUpdate()方法,如
果在 executeUpdate()方法中的 sql 语句为 insert 语句,即可向数据库中增加数据。
下面在 CoffeesDAO 类中增加一个删除 Coffees 表指定记录的方法:
//删除某个咖啡的信息
public void remove(String coffeeName) {
String updateStr = "delete from coffees where cof_name='" + coffeeName + "'";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement()) {
int result = stmt.executeUpdate(updateStr); 
18
if (result > 0) {
System.out.println("删除成功");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
编写一个测试程序,测试上面的方法。测试的部分代码如下:
CoffeesDAO coffeesDAO = new CoffeesDAO();
System.out.println("删除记录前:");
coffeesDAO.viewTable();
coffeesDAO.remove("China Tea");  //删除指定的名称的咖啡记录
System.out.println("删除记录后:");
coffeesDAO.viewTable();
23.3.7   ResultSetMetaData 对象
ResultSet 有一个方法 getMetaData,它返回一个代表结果集元数据的 java.sql.ResultSetMetaData 对象。
对过 ResultSetMetaData 对象所提供的方法,可以获得结果集的详细信息。
下面向 CoffeesDAO 中增加一个重载的 viewTable 方法,它是一个通用的查看表数据的方法,要查
询的数据表由传进来的参数指定。
public void viewTable(String tableName) {
String queryStr = "select * from " + tableName;
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(queryStr);    //返回结果集对象
ResultSetMetaData rsmd = rs.getMetaData();        //获得结果集元数据(对结果集结构的描述)
int numberOfColumns = rsmd.getColumnCount();     //由 rsmd 可获得返回结果集中记录的列数
for (int index = 1; index <= numberOfColumns; index++) {//遍历每一列,获得列名
String columnName = rsmd.getColumnName(index);
System.out.print(columnName + "\t");
}
System.out.println();
while (rs.next()) { //遍历结果集每一行
for (int index = 1; index <= numberOfColumns; index++) {//遍历每一列,获得列值
//String value = rs.getString(index);    //通过列的索引获取列值
String value = rs.getString(rsmd.getColumnName(index));//通过列名获取列值
System.out.print(value + "\t");
}
System.out.println(); 
·19·
}
} catch (SQLException e) {
e.printStackTrace();
}
}
23.3.8   可滚动的和可更新的 ResultSet
在前面的示例中,通过查询所返回的 ResultSet 对象,只能单向遍历。实现上,ResultSet 的类型有
三种:
  ResultSet.TYPE_FORWARD_ONLY:只能向前单向移动
  ResultSet.TYPE_SCROLL_INSENSITIVE:结果集指针可滚动的,但是对数据变化是不敏感的
  ResultSet.TYPE_SCROLL_SENSITIVE:结果集指针是可滚动的,并且对数据变化是敏感的
同时,ResultSet 也支持不同级别的更新功能:
  ResultSet.CONCUR_READ_ONLY:只读的结果集
  ResultSet_UPDATABLE:可更新的结果集
但是,并不是所有的数据库都支持以上的 ResultSet 的各种类型。我们可以通过代码测试目标数据
库所支持的 ResultSet 类型:
//……
DatabaseMetaData dmd = con.getMetaData();    //获得数据库的元数据
//测试是否支持指定的 ResultSet 类型
System.out.println("支持的 ResultSet 类型");
System.out.println(dmd.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY));
System.out.println(dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE));
System.out.println(dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE));
//测试是否支持指定的并发类型与 ResultSet 类型联合使用
System.out.println("支持的 ResultSet 并发类型");
System.out.println(dmd.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY,                
ResultSet.CONCUR_READ_ONLY));
System.out.println(dmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, 
ResultSet.CONCUR_READ_ONLY));
System.out.println(dmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE,        
ResultSet.CONCUR_READ_ONLY));
System.out.println(dmd.supportsResultSetConcurrency(ResultSet.TYPE_FORWARD_ONLY, 
ResultSet.CONCUR_UPDATABLE));
System.out.println(dmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, 
ResultSet.CONCUR_UPDATABLE));
System.out.println(dmd.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_SENSITIVE, 
ResultSet.CONCUR_UPDATABLE));
//……
通过可滚动的 ResultSet,我们可以很方便地前后移动指针,或者定位到指定的位置。在 CoffeesDAO
中,新增加一个 scrollViewTable 方法,使用可滚动的 ResultSet 查看指定位置处的记录。
//通过可滚动的结果集查看数据 
20
public void scrollViewTable(){
String queryStr = "select cof_name,sup_id,price,sales,total from coffees";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, 
ResultSet.CONCUR_UPDATABLE); 
ResultSet scrollrs = stmt.executeQuery(queryStr)) {
System.out.println("第一条记录");
scrollrs.first();
System.out.println(scrollrs.getString("cof_name") + "\t" 
+ scrollrs.getInt("sup_id") + "\t"
+ scrollrs.getFloat("price") + "\t" 
+ scrollrs.getInt("sales") + "\t" 
+ scrollrs.getInt("total"));
System.out.println("最后一条记录");
scrollrs.last();
System.out.println(scrollrs.getString("cof_name") + "\t" 
+ scrollrs.getInt("sup_id") + "\t"
+ scrollrs.getFloat("price") + "\t" 
+ scrollrs.getInt("sales") + "\t" 
+ scrollrs.getInt("total"));
System.out.println("倒数第二条记录");
scrollrs.previous();
System.out.println(scrollrs.getString("cof_name") + "\t" 
+ scrollrs.getInt("sup_id") + "\t"
+ scrollrs.getFloat("price") + "\t" 
+ scrollrs.getInt("sales") + "\t" 
+ scrollrs.getInt("total"));
System.out.println("第二条记录");
scrollrs.absolute(2);
System.out.println(scrollrs.getString("cof_name") + "\t" 
+ scrollrs.getInt("sup_id") + "\t"
+ scrollrs.getFloat("price") + "\t" 
+ scrollrs.getInt("sales") + "\t" 
+ scrollrs.getInt("total"));            
System.out.println("删除完毕!");
} catch (SQLException e) {
System.out.println("删除失败!");
e.printStackTrace();
}
}
通过可更新的 ResultSet,我们可以修改数据表中的数据。在 CoffeesDAO 中,增加一个修改咖啡价
格的方法。这个方法会按指定的比例调整咖啡的价格,但是如果调整后的价格超过了最高限价,则取消
调价。方法实现代码如下:
//修改咖啡的价格(通过 ResultSet 对象) 
·21·
public void modifyPrice(float percentage){
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, 
ResultSet.CONCUR_UPDATABLE)) {
ResultSet uprs = stmt.executeQuery("select cof_name,sup_id,price,sales,total from coffees");
while (uprs.next()) {
float f = uprs.getFloat(3);
System.out.println("price:" + f);
uprs.updateFloat(3, f * percentage); //更新指定列的数据
if(f>30){
uprs.cancelRowUpdates();     //可以撤销对某行的更新。必须在调用 updateRow()之前调用
}
uprs.updateRow();        //更新当前行
}
System.out.println("价格更新完毕!");
} catch (SQLException e) {
System.out.println("修改价格失败!");
e.printStackTrace();
}
}
在 CoffeesDAO 中,分别增加一个插入新咖啡信息的方法和删除指定咖啡信息的方法。方法实现代
码如下所示:
//插入一条咖啡记录(通过 ResultSet 对象)
public void insertRow(String coffeeName, int supplierID,float price, int sales, int total){
String queryStr = "select cof_name,sup_id,price,sales,total from coffees";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, 
ResultSet.CONCUR_UPDATABLE);
ResultSet uprs = stmt.executeQuery(queryStr)) {
uprs.moveToInsertRow();  //将指针移动到插入行
uprs.updateString("cof_name", coffeeName);
uprs.updateInt("sup_id", supplierID);
uprs.updateFloat("price", price);
uprs.updateInt("sales", sales);
uprs.updateInt("total", total);
uprs.insertRow();            //执行插入操作
uprs.beforeFirst();          //将指针移动到第一行记录之前
System.out.println("新记录插入完毕!");
} catch (SQLException e) {
System.out.println("新记录插入失败!");
e.printStackTrace();
}
}
//删除一条记录(通过 ResultSet 对象)
public void deleteRow(String coffeeName){ 
22
String queryStr = "select cof_name,sup_id,price,sales,total from coffees";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, 
ResultSet.CONCUR_UPDATABLE); 
ResultSet uprs = stmt.executeQuery(queryStr)) {
while(uprs.next()){
String coffee_name = uprs.getString("cof_name");
if(coffee_name.equals(coffeeName)){
uprs.deleteRow();
break;
}
}
System.out.println("删除完毕!");
} catch (SQLException e) {
System.out.println("删除失败!");
e.printStackTrace();
}
}
编写测试代码,测试以上方法。测试部分代码如下所示:
System.out.println("-------------调价前:-------------");
coffeesDAO.viewTable();
coffeesDAO.modifyPrice("China Tea2", 0.1f);      //涨价 10%
System.out.println("-------------调价后:-------------");
coffeesDAO.viewTable();
System.out.println("-------------插入新记录后:-------------");
coffeesDAO.insertRow("aaa", 49, 12.34f, 0, 0);
coffeesDAO.viewTable();
System.out.println("-------------删除刚插入的记录后:--------------------");
coffeesDAO.deleteRow("aaa");
coffeesDAO.viewTable();
System.out.println("-------------查看结果集中指定位置处的记录-------------");
coffeesDAO.scrollViewTable();
23.3.9   事务
在 Connection 对象上可以使用事务(Transaction)。用户可以把一系列对数据库的 DML 操作作为
一个整体来对待,这一个整体就是一个事务。
例如,当用户打算将一笔帐转移到另一笔帐上时,用户从一笔帐上减去一部分,则必须在另外一笔
帐上增加这部分。如果任何一次增减动作失败,都将使用户的帐单不再平衡。如果将这些增减动作放在
一起处理,可以确保所有的增减操作或者全部执行,或者全部不执行,如果所有的改变都正确,则使用
commit 将这些改变动作全部存入数据库,否则就使用 rollback 取消所有的改变动作,而这时数据库中的
数据和执行事务前的数据是相同的。
默认情况下 JDBC 新创建的连接为自动提交模式,即每一个 SQL 语句是独立的事务,执行完成 SQL
·23·
语句就立即提交了。Connection 对象有一个 setAutoCommit(false)方法,使用它可以关闭自动提交模式。
这让用户能够自己组合 SQL 语句,正常执行完成所有的 SQL 语句后再调用 commit()提交事务。若其中
某个 SQL 语句执行失败,则必须调用 rollback()撤销这次事务。
例如,我们要向 Coffees 表中新插入两个咖啡记录信息,要求这两条新记录要么同时插入成功,要
么都不成功。这需要将两个插入操作放在一个事务当中。在下面的示例代码中,演示了 JDBC 中事务的
管理操作。
import com.citymango.util.JDBCUtilities;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.sql.Statement;
public class JDBCTransaction {
public static void main(String[] args) throws SQLException {
CoffeesDAO coffeesDAO = new CoffeesDAO();    //创建一个 DAO 对象
Connection con = JDBCUtilities.getConnection();      //获得数据库连接
String updateStr1 = "insert into coffees values('a3a3a3',49,23.56,0,0)";  //DML 语句 1
String updateStr2 = "insert into coffees values('a2a2a2',49,33.56,0,0)";  //DML 语句 2
System.out.println("批量插入之前");
coffeesDAO.viewTable("coffees");
con.setAutoCommit(false);              //禁用自动提交,开始一个事务
try(Statement stmt1 = con.createStatement();
Statement stmt2 = con.createStatement()) {
stmt1.executeUpdate(updateStr1);  //执行插入操作
Savepoint sp = con.setSavepoint();    //设置存储点
stmt2.executeUpdate(updateStr2);  //执行另一个插入操作
con.rollback(sp);          //回滚到指定事务存储点
con.commit();          //提交事务
} catch (SQLException ex) {
try {
con.rollback();        //如果操作过程中发生 SQL 异常,则回滚整个事务
} catch (SQLException ex1) {
ex1.printStackTrace();
}
ex.printStackTrace();
}finally{
if(con != null){
try {
con.setAutoCommit(true);  //重置连接对象为自动提交模式
con.close();        //关闭连接 
24
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
System.out.println("批量插入之后(应用事务)");
coffeesDAO.viewTable("coffees");    //查看
}
}
一个 JDBC 驱动可能并不支持所有的事务隔离级别。如果一个驱动不支持在 setTransactionIsolation
方法中指定的事务隔离级别,那么驱动会以一个更高的、更严格的事务隔离级别来代替。如果一个驱动
不能代之以更高的事务级别,则会抛出一个 SQLException 异常。
可以使用方法 DatabaseMetaData.supportsTransactionIsolationLevel 来判断所使用的数据库驱动是否
支持一个给定的事务隔离级别。
在 JDBC 事务管理中,我们还可以通过 Connection 对象提供的 getTransactionIsolation 方法,获得当
前连接会话的事务隔离级别;使用 Connection 对象提供的 setTransactionIsolation 方法,来设置会话的事
务隔离级别。
请看下面的示例代码:
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.sql.Savepoint;
import com.citymango.util.JDBCUtilities;
public class TransactionDemo {
public static void main(String[] args) {        
//获得与数据库的连接
Connection con = JDBCUtilities.getConnection();
if (con == null) {
return;
}
//测试数据库的默认事务隔离级别
try {
int isolationLevel = con.getTransactionIsolation();      //获取当前默认的事务隔离级别
System.out.print("当前数据库默认事务隔离级别:");
printTransactionIsolationLevel(isolationLevel);
con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);//设置新的事务隔离级别
isolationLevel = con.getTransactionIsolation();
System.out.print("现在数据库默认事务隔离级别是:");
printTransactionIsolationLevel(isolationLevel);
·25·
//判断当前 jdbc 驱动所支持的事务隔离级别有哪些
System.out.println("当前 JDBC 支持的事务隔离级别有:");
DatabaseMetaData dmd = con.getMetaData();
for (int i = Connection.TRANSACTION_NONE;
i <= Connection.TRANSACTION_SERIALIZABLE; i++) {
boolean isSupported = dmd.supportsTransactionIsolationLevel(isolationLevel);
System.out.println(i + ":" + isSupported);
}
//默认情况下,事务是自动提交的。在需要事务的时候,需要不自动提交
con.setAutoCommit(false);
con.commit();
con.rollback();
Savepoint sp = con.setSavepoint();           //在当前事务中设置一个未命名的存储点
Savepoint sp2 = con.setSavepoint("修改数量");  //在当前事务中设置一个命名的存储点
con.releaseSavepoint(sp);    //释放一个存储点
con.rollback(sp2);
con.setAutoCommit(true);    //任何存储点,在执行 commit 或 rollback 以后,都会自动地被释放
} catch (SQLException ex) {
System.out.println("无法获取数据库事务隔离级别");
}
}
//输出数据库事务隔离级别的方法
public static void printTransactionIsolationLevel(int isolationLevel) {
switch (isolationLevel) {
case Connection.TRANSACTION_NONE:
System.out.println("transaction_none");
break;
case Connection.TRANSACTION_READ_UNCOMMITTED:
System.out.println("transaction_uncommitted");
break;
case Connection.TRANSACTION_READ_COMMITTED:
System.out.println("transaction_committed");
break;
case Connection.TRANSACTION_REPEATABLE_READ:
System.out.println("transaction_repeatable_read");
break;
case Connection.TRANSACTION_SERIALIZABLE:
System.out.println("transaction_serializable");
break;
default:
System.out.println("未知的事务隔离级别");
}
}
}
在 Connection 类中,定义了许多静态常量,分别代表不同的事务隔离级别。有如下几种常量:
  Connection.TRANSACTION_NONE:整数值 0。不支持事务。 
26
  Connection.TRANSACTION_READ_UNCOMMITTED:整数值 1。未提交读。
  Connection.TRANSACTION_READ_COMMITTED:整数值 2。提交读。           
  Connection.TRANSACTION_REPEATABLE_READ:整数值 4。可重复读。
  Connection.TRANSACTION_SERIALIZABLE:整数值 8。可序列化。
23.3.10   批处理
Statement 支持批处理。所谓批处理,指的是批量更新一批数据。Statement 对象的 addBatch 方法用
来将要执行的 SQL 语句加入到批处理中。Statement 对象的 executeUpdate 方法用来执行批处理。
在我们的咖啡连锁店管理系统中,假设要批量的插入一批新的咖啡信息。这可以通过在一个事务中
应用批处理来完成。在 CoffeesDAO 数据访问对象类中,再增加一个新的方法 addCoffees。实现代码如
下所示:
//批量更新(应用事务)
public void addCoffees(List<CoffeeBean> coffees) throws SQLException{
Connection con = JDBCUtilities.getConnection();      //获得数据库连接
con.setAutoCommit(false);                //禁用自动提交
try(Statement stmt = con.createStatement()) {
for(CoffeeBean coffee : coffees){
String updateStr = "insert into coffees values('" + coffee.getCoffeeName() + "',"
+ coffee.getSupplierId() + ","
+ coffee.getPrice() + ","
+ coffee.getSales() + ","
+ coffee.getTotal() + ")";
stmt.addBatch(updateStr);        //将 SQL 语句加入批处理
}   
int[] results = stmt.executeBatch();  //执行批处理语句
con.commit();                  //提交事务
} catch (SQLException ex) {
con.rollback();          //如果出现 SQL 异常,则回滚整个批处理事务
ex.printStackTrace();
}finally{
if(con != null){
con.setAutoCommit(true);       //恢复自动提交
con.close();
}
}
}
说明:批处理必须放到一个事务当中去。 
·27·
23.3.11   SQL 注入攻击
使用 Statement 对象时,需要先构造要执行的 SQL 语句,这就有可能被 SQL 注入攻击所利用,造成
安全隐患。SQL 注入攻击的原理如下。
首先,在当前数据库方案下,创建一个新的账户信息表 account,创建脚本代码如下:
create table account(
username varchar(20) primary key,
password varchar(20)
);
然后插入一条样本数据,代表已经存在的一个管理员信息:
insert into account values('zhangsan','123456');
如果一个用户要登录的话,通常他必须通过一个界面,输入其账号和密码,然后程序会到数据表
account 表中查询有没有与用户输入一致的账号和密码:
select * from account where username='zhangsan' and password='123456';
如果用户输入的账号和密码正确,则此查询语句的 where 子句条件返回值为真,从而判断该用户为
合法用户,允许登录。如果用户输入的账号或/和密码错误,则此查询语句的 where 子句条件返回值为假,
则拒绝该用户登录。
但是,如果我们用户输入的密码为如下内容:
adsafdf' or 1='1
那么,构造出来的查询语句将会象下面这样:
select * from account where username='anyuser' and password='adsafdf' or 1='1';
这时,where 子句的表达式值恒为真。所以可以在不知道账号和密码的情况下,也可以欺骗性地登
录成功。
下面通过一个示例程序演示 SQL 注入攻击的过程。通过上述讲到的方式,用户首先欺骗性地登录
成功,然后利用合法的身份,创建一个新的用户(或许还带有一定权限),然后就可以以新用户的身份
登录系统。
import com.citymango.util.JDBCUtilities;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;
public class SQLInjectAttack {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
boolean isLogin = false;         //登录状态
//欺骗登录过程
System.out.print("请输入账号:");
String username = sc.nextLine();
System.out.print("请输入密码:");
String password = sc.nextLine();
28
String queryStr = "select * from account where username='" + username + 
"' and password='" + password + "'";
String updateStr = "insert into account values('xlw','xlw')";
//登录
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(queryStr)) {
if (rs.next()) {
System.out.println("登录成功!");
isLogin = true;
} else {
System.out.println("无效的用户名或密码!");
return;
}
} catch (SQLException ex) {
ex.printStackTrace();
}
//如果登录成功,插入一条新的管理员信息
if (isLogin) {
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement()) {
int result = stmt.executeUpdate(updateStr);
if(result>0){
System.out.println("创建新管理员成功。");
}else{
return;
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
//以新管理员身份登录
String queryStr2 = "select * from account where username='xlw' and password='xlw'";
try (Connection con = JDBCUtilities.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(queryStr2)) {
if (rs.next()) {
System.out.println("你的系统有很大的漏洞啊!");

} catch (SQLException ex) {
ex.printStackTrace();
}
}

·29·
23.3.12  使用 PreparedStatement 访问数据库
从前面几节的内容可以看到,使用 java.sql.Statement 语句存在着下面几方便的不足:
  SQL 语句构造复杂,容易出错;
  不够安全;
  执行效率低。
要克服以上不足,我们可以使用 java.sql.PreparedStatement 接口。PreparedStatement 接口继承
Statement 接口,并与之在两方面的有所不同。
  PreparedStatement 实例包含已编译的 SQL 语句。由于 PreparedStatement 对象已预编译过,所以
其执行速度要快于 Statement 对象。因此,多次执行的 SQL 语句经常创建 PreparedStatement 对
象,以提高效率。
  包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 IN 参数。IN 参数的值在 SQL
语句创建时未被指定。相反,该语句为每个 IN 参数保留一个问号("?")作为占位符。每个问
号的值必须在语句执行之前,通过适当的 SetXXX 方法来提供。
接下来,我们使用 java.sql.PreparedStatement 接口重构 CoffeesDAO 类。重构后的 CoffeesDAO.java
类的实现代码如下所示:
import com. citymango.beans.CoffeeBean;
import com. citymango.util.JDBCUtilities;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Coffees2DAO {
public static void main(String[] args) {
Coffees2DAO coffees2DAO = new Coffees2DAO();
//构建一个新的咖啡信息
CoffeeBean coffeeBean = new CoffeeBean("Chinese Tea 01", 49, 45.8f, 0, 0);
coffees2DAO.add(coffeeBean);
CoffeeBean coffee = coffees2DAO.find(coffeeBean.getCoffeeName());
if (coffee != null) {
System.out.println(coffee.getCoffeeName() + "\t"
+ coffee.getSupplierId() + "\t"
+ coffee.getPrice() + "\t"
+ coffee.getSales() + "\t"
+ coffee.getTotal());
} else {
System.out.println("没有您要查询的商品信息");
}
//添加一批新的咖啡信息 
30
List<CoffeeBean> coffees = new ArrayList<>();
CoffeeBean coffeeBean2 = new CoffeeBean("Chinese Tea 02", 49, 45.8f, 0, 0);
CoffeeBean coffeeBean3 = new CoffeeBean("Chinese Tea 03", 49, 46.8f, 0, 0);
CoffeeBean coffeeBean4 = new CoffeeBean("Chinese Tea 04", 49, 47.8f, 0, 0);
CoffeeBean coffeeBean5 = new CoffeeBean("Chinese Tea 05", 49, 48.8f, 0, 0);
coffees.add(coffeeBean2);
coffees.add(coffeeBean3);
coffees.add(coffeeBean4);
coffees.add(coffeeBean5);
try {
coffees2DAO.add(coffees);
} catch (SQLException ex) {
System.out.println("添加一批数据时出现错误");
}
//查询出所有的信息
System.out.println("添加一批新数据后:");
coffees2DAO.viewTable();
//修改其中指定的一条信息
CoffeeBean newCoffeeBean2 = new CoffeeBean("Chinese Tea 02", 49, 145.8f, 0, 0);
coffees2DAO.update(newCoffeeBean2);
System.out.println("修改 Chinese Tea 02 后:");
coffees2DAO.viewTable();
//删除其中指定的一条信息
coffees2DAO.remove("Chinese Tea 02");
System.out.println("删除 Chinese Tea 02 后:");
coffees2DAO.viewTable();
//修改周销售记录
Map<String,Integer> salesPerWeek = new HashMap<>();
salesPerWeek.put("Chinese Tea 03", 5);
salesPerWeek.put("Chinese Tea 04", 15);
salesPerWeek.put("Chinese Tea 05", 25);
try {
coffees2DAO.updateWeekSales(salesPerWeek);
} catch (SQLException ex) {
System.out.println("修改周销售记录时出现错误");
}
System.out.println("修改周销售记录后:");
coffees2DAO.viewTable();
}
//显示表中所有信息
public void viewTable(){
String queryStr = "select * from coffees";
try (Connection con = JDBCUtilities.getConnection(); 
·31·
PreparedStatement pstmt = con.prepareStatement(queryStr);
ResultSet rs = pstmt.executeQuery()) {
while(rs.next()){
System.out.println(rs.getString(1) + "\t"
+ rs.getInt(2) + "\t"
+ rs.getFloat(3) + "\t"
+ rs.getInt(4) + "\t"
+ rs.getInt(5));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
//添加新记录
public void add(CoffeeBean coffee) {
String updateStr = "insert into coffees(cof_name,sup_id,price,sales,total) values(?,?,?,?,?)";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(updateStr)) {
pstmt.setString(1, coffee.getCoffeeName());
pstmt.setInt(2, coffee.getSupplierId());
pstmt.setFloat(3, coffee.getPrice());
pstmt.setInt(4, coffee.getSales());
pstmt.setInt(5, coffee.getTotal());
int result = pstmt.executeUpdate(); //执行
if (result > 0) {
System.out.println("新记录添加成功");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
//添加一批新记录
public void add(List<CoffeeBean> coffees) throws SQLException {
String updateStr = "insert into coffees values(?,?,?,?,?)";
Connection con = JDBCUtilities.getConnection();
con.setAutoCommit(false);        //禁用自动提交
try (PreparedStatement pstmt = con.prepareStatement(updateStr)) {
for (CoffeeBean coffee : coffees) {
pstmt.setString(1, coffee.getCoffeeName());
pstmt.setInt(2, coffee.getSupplierId());
pstmt.setFloat(3, coffee.getPrice());
pstmt.setInt(4, coffee.getSales());
pstmt.setInt(5, coffee.getTotal());
pstmt.addBatch();        //添加到批处理当中 
32
}
pstmt.executeBatch();        //执行批处理
con.commit();                //提交事务
} catch (SQLException ex) {
con.rollback();
ex.printStackTrace();
} finally {
if (con != null) {
con.close();
}
}
}
//修改咖啡的周销售记录
public void updateWeekSales(Map<String, Integer> salesPerWeek) throws SQLException {
String updateStr = "update coffees set sales=?,total=total+? where cof_name=?";
Connection con = JDBCUtilities.getConnection();
con.setAutoCommit(false);
try (PreparedStatement pstmt = con.prepareStatement(updateStr)) {
for(Map.Entry<String,Integer> coffeeItem : salesPerWeek.entrySet()){
String coffeeName = coffeeItem.getKey();
int sales = coffeeItem.getValue();
pstmt.setInt(1, sales);
pstmt.setInt(2, sales);
pstmt.setString(3, coffeeName);
pstmt.addBatch();
}
/*或者使用以下方式提取 Map 中的键和值
for (String coffeeName : salesPerWeek.keySet()) {
int sales = salesPerWeek.get(coffeeName);        //根据 key 获取对应的 value
pstmt.setInt(1, sales);
pstmt.setInt(2, sales);
pstmt.setString(3, coffeeName);
pstmt.addBatch();
}
*/
pstmt.executeBatch();
con.commit();
} catch (SQLException ex) {
con.rollback();
ex.printStackTrace();
} finally {
if (con != null) {
con.close();

·33·
}
}
//修改指定的记录(按主键)
public void update(CoffeeBean coffee) {
String updateStr = "update coffees set sup_id=?,price=?,sales=?,total=? where cof_name=?";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(updateStr)) {
pstmt.setInt(1, coffee.getSupplierId());
pstmt.setFloat(2, coffee.getPrice());
pstmt.setInt(3, coffee.getSales());
pstmt.setInt(4, coffee.getTotal());
pstmt.setString(5, coffee.getCoffeeName());
int result = pstmt.executeUpdate();
if (result > 0) {
System.out.println("修改成功");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
//删除指定的记录(按主键)
public void remove(String coffeeName) {
String updateStr = "delete from coffees where cof_name=?";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(updateStr)) {
pstmt.setString(1, coffeeName);
int result = pstmt.executeUpdate();
if (result > 0) {
System.out.println("删除成功");
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
//查找指定的记录(按主键)
public CoffeeBean find(String coffeeName) {
CoffeeBean coffee = null;
String queryStr = "select cof_name,sup_id,price,sales,total from coffees where cof_name=?";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(queryStr)) {
pstmt.setString(1, "Chinese Tea 01");
ResultSet rs = pstmt.executeQuery(); 
34
if (rs.next()) {
coffee = new CoffeeBean();
coffee.setCoffeeName(rs.getString("cof_name"));
coffee.setSupplierId(rs.getInt("sup_id"));
coffee.setPrice(rs.getFloat("price"));
coffee.setSales(rs.getInt("sales"));
coffee.setTotal(rs.getInt("total"));
}
} catch (SQLException ex) {
ex.printStackTrace();
}
return coffee;
}
//查找所有的记录
public List<CoffeeBean> findAll() {
List<CoffeeBean> coffees = new ArrayList<>();    //jdk 7
String queryStr = "select * from coffees";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(queryStr);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
CoffeeBean coffee = new CoffeeBean();
coffee.setCoffeeName(rs.getString("cof_name"));
coffee.setSupplierId(rs.getInt("sup_id"));
coffee.setPrice(rs.getFloat("price"));
coffee.setSales(rs.getInt("sales"));
coffee.setTotal(rs.getInt("total"));
coffees.add(coffee);
}
} catch (SQLException ex) {
ex.printStackTrace();
}
return coffees;
}
}
23.3.13   使用属性文件分离数据库连接配置信息
回顾我们之前编写的 java.citymango.util.JDBCUtilities.java 类,其实现代码如下:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException; 
·35·
import java.util.Properties;
public class JDBCUtilities {
private static final String ORACLE_DB_DRIVER = "oracle.jdbc.driver.OracleDriver";    
private static final String ORACLE_CON_URL = "jdbc:oracle:thin:@localhost:1521/orcl";
private static String USER = "xlw";
private static String PWD = "xlw";
//获得数据库连接的方法
public static Connection getConnection(){
Connection con = null;
try {
System.out.println("正在连接数据库......");
con = DriverManager.getConnection(ORACLE_URL, USER, PWD);
System.out.println("已经和数据库建立连接。");
} catch (SQLException ex) {
System.out.println("无法和数据库建立连接。");
ex.printStackTrace();
}
return con;
}
}
上面的代码实现存在着一些不足的地方。因为我们的程序可以会经常在不同的数据库平台移植,或
者数据库分配的用户账号和密码需要定期更新(出于安全方面的考虑),那么因为这些数据库配置和连
接信息是硬编码在程序中的,所以每次都必须重新修改源代码并重新编译和部署。
恰当的做法是,将可变的数据库配置和连接信息与相对不经常变动的代码分离开。将需要经常变动
的配置信息保存一个属性文件当中,程序从属性文件中加载这些配置信息。如果配置信息改动了,我们
只需要修改配置文件即可,而无需修改和重新编译源代码、重新部署程序。
接下来,我们重构数据库连接类,将数据库连接信息移到属性文件中,程序代码从属性文件中加载
所需的信息。请按以下步骤操作。
1)创建属性文件
在项目的源包下,创建一个属性文件 dbconfig.properties。将数据库连接信息配置到属性文件中。这
个文件在项目编译后,会被编译到与.class 文件相同的根目录下。
# Oracle
jdbc.driver = oracle.jdbc.driver.OracleDriver
jdbc.url = jdbc:oracle:thin:@localhost:1521/orcl
jdbc.user = xlw
jdbc.password = xlw
# MySQL
# jdbc.driver = com.mysql.jdbc.Driver
# jdbc.url = jdbc:mysql://localhost:3306/xuejava
# jdbc.user = oraclewdp
# jdbc.password = oraclewdp
# postgreSQL 
36
# jdbc.driver = org.postgresql.Driver
# jdbc.url = jdbc:postgresql://localhost:5432/testdb   
# jdbc.user = oraclewdp
# jdbc.password = oraclewdp
# SQL Server 2005
# jdbc.driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
# jdbc.url = jdbc:sqlserver://localhost:1433;DatabaseName=testdb    
# jdbc.user = sa
# jdbc.password =
上面的配置文件内容中,包含了几种主要数据库的配置信息。如果我们应用程序的数据库平台切换
的话,只需要针对特定的数据库注释或取消注释即可。其中“#”为属性文件中的注释符号。
2)创建属性信息加载类 DBConfig.java
根据类的单一职责原则,我们把加载属性文件 dbconfig.properties 中配置信息的功能封装到一个单独
的类 DBConfig.java 中。其实现代码如下所示:
package com.citymango.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class DBConfig {
private static final Properties props = new Properties();  //创建一个属性对象
static{        
try(InputStream input = DBConfig.class.getResourceAsStream("/dbconfig.properties")) { //加载属性文件
props.load(input);    //通过输入流,将属性文件中的内容加载到属性对象中
} catch (IOException ex) {
ex.printStackTrace();
}
}
//获得相应属性 key 所对应的 value 值
public static String getProperty(String propertyKey){
return props.getProperty(propertyKey);
}
}
3)重构 JDBCUtilities.java。其重构后的代码如下所示:
package com.citymango.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class JDBCUtilities{
private static String CON_URL = DBConfig.getProperty("jdbc.url");
private static String USER = DBConfig.getProperty("jdbc.user");
private static String PWD = DBConfig.getProperty("jdbc.password"); 
·37·
public static Connection getConnection(){
Connection con = null;
try {
con = DriverManager.getConnection(CON_URL, USER, PWD);
} catch (SQLException ex) {
ex.printStackTrace();
}
return con;
}
}
可以看出,重构后的 JDBCUtilities.java 已经不再硬编码数据库连接信息了。重新编译项目并测试,
会发现执行一切正常。
23.3.14   处理大数据对象 CLOB
对于 Blob、Clob 和 NClob 这些 Java 对象来说,有一个重要的特性是,不需要从数据库服务器中把
其所有数据都提取出来到客户端电脑上,就可以操纵它们。这些类型的一些具有代表性的实现,是带有
定位器(逻辑指针)指向数据库中这些实例所代表的对象。因为一个 BLOB、CLOB 或 NCLOB 对象可
能会非常大,使用定位器能使性能得到显著的提升。大对象类型对象将相应的对象的数据实现为它们代
表的流。
Blob、Clob 和 NClob 这些 Java 对象至少在它们被创建的事务中保持有效。这有可能潜在地导致一
个应用程序在长期运行的事务中耗尽资源。应用程序可以通过调用这些大对象的 free 方法来释放资源。
在下面的代码片段中,方法 Clob.free 被调用,以释放之前所创建的 Clob 对象所持有的资源。
Clob aClob = con.createClob();
int numWritten = aClob.setString(1,val);
//......
aClob.free();
在我们的咖啡连锁店管理系统数据表中,有一个 COFFEE_DESCRIPTIONS 表,其带有两个大数据
类型的字段,一个是 clob 类型,用来存储对某个咖啡信息的详细描述;另一个是 blob 类型,用来存储
某个咖啡的图片信息。在接下来的部分,我们要实现对 COFFEE_DESCRIPTIONS 表中大文本字段的存
写和大二进制字段的读写。
首先在项目的源包下面,创建一个名为 coffeedesc.txt 的文本文档,其内容代表要插入到数据表中 clob
列的文本:
咖啡是个好东西,但不宜多喝。
接下来实现一个 CoffeeDescriptionsDAO 类,用来完成对咖啡描述信息(大文本字段)的读写操作。
其实现代码如下所示:
import com.citymango.JDBCUtilities;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement; 
38
import java.sql.ResultSet;
import java.sql.SQLException;
public class CoffeeDescriptionsDAO{
//向表 COFFEE_DESCRIPTIONS 中添加一个 CLOB SQL 值。
public void addRowToCoffeeDescriptions(String coffeeName, String fileName){
String sql = "INSERT INTO COFFEE_DESCRIPTIONS(COF_NAME,COF_DESC) VALUES(?,?)";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql)) { 
//一行接一行地读取 fileName 指定的文件,并返回文件内容
//从与当前 class 文件同一目录下加载资源
InputStream input = this.getClass().getResourceAsStream("/" + fileName);
BufferedReader br = new BufferedReader(new InputStreamReader(input));
String nextLine = "";
StringBuilder sb = new StringBuilder();
while ((nextLine = br.readLine()) != null) {     
sb.append(nextLine);
}        
String content = sb.toString();           //  将内容转换为字符串
Clob myClob = con.createClob();                //创建一个 Clob Java 对象
myClob.setString(1, content);                   //设置 myClob 的内容
pstmt.setString(1, coffeeName);               //设置 SQL 语句参数
pstmt.setClob(2, myClob);                  //设置 SQL 语句参数
pstmt.executeUpdate();                   //执行插入操作
myClob.free();                              //释放资源
} catch (SQLException sqlex) {
sqlex.printStackTrace();
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.toString());
ex.printStackTrace();
}
}
//获取 COFFEE_DESCRIPTIONS 表中某一指定咖啡的描述信息(COF_DESC 列中存储的 CLOB SQL 值)
public String retrieveExcerpt(String coffeeName) {
String sql = "select COF_DESC from COFFEE_DESCRIPTIONS where COF_NAME = ?";
String description = null;
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setString(1, coffeeName);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
Clob myClob = rs.getClob(1);          //获取 Clob 列的值
System.out.println("获得的 Clob 的长度: " + myClob.length()); 
·39·
//获取 Clob 中的字符串
description = myClob.getSubString(1, (int)myClob.length());
myClob.free();       //释放资源
}             
} catch (SQLException sqlex) {
sqlex.printStackTrace();
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.toString());
ex.printStackTrace();

return description;
}    
//测试程序
public static void main(String[] args){
CoffeeDescriptionsDAO coffeeDescriptionsDAO = new CoffeeDescriptionsDAO ();
coffeeDescriptionsDAO.addRowToCoffeeDescriptions("Chinese Tea 10", "coffeedesc.txt");
System.out.println(lod.retrieveExcerpt("Chinese Tea 10"));
}
}
对上述代码进行优化,将读取文本文件内容的代码独立到一个单独的 readFile 方法中。优化重构后
的代码如下所示:
import com.citymango.JDBCUtilities;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CoffeeDescriptionsDAO{
//例:向表 COFFEE_DESCRIPTIONS 中添加一个 CLOB SQL 值。
public void addRowToCoffeeDescriptions(String coffeeName, String fileName){
String sql = "INSERT INTO COFFEE_DESCRIPTIONS(COF_NAME,COF_DESC) VALUES(?,?)";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql)) { 
//一行接一行地读取 fileName 指定的文件,并返回文件内容
String content = this.readFile("/" + fileName); 
Clob myClob = con.createClob();                //创建一个 Clob Java 对象
myClob.setString(1, content);                   //设置 myClob 的内容
40
pstmt.setString(1, coffeeName);               //设置 SQL 语句参数
pstmt.setClob(2, myClob);                  //设置 SQL 语句参数
pstmt.executeUpdate();                   //执行插入操作
myClob.free();                              //释放资源
} catch (SQLException sqlex) {
sqlex.printStackTrace();
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.toString());
ex.printStackTrace();
}
}
//获取 COFFEE_DESCRIPTIONS 表中某一指定咖啡的描述信息(COF_DESC 列中存储的 CLOB SQL 值)
public String retrieveExcerpt(String coffeeName) {
String sql = "select COF_DESC from COFFEE_DESCRIPTIONS where COF_NAME = ?";
String description = null;
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setString(1, coffeeName);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
Clob myClob = rs.getClob(1);          //获取 Clob 列的值
System.out.println("获得的 Clob 的长度: " + myClob.length());
//获取 Clob 中的字符串
description = myClob.getSubString(1, (int)myClob.length());
myClob.free();       //释放资源
}             
} catch (SQLException sqlex) {
sqlex.printStackTrace();
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.toString());
ex.printStackTrace();

return description;
}
//读取字符文件,并将内容转换为字符串返回
private String readFile(String fileName) throws FileNotFoundException, IOException{
//从与当前 class 文件同一目录下加载资源
InputStream input = this.getClass().getResourceAsStream(fileName);
BufferedReader br = new BufferedReader(new InputStreamReader(input));
String nextLine = "";
StringBuilder sb = new StringBuilder();
while ((nextLine = br.readLine()) != null) {     
sb.append(nextLine);

·41·
String clobData = sb.toString();     //  将内容转换为字符串
return clobData;                     //  返回值
}
//测试程序
public static void main(String[] args){
CoffeeDescriptionsDAO coffeeDescriptionsDAO = new CoffeeDescriptionsDAO ();
coffeeDescriptionsDAO.addRowToCoffeeDescriptions("Chinese Tea 10", "coffeedesc.txt");
System.out.println(lod.retrieveExcerpt("Chinese Tea 10"));
}
}
23.3.15   处理大数据对象 BLOB
接下来继续实现 CoffeeDescriptionsDAO 类,在其中添加用来完成对咖啡图片信息(大二进制字段)
的读写操作。其实现代码如下所示:
import com.citymango. JDBCUtilities;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class CoffeeDescriptionsDAO{
//向表 COFFEE_DESCRIPTIONS 中添加一个 BLOB SQL 值。
public void addPictureToCoffeeDescriptions(String coffeeName, String fileName){
String sql = "INSERT INTO COFFEE_DESCRIPTIONS(COF_NAME,COF_PIC) VALUES(?,?)";
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql)) {
InputStream input = this.getClass().getResourceAsStream("/" + fileName);   
pstmt.setString(1, coffeeName);
pstmt.setBlob(2, input);             //设置 blob 值
//            pstmt.setBinaryStream(2, input);     //也可
pstmt.executeUpdate();
input.close();
} catch (SQLException sqlex) {
sqlex.printStackTrace();
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.toString());
ex.printStackTrace();
}
}
//获取 COFFEE_DESCRIPTIONS 表中某一指定咖啡的图片(COF_PIC 列中存储的 BLOB SQL 值)
public void retrievePicture(String coffeeName) { 
42
String sql = "select COF_PIC from COFFEE_DESCRIPTIONS where COF_NAME = ?";    
try (Connection con = JDBCUtilities.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql)) {
pstmt.setString(1, coffeeName);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
//                 InputStream input = rs.getBinaryStream("COF_PIC");     //也可
InputStream input = rs.getBlob("COF_PIC").getBinaryStream();
OutputStream output = new FileOutputStream("D:\\coffee.jpg");
int c;
while((c = input.read()) != -1){
output.write(c);
}
output.flush();
input.close();
output.close();
}            
} catch (SQLException sqlex) {
sqlex.printStackTrace();
} catch (Exception ex) {
System.out.println("Unexpected exception: " + ex.toString());
ex.printStackTrace();

}    
public static void main(String[] args){
CoffeeDescriptionsDAO coffeeDescriptionsDAO = new CoffeeDescriptionsDAO ();
// coffeeDescriptionsDAO.addPictureToCoffeeDescriptions("Chinese Tea 09", "3-5.jpg");
coffeeDescriptionsDAO.retrievePicture("Chinese Tea 09");
}
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值