<Java>23 JDBC 和数据库连接池
本文最后更新于:2022年5月24日 早上
23 JDBC 和数据库连接池
JDBC:Java 提供的一套用于操作数据库的接口 API。Java 程序员只需要面向该接口即可连接任何提供了 JDBC 驱动程序的数据库,完成对数据库的各种操作。不同的数据库厂商,需要针对这套接口提供不同的实现。
JDBC 程序编写步骤(示范):
/* [0] 前置操作 */ /* [1] 注册驱动 */ Driver driver = new com.mysql.cj.jdbc.Driver(); /* [2] 获取连接 */ String url = "jdbc:mysql://localhost:3306/melody"; Properties properties = new Properties(); properties.setProperty("user", "root"); properties.setProperty("password", "******"); Connection connect = driver.connect(url, properties); /* [3] 执行语句 */ String sql = "insert into customer (customer_id,name,card_id) values(0004,'赫尔','00000000000000000O')"; Statement statement = connect.createStatement(); int rows = statement.executeUpdate(sql); System.out.println(rows); /* [4] 释放资源 */ statement.close(); connect.close();
前置操作:将
mysql-connector-java-8.0.27.jar
该文件拷贝到项目目录下,选中后右键选择Add as library
注册驱动:加载
Driver
类这里我导入了
com.mysql.cj.jdbc.Driver
。根据前面的jar
包的版本不同,应该导入的路径也会不同。旧版本的
jar
包可能应该导入com.mysql.jdbc.Driver
这个包。获取连接:得到
Connection
String url = "jdbc:mysql://localhost:3306/melody";
这里表示进入
melody
这个数据库。执行语句:发送 SQL 命令给 MySQL 执行
int rows = statement.executeUpdate(sql);
如果是 dml 语句,这里返回的 rows 是影响的行数。返回 0 表示失败。
释放资源:关闭相关连接**(切记!)**
23.1 连接数据库的 5 种方式
-
方式 1:直接创建
Driver
,调用driver.connet(url, properties);
这个方法就是 [23.0] 示范的方法
Driver driver = new com.mysql.cj.jdbc.Driver(); String url = "jdbc:mysql://localhost:3306/melody"; Properties properties = new Properties(); properties.setProperty("user", "root"); properties.setProperty("password", "******"); Connection connect = driver.connect(url, properties);
-
方式 2:使用反射加载
Driver
类这个方式是动态加载,更加灵活。
Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver"); Driver driver = (Driver) aClass.newInstance(); Properties properties = new Properties(); properties.setProperty("user", "root"); properties.setProperty("password", "******"); Connection connect = driver.connect(url, properties);
-
方式 3:使用
DriverManager
替代Driver
进行统一管理这个方式扩展性更好,更加清晰
Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver"); Driver driver = (Driver) aClass.newInstance(); String url = "jdbc:mysql://localhost:3306/melody"; String name = "root"; String password = "******"; DriverManager.registerDriver(driver); Connection connection = DriverManager.getConnection(url, name, password);
这里,
DriverManager.getConnetion()
有三种方法。分别是:getConnetion(url);
getConnetion(url, properties);
getConnetion(url, name, password);
-
方式 4:使用
Class.forName()
自动完成注册驱动Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/melody"; String name = "root"; String password = "******"; Connection connection = DriverManager.getConnection(url, name, password);
Class.forName()
在加载Driver
类时,会自动完成注册。原理:
Driver
类的源码中有这样一段static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } }
这个静态代码块在类加载时会被执行。
另外,其实不执行
Class.forName(...);
语句的场合,也能正确获取Connection
。这是因为 JDK 1.5 以后使用了 jdbc4。这个场合,系统会自动调用 jar 包下
META-INF\services\java.sql.Driver
这个文件中的类名称去注册。……打开上述文件看看,里面赫然写着:
com.mysql.cj.jdbc.Driver
即使如此,还是建议写上
Class.forName(...)
语句! -
方式 5(推荐):在方式 4 的基础上,使用配置文件,连接数据库更灵活。
这种方式是实际开发最常用的方式。
配置文件(创建 src\mysql.properties):
url=jdbc:mysql://localhost:3306/melody user=root password=****** driver=com.mysql.cj.jdbc.Driver
代码:
Properties pro = new Properties(); pro.load(new FileInputStream("src\\mysql.properties")) String url = pro.getProperties("url"); String user = pro.getProperties("user"); String password = pro.getProperties("password"); String driver = pro.getProperties("driver"); Class<?> aClass = Class.forName(driver); Connection connection = DriverManager.getConnection(url, user, password);
23.2 ReaultSet
结果集
ResultSet:表示数据库结果集的数据表。通常通过执行查询数据库的语句生成。
ResultSet 对象保持一个光标指向其当前的数据行。该光标的初始位置在第一行之前。调用 next 方法将光标下移,移动到末端的场合会返回 false。
如何取出数据(示例):
String sql = "select * from customer";
ResultSet resset = statement.executeQuery(sql);
while (resset.next()) {
int id = resset.getInt(1);
String name = resset.getString(2);
String sex = resset.getString(5);
String card_id = resset.getString(6);
System.out.println(id + "\t" + name + "\t" + sex + "\t" + card_id);
}
其中 String sex = resset.getString(5);
表示取出该行数据的第 5 列的字符串数据。
ResultSet 的数据是以类似二维数组的形式保存在对象中。这不难理解,毕竟表格是二维的嘛。
23.3 Statement
Statment
对象用于执行静态 SQL 语句,并返回其生成的结果的对象在连接建立后,需要访问数据库、执行命名或 SQL 语句,有如下方式:
Statment
(存在 SQL 注入问题,在实际开发中基本不使用)PerparedStatement
(预处理)CallableStatement
(存储过程)SQL 注入:利用某些系统没有对用户输入的数据进行充分的检查,故意注入非法的 SQL 语句段或命令,恶意攻击数据库。
使用
PreparedStatement
可以防范 SQL 注入
23.3.1 PreparedStatement
预处理
/* [1] */
String sql = "select * from customer where customer_id = ? and name = ?";
PreparedStatement ps = connection.prepareStatement(sql);
/* [2] */
ps.setInt(1, 0);
ps.setString(2, "萝茵");
/* [3] */
ResultSet rs = ps.executeQuery();
-
PreparedStatement
执行的 SQL 语句的参数用?
表示。 -
调用方法设置 SQL 语句中的参数。
这些方法的两个参数中,第一个是设置的参数的索引,第二个是设置值
-
调用
executeQuery()
返回ResultSet
对象。或者调用executeUpdate()
执行增删改这里,调用的方法不需要再传入参数
预处理的好处
- 不再使用拼接语句,增加代码可读性,减少语法错误
- 解决了 SQL 注入问题
- 大大减少了编译次数,效率提高
类 / 接口 | 方法 |
---|---|
DriverManager 驱动管理类 | getConnection(url, user, pwd) 获取连接 |
Connection 接口 | createStatement() 创建 Statement 对象 |
preparedStatement(sql) 生成预处理 PreparedStatement 对象 | |
Statement 接口 | executeUpdate(sql) 执行 dml 语句,返回影响行数 |
executeQuery(sql) 执行查询语句,返回 ResultSet | |
execute(sql) 执行任意 SQL 语句,返回布尔值 | |
PreparedStatement 接口 | executeUpdate() 执行 dml 语句,返回影响行数 |
executeQuery() 执行查询语句,返回 ResultSet | |
execute() 执行任意 SQL 语句,返回布尔值 | |
setXXX(int, xxx) 设置 SQL 语句中占位符的值 | |
setObject(int, xxx) 设置 SQL 语句中占位符的值 | |
ResultSet 结果集 | next() 向下移动一行。没有下一行的场合返回 false |
previous() 向上移动一行。没有上一行的场合返回 false | |
getXXX(int)、getXXX(name) 返回 int 列 / name 列的值 | |
getObject(int)、getObject(name) 返回 int 列 / name 列的值 |
23.4 JDBCUtils
实际操作中,获取连接 和 释放资源 操作经常使用。可以把这些操作封装成工具类 JDBCUtils
JDCUtils.java
package com.melody;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
/**
* @author Melody
* @version 1.0
*/
public class JDBCUtils {
private static String url;
private static String user;
private static String pw;
private static String driver;
static {
try {
Properties properties = new Properties();
properties.load(new FileInputStream("src/com/melody/mysql.properties"));
url = properties.getProperty("url");
user = properties.getProperty("user");
pw = properties.getProperty("password");
driver = properties.getProperty("driver");
Class<?> aClass = Class.forName(driver);
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e); //[1]
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, pw);
}
public static boolean close(Connection c, Statement s, ResultSet r) {
try {
if (!(c == null)){
c.close();
}
if (!(s == null)){
s.close();
}
if (!(r == null)){
r.close();
}
return true;
} catch (SQLException e) {
throw new RuntimeException(e); //[1]
}
}
}
- 实际开发中,可以把编译异常转换为运行异常。这样,调用者可以自行选择捕获异常还是默认处理。
23.5 事务
事务:JDBC 程序中,当一个
Connection
对象创建时,默认情况下会自动提交事务。为了让多个 SQL 语句一体执行,需要使用事务。
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
connection.setAutoCommit(false); //[1] 取消自动提交事务
String sql = "insert into actor values(9, '赫尔萝茵')";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.executeUpdate();
sql = "delete from actor where name = '萝茵' and id > (select * from (select min(id) from actor where name = '萝茵') a)";
//[2]
preparedStatement = connection.prepareStatement(sql);
preparedStatement.executeUpdate();
connection.commit(); //[3] 提交事务
System.out.println("complete");
} catch (SQLException e) {
System.out.println("fail");
System.out.println(e);
connection.rollback(); //[4] 回滚事务
} finally {
connection.close(); //[5] 关闭连接
}
-
取消自动提交事务:
connection.setAutoCommit(false);
-
这是一个 SQL 语句。表示在 name = 萝茵 的数据中只保留 id 最小的一条。
delete from actor where name = '萝茵' and id > (select * from (select min(id) from actor where name = '萝茵') a)
正常写法会提示不能同表查询(在 MySQL 中,不能在同一语句中先 select 出同一表中的某些值,再 update 这个表)。使用一个额外的 select 过渡就解决了这个问题。特此记录。
-
提交事务:
connection.commit();
-
回滚事务:
connection.rollback();
写在 catch 里。这样,语句错误抛出异常的场合会执行到这句话。
-
关闭连接:
connection.close();
写在 finally 里。这样,执行完语句总会关闭连接。这很好。
23.6 批处理
批处理:把多条语句一次性提交给数据库进行批量处理。这样做比单独提交更有效率。
要使用批处理功能,需要在 url 中加入 ?rewriteBatchedStatements=true
,即在配置文件中写成:
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
批处理往往和 PerparedStatement 搭配使用,既减少编译次数,又减少运行次数,岂不美哉?
示例:
//[0] 事前准备 Connection connection = JDBCUtils.getConnection(); String sql = "insert into test1 values(?)"; PreparedStatement ps = connection.prepareStatement(sql); for (int i = 0; i < 1000; i++) { ps.setInt(1,i); ps.addBatch(); //[1] } ps.executeBatch(); //[2] JDBCUtils.close(connection, ps, null);
修改配置文件
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true ...
添加需要批量处理的 SQL 语句
preparedStatement.addBatch();
执行批量处理语句
preparedStatement.executeBatch();
(前面的例子里没有这句)清空批处理包的语句
preparedStatement.clearBatch();
23.6.1 说明
-
第一次添加批处理语句时,创建一个 ArrayList。
... if (this.batchedArgs == null) { this.batchedArgs = new ArrayList(); } ...
……会在其中放置批处理语句(指令的数据)
-
该数组满后,按照 1.5 倍扩容
-
达到指定的值后,执行 executeBatch 吧
-
批处理能减少编译次数,也能减少发送 SQL 语句的网络开销。
23.7 数据库连接池
传统获取 Connection 方法的问题:
- 传统的 JDBC 数据库连接使用 DriverManager 获取,每次建立连接都会把 Connection 载入内存,再进行身份验证。每次连接都会重复验证请求,这样会占用过多系统资源,容易造成服务器崩溃。
- 每次连接,使用完毕后必须断开。如果不断开操作,会致使数据库内存泄漏,最终不得不重启数据库。
- 传统连接方式不能控制创建的连接数量。连接数量过多的场合,也可能导致内存泄漏,MySQL 崩溃
因此,需要使用连接池技术:
- 预先在缓冲池放入一定数量的连接。需要建立数据库连接时,从缓冲池中取出一个连接。使用完后,把该连接放回缓冲池。
- 数据库连接池负责分配、管理和释放数据库连接。其允许应用程序重复使用一个现有的数据库连接,而非建立新的连接。
- 当请求连接的数量超过最大数量,这些连接请求会被加入等待队列。
数据库连接池种类:
JDBC 数据库连接池使用 javax.sql.DataSource 表示。DataSource 是一个接口,通常由第三方提供实现(提供 jar 包)
- C3P0:速度稍慢,稳定性好……但是速度再慢也不是传统 JDBC 能比得上的,不自量力!
- DBCP:速度稍快,稳定性差
- Proxool:可以监控连接池状态,稳定性稍差
- BoneCP:速度快
- Druid:阿里提供的数据库连接池,集 DBCP、C3P0、Proxool 优点于一身
23.7.1 C3P0
写在前面:C3P0 连接时跳一堆红字,不一定是报错……他就这样。
使用 C3P0 的前置工作:
-
……和前面一样,把 jar 包装载好。
为了正常运行程序,这里(上面连接下载的) jar 包(似乎)至少是:
mchange-commons-java-0.2.19.jar
c3p0-0.9.5.5.jar
-
方式一:在程序中指定相关参数
/* [1] */ ComboPooledDataSource cpds = new ComboPooledDataSource(); /* [2] */ cpds.setDriverClass(dirver); cpds.setUser(user); cpds.setPassword(password); cpds.serJdbcUrl(url); /* [3] */ cpds.setInitialPoolSize(10); /* [4] */ cpds.setMaxPoolSize(50); /* [5] */ Connection connection = cpds.getConnection(); ... connection.close();
-
创建数据源对象
ComboPooledDataSource cpds = new ComboPooledDataSource();
-
设置参数
cpds.setDriverClass(String dirver); cpds.setUser(String user); cpds.setPassword(String password); cpds.serJdbcUrl(String url);
……上面输入的形参
user
是字符串"root"
。其他同理。 -
设置初始化连接数
cpds.setInitialPoolSize(10);
初始化连接数,就是指连接池创建时,初始持有的连接数
-
设置最大连接数
cpds.setMaxPoolSize(50);
-
获取一个连接
Connection connection = cpds.getConnection();
-
-
方式二:使用配置文件模板完成
/* [0] 前置操作 */ /* [1] 获取数据源对象 */ ComboPooledDataSource cpds = new ComboPooledDataSource("Heruin"); /* [2] 获取连接 */ Connection connection = cpds.getConnection(); ... connection.close();
-
加入配置文件
**c3p0-config.xml:**配置文件,文件名固定,放在
src/c3p0-config.xml
这个位置<c3p0-config> <!-- 数据源(连接池)名称 --> <named-config name="Heruin"> <!-- 驱动类 --> <property name="driverClass">com.mysql.cj.jdbc.Driver</property> <!-- url --> <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property> <!-- 用户名 --> <property name="user">root</property> <!-- 密码 --> <property name="password">******</property> <!-- 初始化连接数 --> <property name="initialPoolSize">10</property> <!-- 最大空闲时间 --> <property name="maxIdleTime">30</property> <!-- 最大连接数 --> <property name="maxPoolSize">100</property> <!-- 最小连接数 --> <property name="minPoolSize">10</property> <!-- 每次增长的连接数 --> <property name="acquireIncrement">10</property> <!-- 可连接的最多的命令对象数 --> <property name="maxStatements">10</property> <!-- 每个连接可连接的最多的命令对象数 --> <property name="maxStatementsPerConnection">5</property> </named-config> </c3p0-config>
-
获取数据源对象
ComboPooledDataSource cpds = new ComboPooledDataSource(String name);
这里的
name
就是配置文件中的数据源名称
-
获取连接
-
23.7.2 Druid(德鲁伊)连接池
~有的人因为看见而相信,有的人因为相信而看见!~
使用 Druid 的前置工作:
-
……这个链接打不开的话,试试 这个
-
配置 jar 包
-
加入配置文件
**durid.properties:**配置文件,文件名任意,放在
src/druid.properties
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true username=root password=720mel\im initialSize=10 minIdle=5 maxActive=20 maxWait=5000
-
使用 Druid:
/* [1] 加载配置文件 */ Properties properties = new Properties(); properties.load(new FileInputStream("src\\Druid.properties")); /* [2] 初始化数据池 */ DataSource dataSource = DruidDataSourceFactory.createDataSource(properties); /* [3] 获取连接 */ Connection connection = dataSource.getConnection(); ... connection.close();
-
初始化数据池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
-
……没什么好说的,但要说一下这个:
connection.close();
Connection
是一个接口,对于方法close()
,不同供应商有不同实现方法。原生的
MySQL
的实现方法是关闭连接,而这些连接池的实现方法是取消引用(放回连接池)。
-
23.8 Apache - DbUtils
commons-DbUtils
:是 Apache 组织提供的一个开源的 JDBC 工具库。它是对 JDBC 的封装。使用 dbutils 能极大简化 JDBC 编码的工作量DbUtils 的常用类:
QueryRunner
类:封装了 SQL 的执行。是线程安全的,可以实现增、删、改、查、批处理操作ResultSetHandler
接口:用于处理ResultSet
,按照需求将数据转化为其他形式
JDBC 传统方法的不足:
- 结果集和连接是关联的。在连接关闭后,就不能使用结果集。
- 结果集只能使用一次,这样不便于数据管理
- 使用返回信息不方便
解决方案:
-
土方法:创建一个类,其属性与表格的列一一对应。将数据遍历并读取到一个个类对象中,再将这些类对象放置到集合中。这样,就得到了一个与表格数据关联的数据集合。
数据类,根据需要创建:
public class SQL_Data { private int id; private Date date; private String name; /* 一定要有无参构造器。这是因为后面会用到反射 */ public SQL_Data(){} public SQL_Data(int id, Date date, String name) { this.id = id; this.date = date; this.name = name; } /* Geter & Seter 笔记里就省略不记了。光占地方又没技术含量 */ ... }
……然后遍历 ResultSet,存放。就不写了。
-
使用
DbUtils
:/* [0] 前置操作 */ String sql = "select * from account where ? = ?"; /* [1] 创建 QueryRunner */ QueryRunner qr = new QueryRunner(); /* [2] 执行相关方法 */ List<SQL_Data> list = qr.query(connection, sql, new BeanListHandler<>(SQL_Data.class), "id", 1); ... /* [3] 释放资源*/ connection.close();
-
前置操作:
获取 jar 包,并完成配置
得到连接
Connection connection
,创建数据类(这里是SQL_Data
) -
执行相关方法,返回结果集
List<SQL_Data> list = queryRunner.query(connection, sql, new BeanListHandler<>(SQL_Data.class), ...);
-
queryRunner.query
方法:执行 SQL 语句,把得到的ResultSet
封装到 List 集合。这个方法会自动关闭获得的ResultSet
,所以不会造成资源泄漏 -
connection
:前置操作中得到的连接 -
sql
:SQL 语句 -
new BeanListHander<>(SQL_Data.class)
:利用反射机制,将数据封装到SQL_Data
对象中 -
"id", 1
:给 SQL 语句中的?
赋值。因为是可变参数,可以写多个,如:queryRunner.query(connection, sql, new BeanListHandler<>(SQL_Data.class), "id", 1, "name", "识之律者");
……这里,可变参数也包含列名的场合,筛选似乎会失效。还不知道原理。特此记录。
-
-
此时只需要关闭
Connection
query()
方法已经关闭了ResultSet
和PreparedStatement
-
23.8.1 Apache-DbUtils 常用方法
-
queryRunner.update(connection, sql, ...)
:DML 语句。增删改。后面传入可变参数,用于给 SQL 语句中的
?
赋值返回值 int 代表被影响的行数。
-
queryRunner.query(connection, sql, ResultSetHandler, ...)
:查询。后面传入可变参数,用于给 SQL 语句中的
?
赋值关于
ResultSetHandler
:- 传入
new BeanListHander<>(SQL_Data.class)
:返回多行多列。这个场合,用List
接收返回值。 - 传入
new BeanHandler<>(SQL_Data.class)
:返回单行。这个场合,用SQL_Data data
接收返回值。 - 传入
new ScalarHander<>()
:返回单行单列。这个场合,用Object
接收返回值。
- 传入
23.8.2 表和 JavaBean 的对应关系
表类型 | JavaBean类型 |
---|---|
int、samllint…… | Integer |
char、varchar…… | String |
double | Double |
date | java.util.Date |
23.9 BasicDAO
Apache - DbUtils + Druid 简化了 JDBC 开发,但还有不足。
- SQL 语句固定,不能通过传入参数控制,通用性不好。
- select 操作的返回类型未知,需要使用泛型。
- 将来的表很多,业务需求复杂,不可能只靠一个 Java 类完成
DAO:数据访问对象(data access object)
-
我们把通用的和数据库交互的操作封装到一个通用类中,称为 BasicDAO。
-
在 BasicDAO 基础上,每张表对应一个特化的 DAO(继承 BasicDAO),从而更好地完成功能。
比如:Data 表 -> Data.java 类(JavaBean)-> DataDAO.java
BasicDAO.java(示例)
public class BasicDAO<T> {
private QueryRunner qr = new QueryRunner();
public int update(String sql, Object... parameters){
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
return qr.update(connection, sql, parameters);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCUtils.close(connection, null, null);
}
}
public List<T> queryMulti(String sql, Class<T> tClass, Object... parameters){
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
return qr.query(connection, sql, new BeanListHandler<T>(tClass), parameters);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCUtils.close(connection, null, null);
}
}
public T querySingle(String sql, Class<T> tClass, Object... parameters){
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
return qr.query(connection, sql, new BeanHandler<T>(tClass), parameters);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCUtils.close(connection, null, null);
}
}
public Object queryScalar(String sql, Object... parameters){
Connection connection = null;
try {
connection = JDBCUtils.getConnection();
return qr.query(connection, sql, new ScalarHandler<>(), parameters);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCUtils.close(connection, null, null);
}
}
}