12.1 JDBC快速入门

12.1.1 什么是JDBC

它是Java连接数据库的一种技术,由于有许多数据库厂商,Java不可能为每一个厂商单独写一套连接他们家数据库的实现,所以抽象出了一组接口。由各数据库厂商负责实现这些接口来驱动他们家的数据库,这样实际上在API层面就达到了统一。

12.1.2 JDBC原理图

图片.png
每个厂商将他们的具体实现经过编译后打成jar包,如果项目中需要操作他们家的数据库,就需要引入这个jar包。jar包中包含了若干经过编译后的.class字节码文件。引入的过程也很简单,过程如下:

  1. 下载所需要的jar文件
  2. 在项目目录下新建一个libs文件,并将jar包文件拷贝进去
  3. add library...

12.1.3 JDBC快速入门demo

使用JDBC操作数据库主要分为4步:

  1. 注册驱动,获取一个Driver对象
  2. 建立连接,获取一个connect对象
  3. 执行sql语句,通过Statement对象来执行
    1. 如果是执行 select 语句,使用 executeQuery() 方法
    2. 如果执行 insert、update、delete,或 DDL 语句,则使用 executeUpdate() 方法
  4. 关闭连接,先关闭statement,再关闭connect ```java public class JDBCStartDemo { @SuppressWarnings(“all”) public static void main(String[] args) throws SQLException {

    1. // 1. 注册驱动
    2. Driver driver = new Driver();
    3. // 2. 得到连接
    4. String url = "jdbc:mysql://localhost:3306/practice";
    5. Properties properties = new Properties();
    6. properties.setProperty("user", "root");
    7. properties.setProperty("password", "flynessme666");
    8. Connection connect = driver.connect(url, properties);
    9. // 3. 执行sql

    // String sql = “insert into actor values(null, ‘张译’, ‘男’, ‘1978-2-17’, 17828185158)”;

     String sql2 = "delete from actor where id = 2";
     Statement statement = connect.createStatement();
     int row = statement.executeUpdate(sql2);
     System.out.println(row > 0 ? "success" : "failure");
    
     // 4. 关闭连接
     statement.close();
     connect.close();
    

    }

}

有关`Driver`的简单理解:<br />在上面的第一步中,就是注册驱动获取一个`Driver`对象,这里的`Driver`对象实际类型为厂商提供的`Driver`类,当然不同厂商提供了不同的`Driver`类实现。

在`java.sql`包下,定义了一个名为`Driver`的接口,来看看官方文档是如何描述它的。
> The interface that every driver class must implement.   每一个driver类都必须实现这个接口
> The Java SQL framework allows for multiple database drivers.    Java SQL框架允许使用多个数据库驱动程序
> Each driver should supply a class that implements the Driver interface.   每个驱动程序都应该提供一个实现了驱动程序接口的类

`java.sql.Driver`是`java`提供的驱动程序接口,查看源码发现`com.mysql.cj.jdbc.Driver`类实现了这个接口。当这个类被加载时,它做了一个动作 —— 注册驱动。
```java
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

换句话说,当执行new Driver()时,实际底层执行了registerDriver。此时驱动管理器会注册一个mysql的驱动程序。我们就可以通过这个驱动程序来建立与数据库的连接了。

12.2 获取数据库连接的5种方式

方式一

public void connect_1() throws SQLException{
    Driver driver = new Driver();  // 获得驱动器
    String url = "jdbc:mysql://localhost:3306/practice";
    Properties properties = new Properties();
    properties.setProperty("user", "root");
    properties.setProperty("password", "flynessme666");
    Connection connect = driver.connect(url, properties);
    System.out.println(connect);
}

代码分析: 这种方式愚蠢在第2行是一个静态加载(通过 new 创建对象),之前学反射就知道这种方式依赖性强,于是有了方式二

方式二

public void connect_2()
    throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException {
    Class<?> Driver = Class.forName("com.mysql.jdbc.Driver");
    Driver driver = (Driver) Driver.newInstance();
    String url = "jdbc:mysql://localhost:3306/practice";
    Properties properties = new Properties();
    properties.setProperty("user", "root");
    properties.setProperty("password", "flynessme666");
    Connection connect = driver.connect(url, properties);
    System.out.println(connect);
}

代码分析: 与方式一的区别在于第三行使用了反射去获取 Driver 类对象,属于动态加载,灵活性更强

方式三

public void connect_3()
    throws ClassNotFoundException, IllegalAccessException, InstantiationException, SQLException {
    Class Driver = Class.forName("com.mysql.jdbc.Driver");
    Driver driver = (Driver) Driver.newInstance();
    String url = "jdbc:mysql://localhost:3306/practice";
    String user = "root";
    String password = "flynessme666";
    DriverManager.registerDriver(driver);
    Connection connection = DriverManager.getConnection(url, user, password);
    System.out.println(connection);
}

代码分析: 

该方式与方式二的区别在于,方式二使用driver获取连接,而方式三使用DriverManager来获取。DriverManager相当与driver的总管,只有在DriverManager注册过的driver才能驱动指定的数据库。这就好比你入学要进行新人注册,证明你是该学校的学生后,才能在学校进行各种活动。根据之前的经验来看,当new一个Driver对象时,底层就已经注册了驱动,所以方式三的第8行去掉,程序也是没什么问题的。

此外根据源码可知,DriverManager的getConnection方法底层还是调用的driver的connect方法,所以两种方式本质没啥区别,至于为什么要用DriverManager去获取连接而不是使用driver,就不得而知了,换句话说,如果注册了好多个driver,不同driver驱动不同数据库,那么使用DriverManager获取connection它怎么知道获取的是谁的连接呢?

如果注册了多个驱动,连接哪一个?

方式四

public void connect_4() throws ClassNotFoundException, SQLException {
    Class.forName("com.mysql.jdbc.Driver"); // 已经注册了驱动
    String url = "jdbc:mysql://localhost:3306/practice";
    String user = "root";
    String password = "flynessme666";
    Connection connection = DriverManager.getConnection(url, user, password);
    System.out.println(connection);
}

代码分析: 这种方式代码更少了,它让方式三的第4行变成了摆设,事实上方式三的第4行确实也没什么卵用。
当使用反射加载类时,类中的静态代码块就已经包括了注册的操作,这在上面已经看过源码了。所以就不需要创建这个类的实例。

方式五

public void connect_5() throws ClassNotFoundException, IOException, SQLException {
    Properties properties = new Properties();
    properties.load(new FileInputStream("src/mysql.properties"));
    String url = properties.getProperty("url");
    String driver = properties.getProperty("driver");
    String user = properties.getProperty("user");
    String password = properties.getProperty("password");
//        Class.forName(driver);
    Connection connection = DriverManager.getConnection(url, user, password);
    System.out.println(connection);
}

代码分析: 
方式五和方式四没什么本质区别,就是把获取连接需要的信息放在了配置文件中去读取。方式五的牛逼之处在于第8行的反射加载类的过程也不需要了。原因在于之前加载的类是com.mysql.jdbc.Driver这个类,而这个类已经过时了,新的驱动类是com.mysql.cj.jdbc.Driver, 借用官方的话描述就是
The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary. 简单翻译就是它会通过SPI自动注册,而无需手动加载这个类。

12.3 ResultSet

12.3.1 基本介绍

ResultSet表示通过执行查询操作后返回的一组数据集,可以把它想象成一张表,开始时,一个光标指向第一条数据的上方,然后利用next()往下移动,指向第一条记录依次类推,当光标指向null时,返回false

12.3.2 案例分析

actor表进行查询,对返回的结果集进行遍历。

public static void main(String[] args) throws Exception{
    // 连接数据库
    Properties properties = new Properties();
    properties.load(new FileInputStream("src/mysql.properties"));
    String url = properties.getProperty("url");
    String user = properties.getProperty("user");
    String password = properties.getProperty("password");
    String driver = properties.getProperty("driver");
    Connection connection = DriverManager.getConnection(url, user, password);
    Statement statement = connection.createStatement();

    String query = "select id, name from actor";
    ResultSet resultSet = statement.executeQuery(query);

    // 遍历resultSet
    while (resultSet.next()) {
        int id = resultSet.getInt(1);
        String name = resultSet.getString(2);
        System.out.println(id + "\t" + name + "\t");
    }

    // 关闭资源
    resultSet.close();
    statement.close();
    connection.close();
}

代码分析: 代码实现比较简单,这里的重点是搞清楚resultSet的底层数据结构究竟是什么

12.3.3 ResultSet的底层结构

针对上面的代码进行调试,首先得知resultSet的类型是ResultSetImpl。首先需要明确,ResultSetjava定义的一个接口,而ResultSetImpl是厂商提供的具体实现类。
截屏2021-06-29 上午3.02.36.png截屏2021-06-29 上午3.13.00.png
上图是resultSet中的rowData属性的结构,它有一个成员rows,其类型为ArrayList,这个rows存放的就是返回的结果集每行的信息,当然信息不止具体每列的数据,还有一些其他信息(在这里可以清楚的看到还有sizemodCount),rows中的elementData里面的每个元素中的internalRowData存放的才是具体的每列的数据的值。

再来看看resultSet为什么能够调用next(),首先resultSet的实际类型是ResultSetImpl,查看源码可知,resultSet.next()底层实际上调用的是rowData.next()rowData的类型为ResultsetRowsStatic,接下来看一组继承关系就知道为什么rowData可以调用next方法了。

12. JDBC - 图4

12.4 Statement

12.4.1 基本介绍

statement对象用来执行静态sql statement并且返回它产生的结果。

默认情况下,每个Statement对象在同一时刻只能打开一个ResultSet对象。这句话的简单意思就是说:

// 如果执行了这条语句,那么st1和rs二者就产生了关联
Statement st1 = connection.createStatement();
ResultSet rs = st1.executeQuery(sql);
// 如果用st1操作执行别的查询,那么之前与它关联的ResultSet对象就会关闭
ResultSet rs2 = st1.executeQuery(sql2);

所以原则上说,一个statement对应一个resultset,如果需要执行其他query语句,需要其他的statement执行

JDBC中,执行静态sql有三种方式:

  • Statement:存在 sql 注入问题,所以基本不会使用这个类
  • PreparedStatement:预处理,解决 sql 注入问题
  • CallableStatement:存储过程

12.4.2 PreparedStatement

12.4.2.1 基本介绍

PreparedStatement对象执行的sql语句中,参数部分使用占位符?来替代,然后通过PreparedStatement对象的setXxx()来设置占位符的具体值,其中setXxx()有两个参数,第一个参数为?在sql语句中的索引,从1开始;第二个参数为该参数具体的值。

当设置好参数部分的值后,使用executeQuery()executeUpdate()来分别进行查询和增删改的执行。

使用预处理不仅能够解决 sql 注入问题,还能够防止字符串拼接造成的语法错误,并且能够大大减少编译次数,提高效率。所谓预处理可以简单理解为:数据库服务器端只会再首次接收到 sql 后对其进行语法检查、编译,之后再收到同样的 sql 请求时就直接执行它(因为编译的时间相比起运行时间是比较长的)。

12.4.2.2 PreparedStatement如何防止sql注入

12.5 JDBC API小结

12. JDBC - 图5

12.6 封装JDBCUtils类

由于获取连接和关闭资源操作很常用,所以把它封装成工具类,代码如下(Pretty easy):

public class JDBCUtils {

    private static String driver;
    private static String url;
    private static String user;
    private static String password;
    private static Connection connection = null;

    static {
        try {
            Properties properties = new Properties();
            properties.load(new FileInputStream("src/mysql.properties"));
            driver = properties.getProperty("driver");
            url = properties.getProperty("url");
            user = properties.getProperty("user");
            password = properties.getProperty("password");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }

    public static void close(ResultSet resultSet, Statement statement, Connection connection)
        throws SQLException {
        if (resultSet != null) {
            resultSet.close();
        }
        if (statement != null) {
            statement.close();
        }
        if (connection != null) {
            connection.close();
        }
    }
}

12.7 事务

12.7.1 基本介绍

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

JDBC默认处于自动提交模式,即每个sql语句在完成后都会提交到数据库,且无法回滚。

事务能够控制何时更改提交并应用于数据库。 它将单个SQL语句或一组SQL语句视为一个逻辑单元,如果任何语句失败,整个事务将失败。

使用如下语句可以关闭自动提交:conn.setAutoCommit(false);

12.7.2 提交与回滚

完成更改后,若要提交更改,可在连接对象上调用commit()方法:conn.commit( );,否则,要使用连接名为conn的数据库回滚更新,使用以下代码:conn.rollback( );

12.7.3 经典转账案例

public void Transaction() {
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    String sql = "update account set balance = balance - 1000 where id = 1";
    String sql1 = "update account set balance = balance + 1000 where id = 2";
    try {
        connection = JDBCUtils.getConnection();
        connection.setAutoCommit(false);
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.executeUpdate();
        int num = 1/0;
        preparedStatement = connection.prepareStatement(sql1);
        preparedStatement.executeUpdate();
        connection.commit();
    } catch (SQLException e) {
        try {
            connection.rollback();
        } catch (SQLException throwables) {
            System.out.println("回滚失败");
            throwables.printStackTrace();
        }
        e.printStackTrace();
    } catch (RuntimeException e) {
        System.out.println("产生了运行时异常");
        System.out.println(e.getMessage());
    }
    finally {
        JDBCUtils.close(null, preparedStatement, connection);
    }
}

代码分析: 转账是一个完整的过程,即会执行两条update语句,只有两条update都成功了,才算转账成功。所以将整个业务过程放在try块中,如果执行到最后都没有异常抛出,则执行commit,否则就会执行回滚。

12.8 批处理

批处理允许将相关的SQL语句分组到批处理中,并通过对数据库的一次调用来提交它们,一次执行完成与数据库之间的交互。一次向数据库发送多个SQL语句时,可以减少通信开销,从而提高性能。

  • StatementPreparedStatementCallableStatementaddBatch()方法用于将单个语句添加到批处理
  • executeBatch()用于执行组成批量的所有语句,该方法返回一个整数数组,数组的每个元素表示相应更新语句的更新计数
  • clearBatch()方法将删除所有使用addBatch()方法添加的语句。但是无法指定选择某个要删除的语句
  • 批处理往往和PreparedStatement搭配使用,既减少了编译次数,又减少了运行次数,大大提高效率

12.9 数据库连接池

12.9.1 基本介绍

数据库连接池的核心思想是池化思想,所谓池化思想就是当一个对象需要被频繁使用时,提前创建好来供别人使用,这样就能减少频繁创建对象造成的开销。与之类似的应用还有线程池、字符串常量池等。

数据库连接池的种类有以下几种:

  • Druid:阿里自研的,集成了之前几种数据库连接池的优点,性能优秀,监控能力强
  • HikariCP:最常用,性能最好,也是Spring Boot 2.0默认的数据库连接池
  • C3P0
  • Druid

数据库连接池使用javax.sql.DataSource表示,DataSource是一个接口,具体由厂商进行实现。可以将一个DataSource对象当做一个与物理数据源相连的工厂,该工厂生产与物理数据源相连的连接对象。当java程序想要访问数据库时,就从该工厂获取与数据库连接的连接对象,形象的可以称为此工厂为数据库连接池

官方对于DataSource的描述如下:

A factory for connections to the physical data source that this DataSource object represents. An alternative to the DriverManager facility DataSource对象代表着一个与物理数据源相连的工厂,它是DriverManager的一个替代品。

12.9.2 C3P0应用实例

第一种方法是不用配置文件,而是将所有配置信息在代码中进行设置,这种方式太愚蠢了,就不演示了。

第二种方法是将配置文件放在src目录下,如果想更改配置只需修改文件即可。
配置文件名固定为:c3p0-config.xml

public class C3P0_ {

    @Test
    public void testC3P0() throws SQLException {
        // 1. new一个DataSource对象
        ComboPooledDataSource comboPool = new ComboPooledDataSource("Pool");
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5000; i++) {
            Connection connection = comboPool.getConnection();
            connection.close();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用连接池耗时: " + (end - start));
    }
}

代码分析: 将连接池的配置文件放在src目录下,连接代码就很简单了,获取connection总共需要两步:
1. 获取ComboPooledDataSource对象
2. 调用该对象的getConnection()方法
这里其实就使用DataSource对象代替了之前的DriverManager

12.9.3 Druid应用实例

public class Druid_ {

    @Test
    public void testDruid() throws Exception {
        Properties properties = new Properties();
        properties.load(new FileInputStream("src/druid.properties"));
        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 5000; i++) {
            Connection connection = dataSource.getConnection();
            connection.close();
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时为: " + (end - start));
    }
}

代码分析: 使用DruidDataSourceFactory的静态方法createDataSource来生成DataSource对象,需要给该静态方法传入一个参数properties,即druid的配置文件。

12.10 Apache-DBUtils

12.10.1 基本介绍

这是Apache组织提供的一个开源JDBC处理工具类库,能大大减少JDBC代码的编写量。

该类库提供了较少了类和接口定义(大概20多个),其中比较重要和核心的接口和类有如下三个:

  • **DbUtils**:提供如加载驱动、关闭连接、事务提交、回滚等常规工作的工具类,里面的所有方法都是静态的
  • **QueryRunner**:该类简单化了SQL查询,它与ResultSetHandler组合在一起使用可以完成大部分的数据库操作,能够大大减少编码量;此外还能执行其他的DML操作
  • **ResultSetHandler**接口:该接口的实现类用来以各种方式处理ResultSet对象
    • ArrayHandler
    • ArrayListHandler
    • BeanHandler
    • BeanListHandler
    • ColumnListHandler
    • KeyedHandler
    • MapHandler
    • MapListHandler

12.10.2 基本思想

图片.png上图展示了之前实现的工具类的一些存在问题以及在DbUtils工具包中是如何对这些问题进行解决的。

在之前实现的工具类中,无论是使用了连接池还是不使用连接池,都对数据库的连接获取和资源关闭这两个操作进行了封装。但是当执行完sql语句后,都会涉及到对Connecion对象的关闭,一旦关闭了Connection对象,意味着返回的ResultSet对象也就无法使用了。如果其他地方要使用这个结果集,也就无法实现,换句话说,数据的复用性很差。

所以自然而然想到的方法就是将返回的结果集进行封装,并保存在一个数据类型中。具体的类型取决于返回的是什么,如果是很多行数据,就可以把每行数据抽象成一个对象,放在List中;如果只是一行数据,可以放在Bean对象中等等。

简单解释下BeanBean其实就是一个类,只不过要遵循一定的规则去写:

  • getters可以访问的属性(如果这些属性不是只读的,那么setter可以访问这些属性)
  • 无参数的公共构造函数
  • 序列化

比如说在数据库中有一张actor表,它由idnamegenderbirthdayphonenum5个字段组成,查询该表的结果集的每一行都存在这五个字段,那么就可以定义一个actor类来将这些属性封装起来,提供对应的getset方法来访问和操作这些属性。其中类中的属性和表中的字段一一对应。有这种特征的类就可以成为一个Bean类(当然这里对于Bean类的定义肯定是不严谨的,但是暂时可以这么理解)。

需要注意的是数据表和类属性之前的类型对应关系,由于表的字段是可以为null,所以对于类中属性的基本数据类型要使用其包装类定义,日期类可以使用java.util.Date表示,字符串和字符用String表示即可。

有了以上的基础后,就可以把结果集中的每行封装到一个Bean类中,然后将每个Bean类对象存在一个List中,这样即使关闭了连接,也不妨碍其他程序使用结果集。

12.10.3 使用案例

接下里将结合Dbutils工具包和之前的druid连接池实现对actor表的crud操作。

// 对多行结果查询进行封装并输出
public void TestQueryMany() throws SQLException {
    Connection conn = JDBCUtilsByDruid.getConnection();
    QueryRunner queryRunner = new QueryRunner();
    String sql = "select * from actor where id > ?";
    List<Actor> actors = queryRunner.query(conn, sql, new BeanListHandler<>(Actor.class), 1);
    for (Actor actor : actors) {
        System.out.println(actor);
    }
    JDBCUtils.close(null, null, conn);
}
// 对单行结果查询进行封装并输出
public void TestQuerySingle() throws SQLException {
    Connection conn = JDBCUtilsByDruid.getConnection();
    QueryRunner queryRunner = new QueryRunner();
    String sql = "select id, name from actor where id = 1";
    Actor actor = queryRunner.query(conn, sql, new BeanHandler<>(Actor.class));
    System.out.println(actor);
    JDBCUtilsByDruid.close(null, null, conn);
}
// // 对单行单列结果查询进行封装并输出
public void testScalar() throws SQLException {
    Connection conn = JDBCUtilsByDruid.getConnection();
    QueryRunner queryRunner = new QueryRunner();
    String sql = "select name from actor where id = 1";
    Object obj = queryRunner.query(conn, sql, new ScalarHandler());
    System.out.println(obj);
    JDBCUtilsByDruid.close(null, null, conn);
}
// 使用该工具包执行insert、delete、update操作和以上大同小异,只不过调用的是queryRunner的
// update()方法,注意这里的update()意思是更广泛的对数据进行更新,包含了增删改的操作, 可以类比之// 前使用st.executeQuery()和st.executeUpdate()

12.11 DAO和增删改查通用方法-BaseDao

12.11.1 基本思想

未命名文件.jpg这种设计实际上体现了一种分层思想,大家各司其职实现解耦,方便代码维护,也方便了问题排查。

12.11.2 BaseDAO的实现

根据以上架构,核心实现BaseDao代码如下:

package com.tyc.jdbc.dao_.dao;

import com.tyc.jdbc.dao_.utils.JDBCUtilsDruid;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

/**
 * @author tianyichen <tianyichen@kuaishou.com> Created on 2021-07-01
 */
public class BaseDao<T> {

    QueryRunner queryRunner = new QueryRunner();

    // 实现对表的增、删、改操作
    public int update(String sql, Object... params) {
        Connection conn = null;
        try {
            conn = JDBCUtilsDruid.getConnection();
            return(queryRunner.update(conn, sql, params));
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtilsDruid.close(null, null, conn);
        }
    }

    // 实现接受多行对象返回
    public List<T> queryMulti(String sql, Class<T> cls, Object... params) {
        Connection conn = null;
        try {
            conn = JDBCUtilsDruid.getConnection();
            List<T> list = queryRunner.query(conn, sql, new BeanListHandler<>(cls), params);
            return list;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtilsDruid.close(null, null, conn);
        }
    }

    // 实现接受单行对象返回
    public T querySingle(String sql, Class<T> cls, Object... params) {
        Connection conn = null;
        try {
            conn = JDBCUtilsDruid.getConnection();
            T t = queryRunner.query(conn, sql, new BeanHandler<>(cls), params);
            return t;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtilsDruid.close(null, null, conn);
        }
    }

    // 实现接受单行单列对象返回
    public Object queryScalar(String sql, Object... params) {
        Connection conn = null;
        try {
            conn = JDBCUtilsDruid.getConnection();
            Object obj = queryRunner.query(conn, sql, new ScalarHandler(), params);
            return obj;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            JDBCUtilsDruid.close(null, null, conn);
        }
    }
}