《Java核心技术 卷2 高级特性》五
Posted ase265
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Java核心技术 卷2 高级特性》五相关的知识,希望对你有一定的参考价值。
第5章 数据库编程
JDBC的设计
JDBC是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。
1996年,Sun公司发布了第一版的Java数据库连接(JDBC)API,使编程人员可以通过这个API接口连接到数据库
并使用结构化查询语言(SQL,是数据库访问的业界标准)完成对数据库的查找与更新
JDBC自此成为Java类库中最常用的API之一。
Java技术开发人员最初希望通过扩展Java,就可以让人们“纯”用Java语言与任何数据库进行通信
但因为业界内有许多不用的数据库,且它们所使用的协议也各不相同
实际上,最后Sun公司参考了微软公司非常成功的ODBC(Open Database Connectivity.开放数据库连接)模式
为JAVA设计了专用的数据库连接规范,JDBC和ODBC都基于同一个思想:
根据API编写的程序可以与驱动管理器进行通信,而驱动管理器则通过驱动程序与实际的数据库库进行通信。
简而言之,最后JDBC的设计变成了Java为SQL访问提供一套API和驱动管理器,然后数据库供应商或者第三方提供自己的驱动程序
将其插入到驱动管理器中,这样便可以利用Java语言来开发访问数据库的程序了
JDBC最终实现了以下目标:
- 通过标准的SQL语句,甚至是专门的SQL扩展,程序员就可以利用Java语言开发访问数据库的应用,同时还遵守了Java原因的规范
- 数据库供应商和数据库工具开发商可以提供底层的驱动程序。因此它们可以优化各自数据库产品的驱动程序
在传统的客户端/服务器模型中,通常是服务器端部署数据库,而在客户端安装富GUI程序,JDBC驱动程序也部署在客户端
但现如今,三层模型更加常见。在该模型中,客户端不直接调用服务器上的数据库,而是调用服务器上的中间件层,由中间件层完成数据库查询操作
客户端和中间件层之间的通信在典型情况下是通过HTTP实现的,JDBC管理着中间件层和后台数据库之间的通信
这种三层模型的好处在于:将可视化表示从业务逻辑和原始数据中分离出来
JDBC配置
书本上举了Apache Derby数据的例子,而其实接触比较多的是mysql数据库
因此,这里的读书笔记将以mysql为例
1.数据库URL
JDBC使用了一种与普通URL类似的语法来描述数据源
"jdbc:mysql://localhost:3306/Stu?&useSSL=false&serverTimezone=UTC";
上述JDBC URL指定了名为Stu的一个mysql数据库,JDBC URL的一般语法为:
jdbc:subprotocol:other stuff
其中,subprotocol用于选择连接到数据库的具体驱动程序
other stuff参数的格式随使用的subprotocol不同而不同,若想要知道具体格式,则需要查阅数据库供应商提供的相关文档
2.驱动程序JAR文件
我们需要获得所使用数据库的驱动程序的JAR文件,换句话说我们需要mysql数据库的驱动文件
而对应的JAR文件需要到https://dev.mysql.com/downloads/connector/j/这个网址下载
下载完解压后,我们就得到了mysql-connector-java-8.0.19.jar文件
这个文件就是数据库供应航mysql提供的驱动程序,我们需要将该JAR文件导入所需的JAVa工程项目中
不用的IDEA有不同的方法,导入也非常简单,百度即可知道答案
3.注册驱动器类
许多JDBC的JAR文件会自动注册驱动器类,而mysql的驱动程序文件JAR是可以自动注册的,在将JAR文件导入工程项目后
不能自动注册的JAR文件需要手动注册,手动注册步骤如下:
1) 首先找出数据库提供商使用的JDBC驱动器类的名字
这里mysql的JDBC驱动器的名字为:com.mysql.cj.jdbc.Driver。另:com.mysql.jdbc.Driver已被弃用
2) 通过使用DriverManager,可以使用两种方式来注册驱动器
Class.forName("DirverName");
这要语句将使得驱动器被类加载,由此将执行可以注册驱动器的静态初始器
另一种方式是设置jdbc.drivers属性
System.setProperty("jdbc.drivers","DriverName");
这种方式可以注册多个驱动器,用冒号将它们分开即可
4.连接到数据库
在Java程序中,我们可以在一个代码中打开一个数据库连接
String url = "jdbc:mysql://localhost:3306/Stu?&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "root";
Connction conn = DriverManager.getConnection(url,username,password);
驱动管理器,即DriverManager类将遍历所有注册过的驱动程序,以便能找到一个能够使用数据库URL中之指定的子协议的驱动程序
getConnection方法返回一个Connection对象,我们可以使用Connection对象来执行SQL语句
使用JDBC语句
在执行JDBC语句之前,需要创建一个Statement对象。要创建Statement对象,需要调用DriverManager.getConnection方法获得的Connection对象
Statement stat = conn.createStatement();
//然后将SQL语句放入字符串中
String command ="update ...";
//然后调用Statement接口的executeUpdate方法
stat.executeUpdate(command);
exceuteUpdate方法可以执行INSERT,UPDATE和DELETE之类的操作,也可以执行诸如CREATE TABLE和DROP TABLE之类的数据定义语句
但是执行SELECT查询时必须使用executeQuery方法。excecute语句可以执行任意的SQL语句,但是通常只用于由用户提供的交互式查询
exceuteQuery方法会返回一个ResultSet类型的对象,可以通过它来每次一行地遍历所有查询结果
ResultSet rs = stat.executeQuery("select ...");
//分析结果集
while(rs.next())
{
look at a row of the result set
//ResultSet也提供了很多访问器方法来获取列的内容
//例如获取该行的学生姓名
String name = rs.getString("Sname");
}
结果集中的行的顺序是任意排列的,除非使用ORDER BY子句指定的顺序
Connection对象可以创建多个Statement对象
同一个Statement对象可以用于多个不相关的命令和查询
但Statement对象最多只能有一个打开的结果集
但实际上,我们通常不需要同时处理多个结果集,如果结果集相互关联,我们可以使用组合查询,只对一个结果进行分析
使用完ResultSet、Statement或Connection对象后,应立即close方法
因为这些对象都使用了规模较大的数据结构,会占用数据库服务器上的有限资源
每个SQL异常(SQLException)都有一个或者多个SQLException对象构成的链,这些对象可以通过getNextException方法获取
Java SE 6改进了SQLException类,让其实现了Iterable<Throwable>接口,其iterator()方法可以产生一个Iterator<Throwable>
这个迭代器可以迭代两个链,首先迭代第一个SQLException的成因链,然后迭代下一个SQLException
对应的for循环:
for(Throwable t : sqlException)
{
do something with t
}
执行查询操作
书上还介绍了一个关于数据库库操作的特性:预备语句(prepared statement)
如果不考虑作者字段,查询某个出版社的所有图书,对应的SQL语句如下:
SELECT Books.Price,Books.Title
FROM Books,Publishers
WHERE Books.Pushlisher_Id = Publishers.Publisher_Id
AND Publishers.Name = *the name from list box*
但实际上我们根本没有必要在每次开始这样的一个查询时都建立新的查询语句,而是准备一个带有宿主变量的查询语句
每次查询时只需为该变量填入不同的字符串就可以反复多次使用该语句
在预备查询语句中,每个宿主变量都用"?"来表示。如果存在一个以上的变量,那么在设置变量时必须注意"?"的位置
String publisherQuery =
"SELECT Books.Price,Books.Title"+
"FROM Books,Publishers"+
"WHERE Books.Pushlisher_Id = Publishers.Publisher_Id AND Publishers.Name = ?"
PreparedStatement stat = conn.prepareStatement(puslisherquery);
在执行预备语句之前,必须使用set方法将变量绑定到实际的值上
对于上面的例子,我们为出版社名称设置一个字符串值
stat.setString(1,publisher);
其中,第一个参数指定设置宿主变量的位置,位置1代表第一个"?"
第二个参数指的是赋予宿主变量的值
一旦为所有变量绑定了具体的值,就可以执行查询操作了:
ResultSet rs = stat.executeQuery();
如果想要重用已经执行过的预备查询语句,使用setXxx方法重新绑定需要改变的变量即可
除了数字、字符和日期之外,许多数据库还可以存储大对象,例如图片或者其它数据
在SQL中,二进制大对象被称为BLOB,字符型大对象称为CLOB
要读取LOB,需要执行SELECT语句,然后在ResultSet上调用getBlob或者getClob方法,这样就可以获得Blob和Clob类型的对象
要从Blob中获取二进制对象,可以调用getBytes或者getBinaryStream
//从某个数据库中读取图像
try(ResultSet st = stat.execuQuery())
{
if(st.next())
{
Blob coverBlob = st.getBlob("对应列的名字");
Image coverImage = ImageIO.read(coverBlob.getBinaryStream());
}
}
类似地,如果获得了Clob对象,那么就可以通过调用getSubString或者getCharacterStream方法来获得其中的字符数据
要将LOB数据存储在数据库中,需要在Connection对象上调用createBlob或createClob
然后获取一个用于该LOB的输出流,写出数据,并将对象存储到数据库中
下面展示了如何存储一张图像
Blob coverBlob = connection.createBlob();
int offset = 0;
OutputStream out = new coverBlob.setBinaryStream(offset);
ImageIO.write(coverIamge,"PNG",out);
PreparedStatement stat = conn.preparedStatement("INSERT INTO Cover VALUES(?,?)");
stat.set(1,isbn);
stat.set(2,coverBlob);
stat.executeUpdate();
在执行存储过程,或者使用允许在单个查询中提交多个SELECT语句的数据库时,一个查询有可能会返回多个结果集
获取多个结果集的步骤为:
1.使用execute方法来执行SQL语句
2.获取第一个结果集或更新技术
3.重复调用getMoreResults方法以移动到下一个结果集
4.当不存在更多的结果集或者更新计数时,完成操作
boolean isResult = stat.execute(command);
boolean done = false;
while(!done)
{
if(isResult)
{
ResultSet reuslt = stat.getResultSet();
*do something with result*
}
else
{
int updateCount = stat.getUpdateCount();
if(uodateCount >= 0)
*do something with updateCount*
else
done=true;
}
if(!done) isResult = stat.getMoreResults();
}
可滚动和可更新的结果集
默认情况下,结果集是不可滚动和修改的。为了查询中,必须使用下面的方法得到一个不同的Statement对象:
Statement stat = conn.createStatement(type,concurrency);
//如果想要获得预备语句,请调用下面的方法
PreparedStatement stat = conn.preparedStatement(command,type,concurrency);
对应的type和concurrency的所有可能值,注意:这些值都是ResultSet类的属性
例如,如果只想滚动遍历结果集,而不想编辑它的数据,那么可以使用以下语句:
Statement stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY);
ResultSet rs = stat.executeQuery();
这样得到的结果集将是可滚动的,可滚动的结果集有一个游标,用以指示当前位置
在结果集上滚动是非常简单的,可以使用
if (rs.previous())
可将游标向后滚动。如果游标位于一个实际的行上,返回true。若是位于第一行之前,返回false
rs.relative(n);
可将游标向后或者向前移动多行。若n为正数,则向前移动n行。若n为负数,则向后移动n行。若n为0,那么不起作用
若是游标被移动到最后一行之后或是第一行之前,将返回flase。否则,返回true
rs.absolute(n);
可将将游标设置到指定的行号上
int currentRow = rs.getRow();
可返回当前行的行号
如果想要获得可更新地结果集,应该使用以下方法
Statement stat = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSTIVE,ResultSet.CONCUR_UPDATABLE
);
这样调用executeQuery方法返回的结果集就是可更新的结果集
另:可更新的结果集可以是滚动的,也可以是不滚动的
假如,假设想提高某些图书的价格,但是执行UPDATE语句时又没有一个简单而统一的提价标准
此时就可以根据任意设定的条件。迭代遍历所有的图书并更新它们的价格
String query = "SELECT * from Books";
ResultSet rs = stat.executeQuery(query);
while(rs.next())
{
if(...)
{
double increase = ...;
double price = rs.getDouble("Price");
rs.updateDouble("Price",price+increase);
rs.updateRow();
}
}
最后需要说明的是,可以使用以下方法删除游标所所指的行
rs.deleteRow()
deleteRow方法会立即将改行从结果集和数据库中删除
行集
可滚动结果集的问题在于必须始终与数据库保持连接,于是就有了行集
RowSet接口扩展自ResultSet接口,却无需始终保持与数据库的连接
行集还适用于将查询结果移动到复杂应用的其它层,或是诸如手机之类的其它设备中
以下为javax.sql.rowset包提供的接口,它们都扩展了RowSet接口
- CachedRowSet允许在断开连接状态下执行相关操作
- WebRowSet对象代表了一个被缓存的行集,该行集可以被保存为XML文件
- FilteredRowSet和JoinRowSet接口支持对行集的轻量操作,等同于SQL中的SELECT和JOIN操作
- JdbcRowSet是ResultSet接口的一个瘦包装器。它在RowSet接口中添加了有用的方法
在Java7中,有一种获取行集的标准方式:
RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();
一个被缓存的行集中包含了一个结果集中的所有数据,CachedRowSet是ResultSet接口的子接口
因此完全可以想使用结果集一样来使用被缓存地行集,因为是被缓存过的
所以可以在断开数据库连接后仍然可以使用
可以修改在缓存的数据,当然这些修改不会立即反馈到数据库中中
但可以发起一个显式的请求,以便让数据库真正接受这些修改
可以使用一个结果集来填充CachedRowSet对象:
ResultSet result = ...;
RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSEt();
crs.populate(result);
conn.close();
也可以让CachedRowSet对象自动建立一个数据库连接,首先设置数据库参数:
crs.setURL();
crs.setUsername();
crs.setPassword();
//然后设置语句和所有参数
crs.setCommand("SELECT * FROM Books");
//最后将结果集填充到行集中
crs.execute();
如果查询结果非常大,在这种情况下,可以指定每一页的尺寸:
CachedRowSet crs = ...;
crs.setCommand(command);
crs.setPageSize(20);
...
crs.execute();
现在就只获得20行了,要获取下一批数据需要调用
crs.nextPage();
若是想将修改做的行集写回到数据库中,可以调用
crs.acceptChanges(conn);
或者
crs.acceptChange();
只有在行集中设置了连接数据库所需的信息(URL,用户名,密码)时,上述第二个方法才会有效
元数据
JDBC还提供了关于数据库及其表结构的详细信息
在SQL中,描述数据库及其组成部分的数据称为元数据
我们可以获得三类元数据:关于数据库的元数据;
关于结果集的元数据以及关于预备语句参数的元数据
如果想要了解数据库的更多信息,可以从数据库连接中获取一个DatabaseMetaData独享
DataBaseMetaData meta = conn.getMetaData();
将返回一个包含所有数据库表信息的结果集
该结果集每一行都包含了数据库中一张表的详细信息
DatabaseMetaData接口用于提供有关数据库的数据,ResultSetMetaData元数据接口则用于提供结果集的相关信息
下面是一个用来获取每个结果集的列数、每一列的名称、类型和字段宽度
ResultSet rs = stat.executeQuery("SELECT * FROM"+tableName);
ResultSetMetaData meta = rs.getMetaData();
for(int i=0;i<=meta.getColumnCount();i++)
{
String columnNmae = meta.getColumnLable(i);
int columnWidth = maeta.getColumnDisplaySize(i);
...
}
事务
我们可以将一组语句构建成一个事务。当所有语句都顺利执行之后,事务可以被提交
否则,如果其中某个语句遇到错误,那么事务将被回滚,就好像没有任何语句执行过一样
将多个语句组合成事务是为了确保数据库的完整性
默认情况下,数据库连接处于自动提交模式
每个SQL语句一旦被执行便被提交给数据库,无法对其进行回滚操作
在使用事务时,需要关闭这个默认值:
conn.setAutocommit(false);
然后使用通常的方法创建语句对象:
Statement stat = conn.createStatement();
然后任意地多次调用executeUpdate方法:
stat.executeUpdate(commnad1);
stat.executeUpdate(commnad2);
stat.executeUpdate(commnad3);
...
如果执行了所有命令后没有出错,则调用commit方法:
coon.commit();
如果出现错误,则调用:
conn.roolback();
当事务被SQLException异常中断时,典型的方法就是发起回滚操作。
在使用某些驱动程序时,使用保存点(save point)可以更细粒度地控制回滚操作
创建一个保存点意味着稍后只需返回到这个点,而事务的开头
Statement stat = conn.createStatement(); //rollback() goes here
stat.executeUpdate(command1);
SavePoint svpt = conn.setSavePoint(); // rollback(svpt) goes here
stat.executeUpdate(command2);
if(...) conn.rollback(svpt);
...
conn.commit();
当不再需要保存点时,必须释放它:
conn.releaseSavepoint(svpt);
假设有一个程序需要执行很多INSERT语句,以便将数据填入数据库中,此时可以使用批量更新的方法来提高程序性能
为了执行批量处理,首先必须使用通常的方法创建一个StateMent对象:
Statement stat = conn.createStatement();
现在,应该调用addBatch方法,而非executeUpdate方法:
String command = "CREATE TEBLE ...";
stat.addBatch(command);
while(...)
{
command = "INSERT INTO ...VALUEs("+...+")";
stat.addBbatch(command);
}
//最后提交整个批量更新语句
int[] counts = stat.executeBatch();//返回一个记录数的数据
为了在批量模式下正确地处理错误,必须将批量执行的操作视为单个事务。
以上是关于《Java核心技术 卷2 高级特性》五的主要内容,如果未能解决你的问题,请参考以下文章