Java 7的RowSet
RowSet接口继承了ResultSet接口,RowSet接口下包含JdbcRowSet、CachedRowSet、FilterRowSet、JoinRowSet和WebRowSet常用子接口。除了JdbcRowSet需要保持与数据库的连接之外,其余4个子接口都是离线的RowSet,无须保持与数据库的连接。
与ResultSet相比,RowSet默认是可滚动、可更新、可序列化的结果集,而且作为JavaBean使用,因此能方便地在网络上传输,用于同步两端的数据。对于离线RowSet而言,程序在创建RowSet时已把数据从底层数据库读取到了内存,因此可以充分利用计算机的内存,从而降低数据库服务器的负载,提高程序性能。
Java 7新增的RowSetFactory与RowSet
Java7新增了RowSetProvider类和RowSetFactory接口,其中RowSetProvider负责创建RowSetFactory,而RowSetFactory则提供了如下方法来创建RowSet实例。
创建一个默认的CachedRowSet
public CachedRowSet createCachedRowSet() throws SQLException;
创建一个默认的FilteredRowSet
public FilteredRowSet createFilteredRowSet() throws SQLException;
创建一个默认的JdbcRowSet
public JdbcRowSet createJdbcRowSet() throws SQLException;
创建一个默认的JoinRowSet
public JoinRowSet createJoinRowSet() throws SQLException;
创建一个默认的WebRowSet
public WebRowSet createWebRowSet() throws SQLException;
通过使用RowSetFactory,就可以把应用程序与RowSet实现类分离开,避免直接使用JdbcRowSetImpl等非公开的API,也更有利于后期的升级、扩展。
代码演示
public class RowSetFactoryTest {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public void update(String sql) throws Exception{
Class.forName(driver);
//使用RowSetProvider创建RowSetFactory
RowSetFactory factory = RowSetProvider.newFactory();
try (
//创建默认的JdbcRowSet实例
JdbcRowSet jdbcRs = factory.createJdbcRowSet()
){
//设置必要的连接信息
jdbcRs.setUrl(url);
jdbcRs.setUsername(user);
jdbcRs.setPassword(password);
//设置SQL查询语句
jdbcRs.setCommand(sql);
//执行查询
jdbcRs.execute();
jdbcRs.afterLast();
while (jdbcRs.previous()){
System.out.println(jdbcRs.getString(1)
+ "\t" + jdbcRs.getString(2)
+ "\t" + jdbcRs.getString(3));
if (jdbcRs.getInt("student_id") == 3){
//修改指定记录行
jdbcRs.updateString("student_name", "孙悟空");
jdbcRs.updateRow();
}
}
}
}
public static void main(String[] args) throws Exception{
RowSetFactoryTest rt = new RowSetFactoryTest();
rt.initParam("mysql.ini");
rt.update("select * from student_table");
}
}
离线RowSet
在使用ResultSet时代,程序查询得到ResultSet之后必须立即读取或处理它对应的记录,否则一旦Connection关闭,再去通过ResultSet读取记录就会引发异常。在这种模式下,JDBC编程十分痛苦——假设应用程序被分为两层:数据访问层和视图显示层,当应用程序在数据访问层查询得到ResultSet之后,对ResultSet的处理有如下两种常见方式。
- 使用迭代访问ResultSet里的记录,并将这些记录转换成Java Bean,再将多个Java Bean封装成一个List集合,也就是完成
ResultSet——>Java Bean集合
的转换。转换完成后可以关闭Connection等资源,然后将Java Bean集合传到视图显示层,视图显示层可以显示查询得到的数据。 - 直接将ResultSet传到视图显示层——这要求当视图显示层显示数据时,底层Connection必须一直处于打开状态,否则ResultSet无法读取记录。
第一种方式比较安全,但编程十分繁琐;第二种方式则需要Connection一直处于打开状态,这不仅不安全,而且对程序性能也有较大的影响。
通过使用离线RowSet可以十分优雅地处理上面的问题,离线RowSet会直接将底层数据读入内存中,封装成RowSet对象,而RowSet对象则完全可以当成Java Bean来使用。因此不仅安全,而且编程十分简单。
代码演示
public class CachedRowSetTest {
private static String driver;
private static String url;
private static String user;
private static String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public CachedRowSet query(String sql) throws Exception{
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
//使用RowSetProvider创建RowSetFactory
RowSetFactory factory = RowSetProvider.newFactory();
//创建默认的CachedRowSet实例
CachedRowSet cachedRs = factory.createCachedRowSet();
//使用ResultSet装填RowSet
cachedRs.populate(rs);
//关闭资源
rs.close();
stmt.close();
conn.close();
return cachedRs;
}
public static void main(String[] args) throws Exception{
CachedRowSetTest ct = new CachedRowSetTest();
ct.initParam("mysql.ini");
CachedRowSet rs = ct.query("select * from student_table");
rs.afterLast();
//向前滚动结果集
while(rs.previous()){
System.out.println(rs.getString(1)
+ "\t" + rs.getString(2)
+ "\t" + rs.getString(3));
if (rs.getInt("student_id") == 3){
//修改指定记录行
rs.updateString("student_name", "猪八戒");
rs.updateRow();
}
}
//重新获取数据库连接
Connection conn = DriverManager.getConnection(url, user, password);
conn.setAutoCommit(false);
//把对RowSet所做的修改同步到底层数据库
rs.acceptChanges(conn);
}
}
离线RowSet的查询分页
由于CachedRowSet会将数据记录直接装在到内存中,因此如果SQL查询返回的记录过大,CachedRowSet将会占用大量的内存,在某些极端的情况下,它甚至会直接导致内存溢出。
为了解决该问题,CachedRowset提供了分页功能。所谓分页功能就是一次只装载ResultSet里的某几条记录,这样就可以避免CachedRowSet占用内存过大的问题。
代码演示
public class CachedRowSetPage {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public CachedRowSet query(String sql, int pageSize, int page) throws Exception{
Class.forName(driver);
try (
Connection conn = DriverManager.getConnection(url, user, password);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)
){
RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet cachedRs = factory.createCachedRowSet();
//设置每页显示pageSize条记录
cachedRs.setPageSize(pageSize);
//使用ResultSet装填RowSet,设置从第几条记录开始
cachedRs.populate(rs, (page - 1) * pageSize + 1);
return cachedRs;
}
}
public static void main(String[] args) throws Exception{
CachedRowSetPage cp = new CachedRowSetPage();
cp.initParam("mysql.ini");
CachedRowSet rs = cp.query("select * from student_table", 3, 2);
//向后滚动结果集
while(rs.next()){
System.out.println(rs.getString(1)
+ "\t" + rs.getString(2)
+ "\t" + rs.getString(3));
}
}
}
事务处理
事务的概念和MySQL事务支持
事务是由一步或几步数据库操作序列组成的逻辑执行单元,这系列操作要么全部执行,要么全部放弃执行。程序和事务是两个不同的概念。一般而言,一段程序中可能包含多个事务。
事务具备4个特性
- 原子性(Atomicity):事务是应用中最小的执行单位,就如原子是自然界的最小颗粒,具有不可再分的特征一样,事务是应用中不可再分的最小逻辑执行体。
- 一致性(Consistency):事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。
- 隔离性(Isolation):各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务都是隔离的,也就是说,并发执行的事务之间不能看到对方的中间状态,并发执行的事务之间不能互相影响。
- 持续性(Durability):持续性也称为持久性,指事务一旦提交,对数据所做的任何改变都要记录到永久存储器中,通常就是保存进物理数据库。
这4个特性也简称为ACID性
数据库的事务由下列语句组成。
- 一组DML语句,经过这组DML语句修改后的数据将保持较好的一致性
- 一条DDL语句
- 一条DCL语句
DDL和DCL语句最多只能有一条,因为DDL和DCL语句都会导致事务立即提交。
当事务所包含的全部数据库操作都成功执行后,应该提交事务,使这些修改永久生效。事务提交有两种方式:显式提交和自动提交。
显式提交:使用commit。
自动提交:执行DCL或DDL语句,或者程序正常退出。
当事务所包含的任意一个数据库操作执行失败后,应该回滚事务,使该事务中所做的修改全部失效。事务回滚有两种方式:显式回滚和自动回滚。
显式回滚:使用rollback。
自动回滚:系统错误或者强行退出。
MySQL默认关闭事务(即打开自动提交),在默认情况下,用户在MySQL控制台输入一条DML语句,这条DML语句将会立即保存到数据库里。为了开启MySQL的事务支持,可以显式调用如下命令
## 0为关闭自动提交,即开启事务
SET AUTOCOMMIT = {0 | 1}
自动提交和开启事务恰好相反,如果开启自动提交就是关闭事务;关闭自动提交就是开启事务。
如果只是想临时性地开始事务,则可以使用MySQL提供的start transaction或begin
两个命令,它们都表示临时性地开始一次事务,处于start transaction或begin后的DML语句不会立即生效,除非使用commit显式提交事务,或者执行DDL、DCL语句来隐式提交事务。
JDBC的事务支持
JDBC连接也提供了事务支持,JDBC连接的事务支持由Connection提供,Connection默认打开自动提交,即关闭事务,在这种情况下,每条SQL语句一旦执行,便会立即提交到数据库,永久生效,无法对其进行回滚操作。
可以调用Connection的setAutoCommit()方法来关闭自动提交。
开启事务
void setAutoCommit(boolean autoCommit) throws SQLException;
提交事务
void commit() throws SQLException;
回滚事务
void rollback() throws SQLException;
实际上,当Connection遇到一个未处理的SQLException异常时,系统将会非正常退出,事务也会自动回滚。但如果程序捕获了该异常,则需要在异常处理块中显式地回滚事务。
代码演示
public class TransactionTest {
private String driver;
private String url;
private String user;
private String password;
public void initParam(String paramFile) throws Exception {
//使用Properties类来加载属性文件
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
}
public void insertInTransaction(String[] sqls) throws Exception{
Class.forName(driver);
try(
Connection conn = DriverManager.getConnection(url, user, password)
){
//关闭自动提交,开启事务
conn.setAutoCommit(false);
try (
Statement stmt = conn.createStatement()
){
for (String sql : sqls){
stmt.executeUpdate(sql);
}
}
//提交事务
conn.commit();
}
}
public static void main(String[] args) throws Exception{
TransactionTest tt = new TransactionTest();
tt.initParam("mysql.ini");
String[] sqls = new String[]{
"insert into student_table values(null, 'aaa', 1)",
"insert into student_table values(null, 'bbb', 1)",
"insert into student_table values(null, 'ccc', 1)",
//下面这条SQL语句将会违反外键约束
//因为teacher_table表中没有ID为5的记录
"insert into student_table values(null, 'ccc', 5)"
};
tt.insertInTransaction(sqls);
}
}
批量更新
为了让批量操作可以正确地处理错误,必须把批量执行的操作视为单个事务,如果批量更新在执行过程中失败,则让事务回滚到批量操作开始之前的状态。为了达到这种效果,程序应该在开始批量操作之前先关闭自动提交,然后开始收集更新语句,当批量操作执行结束后,提交事务。
//保存当前的自动的提交模式
boolean autoCommit = conn.getAutoCommit();
//关闭自动提交
conn.setAutoCommit(false);
Statement stmt = conn.createStatement();
//使用Statement同时收集多条SQL语句
stmt.addBatch(sql1);
stmt.addBatch(sql2);
stmt.addBatch(sql2);
...
//提交修改
conn.commit();
//恢复原有的自动提交模式
conn.setAutoCommit(autoCommit);
使用系统表分析数据库信息
MySQL数据库使用information_schema数据库来保存系统表,在该数据库里包含了大量系统表,常用系统表的简单介绍如下。
- tables:存放数据库里所有数据表的信息
- schemata:存放数据库里所有数据库的信息
- views:存放数据库里所有视图的信息
- columns:存放数据库里所有列的信息
- triggers:存放数据库里所有触发器的信息
- routines:存放数据库里所有存储过程和函数的信息
- key_column_usage:存放数据库里所有具有约束的键信息
- table_constraints:存放数据库里全部约束的表信息
- statistics:存放数据库里全部索引的信息
使用连接池管理连接
数据库连接的建立及关闭是极耗费系统资源的操作,在多层结构的应用环境中,这种资源的耗费对系统性能影响尤为明显。通过DriverManager获取数据库连接,一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完后关闭连接。频繁地打开、关闭连接将造成系统性能低下。
数据库连接池的解决方案是:当应用程序启动时,系统主动建立足够的数据库连接,并将这些连接组成一个连接池。每次应用程序请求数据库连接时,无须重新打开连接,而是从连接池中取出已有的连接使用,使用完后不再关闭数据库连接,而是直接将连接归还给连接池。通过使用连接池,将大大提高程序的运行效率。
为了解决数据库连接的频繁请求、释放,JDBC 2.0规范引入了数据库连接池技术。数据库连接池是Connection对象的工厂。数据库连接池的常用参数如下:
- 数据库的初始连接数
- 连接池的最大连接数
- 连接池的最小连接数
- 连接池每次增加的容量
JDBC的数据库连接池使用javax.sql.DataSource来表示,DataSource只是一个接口,该接口通常由商用服务器(如WebLogic、WebSphere)等提供实现,也有一些开源组织提供实现(如DBCP和C3P0等)。
DataSource通常被称为数据源,它包含连接池和连接池管理两个部分,但习惯上我们也经常把DataSource称为连接池。
DBCP数据源
DBCP是Apache软件基金组织下的开源连接池实现,该连接池依赖该组织下的另一个开源系统:common-pool。如果需要使用该连接池实现,则应在系统中增加如下两个jar文件。
- commons-dbcp.jar:连接池的实现
- commons-pool.jar:连接池实现的依赖库
Tomcat的连接池正是采用该连接池实现的。数据库连接池既可以与应用服务器整合使用,也可以由应用程序独立使用。下面的代码片段示范了使用DBCP来获得数据库连接的方式
//创建数据源对象
BasicDataSource ds = new BasicDataSource();
//设置连接池所需的驱动
ds.setDriverClassName("com.mysql.jdbc.Driver");
//设置连接数据库的URL
ds.setUrl("jdbc:mysql://localhost:3306/javaee");
//设置连接数据库的用户名
ds.setUsername("root");
//设置连接数据库的密码
ds.setPassword("pass");
//设置连接池的初始连接数
ds.setInitialSize(5);
//设置连接池最多可有多少个活动连接数
ds.setMaxActive(20);
//设置连接池中最少有2个空闲的连接
ds.setMinIdle(2);
数据源和数据库连接不同,数据源无须创建多个,它是生产数据库连接的工厂,因此整个应用只需要一个数据源即可。也就是说,对于一个应用,上面代码只要执行一次即可。建议把上面程序中的ds设置成static成员变量,并且在应用开始时立即初始化数据源对象,程序中所有需要获取数据库连接的地方直接访问该ds对象,并获取数据库连接即可。
//通过数据源获取数据库连接
Connection conn = ds.getConnection();
当数据库访问结束后,程序还是像以前一样关闭数据库连接。
//释放数据库连接
conn.close();
但上面代码并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给连接池,让其他客户端可以使用该连接。
C3P0数据源
想比之下,C3P0数据源性能更胜一筹,Hibernate就推荐使用该连接池。C3P0连接池不仅可以自动清理不再使用的Connection,还可以自动清理Statement和ResultSet。如果需要使用C3P0连接池,则应在系统中增加如下jar文件。
- c3p0-0.9.1.2.jar:C3P0连接池的实现
下面代码通过C3P0连接池获得数据库连接
//创建连接池实例
ComboPooledDataSource ds = new ComboPooledDataSource();
//设置连接池连接数据库所需的驱动
ds.setDriverClass("com.mysql.jdbc.Driver");
//设置连接数据库的URL
ds.setJdbcUrl("jdbc:mysql://localhost:3306/javaee");
//设置连接数据库的用户名
ds.setUser("root");
//设置连接数据库的密码
ds.setPassword("32147");
//设置连接池的最大连接数
ds.setMaxPoolSize(40);
//设置连接池的最小连接数
ds.setMinPoolSize(2);
//设置连接池的初始连接数
ds.setInitialPoolSize(10);
//设置连接池的缓存Statement的最大数
ds.setMaxStatements(180);