JDBC、数据库连接池与DBUtils工具学习笔记

1.1 JDBC概述

  JDBC全称是Java数据库连接(Java Database Connection)。它是一套用于执行SQL语句的API。之前学习的反射、内省和BeanUtils是用于控制JavaBean对象的【详见:JavaBean组件学习笔记】而这里的内容主要是用于控制数据库的(虽然免不了也会涉及控制JavaBean及访问属性,但是这种情况下一般用getter和setter)
  下面两张图表示的都是Java程序连接数据库的过程,但是侧重点有所不同。上面的图侧重于JDBC的实现,而下面的图侧重于JDBC API部分的内容。



  在JDBC的实现部分,主要需要JDBC驱动管理器(主要通过java.sql.DriverManager类实现)、JDBC驱动器(主要接口是java.sql.Driver类)。

DriverManager.registerDriver(Driver driver);
//getConnection()中三个参数分别对应于数据库url、登录数据库的用户和密码。
Connection conn = DriverManager.getConnection(String url,String user,String pass);

  注:为了避免数据库驱动被重复注册,只需要在程序中加载驱动类,将上面代码替换为:

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(String url,String user,String pass);

  这其实就是用到了反射的知识。得到了一个Class类的实例对象。当然后续并不需要用到这个对象,所以不用将它赋值给变量。并且forName()方法初始化Driver的静态方法和变量,而Driver类中的静态代码块就已经完成了数据库驱动的注册
   到这里已经创建好了数据库连接

  接下来轮到Connection、Statement、PreparedStatement、ResultSet等JDBC API出场。它们的出场方式好像是接力,Connection对象获取Statement对象,Statement对象又获取ResultSet对象。
DriverManager -->Connection --> Statement -->ResultSet
(如果执行后的结果不返回值那么就不需要ResultSet,例如单纯执行修改操作时就不需要ResultSet对象)

Connection conn= DriverManger.getConnection(url,username,password);
Statement stmt = conn.createStatement();
//sql是我们定义好的sql语句,例如“select * from users”
ResultSet rs = stmt.executeQuery(sql) 

  但是如果每一条语句都执行一次stmt.executeQuery()方法,数据库频繁编译相同的SQL语句,就会降低数据库的访问效率。这其实跟我们读取文件(输入输出流)的情况有些类似。一个一个字节地读取,效率很低。可以使用addBatch(String sql)和executeBatch()方法进行批处理

//stmt是Connection对象创建的Statement实例
String sql1 ="..."
String sql2 = "..."
stmt.addBatch(sql1);
stmt.addBatch(sql2);
stmt.executeBatch();

除了Statement接口外,还有PreparedStatement接口,它可以对SQL语句进行预编译(适用于sql语句需要传递参数的场合)

Connection conn =null;
PreparedStatment preStmt = null;
String sql = "INSERT INTO users(name,password,email,birthday)" + "VALUES(?,?,?,?)";
//创建执行sql语句的PreparedStatement对象
preStmt = conn.prepareStatement(sql);
    preStmt.setString(1,"zl");
    ...
    preStmt.setString(4,"1989-12-23")
preStmt.executeUpdate();

  而CallableStatement接口是PreparedStatement的子接口,用于执行SQL存储过程。通俗点说,存储过程就是在Mysql中创建了一段程序。例如先使用SQL语句手动创建一个add_pro存储过程add_pro(a INT,b INT,OUT SUM INT),并规定传出参数SUM = a + b。然后就可以:

cstmt = conn.prepareCall("call add_pro(?,?,?)");
cstmt.setInt(1,4);
cstmt.setInt(2,5);
//注册第三个参数为int类型
cstmt.registerOutParameter(3,Types.INTEGER);
cstmt.execute();

  上面的内容分别介绍了JDBC实现和JDBC API的内容。所谓的实现是指连接到数据库。然后才通过API对数据库进行操作。当然操作完毕后要手动释放资源,也就是将Conection、Statement等对象调用close()方法进行清空。


1.2 案例——JDBC的基本操作

目的:使用JDBC连接数据库后,实现数据库的基本操作(这里特指对Users表进行添加、查询、删除和更新操作)。
User + JDBCUtils + UsersDao
  由于涉及添加及查询操作,因此我们需要建立一个User类(添加不用说,肯定需要创建一个对象,然后将对象的属性作为变量,通过SQL对User表进行更改。查询时,为了保存查询到的结果,需要根据User类创建一个对象,同时需要费力将我们查询到的结果一一赋值给对应的属性,后面学到ResultSetHanlder,就可以自动地将查询结果赋值到对象的属性中这也是后面代码减少的其中一个原因)。

  首先将数据库的连接释放资源的功能整合到一个类JDBCUtils中,如果想要获得连接,就使用JDBCUtils.getConnection()方法,如果想要释放资源就使用JDBC.release()方法,并且release方法根据有没有结果集,需不需要关闭结果集,分为两参数和三参数两种。
release(Statement stmt,Connection conn)
release(ResultSet rs,Statement stmt,Connection conn)
  而UsersDao用于实现数据表中的增删改查操作具体的增删改查是通过Statement对象执行sql语句实现的,而如果sql语句中需要变量,那么就使用setter和getter方法从对象中获取(增和改操作需要从对象获取,而查询和删除的变量由用户决定,不需从对象获取)
  如何保证sql语句成功执行。statement对象执行executeUpdate()方法会返回一个int类型的值,表示数据库中受影响的记录的数目。下面的num>0即是对操作进行判断:

int num = stmt.executeUpdate(sql);
if(num > 0){
    return true;
}
return false;

1.3 JDBC处理事务

针对JDBC处理事务的操作,在Connection接口中,提供了三个相关的方法:
(1)setAutoCommit(boolean autoCommit),设置是否自动提交事务
(2)commit():提交事务。
(3)rollback():撤销事务。

  也就是说,Connection接口除了在上面提到的具有创建Statement语句的功能,还主要用于处理事务

pstmt1 = null;
pstmt2 = null;
conn = JDBCUtils.getConnection();
conn.setAutoCommit(false);
//创建sql语句,此处忽略内容
sql = ...
pstmt1 = conn.prepareStatement(sql);
pstmt1.executeUpdate();
sql2 = ...
pstmt2 = conn.prepareStatement(sql2);
pstmt2.executeUpdate();
//当sql和sql2两条sql语句都执行完毕,才使用conn提交事务
conn.commit();

注:这个案例只是示范了事务处理的过程。但该案例是通过直接对数据库进行运算来实现的,所以不需要一个Account类来对用户进行映射。1.7中会讲到如何用DBUtils的方式来处理事务,DBUtils的方式可以简单理解为只对数据库进行读取和写入,涉及运算的操作则是通过JavaBean对象来实现


1.4 数据库连接池

  每次创建和断开Connection对象都会消耗一定的时间和IO资源,我们现在把建立连接的任务放在JDBCUtils中,从其代码也可以看出来,每次建立连接都需要验证用户名和密码。
  而且从本文1.2 案例中的UsersDao类也可以看出来,无论是调用增删改查哪一个方法,都需要重新调用JDBCUtils的connection方法,也即是说每次使用增删改查,都需要验证用户名和密码,然后才能获得我们的数据库连接。
  而数据库连接池技术允许应用程序重复使用现有的数据库连接。而不是重新建立。

  在上文中,我们说过Connection对象是通过DriverManager类创建出来的。而数据库连接池使用的是DataSource接口来创建对象。说是由DataSource接口来实现,其实是由实现了DataSource接口的类(又称为数据源)来实现。那我们可以预料到, 这个数据源应该存储了建立数据库连接的信息。而不是像我们每次都把数据库连接信息存储在JDBCUtils中。 所以要使用数据源,我们只需要提前把数据库连接的信息告诉我们的数据源就可以了。
  这些验证信息可以放在配置文件中,根据配置文件中的信息创建了连接池以后,我们就可以从从连接池中选择连接,也就是说连接池里面的多个连接只需要读取配置文件时的一次验证
  常用的数据源有DBCP数据源C3P0数据源,先对DBCP数据源做一个介绍。单独使用DBCP数据源时,需要引入commons-dbcp.jar包,它是DBCP数据源的实现包。commons-pool.jar包则是DBCP数据源连接池实现包的依赖包。上面说过需要一个实现DataSource接口的类,而这个类BasicDataSource就在comons-dbcp包中。

public static DataSource ds = null;
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName("com.mysql.jdbc.Driver");
bds.setUrl("jdbc:mysql://localhost:3306/chapter02");
bds.setUsername("root");
bds.setPassword("itcast");
//设置连接池的参数
bds.setInitialSize(5);
bds.setMaxActive(5);
ds = bds

  这等同与是把JDBCUtils中的配置信息都转移到这里来了。
  除了这种方式以外,还可以用配置文件的方法(这里的配置文件使用的是.properties格式,读取时需要以文件输入流的形式进行读取, 而下面C3P0的配置文件使用的是xml格式,只需要通过配置文件中的<name-cofig>标签的值就可以读取相应的配置,方便很多。这里忽略DBCP数据源使用配置文件的内容)。
  接下来介绍C3p0数据源,C3P0是目前最流行的开源数据库连接池之一,著名的开源框架Hibernate和Spring都是使用该数据源。它用于实现DataSource接口的是实现类是ComboPooledDataSource。我们可以使用类似上面的代码的方法进行实现,也可以使用配置文件,这里我们介绍使用配置文件的方法。

<?xml version="1.0" encoding="UTF-8"?>
<c3p0-config>
    <default-config>
        <property name="user">root</property>
        <property name="password">itcast</property>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">
jdbc:mysql://localhost:3306/chapter02</property>
        <property name="checkoutTimeout">30000</property>
        <property name="initialPoolSize">10</property>
        <property name="maxIdleTime">30</property>
        <property name="maxPoolSize">100</property>
        <property name="minPoolSize">10</property>
        <property name="maxStatements">200</property>
    </default-config> 
    <named-config name="itcast">
        <property name="initialPoolSize">5</property>
            ...
        <property name="password">itcast</property>
    </named-config>
</c3p0-config>

读取的时候也很方便:

public static DataSource ds = null;
static {
    ComboPooledDataSource cpds = new ComboPooledDataSource("itcast");
    ds = cpds;
}

  如果有需要,再使用ds.getConnection()方法就可以获取连接。


1.5 DBUtils工具(ResultSetHandler案例)

  我们学习过BeanUtils,它用于对JavaBean属性的访问,主要是利用了BeanUtils类。而这里要学习的是DBUtils工具,主要用于操作数据库。DBUtiils工具包括但不仅包括DBUtils这个类,仅就DBUtils这个类而言,它对应的功能是上面提到的JDBCUtils,主要为如关闭连接、装载JDBC驱动程序之类的常规工作提供方法。
  我们知道JDBCUtils是我们手动编写的一个类,我们为其封装了获取和关闭连接两个功能。当时还没有考虑到数据库连接池的技术。
  而现在有了数据库连接池技术,连接部分不需要通过DBUtils这个类实现,而是通过ComboPooledDataSource类(参见1.4)来实现。所以JDBCUtils只需要关心关闭连接的事情。例如DBUtils这个类就具有close()、closeQuietly()等方法。

  除此以外,DBUtils工具还有QueryRunner类和ResultSetHandler接口。
ResultSetHandler用于处理ResultSet结果集。这是执行完SQL语句之后关闭连接之前进行的工作,是之前的JDBCUtils中没有涉及的功能。之前的示例(参见1.2)是通过极为原始的方法,也就是新建对象,然后将查询的结果一一赋值给新建对象的属性来展现结果。
  我们的Statement对象做的事情,DBUtils工具中的QueryRunner类也能够做到。那么
Connection---> Statement ---> ResultSet这里的每一步我们的DBUtils工具都插手了。

------> 课本案例:ResultSetHandler类的作用

目的:该案例虽然放在数据库连接池知识后面,但是目的并不是使用数据源方法来简化连接和提高效率。该案例重点在于展示ResultSetHandler对于query()结果的封装。因此本案例中数据库连接仍然沿用通过JDBCUtils.getConnection()的方法,而BaseDao更是由“增删改查”精简为“查”一种。
JDBCUtils + BaseDao + User
  当然,这时候就算我们把c3p0-config.xml配置好,两者也不存在什么冲突,当你访问c3p0-config.xml的时候,表明你想要以一种数据库连接池的方式来进行访问,而当你直接用JDBCUtils进行连接时,表明你访问时才创建连接,执行完毕就关闭连接

  而且实际案例中也没有使用QueryRunner类而是直接手动创建了一个BaseDao类,估计主要也是为了让读者了解QueryRunner类的实现原理。这里使用BaseDao,最终返回的是查询的结果,然后用不同的 ResultSetHanlder接口把查询的结果进行处理。
  例如使用BeanHanlder对查询结果进行处理,那么查询到的对象会被自动转化为JavaBean,如果我们需要读取它的属性,用getter和setter方法就可以了。


1.6 DBUtils 实现增删改查

C3p0Utils + DBUtilsDao + user
  经过了上面的示范作用,这里真正的就是使用一些比较便捷的工具(DBUtils框架)来实现增删改查。首先,不再需要JDBCUtils这个类了,而是用C3p0Utills来替代。代码如下:

public class C3p0Utils {
  private static DataSource ds;
  static{
      ds=new ComboPooledDataSource();
  }
  public static DataSource getDataSource() {
      return ds;
  }
}

注:和1.4中的代码非常类似
  接下来就是DBUtilsDao文件,1.5出于演示的需要,新建了BaseDao这个类,这个类使用JDBCUtils进行连接,现在我们使用的是c3p0数据源,不需要JDBCUtils了,那么剩下的BaseDao的(增删改查)功能就直接通过DBUtils工具中的QueryRunner来实现就行了通过下面一句代码就实现了获取连接,并且创建出 QueryRunner对象的功能。

QueryRunner runner= new QueryRunner(C3p0Utils.getDatasSource());

  DBUtilsDao中的每个(增删改查)方法也分别明确要采用哪个ResultSetHandler接口来处理结果。假如在实现查询单个对象的方法中,明确选择BeanHanlder来处理结果集,那么可以这样写:

 User user = (User)runner.query(sql,new BeanHanlder(User.class),new Object[]{id});

  这里再补充一句,queryRunner和之前的Statement对象相比,Statement如果是查询executeQuery(String sql)方法,如果是增删改executeUpdate(String sql)方法。类似地,queryRunner有query()和update()方法。前者都是一个参数(不用引入连接Connection对象作为参数,因为本身Statement就是从Connection对象创建出来的)。而queryRunner中的query()和update()方法可以有2~4个参数。例如可以选择使用哪个连接对象,这在处理事务中会用到(见1.7)。
  并且使用query方法还有一个好处,就是会自动处理PreparedStatment和ResultSet的创建和关闭。无需再手动释放资源。


1.7 DBUtils处理事务

JDBCUtils + AccoutDao + Business+ Account

  当要进行事务处理时,连接的创建和释放就需要程序员自己实现,而不能从数据源中获取。这是因为我们的事务最终需要conn.commit()这一语句来进行最终的提交。所以就必须保证这个conn是同一个conn。如果是从数据源获取conn,调用一次Dao方法可能就换一个conn,这样就不行。所以我们这里需要使用JDBCUtils来获取连接,保证使用同一个Connection对象进行提交。

QueryRunner runner = JDBCUtils.getConnection();
Connection  conn = JDBCUtils.getConnection();
String sql = "select * from account where name =?";
Account account = (Account) runner.query(conn,sql,new BeanHandler(Accout.class),new Object[]{name});
return account;

可以看到,我们的QueryRunner()括号里面没有带参数。表明QueryRunner是通过默认的构造方法进行构造,还没有从数据源中获取数据库连接的信息。等到使用了JDBCUtils方法获取了conn连接以后,才把这个conn作为参数传递到query方法中

  之前的事务案例(1.3)是 JDBCUtils + Example组合,其中JDBCUtils只用于获取和关闭连接,提交事务等功能放在Example中实现。并且由于不需要查询等操作,所以不需要一个Dao文件。
  本案例中,JDBCUtils除了要操心连接的问题,还有开启事务、提交事务、回滚事务的功能。Account是一个JavaBeanAccountDao用于定义查(find)改(update)方法,这是1.3案例中所不具有的功能。其中find(String name)返回一个account,另外一个方法update(Account account)用于更改账户余额。Business封装了transfer方法用于表示转账业务。
  之所以分为AccountDao和Business两部分,也是遵循“使用JavaBean来操控数据库”的原则。(以这里的转账为例,先利用AccountDao中的find方法找到两个对象并封装成Account对象,然后修改这些对象的属性,再把修改后的对象作为参数通过AccountDao中的update方法对数据库进行更新,可以发现,数据库只进行读取写入这两个操作,不用进行复杂的运算。而案例1.3则是直接在数据库中进行运算
(为了保证是同一个连接,在JDBCUtils类中引入了ThreadLocal类,它可以在线程中记录变量,生成一个连接放在这个线程中,只要是这个线程的任何对象都可以共享这个连接,见下面代码)。

public static Connection getConnection() throws SQLException{
//在线程里记录变量,获取连接部分代码省略
    Connection conn = threadLocal.get();
    ...
    return conn
}

//开启事务,这时候调用上面getConnection()方法获取连接
public static void startTransaction(){
    try{
        Connection conn = getConnection();
    }catch(SQLException e){
        e.printStackTrace();
    }
}
//接下来的方法,如commit()、rollback()和close()中获取连接都是使用threadLocal.get()的方法
public static void commit(){
    ...
    Connection conn = threadLocal.get();
    ...
}

推荐阅读更多精彩内容