[Java]JDBC 和数据库连接池

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Java]JDBC 和数据库连接池相关的知识,希望对你有一定的参考价值。

一、JDBC

1. JDBC 原理

  1. JDBC 为访问不同的数据库提供了统一的接口,为使用者屏蔽了使用细节。Java 程序员使用 JDBC API 可以连接任何提供了 JDBC 驱动程序的数据库系统,从而完成对数据库的各种操作
  2. Java 设计者定义了操作数据库的接口规范,由各自数据库厂商具体实现。Java 程序员只需要面向这套接口编程即可
  3. JDBC API 是一系列的接口,它统一和规范了应用程序与数据库的连接、执行 SQL 语句,并得到返回结果等各类操作,相关的接口和类在 java.sql 和 javax.sql 包中
  4. JDBC 程序编写步骤:注册驱动:加载 Driver 类、获取连接:获得 Connection 对象、执行 SQL:获得 Statement 对象、释放资源

2. JDBC 的连接的五种方式

  1. 静态加载方式连接到数据库
/**
* 静态加载方式连接到数据库

* @author Spring-_-Bear
* @version 2021-11-08 20:56
*/
public class Jdbc01 
   public static void main(String[] args) throws SQLException 
       // 连接到的数据库:jdbc:mysql://主机IP地址:端口/db_name
       String url = "jdbc:mysql://localhost:3306/temp";
       // 设置用户名与密码
       Properties properties = new Properties();
       properties.setProperty("user", "springbear");
       properties.setProperty("password","123");
       
       // 加载驱动: new com.mysql.jdbc.Driver()
       Driver driver = new Driver();
       // 获得连接
       Connection connect = driver.connect(url, properties);

       // SQL 语句
       String insert = "INSERT INTO actor VALUES (NULL,'张三','男','1970-01-01','10086');";

       // 得到一个执行静态 SQL 语句并返回其生成的结果的对象
       Statement statement = connect.createStatement();
       // 返回受影响的行数
       int rows = statement.executeUpdate(insert);

       System.out.println("返回受影响的行数 = " + rows);

       statement.close();
       connect.close();
   

  1. 使用反射机制连接到数据库
/**
* 方式1属于静态加载,灵活性差,依赖性强,考虑方式二使用反射机制 
*/
public void connect02() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException 
   // 连接到的数据库:jdbc:mysql://主机IP地址:端口/db_name
   String url = "jdbc:mysql://localhost:3306/temp";
   // 设置用户名与密码
   Properties properties = new Properties();
   properties.setProperty("user", "springbear");
   properties.setProperty("password","123");

   // 加载类信息
   Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
   // 获得类实例
   Driver driver = (Driver)aClass.newInstance();
   // 获得连接
   Connection connect = driver.connect(url, properties);

  1. 使用 DriverManager 替代 Driver,进行统一管理
/**
* 使用 DriverManager 替代 Driver,进行统一管理
*/
public void connect03() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException 
   // 加载类信息
   Class<?> aClass = Class.forName("com.mysql.jdbc.Driver");
   // 获得类实例
   Driver driver = (Driver)aClass.newInstance();

   String url = "jdbc:mysql://localhost:3306/temp";
   String user = "springbear";
   String pwd = "123";

   // 注册驱动
   DriverManager.registerDriver(driver);
   // 获得连接
   Connection connection = DriverManager.getConnection(url, user, pwd);

  1. 使用 Class.forName 自动完成注册驱动
/**
* 使用 Class.forName 自动完成注册驱动
*/
public void connect04() throws ClassNotFoundException, SQLException 
   // 加载类信息,在加载 Driver 的过程中自动完成注册
   Class.forName("com.mysql.jdbc.Driver");
   /*
    * static 
    *      try 
    *          DriverManager.registerDriver(new Driver());
    *       catch (SQLException var1) 
    *          throw new RuntimeException("Can't register driver!");
    *      
    * 
    */
   
   String url = "jdbc:mysql://localhost:3306/temp";
   String user = "springbear";
   String pwd = "123";

   // 获得连接
   Connection connection = DriverManager.getConnection(url, user, pwd);

  1. 使用配置文件:mysql-connector-java-5.1.37-bin.jar 驱动文件 5.1.6 之后无需 Class.forName(“com.mysql.jdbc.Driver”) 也可以直接获得连接。原因:从 jdk5 以后使用了 jdbc4,不再需要显式调用 Class.forName() 注册驱动,而是自动调用驱动 jar 包下的 META-INF\\services\\java.sql.Driver 文本中的类名称去注册
/**
* 在方式4的基础上使用配置文件进行连接,更加灵活
*/
public void connect05() throws IOException, ClassNotFoundException, SQLException 
   // 从配置文件中读取信息
   Properties properties = new Properties();
   properties.load(new FileInputStream("src\\\\mysql.properties"));
   String user = properties.getProperty("user");
   String password = properties.getProperty("password");
   String driver = properties.getProperty("driver");
   String url = properties.getProperty("url");

   // 加载类信息,自动注册驱动,获得连接
   Class.forName(driver);
   Connection connection = DriverManager.getConnection(url, user, password);

3. ResultSet

  1. ResultSet:表示从数据库读取到的数据表的结果集。ResultSet 对象保持一个光标指向当前的数据行。最初,光标位于第一行之前。其有一个 next 方法将光标移动到下一行,并且由于在 ResultSet 对象中没有更多行时返回 false,因此常用 while 循环来遍历结果集
  2. com.mysql.jdbc.JDBC42 ResultSet 类下有一个 RowData(接口) 类型的字段,rowData 中有一个 ArrayList<ByteArrayRow> 类型的集合 rows,rows 中又有 byte[] 类型的 internalRowData,数据真正存储的位置

4. SQL 注入

  1. Statement 对象,用于执行静态 SQL 语句并返回其生成结果的对象
  2. 在建立连接之后,想要对数据库进行操作,一般有以下三种方式:Statement:存在 SQL 注入、PreparedStatement:预处理、CallableStatement:用于执行数据库存储过程
  3. SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入的数据中注入非法的 SQL 语句段或命令,恶意攻击数据库
/**
* @author Spring-_-Bear
* @version 2021-11-09 09:48
*/
public class SqlInjection 
   public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException 
       // 获取用户想要查询的用户名和密码
       // Input userName = 1' or
       // Input pwd = or '1' = '1
       Scanner scanner = new Scanner(System.in);
       System.out.print("Input the name that you want to query:");
       String userName = scanner.nextLine();
       System.out.print("Input the password that you want to query:");
       String pwd = scanner.nextLine();

       // 加载配置文件
       Properties properties = new Properties();
       properties.load(new FileInputStream("config\\\\temp.properties"));

       // 加载驱动类信息,自动注册驱动
       Class.forName(properties.getProperty("driver"));
       // 获得连接
       Connection connection = DriverManager.getConnection(properties.getProperty("url"), properties);
       Statement statement = connection.createStatement();

       String select = "SELECT * FROM admin WHERE name='" + userName + "' AND pwd= '" + pwd + "'";
       ResultSet resultSet = statement.executeQuery(select);
       while (resultSet.next()) 
           userName = resultSet.getString(1);
           pwd = resultSet.getString(2);
           System.out.println(userName + "\\t" + pwd);
       

       resultSet.close();
       statement.close();
       connection.close();
   

5. PreparedStatement

PreparedStatement 执行的 SQL 语句中的参数用问号(?)来表示,调用 PreparedStatement 对象的 setXxx() 方法来设置这些参数。setXxx() 方法有两个参数,第一个参数是要设置的 SQL 语句中的参数的索引,从 1 开始,第二个参数是设置 SQL 语句中的参数的值

String select = "SELECT * FROM admin WHERE name = ? AND pwd= ?";
// SQL 语句预处理
PreparedStatement preparedStatement = connection.prepareStatement(select);
preparedStatement.setString(1, userName);
preparedStatement.setString(2, pwd);

// 执行 SQL 语句,得到结果集
ResultSet resultSet = preparedStatement.executeQuery();

6. JDBC API

接口名方法名功能
ConnectioncreateStatement()创建执行静态 SQL 语句的对象
createPreparedStatement(sql)获得 SQL 语句预处理对象
StatementexecuteUpdate(sql)执行 DML 语句,返回受影响行数
executeQuery(sql)执行 DQL 语句,返回结果集
execute(sql)执行任意 SQL 语句,返回布尔值
PreparedStatementexecuteUpdate(sql)执行 DML 语句,返回受影响行数
executeQuery(sql)执行 DQL 语句,返回结果集
execute(sql)执行任意 SQL 语句,返回布尔值
setXxx(index,value)设置 SQL 语句中的值
setObject(index,value)设置 SQL 语句中的值
ResultSetnext()向下移动一行,到表尾返回 false
previous()向上移动一行,到表头返回 false
getXxx(index || colLabel)获得指定列的值
getObject(index || colLabel)获得指定列的值

7. Java 事务操作

  1. JDBC 程序中当一个 Connection 对象被创建时,默认情况下是自动提交事务:每次执行一条 SQL 语句时,如果执行成功,就会向数据库自动提交,而不能回滚
  2. 可以调用 Connection 接口的 setAutoCommit(false) 方法取消自动提交事务
  3. 在所有的 SQL 语句都执行成功后,调用 commit() 方法提交事务;在其中某个操作失败或出现异常时,调用 rollback() 方法回滚事务
public void transaction() 
   Connection connection = null;
   PreparedStatement preparedStatement = null;
   String sub = "UPDATE account SET balance = balance - 100 WHERE id = 1";
   String add = "UPDATE account SET balance = balance + 100 WHERE id = 2";

   try 
       // 获得连接
       connection = JdbcUtils.getConnection();
       // 关闭自动提交即开启事务
       connection.setAutoCommit(false);
       // 执行 SQL
       preparedStatement = connection.prepareStatement(add);
       preparedStatement.executeUpdate();
       int temp = 1 / 0;
       preparedStatement = connection.prepareStatement(sub);
       preparedStatement.executeUpdate();
       // 提交事务
       connection.commit();
       System.out.println("所有 SQL 操作成功,提交事务!");
    catch (SQLException | ArithmeticException e) 
       try 
           // 发生异常,撤销操作,事务回滚
           System.out.println("程序执行发生了异常,回滚所有操作!!!");
           connection.rollback();
        catch (SQLException ex) 
           ex.printStackTrace();
       
       e.printStackTrace();
    finally 
       JdbcUtils.close(null, preparedStatement, connection);
   

8. 批处理

  1. 当需要成批插入或者更新记录时,可以采用 Java 的批处理更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率
  2. JDBC 连接 MySQL 时,如果需要使用批处理功能,需要在 url 中添加参数:
url = "jdbc:mysql://localhost:3306/temp?rewriteBatchedStatements=true"
  1. 批处理往往和 PreparedStatement 一起搭配使用,既可以减少编译次数,又可以减少运行次数,效率大大提高
preparedStatement.addBatch();
if (i % 1000 == 0) 
   preparedStatement.executeBatch();
   preparedStatement.clearBatch();

方法名功能
addBatch()添加需要批量处理的 SQL 语句或参数
executeBatch()批量发送执行
clearBatch()清空批处理包
  1. 批处理源码剖析
/**
* 第一次会创建 ArrayList<elementData>
* elementData => Object[] 会存放预处理后的 SQL 语句
* 当 elementDate 满后会按照 1.5 倍扩容
* 当达到指定的容量之后,就会发送给 MySQL 执行
* 批处理会减少发送 SQL 语句的网络开销,减少编译次数,从而提高效率

*/
public void addBatch() throws SQLException 
   synchronized(this.checkClosed().getConnectionMutex()) 
       if (this.batchedArgs == null) 
           this.batchedArgs = new ArrayList();
       

       for(int i = 0; i < this.parameterValues.length; ++i) 
           this.checkAllParametersSet(this.parameterValues[i], this.parameterStreams[i], i);
       

       this.batchedArgs.add(new com.mysql.jdbc.PreparedStatement.BatchParams(this.parameterValues, this.parameterStreams, this.isStream, this.streamLengths, this.isNull));
   

9. 数据库连接池原理

  1. 传统的 JDBC 数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都需要将 Connection 加载到内存中,再验证 IP 地址、用户名、密码(耗时0.05s ~ 1s)是否正确。需要向数据库连接的时候,就向数据库请求一个连接,频繁的请求操作将占用过多的系统资源,容易造成服务器崩溃
  2. 每一次数据库连接,使用完后都得及时断开,如果程序出现异常而导致未能正常关闭,将导致数据库内存泄漏,最终导致数据库崩溃重启
  3. 传统获取连接的方式,不能控制创建连接的数量,如果连接过多,也可能导致内存泄漏,从而导致 MySQL 崩溃
  4. 预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕归还给缓冲池(并不断开与数据库的连接)
  5. 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个
  6. 当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列
  7. JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口具体实现留给第三方
连接池特点
C3P0速度相对较慢,稳定性不错(hibernate、spring底层均采用)
Druid阿里巴巴提供的数据库连接池,集 DBCP、C3P0、Proxool 优点于一身
Proxool有监控连接池状态的功能,稳定性较 C3P0 略差
BoneCP速度快
DBCP速度较 C3P0 快,但不稳定

10. C3P0 连接池

/* 方式一 */
// 创建数据源对象
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
// 设置相关信息
comboPooledDataSource.setDriverClass(driver);
comboPooledDataSource.setJdbcUrl(url);
comboPooledDataSource.setUser(user);
comboPooledDataSource.setPassword(pwd);
comboPooledDataSource.setInitialPoolSize(10);
comboPooledDataSource.setMaxPoolSize(50);
// 获得连接
Connection connection = comboPooledDataSource.getConnection();
connection.close();

/* 方式二 */
// 将 c3p0-config.xml 文件拷贝到工程 src 目录下
ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource("spring_bear");
Connection connection = comboPooledDataSource.getConnection();
connection.close();

11. Druid 连接池

Druid 工具类 JdbcUtils

// 加载配置文件
Properties properties = new Properties();
properties.load(new FileInputStream("config\\\\druid.properties"));
// 创建一个的连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 获得连接
Connection connection = dataSource.getConnection();
connection.close();

12. ApacheDbUtils

  1. 关闭 connection 后,resultSet 结果集无法继续使用;然而很多时候我们希望关闭 connection 连接后仍然可以继续使用查询到的数据,resultSet 存储查询结果的方式不利于数据管理;从 resultSet 结果集中获取数据时操作方法不够明确,getXxx() 方法容易出错,含义模糊
  2. 定义一个类与数据库表的字段一一对应,这样的类一般称作 javabean、domain、pojo、entity
  3. 将返回的结果集的字段值封装到自定义的类的对象中,将若干个这样的对象放进集合中,就可以直接访问集合从而获得数据库表的查询结果
  4. commons-dbutils 是 Apache 组织提供的一个开源 JDBC 工具类,它是对 JDBC 的封装,使用 dbutils 能极大简化 JDBC 编码量
  5. QueryRunner 类封装了 SQL 的执行,是线程安全的 ,可以实现增、删、改、查和批处理
  6. RessultSetHandler 接口用于处理 java.sql.ResultSet,将查询到的数据转换为另一种形式
  7. 在创建 JavaBean 类时类的字段的数据类型强制使用八大基本数据类型对应的包装类,因为 MySQL 数据库表中的字段值可能为空,而 Java 只有引用数据类型才有 NULL 值
接口名功能
ArrayHandler将结果集中的第一行数据转换成对象数组
ArrayListHandler将结果集中的每一行都转换成一个数组,再存放到 List 中
BeanHandler将结果集中的第一行数据封装到一个对应的 JavaBean 实例中
BeanListHandler将结果集中的每一行数据封装到对应的 JavaBean 实例中,存放到 List
ColumnListHandler将结果集中的某一列数据存放到 List 中
KeyedHandler(name)将每行数据封装到 Map 中,再将 map 存入另一个 map 中
MapHandler将结果集的第一行数据封装到 Map 中,key 是列名,value 是对应值
MapListHandler将结果集中的每一行数据封装到 Map 中,再存放到 List 里
public void testApache() throws SQLException 
   // 获得连接
   Connection connection = JdbcUtilsByDruid.getConnection();
   // 获得 Apache 实现的查询对象
   QueryRunner queryRunner = new QueryRunner();
   String select = "SELECT * FROM cost WHERE id >= ? AND id <= ?";
   List<Fishing> fishings = queryRunner.query(connection, select, new BeanListHandler<>(Fishing.class), 1, 10);
   for (Fishing fishing : fishings) 
       System.out.println(fishing);
   
   JdbcUtilsByDruid.close(null, null, connection);

13. BaseDao

  1. Apache-dbutils + Druid-connectionPoll 简化了 JDBC 开发,但仍有以下不足:
  2. SQL 语句是固定的,不能通过参数传入,通用性不好,需进行改进,以方便 CRUD 操作
  3. 对于查询 SELECT 操作,如果有返回值,返回类型不能确定,需要使用泛型解决
  4. 未来数据库中的表会有很多,业务需求复杂,不可能只靠一个 Java 类完成
  5. 解决方案:为每个表设计一个 JavaBean 类,同时为每一张数据库表设计一个专门操作它的类 Dao,将所有的具体 Dao 类中的共有部分抽象出父类 BasciDao,以更好地利用多态

    以上是关于[Java]JDBC 和数据库连接池的主要内容,如果未能解决你的问题,请参考以下文章

    mybeats与jdbc问题分析

    JDBC连接池&DBUtils

    JDBC连接池C3P0

    传统JDBC的弊病和mybatis的解决方案

    数据库连接池的Java连接池

    javaWeb_JDBC_数据库连接池概述以及dbcp连接池