MySQL数据库与JDBC编程(四)

Java 7的RowSet 1.1

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的处理有如下两种常见方式。

  1. 使用迭代访问ResultSet里的记录,并将这些记录转换成Java Bean,再将多个Java Bean封装成一个List集合,也就是完成ResultSet——>Java Bean集合的转换。转换完成后可以关闭Connection等资源,然后将Java Bean集合传到视图显示层,视图显示层可以显示查询得到的数据。
  2. 直接将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个特性

  1. 原子性(Atomicity):事务是应用中最小的执行单位,就如原子是自然界的最小颗粒,具有不可再分的特征一样,事务是应用中不可再分的最小逻辑执行体。
  2. 一致性(Consistency):事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。
  3. 隔离性(Isolation):各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务都是隔离的,也就是说,并发执行的事务之间不能看到对方的中间状态,并发执行的事务之间不能互相影响。
  4. 持续性(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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章