1. RowSet系列、创建RowSet实例:
1) RowSet的出现最初是为了解决离线缓存的问题,因为在使用ResultSet的时候必须保证在线(即保持与数据库的连接),连接后必须立即处理,否则连接断开则ResultSet也将关闭,这就非常麻烦,再有些设计模式中,要求逻辑和视图分开,因此就必须自己先开一段内存暂存ResultSet中的内容(这样ResultSet关闭了里面的数据已经拿出来了),然后再交给视图层显示,而不能直接将ResultSet作为视图层的绘图数据使用;
2) 但现在RowSet的标准已经更加全面和完善,与ResultSet相比RowSet默认就是可滚动、可更新、可序列话的,无需打开任何开关,并且操作上比ResultSet更加简单;
!!最重要的是RowSet直接被设计成JavaBean,因此可以方便地用于网络传输、MVC设计模式,而离线缓存更是让其如虎添翼;
3) RowSet在Java中只是一个上层类(抽象类),实际中有很多RowSet的具体实现来适应不同环境中的应用:
i. 首先是ResultSet的直接扩展——JdbcRowSet:直接继承于RowSet,仅仅就是ResultSet的扩展,和ResultSet一样不是离线的,使用时必须保持在线状态!
ii. 另一大分支(离线RowSet)——CachedRowSet:名字就叫离线缓存RowSet,也直接继承于RowSet,比JdbcRowSet更进一步,支持离线缓存;
iii. 从CachedRowSet派生出了应用于各种场景的离线RowSet:WebRowSet(支持网络数据,直接继承于CachedRowSet)、JoinRowSet(继承于WebRowSet)、FilteredRowSet(继承于WebRowSet);
4) RowSet的实现仍然在时刻发展着:
i. 现代IT环境迭代迅速,RowSet为了不断适应新的环境,JDK提供的实现也是时不时地更新(RowSet的标准也仍然在不断完善中),至今还没有一个彻底稳定、固定的实现版本;
ii. 目前JDK也只是暂时给各种RowSet提供了一个实现类,类名就是相应RowSet名后加一个Impl后缀(即Implement的缩写),例如:JdbcRowSetImpl、CachedRowSetImpl等;
iii. 这些实现类都是JDK未公开的API,在未来可能删除用新的代替(因此名称也许会彻底改变),虽然可以直接在程序中使用,但是使用后编译器会发出警告,如果这些API一旦发生更新,那程序就非常不好维护和升级;
5) 因此Java提供了RowSetFactory和RowSetProvider使RowSet的实现和应用程序彻底分离,这样更有利于程序后期的升级和扩展:
i. 要获取具体的RowSet实例就必须先通过RowSetProvider(即RowSet供应商)提供一个可以制造RowSet的工厂(RowSetFactory实例),然后再用工厂来制造具体的RowSet实例:
ii. 上述过程中用到的方法:
a. static RowSetFactory RowSetProvider.newFactory(); // 静态方法,创建一个工厂实例
b. XxxRowSet RowSetFactory.createXxxRowSet(); // 对象方法,用工厂创建一个具体的RowSet实例
!!Xxx可以是Jdbc、Cached、Web、Join、Filtered的任意一种,都是具体的RowSet实现类;
iii. 示例:
RowSetFactory factory = RowSetProvider.newFactory(); CachedRowSet crs = factory.createCachedRowSet();
2. 如何关联RowSet和数据库的连接:
1) 从上面可以看到,只能通过RowSetProvider和RowSetFactory的方式创建RowSet实例,但这两个方法中并没有让RowSet和数据库建立连接!!
2) 两种建立连接的方式:这些方法都是在上层抽象类RowSet中定义的,因此其所有派生类(Jdbc、Web、Cached等也都拥有)
i. 直连:直接让RowSet连接数据库,不再需要用Connection连接了,这是最常用的,简化了很多步骤!!因此在这里可以看到RowSet的强大!
a. 提供了setUrl、setUsername、setPassword方法设置数据库连接三要素;
b. void setUrl(String url);
c. void setUsername(String name);
d. void setPassword(String password);
e. 三要素设置好之后就完成连接了!
ii. 被动地包装ResultSet:void populate(ResultSet data);
!!这种方式需要先用传统的Connection方法建立连接、获得SQL语句句柄并执行返回ResultSet后再用populate将其包装成可滚动、可更新、可序列化的同时也是JavaBean的RowSet;
!!可以看到过程非常麻烦,但还是很有用武之地的,这个后面会解释;
3. 如果采用直连法让RowSet连接数据库,那么就需要让其执行SQL语句(这样才能得到想要的查询结果呀!):
1) 首先需要设置RowSet要执行的SQL语句:void setCommand(String cmd); // cmd就是要执行的SQL语句,既然是和ResultSet是一脉相承的,那必然也只能执行Query语句!
2) 接着就是执行查询了:void execute(); // 很简单,就是直接(提交)执行
!!注意:RowSet直接执行SQL语句是不支持预编译的,都是直接提交执行的!!
!!因此需要对查询预编译还是得走ResultSet的老路,然后用populate包装成RowSet;
!!因此有预编译需求的还是得先走ResultSet的老路;
4. 结果集的分析和修改:
1) 完全和ResultSet一模一样!!!
2) 记录指针定位方法一模一样(next、previous、afterlast等);
3) 获取列值的方法一模一样(getXxx(可以用列索引指定也可以用列名指定));
4) 修改结果集并更新到数据库的方法也一模一样(updateXxx在结果集中临时修改、updateRow将修改更新到数据库中);
5. 离线状态下如何将修改更新到真实数据库?
1) 如果处于在线状态,那么调用完updateRow之后会立即更新到数据库,但是如果RowSet已经离线了,那么及时updateRow也无法更新到数据库中,因为此时连接已经断开;
2) 因此首先必须得重新连接数据库,还是使用DriverManager.getConnection的那一套重新连接;
3) 然后调用RowSet的acceptChanges方法(将重新连接的连接句柄conn作为参数)接受RowSet的修改(将更新同步到数据库):
i. void RowSet.acceptChanges(Connection con); // 将RowSet中对记录的更改同步到conn所连接的数据库中
!!还有一种是无参版本的:
ii. void RowSet.acceptChanges();
a. 此版本在没有离线的状态下使用,因此无需指定连接句柄;
b. 但是在线状态下直接updateRow不就自动同步到数据库了吗?为啥还需要再调用acceptChanges呢?
c. 设想,每次updateRow就要同步一遍数据库(底层其实是构造了一个SQL查询并提交执行),那如果这样的修改非常非常多呢?(特别是在段时间内要执行很多次)那岂不是效率太低了吗?能不能一次将所有的更改一步到位批量送入数据库进行更新呢?
d. 答案是肯定的,你可以先通过Connection的setAutoCommit方法关闭自动提交的功能:void Connection.setAutoCommit(boolean autoCommit); // true开启(默认),false关闭
!!关闭后每次调用updateRow就不会立即同步到数据库,而是把此次更新送入批处理队列中,知道调用无参版的accpetChanges之后才会一次性将批处理队列中的所有更新全部提交到数据库运行!
4) 批处理更新示例(在线状态下,离线状态重连后也必须关掉自动提交才能实现批处理):
conn.setAutoCommit(false); // 一定要在更新语句之前关闭! 多次updateXxx、updateRow; rs.acceptChanges();
5. 示例:以CachedRowSet为例
public class Test { private String driver; private String url; private String user; private String pass; public void initParam() throws FileNotFoundException, IOException { Properties props = new Properties(); props.load(new FileInputStream("mysql.ini")); driver = props.getProperty("driver"); url = props.getProperty("url"); user = props.getProperty("user"); pass = props.getProperty("pass"); } public CachedRowSet query(String sql) throws ClassNotFoundException, SQLException { Class.forName(driver); Connection conn = DriverManager.getConnection(url, user, pass); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql); RowSetFactory factory = RowSetProvider.newFactory(); CachedRowSet crs = factory.createCachedRowSet(); crs.populate(rs); // 显式关闭所有连接资源 rs.close(); stmt.close(); conn.close(); return crs; // 这样返回的RowSet仍然能用,说明被离线缓存了 } public void init() throws FileNotFoundException, IOException, ClassNotFoundException, SQLException { initParam(); CachedRowSet rs = 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", "QQQ"); rs.updateRow(); } } // 重连,同步到真实数据库 Connection conn = DriverManager.getConnection(url, user, pass); conn.setAutoCommit(false); // 先不管,这个跟事务管理有关 rs.acceptChanges(conn); // 由于之前连接资源已断,因此要调用有参版本的 } public static void main(String[] args) throws FileNotFoundException, ClassNotFoundException, IOException, SQLException { new Test().init(); } }
1) 首先补充装填的概念:
i. RowSet调用populate包装ResultSet的过程就叫装填,即直接将ResultSet连接数据库返回的全部结果集装填到RowSet中;
ii. 如果是可离线缓存的RowSet,那么这种装填会将全部结果一次性全部装入内存缓冲区中;
iii. 设想,如果结果的量非常庞大,那么RowSet将会占用庞大的内存,不仅降低效率甚至容易导致内存溢出!!
2) 因此RowSet提供了分页控制方法来限定你每次可装填的内容的大小,让你多次分页装填记录并分析;
3) 首先你需要设置每页的大小(即每次可以装填多少条记录):void RowSet.setPageSize(int size); // size即页的大小(单位是条记录),默认状态下是无限条(即有多少装多少)
4) 接着就是装填了:
i. void populate(ResultSet data); // 默认从第一条记录开始装填一页的大小
ii. void populate(ResultSet rs, int startRow); // 指定从第startRow条记录开始装填一页的大小
!!要现调用setPageSize设定的页的大小,否则就默认从指定记录开始装填到整个ResultSet的最后一条记录
5) 定位页:定位到要访问的页,其实就是将要访问的页自动装填到当前RowSet中
i. boolean RowSet.nextPage(); // 将当前页的下一页装填到RowSet中,如果当前已经是最后一页了则返回false
ii. boolean previousPage(); // 装载上一页,如果已经是第一页了则返回false
6) 示例:
public class Test { private String driver; private String url; private String user; private String pass; public void initParam() throws FileNotFoundException, IOException { Properties props = new Properties(); props.load(new FileInputStream("mysql.ini")); driver = props.getProperty("driver"); url = props.getProperty("url"); user = props.getProperty("user"); pass = props.getProperty("pass"); } // pageSize是页大小,page表示从第几页开始装载 public CachedRowSet query(String sql, int pageSize, int page) throws ClassNotFoundException, SQLException { Class.forName(driver); Connection conn = DriverManager.getConnection(url, user, pass); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql); RowSetFactory factory = RowSetProvider.newFactory(); CachedRowSet crs = factory.createCachedRowSet(); crs.setPageSize(pageSize); crs.populate(rs, (page - 1) * pageSize + 1); // 所有连接资源统统关闭 rs.close(); stmt.close(); conn.close(); return crs; // 这样返回的RowSet仍然能用,说明被离线缓存了 } public void init() throws FileNotFoundException, IOException, ClassNotFoundException, SQLException { initParam(); CachedRowSet rs = query("select * from student_table", 3, 2); // 每页3条记录,查询第2页的内容 rs.afterLast(); // 可滚动 while (rs.previous()) { System.out.println( // 解析 rs.getString(1) + '\t' + rs.getString(2) + '\t' + rs.getString(3) ); } // 重连,同步到真实数据库 Connection conn = DriverManager.getConnection(url, user, pass); conn.setAutoCommit(false); rs.acceptChanges(conn); // 由于之前连接资源已断,因此要调用有参版本的 } public static void main(String[] args) throws FileNotFoundException, ClassNotFoundException, IOException, SQLException { new Test().init(); } }