Jdbc和数据库连接池
Jdbc和数据库连接池
sinarcsinxJDBC:Java 提供的一套用于操作数据库的接口 API。Java 程序员只需要面向该接口即可连接任何提供了 JDBC 驱动程序的数据库,完成对数据库的各种操作。不同的数据库厂商,需要针对这套接口提供不同的实现。
JDBC 程序编写步骤(示范):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 /* [0] 前置操作 */
/* [1] 注册驱动 */
Driver driver = new com.mysql.cj.jdbc.Driver();
/* [2] 获取连接 */
String url = "jdbc:mysql://localhost:3306/sinarcsinx";
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
1 String url = "jdbc:mysql://localhost:3306/sinarcsinx";这里表示进入
sinarcsinx
这个数据库。执行语句:发送 SQL 命令给 MySQL 执行
1 int rows = statement.executeUpdate(sql);如果是 dml 语句,这里返回的 rows 是影响的行数。返回 0 表示失败。
释放资源:关闭相关连接(切记!)
连接数据库的 5 种方式
方式 1:直接创建
Driver
,调用driver.connet(url, properties);
这个方法就是 [23.0] 示范的方法
1
2
3
4
5
6Driver driver = new com.mysql.cj.jdbc.Driver();
String url = "jdbc:mysql://localhost:3306/sinarcsinx";
Properties properties = new Properties();
properties.setProperty("user", "root");
properties.setProperty("password", "******");
Connection connect = driver.connect(url, properties);方式 2:使用反射加载
Driver
类这个方式是动态加载,更加灵活。
1
2
3
4
5
6Class<?> 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
进行统一管理这个方式扩展性更好,更加清晰
1
2
3
4
5
6
7Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver");
Driver driver = (Driver) aClass.newInstance();
String url = "jdbc:mysql://localhost:3306/sinarcsinx";
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()
自动完成注册驱动1
2
3
4
5Class<?> aClass = Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/sinarcsinx";
String name = "root";
String password = "******";
Connection connection = DriverManager.getConnection(url, name, password);Class.forName()
在加载Driver
类时,会自动完成注册。原理:
Driver
类的源码中有这样一段1
2
3
4
5
6
7static {
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.properties):
1
2
3
4url=jdbc:mysql://localhost:3306/sinarcsinx
user=root
password=******
driver=com.mysql.cj.jdbc.Driver代码:
1
2
3
4
5
6
7
8Properties 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);
ReaultSet 结果集
ResultSet:表示数据库结果集的数据表。通常通过执行查询数据库的语句生成。
ResultSet 对象保持一个光标指向其当前的数据行。该光标的初始位置在第一行之前。调用 next 方法将光标下移,移动到末端的场合会返回 false。
如何取出数据(示例):
1 | String sql = "select * from customer"; |
其中 String sex = resset.getString(5);
表示取出该行数据的第 5 列的字符串数据。
ResultSet 的数据是以类似二维数组的形式保存在对象中。这不难理解,毕竟表格是二维的嘛。
Statement
Statment
对象用于执行静态 SQL 语句,并返回其生成的结果的对象在连接建立后,需要访问数据库、执行命名或 SQL 语句,有如下方式:
Statment
(存在 SQL 注入问题,在实际开发中基本不使用)PerparedStatement
(预处理)CallableStatement
(存储过程)SQL 注入:利用某些系统没有对用户输入的数据进行充分的检查,故意注入非法的 SQL 语句段或命令,恶意攻击数据库。
使用
PreparedStatement
可以防范 SQL 注入
PreparedStatement 预处理
1 | /* [1] */ |
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 列的值 |
JDBCUtils
实际操作中,获取连接 和 释放资源 操作经常使用。可以把这些操作封装成工具类 JDBCUtils
JDCUtils.java
1 | package com.sinarcsinx; |
- 实际开发中,可以把编译异常转换为运行异常。这样,调用者可以自行选择捕获异常还是默认处理。
事务
事务:JDBC 程序中,当一个
Connection
对象创建时,默认情况下会自动提交事务。为了让多个 SQL 语句一体执行,需要使用事务。
1 | Connection connection = null; |
取消自动提交事务:
1
connection.setAutoCommit(false);
这是一个 SQL 语句。表示在 name = 萝茵 的数据中只保留 id 最小的一条。
1
delete from actor where name = '萝茵' and id > (select * from (select min(id) from actor where name = '萝茵') a)
正常写法会提示不能同表查询(在 MySQL 中,不能在同一语句中先 select 出同一表中的某些值,再 update 这个表)。使用一个额外的 select 过渡就解决了这个问题。特此记录。
提交事务:
1
connection.commit();
回滚事务:
1
connection.rollback();
写在 catch 里。这样,语句错误抛出异常的场合会执行到这句话。
关闭连接:
1
connection.close();
写在 finally 里。这样,执行完语句总会关闭连接。这很好。
批处理
批处理:把多条语句一次性提交给数据库进行批量处理。这样做比单独提交更有效率。
要使用批处理功能,需要在 url 中加入 ?rewriteBatchedStatements=true
,即在配置文件中写成:
1 url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
批处理往往和 PerparedStatement 搭配使用,既减少编译次数,又减少运行次数,岂不美哉?
示例:
1
2
3
4
5
6
7
8
9
10 //[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);
修改配置文件
1
2 url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
...添加需要批量处理的 SQL 语句
1 preparedStatement.addBatch();执行批量处理语句
1 preparedStatement.executeBatch();(前面的例子里没有这句)清空批处理包的语句
1 preparedStatement.clearBatch();
说明
第一次添加批处理语句时,创建一个 ArrayList。
1
2
3
4
5...
if (this.batchedArgs == null) {
this.batchedArgs = new ArrayList();
}
...……会在其中放置批处理语句(指令的数据)
该数组满后,按照 1.5 倍扩容
达到指定的值后,执行 executeBatch 吧
批处理能减少编译次数,也能减少发送 SQL 语句的网络开销。
数据库连接池
传统获取 Connection 方法的问题:
- 传统的 JDBC 数据库连接使用 DriverManager 获取,每次建立连接都会把 Connection 载入内存,再进行身份验证。每次连接都会重复验证请求,这样会占用过多系统资源,容易造成服务器崩溃。
- 每次连接,使用完毕后必须断开。如果不断开操作,会致使数据库内存泄漏,最终不得不重启数据库。
- 传统连接方式不能控制创建的连接数量。连接数量过多的场合,也可能导致内存泄漏,MySQL 崩溃
因此,需要使用连接池技术:
- 预先在缓冲池放入一定数量的连接。需要建立数据库连接时,从缓冲池中取出一个连接。使用完后,把该连接放回缓冲池。
- 数据库连接池负责分配、管理和释放数据库连接。其允许应用程序重复使用一个现有的数据库连接,而非建立新的连接。
- 当请求连接的数量超过最大数量,这些连接请求会被加入等待队列。
数据库连接池种类:
JDBC 数据库连接池使用 javax.sql.DataSource 表示。DataSource 是一个接口,通常由第三方提供实现(提供 jar 包)
- C3P0:速度稍慢,稳定性好……但是速度再慢也不是传统 JDBC 能比得上的,不自量力!
- DBCP:速度稍快,稳定性差
- String regular = “(\w)\1+”; //即,重复的字母或数字Matcher matcher = Pattern.compile(regular).mathcer(content);content = matcher.replaceAll(“$1”); //这样,就完成了去重java
- BoneCP:速度快
- Druid:阿里提供的数据库连接池,集 DBCP、C3P0、Proxool 优点于一身
C3P0
写在前面:C3P0 连接时跳一堆红字,不一定是报错……他就这样。
使用 C3P0 的前置工作:
[C3P0 jar 包下载]
……和前面一样,把 jar 包装载好。
为了正常运行程序,这里(上面连接下载的) jar 包(似乎)至少是:
mchange-commons-java-0.2.19.jar
c3p0-0.9.5.5.jar
方式一:在程序中指定相关参数 > >
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/* [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();- 创建数据源对象 >
1
ComboPooledDataSource cpds = new ComboPooledDataSource();
- 设置参数 > >
1
2
3
4cpds.setDriverClass(String dirver);
cpds.setUser(String user);
cpds.setPassword(String password);
cpds.serJdbcUrl(String url);……上面输入的形参
user
是字符串"root"
。其他同理。 > > 3. 设置初始化连接数 >1
cpds.setInitialPoolSize(10);
初始化连接数,就是指连接池创建时,初始持有的连接数 >
4. 设置最大连接数 > >1
cpds.setMaxPoolSize(50);
- 获取一个连接 >
1
Connection connection = cpds.getConnection();
方式二:使用配置文件模板完成
1
2
3
4
5
6
7/* [0] 前置操作 */
/* [1] 获取数据源对象 */
ComboPooledDataSource cpds = new ComboPooledDataSource("Heruin");
/* [2] 获取连接 */
Connection connection = cpds.getConnection();
...
connection.close();加入配置文件
c3p0-config.xml:配置文件,文件名固定,放在
src/c3p0-config.xml
这个位置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27<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>获取数据源对象
1
ComboPooledDataSource cpds = new ComboPooledDataSource(String name);
这里的
name
就是配置文件中的数据源名称
获取连接
Druid(德鲁伊)连接池
有的人因为看见而相信,有的人因为相信而看见!
使用 Druid 的前置工作:
[Druid jar 包下载]
配置 jar 包
加入配置文件
durid.properties:配置文件,文件名任意,放在
src/druid.properties
1
2
3
4
5
6
7
8driverClassName=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
2
3
4
5
6
7
8
9/* [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();初始化数据池
1
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
……没什么好说的,但要说一下这个:
1
connection.close();
Connection
是一个接口,对于方法close()
,不同供应商有不同实现方法。原生的
MySQL
的实现方法是关闭连接,而这些连接池的实现方法是取消引用(放回连接池)。
Apache - DbUtils
commons-DbUtils
:是 Apache 组织提供的一个开源的 JDBC 工具库。它是对 JDBC 的封装。使用 dbutils 能极大简化 JDBC 编码的工作量DbUtils 的常用类:
QueryRunner
类:封装了 SQL 的执行。是线程安全的,可以实现增、删、改、查、批处理操作ResultSetHandler
接口:用于处理ResultSet
,按照需求将数据转化为其他形式
JDBC 传统方法的不足:
- 结果集和连接是关联的。在连接关闭后,就不能使用结果集。
- 结果集只能使用一次,这样不便于数据管理
- 使用返回信息不方便
解决方案:
土方法:创建一个类,其属性与表格的列一一对应。将数据遍历并读取到一个个类对象中,再将这些类对象放置到集合中。这样,就得到了一个与表格数据关联的数据集合。
数据类,根据需要创建:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public 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
:1
2
3
4
5
6
7
8
9
10/* [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
)执行相关方法,返回结果集
1
2List<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
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
接收返回值。
- 传入
表和 JavaBean 的对应关系
表类型 | JavaBean类型 |
---|---|
int、samllint…… | Integer |
char、varchar…… | String |
double | Double |
date | java.util.Date |
BasicDAO
Apache - DbUtils + Druid 简化了 JDBC 开发,但还有不足。
- SQL 语句固定,不能通过传入参数控制,通用性不好。
- select 操作的返回类型未知,需要使用泛型。
- 将来的表很多,业务需求复杂,不可能只靠一个 Java 类完成
DAO:数据访问对象(data access object)
我们把通用的和数据库交互的操作封装到一个通用类中,称为 BasicDAO。
在 BasicDAO 基础上,每张表对应一个特化的 DAO(继承 BasicDAO),从而更好地完成功能。
比如:Data 表 -> Data.java 类(JavaBean)-> DataDAO.java
BasicDAO.java(示例)
1 | public class BasicDAO<T> { |