在学习完先前的知识后,会发现有多数的代码是重复以及Dao和Service层没有明确分开,为了更好理解JSP的MVC模式,有一个JDBC小工具,将其称为JdbcUtils。
目录
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
后话
'''
小工具暂时写到这里,若还有遇到什么问题再继续更新
'''