1、RowSet
RowSet接口继承自ResultSet,它是可序列化的结果集,而且作为JavaBean使用,因此能方便的在网络上传输。RowSet中的数据并不一定是数据库中的数据,也可以是XML数据等任何具有列集合概念的数据源,RowSet下包含JdbcRowSet、CachedRowSet、FilteredRowSet、JoinRowSet、WebRowSet常用子接口,这些子接口里只有JdbcRowSet需要保持与数据的连接,其他的4个都是可离线的RowSet,JdbcRowSet可简化JDBC程序的编写,或作为JavaBean使用。
JavaBean是指符合特定写法的Java类,这种类主要用于传递数据信息,而且数据在类中是私有的,只能通过public方法来访问数据,类中必须具有一个public无参数构造函数,且这个类是可序列化的,如:
public class User
{
private long id;
private String name;
public void setId(long id)
{
this.id = id;
}
public long getId()
{
return id;
}
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
javax.sql.rowset包在JDK9中是放在java.sql.rowset模块中的,若采用模块化设计,记得在模块描述文件加上requires java.sql.rowset。 自JDK7后,RowSet都是java.lang.AutoCloseable的子接口,可以使用尝试自动关闭资源语法。
RowSet接口中常用方法有以下,通过RowSet的接口可以看出,使用RowSet的话可以不再使用Statement和Connection:
setUrl(),setUsername(),setPassword():设置要访问的数据库的URL、用户名、密码。
setCommand(String sql):设置使用该sql语句的查询结果来装填该RowSet。
execute():执行查询。
populate(ResultSet rs):通过ResultSet对象装填该RowSet。
下面是使用RowSet的示例,其中的JdbcRowSetImpl是RowSet的子接口,使用需要包含sun公司的com.sun.rowset.jar:
package xu;
import java.sql.Connection;
import java.sql.DriverManager;
import java.io.FileInputStream;
import java.util.Properties;
import javax.sql.rowset.JdbcRowSet;
import com.sun.rowset.JdbcRowSetImpl;
import javax.sql.rowset.RowSetProvider;
import javax.sql.rowset.RowSetFactory;
public class JdbcRowSetTest
{
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");
pass = props.getProperty("pass");
}
public void update1(String sql)throws Exception
{
Class.forName(driver);
try(
Connection conn = DriverManager.getConnection(url, user, pass);
JdbcRowSetImpl jdbcRs = new JdbcRowSetImpl(conn); //创建JdbcRowSetImpl对象,使用指定的Connection对象作为数据库连接
)
{
jdbcRs.setCommand(sql); //设置SQL语句
jdbcRs.setInt(1, 100);
jdbcRs.execute(); //执行
while(jdbcRs.next())
{
int id = jdbcRs.getInt(1);
String str = jdbcRs.getString(2);
System.out.println(id + str);
}
}
}
public void update2(String sql)throws Exception
{
Class.forName(driver);
try(
JdbcRowSetImpl jdbcRs = new JdbcRowSetImpl(); //创建JdbcRowSetImpl对象
)
{
//设置连接信息
jdbcRs.setUrl(url);
jdbcRs.setUsername(user);
jdbcRs.setPassword(pass);
jdbcRs.setCommand(sql);
jdbcRs.setInt(1, 100);
jdbcRs.execute();
while(jdbcRs.next())
{
int id = jdbcRs.getInt(1);
String str = jdbcRs.getString(2);
System.out.println(id + str);
}
}
}
public void update3(String sql)throws Exception
{
Class.forName(driver);
RowSetFactory factory = RowSetProvider.newFactory(); //创建RowSetFactory
try(
JdbcRowSet jdbcRs = factory.createJdbcRowSet(); //使用RowSetFactory创建默认的JdbcRowSet实例
)
{
//设置连接信息
jdbcRs.setUrl(url);
jdbcRs.setUsername(user);
jdbcRs.setPassword(pass);
jdbcRs.setCommand(sql);
jdbcRs.setInt(1, 100);
jdbcRs.execute();
while(jdbcRs.next())
{
int id = jdbcRs.getInt(1);
String str = jdbcRs.getString(2);
System.out.println(id + str);
}
}
}
public static void main(String[] args)throws Exception
{
JdbcRowSetTest jt = new JdbcRowSetTest();
jt.initParam("MySqlConfig.ini");
jt.update1("select * from newtable where id >= ?");
}
private String driver;
private String url;
private String user;
private String pass;
}
CachedRowSet是FilteredRowSet、JoinRowSet、WebRowSet的父接口,它是离线的RowSet,当Connection关闭后也可以读取其中的数据。CachedRowSet会直接将数据封装成RowSet对象,而RowSet对象可以当成Java Bean来使用:
import java.util.*;
import java.io.*;
import java.sql.*;
import javax.sql.*;
import javax.sql.rowset.*;
public class CachedRowSetTest
{
private static String driver;
private static String url;
private static String user;
private static String pass;
public void initParam(String paramFile)throws Exception
{
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
}
public CachedRowSet query(String sql)throws Exception
{
Class.forName(driver);
Connection conn = DriverManager.getConnection(url , user , pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet cachedRs = factory.createCachedRowSet(); // 创建默认的CachedRowSet实例
cachedRs.populate(rs); // 使用ResultSet装填RowSet
rs.close();
stmt.close();
conn.close();
return cachedRs;
}
public static void main(String[] args)throws Exception
{
CachedRowSetTest ct = new CachedRowSetTest();
ct.initParam("MySqlConfig.ini");
CachedRowSet rs = ct.query("select * from newtable");
rs.afterLast();
while (rs.previous())
{
System.out.println(rs.getInt(1)
+ "\t" + rs.getString(2));
if (rs.getInt("id") == 101)
{
// 修改指定记录行
rs.updateString("city", "新城市");
rs.updateRow();
}
}
Connection conn = DriverManager.getConnection(url, user , pass);// 重新获取数据库连接
conn.setAutoCommit(false);
rs.acceptChanges(conn); // 把对RowSet所做的修改同步到数据库
}
}
CachedRowSet提供了分页的功能,可以通过成员方法populate(ResultSet rs, int startRow)来设置只从rs的第startRow条记录开始装填指定条数的数据。成员方法setPageSize()用来设置装填数据的条数,previousPage()/nextPage()设置在ResultSet可用的情况下读取其上/下一页记录。
WebRowSet不仅具备脱机操作,还能进行XML读写,FilteredRowSet可以对列集合进行过滤,实现类似SQL中where等条件式的功能,JoinRowSet可以让你结合两个RowSet对象,实现类似SQL中的join功能。
2、事务(交易)
调用Connection的setAutoCommit()来开启事务,commit()来提交事务,setSavepoint()来设置中间点。当Connection遇到一个未处理的SQLException异常时,事务会自动回滚到开始之前的数据状态,如果程序中捕获了该异常,那么应该手动调用rollback()来回滚。
import java.sql.*;
import java.io.*;
import java.util.*;
public class TransactionTest
{
private String driver;
private String url;
private String user;
private String pass;
public void initParam(String paramFile)throws Exception
{
Properties props = new Properties();
props.load(new FileInputStream(paramFile));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
}
public void insertInTransaction(String[] sqls)
{
Connection conn = null;
Statement stmt = null;
try{
Class.forName(driver);
conn = DriverManager.getConnection(url , user , pass);
conn.setAutoCommit(false); // 关闭自动提交,即开启事务
stmt = conn.createStatement();
for (String sql : sqls)// 循环多次执行SQL语句
{
stmt.executeUpdate(sql);
}
conn.commit();// 提交事务
}catch (Exception e){
e.printStackTrace();
if(conn != null){
try{
conn.rollback(); //撤回
}catch (SQLException ex){
ex.printStackTrace();
}
}
}
finally {
if(conn != null){
try{
if(stmt != null)
stmt.close();
conn.setAutoCommit(true); //恢复自动提交
conn.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
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);
}
}
Connection的roolBack()是回退到事务开始之前的数据状态,也可以回退到指定的点:
try{
......
stmt.executeUpdate("insert into ...");
Savepoint point = conn.setSavepoint(); //设置存储点
stmt.executeUpdate("insert into ...");
......
}catch (Exception e){
e.printStackTrace();
if(conn != null){
try{
if(point == null){
conn.rollback();
}else{
conn.rollback(point); //撤回存储点
conn.releaseSavepoint(point); //释放存储点
}
}catch (SQLException ex){
ex.printStackTrace();
}
}
}
如果需要大量数据更新的话,一次只更新一条数据的话是很慢的,JDBC提供了批量更新的功能,使多条SQL语句可以被同时提交,为了让批量操作可以正确的处理错误,应该把批量操作作为单个事务。需要注意的是批量更新的addBatch()方法中不能有select查询语句,否则会出现错误。对于MySQL,要支持批量更新的话还需要在JDBC URL上添加"rewriteBatchedStatements=true":
boolean autoCommit = conn.getAutoCommit(); -- 保存当前的提交模式
conn.setAutoCommit(false); -- 开启事务
Statement stmt = conn.createStatement();
stmt.addBatch(sql1); --使用addBatch()收集多条SQL语句
stmt.addBatch(sql2);
stmt.addBatch(sql3);
......
stmt.executeBatch(); --同时提交所有的SQL语句,如果SQL语句中影响的记录条数会超过Integer.MAX_VALUE应该使用executeLargeBatch(),但现MySql的驱动暂不支持该方法。其返回int[],代表每笔SQL造成的数据异动列数,出错抛出的异常类型为BatchUpdateException,该类型的getUpdateCounts()方法可以获得执行成功的SQL所造成的异动笔数。
conn.commit(); --提交事务
conn.setAutoCommit(autoCommit); -- 恢复原来的自动提交模式
事务(又称交易)具有四个基本要求:原子性(事务中多个步骤全部成功则为成功,有一个失败即为失败)、一致性(事务成功表示所有步骤全部成功,事务失败则应该回滚到所有步骤之前的状态)、隔离行为、持续性(交易成功则所有变更必须保持下来,即使系统挂了,交易结果也不能遗失,这通常需要软硬件的支持)。
事务的隔离行为可以通过Connection的setTransactionIsolation()方法来设定,其中有五种隔离级别,如下所示,getTransactionIsolation()方法用来获得目前的隔离级别设定。
TRANSACTION_NONE:不设定任何隔离行为,对于支持事务的数据库来说可能不会理会该设置。
TRANSACTION_READ_UNCOMMITED(可读取未确认):在交易中对数据进行了修改后,其它用户的交易中只能做读取,若想要对数据进行修改,必须等到当前交易结束。指定该选项后可避免“更新遗失”现象,如下所示,数据原为ZZZ,当事务A更新数据为OOO还未关闭事务的时候,事务B又更新数据为XXX,然后事务B撤销后数据又还原为了ZZZ,对于事务A来说发生了更新遗失。数据库一般不会默认采用该隔离级别,因为该选项不能避免下面所说的“脏读”问题。
TRANSACTION_READ_COMMITTED(可读取确认):在交易中对数据进行了修改后,其它用户的交易中若想要对数据进行读取(包括修改),必须等到当前交易结束。指定该隔离级别后可避免“脏读”现象,如下所示,交易A修改数据为OOO后还未关闭交易,然后交易B查询数据为OOO后交易A又撤销了交易,对于交易B来说读取到了脏数据。执行该选项的话也会避免上面的“更新遗失”现象。因为该隔离级别对性能影响较大,所以指定该选项后数据库的动作实际上依赖于各家数据库的实现,交易A开始之前会暂存一个表格,以后交易A是在暂存的表格上进行,或者是交易B是在暂存表格上进行,这样也可以避免读取到不干净的数据。
TRANSACTION_REPEATABLE_READ(可重复读取):在交易中对数据进行了读取后,其它交易只能读取该数据,若想要对数据进行修改,必须等到当前交易结束(该选项对效能影响较大,数据库可能会选择其它合适的方法而不用等待交易结束,比如其它用户交易的修改会在暂存的表格上进行),指定该隔离级别后可避免“无法重复读取”现象。如下所示,交易A查询数据为ZZZ后交易B修改数据为OOO,交易A再次查询数据就变成了OOO,这就造成了交易A两次读取的数据有差异,也被叫做“无法重复读取”现场,执行该隔离级别的话也会避免上面的“更新遗失”现象和“脏读”现象。因为该隔离级别能影响较大,所以指定该选项后数据库的动作实际上依赖于各家数据库的实现,交易A开始之前会暂存一个表格,以后交易A是在暂存的表格上进行,或者是交易B是在暂存表格上进行。实际测试发现对于MySql,默认采取的是该策略,而且交易A是在暂存的表格上进行的,因为在交易A中一开始查询到数据为ZZZ,然后交易B修改数据为OOO后可以正常结束而不用等待交易A结束,交易A再次查询数据依然为ZZZ,当交易A结束后再次查询数据为OOO。
TRANSACTION_SERIALIZABLE(可循序):所有用户的交易都是一个一个的循序执行,不允许交叉执行用户的交易(该隔离级别对效能影响较大,数据库可能会选择其它合适的方法,比如采取暂存表格方式),这可以避免“幻读”现象,比如交易A读取到数据为ZZZ后交易B修改数据为OOO,交易A再次读取数据的话还是ZZZ,而交易B读取的话就为OOO,在同一交易期两个交易读取到的数据不同,这就是“幻读”。执行该隔离级别的话会自动避免上面的“更新遗失”、“脏读”、“无法重复读取”现象。执行该选项相当于是在交易前加锁,交易后释放锁,比如对于银行取钱业务,必须是使用锁机制,避免交易A在查询余额不为0后交易B先执行了取款动作,此时余额实际为0,而后交易A又执行了取款动作,导致了不正常的取款。
以下为在MySql开启“可循序”的隔离级别后对应的动作:①、交易A开启后什么都不做,交易B可读取或写入,不必等待交易A结束。
②、交易A开启后进行读取,交易B可进行读取,不必等待交易A结束。
③、交易A开启后对指定数据进行读取,交易B对该数据写入的话会等待,直到交易A结束(如果交易B写入数据地方与交易A读取地方不同的话,会直接写入不等待)。
④、交易A开启后对指定数据进行写入,交易B读取该数据的话会等待,直到交易A结束(如果交易B读取数据地方与交易A写入地方不同的话,会直接读取不等待)。
⑤、交易A开启后对指定数据进行写入,交易B对该数据写入的话会等待,直到交易A结束(如果交易B写入数据地方与交易A写入地方不同的话,会直接写入不等待)。
3、数据库信息
Metadata元数据,即诠释数据的数据,例如数据库名,数据库中表格个数、名称,表格中的字段个数、名称、类型、可否为空等。可以通过Connection提供的getMetaData()方法获得数据库对应的DatabaseMetaData对象,通过该对象的成员方法获得数据库的相关信息,如supportsBatchUpdates()接口用来查看底层数据库是否支持批量更新,getTables()获得当前数据库的所有表,getPrimaryKey()获得指定表的主键,getProcedures()获得当前数据库的所有存储过程。 ResultSet中的getMetaData可以获得一个ResultSetMetaData类型的对象,该对象包含了大量描述ResultSet信息的方法,比如getColumnCount()获取ResultSet中列的数量,getColumnName()获得指定索引的列名,getColumnType()获得指定索引列的类型。
还可以通过系统表来获得数据库的信息,MySQL中的information_schema数据库保存了系统表,常用的表如下所示,想要查询对应信息可以对指定的表进行select查询:
4、数据库连接池
为避免频繁打开、关闭数据库连接带来的性能低下,可以使用连接池,连接池会提前建立一定的数据库连接,程序请求数据库连接时直接从池中取出已有连接使用,使用完毕再归还给连接池,如下图所示:
JDBC连接池有一个标准的接口javax.sql.DataSource,
要使用JDBC连接池,我们必须选择一个JDBC连接池的实现,常用的JDBC连接池有 HikariCP、C3P0、DBCP等,C3P0不仅可以自动清理不再使用的Connection,还可以自动清理Statement和ResultSet。
如下的代码示例,SimpleConnectionPoolDataSource操作了接口DataSource,实现了简单的连接池。首先从配置文件(配置文件内如可以为如下所示)读取JDBC URL、连接池最大连接数等配置信息,然后可以通过getConnection()方法可以获得一个连接,当这个连接不再被使用的时候就把它加入到连接池中以备用而不是关闭该连接,如果连接池达到了最大的话则真正关闭这个连接:
//配置文件内容
cc.openhome.url=jdbc:mysql:jdbc::mysql://localhost:3306/demo?user=root&password=123
cc.openhome.poolmax=10
下面代码摘自《Java JDK9学习笔记》,只有getConnection()为同步方法,个人认为应该使用一个同步锁来同步getConnection()方法和close()方法。
import java.util.*;
import java.io.*;
import java.sql.*;
import javax.sql.DataSource;
class SimpleConnectionPoolDataSource implements DataSource{
private List<Connection> conns; //维护可重用的Connection对象
private int max; //连接池中最大Connection数目
private String url; //JDBC URL
public SimpleConnectionPoolDataSource()throws IOException, ClassNotFoundException{
this("jdbc.properties"); //默认从jdbc.properties文件读取配置信息
}
public SimpleConnectionPoolDataSource(String configFile/*从指定文件读取配置信息*/)throws IOException, ClassNotFoundException{
Properties props = new Properties();
props.load(new FileInputStream(configFile));
url = props.getProperty("cc.openhome.url");
max = Integer.parseInt(props.getProperty("cc.openhome.poolmax"));
conns = Collection.synchronizedList(new ArrayList<>());
}
public synchronized Connection getConnection()throws SQLException{
if(conns.isEmpty()){
return new ConnectionWrapper(DriverManager.getConnection(url), conns, max);
}else{
return conns.remove(conns.size() - 1);
}
}
private class ConnectionWrapper implements Connection{
private Connection conn;
private List<Connection> conns;
private int max;
public ConnectionWrapper(Connection conn, List<Connection>conns, int max){
this.conn = coon;
this.conns = conns;
this.max = max;
}
@Override
public void close()throws SQLException{
if(conns.size() == max){
conn.close();
}else{
conns.add(this);
}
}
@Override
public Statement createStatement()throws SQLException{
return conn.createStatement();
}
}
}
最小连接数:连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费。
最大连接数:连接池能申请的最大连接数,如果数据库连接请求超过此数,后面的数据库连接请求将被加入到等待队列中,这会影响之后的数据库操作。
最小连接数与最大连接数差距:最小连接数与最大连接数相差太大,那么最先的连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时后被释放。
//DBCP连接池设置
BasicDataSource ds = new BasicDataSource(); //创建数据源对象
ds.setDriverClassName("com.mysql.jdbc.Driver"); //设置连接池所需的驱动
ds.setUrl("jdbc:mysql://localhost:3306/javaee"); //设置连接数据库的URL
ds.setUsername("root");
ds.setPassword("pass");
ds.setInitialSize(5); //设置连接池的初始连接数
ds.setMaxActive(20); //设置连接池最多可有多少个活动连接数
ds.setMinIdle(2); //设置连接池中最少有2个空闲的连接
//C3P0连接池设置
ComboPooledDataSource ds = new ComboPooledDataSource(); //创建数据源对象
ds.setDriverClassName("com.mysql.jdbc.Driver"); //设置连接池所需的驱动
ds.setJdbcUrl("jdbc:mysql://localhost:3306/javaee"); //设置连接数据库的URL
ds.setUser("root");
ds.setPassword("pass");
ds.setMaxPoolSize(40); //设置连接池的最大连接数
ds.setMinPoolSize(40); //设置连接池的最小连接数
ds.setInitialPoolSize(10); //设置初始连接数
ds.setMaxStatements(180); //设置连接池的缓存Statement的最大数
//获取连接池中连接和关闭连接
Connection conn = ds.getConnection();
conn.close();