JavaWeb(九)JDBC——JDBC小工具

在学习完先前的知识后,会发现有多数的代码是重复以及Dao和Service层没有明确分开,为了更好理解JSP的MVC模式,有一个JDBC小工具,将其称为JdbcUtils。

目录

    Version1.0

    Version1.1

    Version2.0

    Version2.1

补充

    ExQueryRunner

后话


    Version1.0

JDBC连接不同得到数据库的区别只在于四大参数,即driverClass(驱动类)、url(数据库路径)、用户名以及密码。那么为了便利,则可以通过配置文件来连接不同的数据库,将这个配置文件命名为dbconfig,而后缀则为properties。如下所示:

    # properties里由键值对构成,键的名称可自定义
    driverClassName = com.mysql.jdbc.Driver
    url = jdbc:mysql://localhost:3306/student
    username = root
    password = 123456

通过Java中加载内部类资源的方式去加载配置文件以连接不同的数据库。其代码如下所示:

public class JdbcUtils {
    private static Properties props = null;

    // static块可以不用多次加载配置文件以及加载驱动类
    static {
        InputStream in = JdbcUtils01.class.getClassLoader().
                        getResourceAsStream("dbconfig.properties");
        props = new Properties();
        try {
            props.load(in);
            Class.forName(props.getProperty("driverClassName"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
	
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(props.getProperty("url"), 
                    props.getProperty("username"), props.getProperty("password"));
    }
}

使用例子如下所示:

    @Test
    public void add() {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = JdbcUtils.getConnection();
            String sql = "insert into message values(?, ?, ?)";
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, "101");
            pstmt.setString(2, "wangwu");
            pstmt.setInt(3, 20);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if(pstmt != null) pstmt.close();
                if(con != null) con.close();
            } catch (SQLException e1) {
                throw new RuntimeException(e1);
            }
        }
    }

通过例子发现,会发现只要每次对数据库操作,就需要调用一次getConnection()方法来获取一个新的连接,这大大降低了程序的运行效率。在前一章学习了连接池,那么就可以对这个小工具进行再次更新。

    Version1.1

根据1.0版本中提及的问题,将小工具重构。运用连接池的性质,来获取连接。而连接池也是需要配置文件的,以C3P0连接池为例,配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>  
<c3p0-config>
    <default-config>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/student</property>
        <property name="user">root</property>
        <property name="password">123456</property>
		
        <property name="initialPoolSize">10</property>
        <property name="acquireIncrement">5</property>
        <property name="minPoolSize">4</property>
        <property name="maxPoolSize">20</property>
    </default-config>
</c3p0-config>

小工具代码如下所示:

 

这个小工具在讲解连接池的时候已经提及过,而在使用这个小工具过程中,会发现事务开启关闭都在DAO中,其实这是不对的,因为每一个Dao中的方法都是对数据库的一次操作,而service层中的方法才对应一个业务。这句话的意思就是,由service层的一个方法调用多个Dao中的多个方法。即如下所示:

public class Service() {
    private Dao dao = new Dao();
    public void method1() {
        con = JdbcUtils.getConnection();
        con.setAutoCommitted(false);
        dao.method1(con, ...);
        dao.method2(con, ...);
        con.commmit();
    }
}

说到这里,就会有一个问题产生,根据“一个事务是通过同一个连接完成的”,如何保证多个方法使用相同的Connection呢?那么就得通过外界传递Connection进去,而不是在每一个方法中获得Connection。但这里又有一个问题,在service层中出现了不应该出现的Connection连接对象。直白地说,在service层不应该出现跟sql有关的包。

那么这里就将事务的开启和关闭放到了小工具里以解决这个问题。

    Version2.0

根据上述的描述,我们将JdbcUtils更新一下,使service层中的代码变成如下:

public class Service() {
    private Dao dao = new Dao();
    public void method1() {
        JdbcUtils.beginTransaction();
        dao.method1(con, ...);
        dao.method2(con, ...);
        JdbcUtils.commitTransaction();
    }
}

根据上面这个代码,为了保证同一个事务中所有对数据库的操作必须由同一个连接完成,那么就得在小工具中创建一个专用于事务连接的连接对象,并且在开启事务的时候创建这个连接。相应即如下所示:


    // 事务专用Connection
    private static Connection con = null;

    public static void beginTransaction() throws SQLException {
        if(con != null) throw new SQLException("事务已开启,不要重复开启"); 
		
        con = getConnection();
        con.setAutoCommit(false);
    }
	
    public static void commitTransaction() throws SQLException {
        if(con == null) throw new SQLException("还未开启事务,无法提交"); 
        
        con.commit();
        con.close();
        /*
         * close方法在这里是关闭连接
         * 如果不设置connection为null的话,在想使用事务时
         * 调用getConnection方法则依旧返回一个已经关闭的连接
        */
        con = null; // 设置为null表示事务已经结束
    }

    public static void rollbackTransaction() throws SQLException {
        if(con == null) throw new SQLException("还未开启事务,无法回滚"); 

        con.rollback();
        con.close();
        con = null;
    }

现在更改Dao中的方法,如下所示:

public static void update(String name, double money) throws SQLException {
        QueryRunner qr = new QueryRunner();
        String sql = "update account set balance=balance+? where name=?";
        Object[] params = {money, name};
		
        //自己提供连接 保证多次调用使用的是同一个连接
        Connection con = JdbcUtils.getConnection();
        qr.update(con, sql, params); 
		
}

这里产生了一个问题:在Dao里获取的连接对象connection是否能够使用close方法?

前面就说过,Dao中的方法是只对数据库的一次操作,事务的开启是在service层中。如果关闭连接,那么在这里就会出现一个问题:如果事务还未提交时,在update方法第一次调用的时候connection对象已经关闭连接,那么第二次调用同一个连接对象时就会抛出异常。

那么必须通过小工具来判断是否需要关闭这个连接,即判断这个连接是否是事务专用连接。如下所示:

    public static void releaseConnection(Connection connection) throws SQLException {
        // 如果con为null 说明此时没有事务 直接关闭
        if(con == null) connection.close()
        // 如果con不为null 说明有事务,那么需要判断参数连接是否与con相等
        if(con != connection) connection.close(); 
    }

这样,就不需要我们去自己考虑是否需要关闭连接,由工具自己决定是否关闭连接。

将代码汇总,如下所示:

public class JdbcUtils02 {
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource();

    // 事务专用Connection
    private static Connection con = null;
	
    public static ComboPooledDataSource getDataSource() {
        return dataSource;
    }

    public static Connection getConnection() throws SQLException {
        // 保证事务的connection对象相同 不等于null说明已经开启事务
        if (con != null) return con;
        return dataSource.getConnection();
    }
	
    public static void beginTransaction() throws SQLException {
        if(con != null) throw new SQLException("事务已开启,不要重复开启"); 
		
        con = getConnection();
        con.setAutoCommit(false);
    }
	
    public static void commitTransaction() throws SQLException {
        if(con == null) throw new SQLException("还未开启事务,无法提交"); 
		
        con.commit();
        con.close();
        /*
         * close方法在这里是关闭连接
         * 如果不设置connection为null的话,在想使用事务时
         * 调用getConnection方法则依旧返回一个已经关闭的连接
         * 设置为null表示事务已经结束
        */
        con = null;
    }

    public static void rollbackTransaction() throws SQLException {
        if(con == null) throw new SQLException("还未开启事务,无法回滚"); 
		
        con.rollback();
        con.close();
        con = null;
    }
	
    public static void releaseConnection(Connection connection) throws SQLException {
        // 如果con为null 说明此时没有事务 关闭
        if(con == null) connection.close();
        // 如果con不为null 说明有事务,那么需要判断参数连接是否与con相等
        if(con != connection) connection.close(); 
    }
}

在这里又出现了新的问题,若开启事务的方法被多个线程同时调用,就会抛出异常。或者一个线程调用开启事务,另一个线程提交了事务,轮到第一个线程提交线程也会抛出异常。其实这个问题就是并发问题,为了保证每一个线程用自己的connection,就得运用到一个东西ThreadLocal。

在这里先提及一下ThreadLocal,ThreadLocal是一个工具类,其是为了解决多线程的并发问题。ThreadLocal会为每个使用该变量的线程提供独立的副本,所以每一个线程都可以独立地改变自己的变量,而不会影响其它线程所对应的变量。ThreadLocal只有四个方法,分别是set、get、remove、initialValue方法,分别用于设置、获取、删除当前线程的线程局部变量的值以及返回线程局部变量的初始值。

    Version2.1

为了解决并发问题,需要将事务专用的Connection对象用ThreadLocal维护,即如下所示:

    private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

那么相应的,就得对所有涉及到Connection的方法进行改动。小工具最终代码如下所示:

public class JdbcUtils {
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource();

    // 事务专用Connection
    private static ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
	
    public static ComboPooledDataSource getDataSource() {
        return dataSource;
    }

    public static Connection getConnection() throws SQLException {
        // 保证事务的connection对象相同 不等于null说明已经开启事务
        Connection con = tl.get();

        if (con != null) return con;
        return dataSource.getConnection();
	}
	
    public static void beginTransaction() throws SQLException {
        Connection con = tl.get();
        if(con != null) throw new SQLException("事务已开启,不要重复开启"); 
		
        con = getConnection();
        con.setAutoCommit(false);
		
        tl.set(con);
    }
	
    public static void commitTransaction() throws SQLException {
        Connection con = tl.get();		
        if(con == null) throw new SQLException("还未开启事务,无法提交"); 
		
        con.commit();
        con.close();
        /*
         * close方法在这里是关闭连接
         * 如果不设置connection为null的话,在想使用事务时
         * 调用getConnection方法则依旧返回一个已经关闭的连接
         * 设置为null表示事务已经结束
        */
        tl.remove();
    }

    public static void rollbackTransaction() throws SQLException {
        Connection con = tl.get();		
        if(con == null) throw new SQLException("还未开启事务,无法回滚"); 
		
        con.rollback();
        con.close();
        tl.remove();
    }
	
    public static void releaseConnection(Connection connection) throws SQLException {
        Connection con = tl.get();		
        // 如果con为null 说明此时没有事务 关闭
        if(con == null) connection.close();
        // 如果con不为null 说明有事务,那么需要判断参数连接是否与con相等
        if(con != connection) connection.close(); 
    }
}

补充

    ExQueryRunner

 

后话

    '''
        小工具暂时写到这里,若还有遇到什么问题再继续更新
    '''

 

上一篇:JDBC数据库连接时发生时区错误


下一篇:JAVA实训7-常见异常