avatar


10.JDBC

什么是JDBC

JDBC,Java DataBase Connectivity,Java数据库连接。

JDBC

  • 是Java官方定义的一套操作所有关系型数据库的规则(接口)。
  • 各个数据库厂商去实现这套接口,提供数据库驱动JAR包。

另一个角度,在《MySQL从入门到实践:4.结构》,我们提到了Connectors(客户端),JDBC属于MySQL的Connectors(客户端)的一部分(但这个描述没有体现JDBC的特点)。

用法

快速入门

步骤

  1. 注册驱动,即指定需要连接哪一个客户端,示例代码:
    1
    Class.forName("com.mysql.cj.jdbc.Driver");
    在这里,我们使用了反射,将一个类加载进内存。
  2. 获取连接,客户端与MySQL服务端建立连接,示例代码:
    1
    Connection conn = DriverManager.getConnection(url, username, password);
  3. 编写SQL语句,示例代码:
    1
    String sql = "【SQL】";
  4. 获取执行SQL的对象,示例代码:
    1
    Statement stmt = conn.createStatement();
    执行SQL的对象,不是说这个对象被SQL执行,而是说这个对象主动的去执行SQL。
  5. 执行SQL,即发送给数据库服务端,示例代码:
    1
    stmt.executeUpdate(sql);
  6. 处理返回结果
  7. 释放资源
    1
    2
    stmt.close();
    conn.close();

例子

接下来,我们来看一个具体的例子。

准备工作

新建项目
我们创建一个新的空项目,并为该项目指定名称、目录以及JDK版本。

创建一个新项目

为新项目指定名称和目录

指定JDK

新建模块

点击右上角,选择Project Structure,新建模块。

创建模块

导入驱动包

在新建模块之后,在模块上点击右键,新建一个Dictionary。

新建目录

  • 用于存放第三方的JAR包的目录,一般取名为lib或者libs

访问地址:https://mvnrepository.com/artifact/mysql/mysql-connector-java

点击箭头所指处,下载JAR包:
下载JAR包。

将下载的JAR包,复制到lib目录中,右键点击JAR包,Add a Library

Add a Library

此时有三个选项,分别是:

  1. Global Library:全局有效
  2. Project Library:项目有效
  3. Module Library:模块有效

在这里,我们选择Module Library

Module Library

在本文,我们的JDBC版本是8.0.29

题外话,如果我们想从Library中移除某个包:
右键,选择Open Module SettingDependencies,然后移除。

Module Setting

Dependencies

编写代码

接下来,就是编写代码了,在src目录下,点击新建Java Class

新建Class

示例代码:

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
28
29
package com.kakawanyifan;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc";
String username = "root";
String password = "MySQL@2022";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "update a set a2 = 22 where a1 = 2;";
//4. 获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
//5. 执行sql
int count = stmt.executeUpdate(sql);
//6. 处理结果
System.out.println(count);
//7. 释放资源
stmt.close();
conn.close();
}
}

运行结果:

1
1

在注册驱动的代码中,我们写的是Class.forName("com.mysql.cj.jdbc.Driver");,如果将com.mysql.cj.jdbc.Driver改成com.mysql.jdbc.Driver的话,还会有如下的提示:

1
Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.

注意:The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.,驱动不用自动注册。

接下来,我们依次介绍上述例子中的各种组件。

DriverManager

DriverManager,驱动管理,主要作用:

  1. 注册驱动
  2. 获取数据库连接

注册驱动

注册驱动有四种方式:

  1. Class.forName("com.mysql.cj.jdbc.Driver");
    我们上文采用的是就是这种方法。
  2. System.setProperty("jdbc.drivers","com.mysql.cj.jdbc.Driver");
  3. DriverManager.registerDriver(new com.mysql.cj.jdbc.Driver());
  4. 自动注册,在5版本之后的JAR包中,JDBC可以根据数据库连接的URL,自动注册驱动了,所以上述我们注册驱动的代码也可以不写。

获取数据库连接

在上文中,我们获取数据库连接的代码是:

1
2
3
4
String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc";
String username = "root";
String password = "MySQL@2022";
Connection conn = DriverManager.getConnection(url, username, password);

即,我们是通过连接字符串,用户名,密码来获取数据库的连接对象的,方法如下:

1
Connection getConnection (String url, String user, String password)

解释一下URL部分,格式如下:

1
jdbc:mysql://【IP或域名】:【端口号】/【数据库名称】?【参数键值对1】&【参数键值对2】...
  • jdbc是协议名
  • mysql是子协议

如果数据库出现乱码,可以指定参数characterEncoding=utf8,表示让数据库以UTF-8编码来处理数据。例如:

1
jdbc:mysql://localhost:3306/test_jdbc?characterEncoding=utf8

除了上述方法,我们还可以通过连接字符串,属性对象来得到连接对象,方法如下:

1
Connection getConnection (String url, Properties info)

示例代码:

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
package com.kakawanyifan;


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class JdbcTest {
public static void main(String[] args) throws SQLException {

String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc";
String username = "root";
String password = "MySQL@2022";

Properties info = new Properties();
info.setProperty("user",username);
info.setProperty("password",password);
Connection conn = DriverManager.getConnection(url, info);

System.out.println(conn);

conn.close();
}
}

运行结果:

1
com.mysql.cj.jdbc.ConnectionImpl@53ca01a2

Connection

Connection,数据库连接对象,主要作用:

  1. 获取执行SQL的对象
  2. 管理事务

获取执行SQL对象

有三种执行SQL的对象:

  1. 普通执行SQL对象,方法如下:
    1
    Statement createStatement()
    在上文,我们用的就是这种。
  2. 预编译SQL的执行SQL对象
    1
    PreparedStatement  prepareStatement(sql)
    通过这种方法,可以防止SQL注入,我们会在下文做更多的讨论。
  3. 执行存储过程的对象
    1
    CallableStatement prepareCall(sql)

管理事务

《MySQL从入门到实践:6.事务》,我们讨论过事务,最基本的操作有;

  1. 开启事务:BEGIN;或者START TRANSACTION;
  2. 提交事务:COMMIT;
  3. 回滚事务:ROLLBACK;

Connection中,有对应的方法:

  1. 开启事务:
    1
    setAutoCommit(boolean autoCommit)
    autoCommittrue表示自动提交事务,false表示手动提交事务,开启事务的方法为将该参数设为false
  2. 提交事务
    1
    commit()
  3. 回滚事务
    1
    rollback()

示例代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.kakawanyifan;


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcTest {
public static void main(String[] args) throws SQLException {
//1. 注册驱动
//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc";
String username = "root";
String password = "MySQL@2022";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql1 = "update a set a2 = 100 where a1 = 1";
String sql2 = "update a set a2 = 200 where a1 = 2";
//4. 获取执行sql的对象 Statement
Statement stmt = conn.createStatement();

try {
// ============开启事务==========
conn.setAutoCommit(false);
//5. 执行sql
int count1 = stmt.executeUpdate(sql1);//受影响的行数
//6. 处理结果
System.out.println(count1);
int i = 3/0;
//5. 执行sql
int count2 = stmt.executeUpdate(sql2);//受影响的行数
//6. 处理结果
System.out.println(count2);
// ============提交事务==========
//程序运行到此处,说明没有出现任何问题,则需求提交事务
conn.commit();
} catch (Exception e) {
// ============回滚事务==========
//程序在出现异常时会执行到这个地方,此时就需要回滚事务
conn.rollback();
e.printStackTrace();
}

//7. 释放资源
stmt.close();
conn.close();
}
}

运行结果:

1
2
3
1
java.lang.ArithmeticException: / by zero
at com.kakawanyifan.JdbcTest.main(JdbcTest.java:30)

Statement

Statement对象,用来执行SQL语句,对于不同类型的SQL语句,执行方法也不一样。

  1. 执行DDL、DML语句:
    1
    statement.executeUpdate(【SQL】)
    返回内容是int类型,表示受影响条数。
  2. 执行DQL语句:
    1
    statement.executeQuery(【SQL】)
    返回内容是ResultSet类型。

ResultSet

ResultSet,结果集对象,主要方法有:

  1. boolean next(),将"光标"移动到下一行,并判断是否为有效行:true,有效行,即当前行有数据;false,无效行,当前行没有数据。
  2. close(),关闭,释放资源
  3. getXxx(),获取数据,有两种类型的入参:int,列的编号(从1开始);String,列的名称。
SQL类型 JDBC对应方法 返回类型
BIT(1) getBoolean() boolean
TINYINT getByte() byte
SMALLINT getShort() short
INT getInt() int
BIGINT getLong() long
CHARVARCHAR getString() String
TextClob getClob() Clob
Blob getBlob() Blob
DATE getDate() java.sql.Date(只代表日期)
TIME getTime() java.sql.Time(只表示时间)
TIMESTAMP getTimestamp() java.sql.Timestamp(同时有日期和时间)
  • java.sql.Datejava.sql.Timejava.sql.Timestamp,三个共同父类是java.util.Date

示例代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.kakawanyifan;


import java.sql.*;

public class JdbcTest {
public static void main(String[] args) throws SQLException {
//1. 注册驱动
//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc";
String username = "root";
String password = "MySQL@2022";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "select * from a";
//4. 获取statement对象
Statement stmt = conn.createStatement();
//5. 执行sql
ResultSet rs = stmt.executeQuery(sql);
//6. 处理结果, 遍历rs中的所有数据
while (rs.next()){
// 获取数据 getXxx()
int a1 = rs.getInt(1);
System.out.println("int a1 = rs.getInt(1); " + a1);
int a2 = rs.getInt(2);
System.out.println("int a2 = rs.getInt(2); " + a2);
String a1Str = rs.getString(1);
System.out.println("String a1Str = rs.getString(1); " + a1Str);
String a2Str = rs.getString(2);
System.out.println("String a2Str = rs.getString(2); " + a2Str);
String a3 = rs.getString(3);
System.out.println("String a3 = rs.getString(3); " + a3);
double a4 = rs.getDouble(4);
System.out.println("double a4 = rs.getDouble(4); " + a4);

a1 = rs.getInt("a1");
System.out.println("a1 = rs.getInt(\"a1\"); " + a1);
a2 = rs.getInt("a2");
System.out.println("a2 = rs.getInt(\"a2\"); " + a2);
a1Str = rs.getString("a1");
System.out.println("a1Str = rs.getString(\"a1\"); "+ a1Str);
a2Str = rs.getString("a2");
System.out.println("a2Str = rs.getString(\"a2\"); " + a2Str);
a3 = rs.getString("a3");
System.out.println("a3 = rs.getString(\"a3\"); " + a3);
a4 = rs.getDouble("a4");
System.out.println("a4 = rs.getDouble(\"a4\"); " + a4);
}
//7. 释放资源
rs.close();
stmt.close();
conn.close();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int a1 = rs.getInt(1);   1
int a2 = rs.getInt(2); 11
String a1Str = rs.getString(1); 1
String a2Str = rs.getString(2); 11
String a3 = rs.getString(3); abc
double a4 = rs.getDouble(4); 2.71
a1 = rs.getInt("a1"); 1
a2 = rs.getInt("a2"); 11
a1Str = rs.getString("a1"); 1
a2Str = rs.getString("a2"); 11
a3 = rs.getString("a3"); abc
a4 = rs.getDouble("a4"); 2.71
int a1 = rs.getInt(1); 2
int a2 = rs.getInt(2); 22
String a1Str = rs.getString(1); 2
String a2Str = rs.getString(2); 22
String a3 = rs.getString(3); xyz
double a4 = rs.getDouble(4); 3.14
a1 = rs.getInt("a1"); 2
a2 = rs.getInt("a2"); 22
a1Str = rs.getString("a1"); 2
a2Str = rs.getString("a2"); 22
a3 = rs.getString("a3"); xyz
a4 = rs.getDouble("a4"); 3.14

在上文的代码的释放资源中,有三个行:

  1. rs.close(),关闭ResultSet。
  2. stmt.close(),关闭Statement。
  3. conn.close(),关闭Connection。

解释一下:
关闭Statement(PreparedStatement,CallableStatement)对象时,该Statement(PreparedStatement,CallableStatement)对象所创建的ResultSet对象,也会被关闭;关闭Connection对象时,该Connection对象所创建的Statement对象,也会被关闭。
建议在一个对象不再使用的时候,主动关闭。

PreparedStatement

作用:

  • 防止SQL注入
  • 预编译,提高效率

什么是SQL注入

SQL注入,一种攻击方法。

例如,用户登录校验,我们需要去数据库的表中,比对一下用户名和密码是否匹配,所以在程序中,可能会有类似于如下的SQL,示例代码:

1
SELECT * FROM account WHERE name = '【用户名】' AND password = '【密码】'

但,如果用户输入的密码,以'OR '1' = '1结尾的话,此时的SQL如下:

1
SELECT * FROM account WHERE name = '【用户名】' AND password = '【密码】' OR '1' = '1'

或者,用户输入的密码以';DROP TABLE account;结尾的,表都可能会被删除。

例如,密码是随便'or '1' = '1

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
28
29
30
31
32
33
34
35
36
package com.kakawanyifan;


import java.sql.*;

public class JdbcTest {
public static void main(String[] args) throws SQLException {
//1. 注册驱动
//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc";
String username = "root";
String password = "MySQL@2022";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
// 接收用户输入 用户名和密码
String name = "q";
String pwd = "随便'or '1' = '1";

// 定义sql
String sql = "select * from account where name = '" + name + "' and password = '" + pwd + "'";
// 获取pstmt对象
Statement statement = conn.createStatement();
// 执行sql
ResultSet rs = statement.executeQuery(sql);
// 判断登录是否成功
if(rs.next()){
System.out.println("登录:成功");
}else{
System.out.println("登录:失败");
}
//7. 释放资源
rs.close();
statement.close();
conn.close();
}
}

运行结果:

1
登录:成功

防止SQL注入的操作

  1. 获取PreparedStatement对象,SQL语句中的参数值,使用?占位符替代。

    1
    2
    3
    4
    // SQL语句中的参数值,使用?占位符替代
    String sql = "select * from account where name = ? and password = ?";
    // 通过Connection对象获取,并传入对应的sql语句
    PreparedStatement pstmt = conn.prepareStatement(sql);
  2. 设置参数值

    1
    pstmt.setXxx(参数1,参数2)

    参数1,位置编号,从1开始;参数2:值。
    相关方法有:setDoublesetFloatsetIntsetLongsetObjectsetString

  3. 执行SQL
    executeUpdate():执行DDL和DML
    executeQuery():执行DQL
    注意:调用这两个方法时不需要传参SQL语句。

同样,密码是随便'or '1' = '1,再试一下:

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
28
29
30
31
32
33
34
35
36
37
38
39
package com.kakawanyifan;


import java.sql.*;

public class JdbcTest {
public static void main(String[] args) throws SQLException {
//1. 注册驱动
//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc";
String username = "root";
String password = "MySQL@2022";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
// 接收用户输入 用户名和密码
String name = "q";
String pwd = "随便'or '1' = '1";

// 定义sql
String sql = "select * from account where name = ? and password = ?";
// 获取pstmt对象
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置?的值
pstmt.setString(1,name);
pstmt.setString(2,pwd);
// 执行sql
ResultSet rs = pstmt.executeQuery();
// 判断登录是否成功
if(rs.next()){
System.out.println("登录:成功");
}else{
System.out.println("登录:失败");
}
//7. 释放资源
rs.close();
pstmt.close();
conn.close();
}
}

运行结果:

1
登录:失败

防止SQL注入的原理

为了查看PreparedStatement的原理,我们需要开启MySQL的general_log(普通日志)。

查看general_log(普通日志)是否已经被开启,示例代码:

1
show variables like '%general%';

运行结果:

1
2
3
4
5
6
+----------------+-------------------------------------------+
|Variable_name |Value |
+----------------+-------------------------------------------+
|general_log |OFF |
|general_log_file|/usr/local/mysql/data/Kakas-MacBook-Pro.log|
+----------------+-------------------------------------------+

开启general_log:set global general_log=on;
关闭general_log:set global general_log=off;

这种方法修改的general_log,会在重启后失效,我们还可以通过修改配置文件的方式持久化。

我们先试试"Statement",观察日志,如下:

1
2
3
4
5
6
2022-06-07T15:48:41.480522Z	   15 Connect	root@localhost on test_jdbc using SSL/TLS
2022-06-07T15:48:41.483873Z 15 Query /* mysql-connector-java-8.0.29 (Revision: dd61577595edad45c398af508cf91ad26fc4144f) 【部分日志内容略】
2022-06-07T15:48:41.493203Z 15 Query SET character_set_results = NULL
2022-06-07T15:48:41.494117Z 15 Query SET autocommit=1
2022-06-07T15:48:41.505015Z 15 Query select * from account where name = 'q' and password = '随便'or '1' = '1'
2022-06-07T15:48:41.516186Z 15 Quit

再用"PreparedStatement",当输入随便'or '1' = '1时:

1
2
3
4
5
6
2022-06-07T15:50:06.345176Z	   16 Connect	root@localhost on test_jdbc using SSL/TLS
2022-06-07T15:50:06.349145Z 16 Query /* mysql-connector-java-8.0.29 (Revision: dd61577595edad45c398af508cf91ad26fc4144f) 【部分日志内容略】
2022-06-07T15:50:06.357397Z 16 Query SET character_set_results = NULL
2022-06-07T15:50:06.357864Z 16 Query SET autocommit=1
2022-06-07T15:50:06.372828Z 16 Query select * from account where name = 'q' and password = '随便''or ''1'' = ''1'
2022-06-07T15:50:06.384482Z 16 Quit

用"PreparedStatement",当输入随便';DROP TABLE account;时:

1
2
3
4
5
6
2022-06-07T23:29:22.429634Z	   26 Connect	root@localhost on test_jdbc using SSL/TLS
2022-06-07T23:29:22.432921Z 26 Query /* mysql-connector-java-8.0.29 (Revision: dd61577595edad45c398af508cf91ad26fc4144f) 【部分日志内容略】
2022-06-07T23:29:22.442774Z 26 Query SET character_set_results = NULL
2022-06-07T23:29:22.443217Z 26 Query SET autocommit=1
2022-06-07T23:29:22.459962Z 26 Query select * from account where name = 'q' and password = '随便'';DROP TABLE account;'
2022-06-07T23:29:22.470754Z 26 Quit

解释说明:被转义了。

这就是防止SQL注入的原理:对特殊字符进行转义

预编译

PreparedStatement的另一个作用,预编译SQL,这样效率能更高。

需要在URL中加上如下参数,以开启预编译功能

1
useServerPrepStmts=true

示例代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.kakawanyifan;


import java.sql.*;

public class JdbcTest {
public static void main(String[] args) throws SQLException {
//1. 注册驱动
//2. 获取连接
String url = "jdbc:mysql://127.0.0.1:3306/test_jdbc?useServerPrepStmts=true";
String username = "root";
String password = "MySQL@2022";
Connection conn = DriverManager.getConnection(url, username, password);
//3. 定义sql
String sql = "select * from account where name = ? and password = ?";
// 获取pstmt对象
PreparedStatement pstmt = conn.prepareStatement(sql);

pstmt.setString(1,"1");
pstmt.setString(2,"2");
ResultSet rs = pstmt.executeQuery();

pstmt.setString(1,"11");
pstmt.setString(2,"22");
rs = pstmt.executeQuery();

pstmt.setString(1,"111");
pstmt.setString(2,"222");
rs = pstmt.executeQuery();

pstmt.setString(1,"1111");
pstmt.setString(2,"2222");
rs = pstmt.executeQuery();


//7. 释放资源
rs.close();
pstmt.close();
conn.close();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
2022-06-07T23:37:01.245012Z	   27 Connect	root@localhost on test_jdbc using SSL/TLS
2022-06-07T23:37:01.249296Z 27 Query /* mysql-connector-java-8.0.29 (Revision: dd61577595edad45c398af508cf91ad26fc4144f) */SELECT @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@collation_server AS collation_server, @@collation_connection AS collation_connection, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_write_timeout AS net_write_timeout, @@performance_schema AS performance_schema, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@transaction_isolation AS transaction_isolation, @@wait_timeout AS wait_timeout
2022-06-07T23:37:01.257040Z 27 Query SET character_set_results = NULL
2022-06-07T23:37:01.257493Z 27 Query SET autocommit=1
2022-06-07T23:37:01.272136Z 27 Prepare select * from account where name = ? and password = ?
2022-06-07T23:37:01.276161Z 27 Execute select * from account where name = '1' and password = '2'
2022-06-07T23:37:01.287837Z 27 Execute select * from account where name = '11' and password = '22'
2022-06-07T23:37:01.288373Z 27 Execute select * from account where name = '111' and password = '222'
2022-06-07T23:37:01.288779Z 27 Execute select * from account where name = '1111' and password = '2222'
2022-06-07T23:37:01.289232Z 27 Close stmt
2022-06-07T23:37:01.289321Z 27 Quit

连接池

概述

结合我们在本章以及《MySQL从入门到实践:4.结构》的讨论,我们梳理一下SQL执行过程中的各种"通信"。

SQL连接

我们看到,各种连接和认证,占据了大多数的通信,真正在执行SQL和返回结果的,却是少数。如果能连上之后,可以多次执行SQL,是不是可以节约资源?
这就是连接池。

连接池:分配和管理数据库连接。
通常当系统初始化好后,容器被创建,容器中会申请一些连接对象,当用户来访问数据库时,从容器中获取连接对象,用户访问完之后,会将连接对象归还给容器。

标准接口:DataSource,在javax.sql下的。(之前JDBC相关的都在java.sql下)。
方法:

  • 获取连接:getConnection()
  • 归还连接:Connection.close(),如果Connection对象是从连接池中获取的,那么调用Connection.close()方法,不会关闭连接,而是归还连接。

当然,一般我们不会去实现这些接口,有开源的连接池,例如:

  1. DBCP
  2. C3P0
  3. Druid
  4. HikariCP

其中DBCP现在不常用了;C3P0性能不佳;Druid是最常用的,而且支持监控功能;HikariCP是后起之秀,性能强劲。
在本文,我们只讨论DruidHikariCP

Druid

Druid,由阿里巴巴提供的,使用方法如下:

  1. 下载并导入JAR包:druid-1.2.10.jar
    (下载和导入方法与上文MySQL的驱动包类似,不赘述)
  2. 定义配置文件
    properties形式,可以叫任意名称,可以放在任意目录下。
  3. 加载配置文件
  4. 获取数据库连接池对象DruidDataSourceFactory
  5. 获取连接getConnection

首先,我们定义配置文件。
在这里,我们将配置文件放在src目录下,src目录是类的加载目录,在下文我们通过DruidDemo.class.getClassLoader()方法定位到这个目录。

配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
# 数据库连接URL
url=jdbc:mysql://127.0.0.1:3306/test_jdbc?useServerPrepStmts=true
# 数据库用户名
username=root
# 数据库密码
password=MySQL@2022
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 获取连接时最大等待时间
maxWait=3000

部分配置参数解释:

  1. url,连接数据库的URL,例如
  2. username,连接数据库的用户名
  3. password,连接数据库的密码
  4. driverClassName:驱动名称,默认值会根据URL自动识别
  5. minIdle:最小连接池数量
  6. initialSize:初始化时建立连接的个数,默认值0
  7. maxActive:最大连接数量,默认值8
  8. maxWait:获取连接时最大等待时间,单位毫秒。配置了maxWait之后,会默认启用公平锁,这样并发效率会有所下降,但能保证整体性能稳定,可以通过配置useUnfairLocktrue使用非公平锁。
  9. minEvictableIdleTimeMillis:连接保持空闲而不被驱逐的最小时间,默认是30分钟,在生产环境中,如果我们想尽快释放,可以将该值调小。

更多的参数配置,可以参考Druid的官方文档:https://github.com/alibaba/druid/wiki/DruidDataSource配置属性列表

示例代码:

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
package com.kakawanyifan;

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;

public class DruidDemo {
public static void main(String[] args) throws Exception {
//1. 导入jar包
//2. 定义配置文件
//3. 加载配置文件
Properties prop = new Properties();
InputStream is = DruidDemo.class.getClassLoader().getResourceAsStream("druid.properties");
prop.load(is);
//4. 获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);

//5. 获取数据库连接 Connection
Connection connection = dataSource.getConnection();
System.out.println(connection);
//获取到了连接后就可以继续做其他操作了
}
}

运行结果:

1
com.mysql.cj.jdbc.ConnectionImpl@20deea7f

特别的,我们还可以建立一个工具类,示例代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.kakawanyifan;

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

public class DruidPoolUtil {
// 1. 定义成员变量 DataSource
private static DataSource ds ;

static {
try {
// 1. 加载配置文件
Properties pro = new Properties();
pro.load(DruidPoolUtil.class.getClassLoader().getResourceAsStream("druid.properties"));
//2.获取DataSource
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (Exception e) {
e.printStackTrace();
}

}

/**
* 获取连接
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}

/**
* 关闭
* @param stmt
* @param conn
*/
public static void close(Statement stmt, Connection conn){
close(null,stmt,conn);
}

/**
* 关闭
* @param rs
* @param stmt
* @param conn
*/
public static void close(ResultSet rs , Statement stmt, Connection conn) {
if (null != rs) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

if (null != stmt) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

if (null != conn) {
// 归还连接
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

public static DataSource getDataSource(){
return ds;
}

}

工具类的使用方法,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.kakawanyifan;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

public class DruidPoolDemo {
public static void main(String[] args) throws SQLException {
Connection connection = DruidPoolUtil.getConnection();
System.out.println(connection);
Statement statement = connection.createStatement();
System.out.println(statement);
DruidPoolUtil.close(statement,connection);
}
}

运行结果:

1
2
com.mysql.cj.jdbc.ConnectionImpl@20deea7f
com.mysql.cj.jdbc.StatementImpl@3835c46

HikariCP

需要JAR包HikariCP-4.0.3.jar

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.kakawanyifan;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.SQLException;

public class HikariCPDemo {
public static void main(String[] args) throws SQLException {

//配置
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl("jdbc:mysql://8.130.9.218:3306/j");
hikariConfig.setUsername("root");
hikariConfig.setPassword("MySQL@2022");

HikariDataSource ds = new HikariDataSource(hikariConfig);
Connection connection = ds.getConnection();
System.out.println(connection);
connection.close();

}
}

运行结果:

1
HikariProxyConnection@1014166943 wrapping com.mysql.cj.jdbc.ConnectionImpl@60dcc9fe

除了上述的配置方法,还可以直接利用PropertiespropertyFileName

如果我们得到如下的报错:

1
2
3
4
java: cannot access com.zaxxer.hikari.HikariConfig
bad class file: /Users/kaka/Documents/j/jdc/jdbc-demo/lib/HikariCP-5.0.1.jar!/com/zaxxer/hikari/HikariConfig.class
class file has wrong version 55.0, should be 52.0
Please remove or make sure it appears in the correct subdirectory of the classpath.

是因为JDK的版本不对,Java 8 is version 52.0 and Java 11 is version 55.0。

Java 11+ maven artifact:

1
2
3
4
5
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.1</version>
</dependency>

Java 8 maven artifact (maintenance mode):

1
2
3
4
5
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>

更多的"HikariCP的不同JDK版本",在如下的地址有描述

https://github.com/brettwooldridge/HikariCP

此外,关于更多的配置及其作用,同样可以参考上述的地址。

Spring JDBC

Spring JDBC,Spring框架对JDBC的简单封装,提供了一个JDBCTemplate对象简化JDBC的开发,我们不用再关注资源的获取、释放和归还,只需要专注于业务。

步骤:

  1. 导入JAR包:spring-core-5.3.20.jarspring-beans-5.3.20.jarspring-jdbc-5.3.20.jarspring-tx-5.3.20.jar以及commons-logging-1.2.jar
  2. 创建JdbcTemplate对象,入参是DataSource对象
    1
    JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
  3. 方法:
    1. update():执行DML语句,增、删、改。
    2. queryForMap():查询结果,将结果集封装为一个Map集合,列名作为Key,值作为Value。(结果集长度只能是1)。
    3. queryForList():查询结果,将结果集封装为List集合,将每一条记录封装为一个Map集合,再将Map集合装载到List集合中。
    4. query():查询结果,将结果封装为List集合,集合的元素是JavaBean对象
      我们可以通过重写RowMapper的方法进行封装,也可以通过利用new BeanPropertyRowMapper<类型>(类型.class)完成自动封装。
    5. queryForObject:查询结果,将结果封装为一个JavaBean对象。

示例代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package com.kakawanyifan;

import org.junit.Test;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

public class JdbcTemplateDemo {
JdbcTemplate jdbcTemplate = new JdbcTemplate(DruidPoolUtil.getDataSource());

/**
* 更新
*/
@Test
public void test_update_1(){
String sql = "update a set a2 = ? where a1 = ?";
int count = jdbcTemplate.update(sql,"101",1);
System.out.println(count);
}

/**
* 插入
*/
@Test
public void test_update_2(){
String sql = "insert into a(a1,a2,a3,a4) values(?,?,?,?)";
int count = jdbcTemplate.update(sql, 3,33, "三十三", 3.33);
System.out.println(count);
}

/**
* 查询结果,将结果集封装为Map集合
*/
@Test
public void test_queryForMap(){
String sql = "select * from a where a1 = ?";
Map<String, Object> map = jdbcTemplate.queryForMap(sql, 1);
System.out.println(map);
}


/**
* 查询结果,将结果集封装为List集合,将每一条记录封装为一个Map集合,再将Map集合装载到List集合中。
*/
@Test
public void test_queryForList(){
String sql = "select * from a";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
System.out.println(list);
}

/**
* 查询结果,将每一行封装为JavaBean对象,RowMapper
*/
@Test
public void test_query_mapRow(){
String sql = "select * from a";
List<A> list = jdbcTemplate.query(sql, new RowMapper<A>() {
public A mapRow(ResultSet rs, int i) throws SQLException {
A a = new A();
a.setA1(rs.getInt("a1"));
a.setA2(rs.getInt("a2"));
a.setA3(rs.getString("a3"));
a.setA4(rs.getDouble("a4"));
return a;
}
});

System.out.println(list);
}


/**
* 查询结果,将每一行封装为JavaBean对象,BeanPropertyRowMapper
*/
@Test
public void test_query_BeanPropertyRowMapper(){
String sql = "select * from a";
List<A> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<A>(A.class));
System.out.println(list);
}

/**
* 查询结果,将结果封装为对象
*/
@Test
public void test_queryForObject(){
String sql = "select count(*) from a";
Long total = jdbcTemplate.queryForObject(sql, Long.class);
System.out.println(total);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.kakawanyifan;

public class A {
int a1;

int a2;

String a3;

double a4;

【Getter和Setter代码略】

@Override
public String toString() {
return "A{" +
"a1=" + a1 +
", a2=" + a2 +
", a3='" + a3 + '\'' +
", a4=" + a4 +
'}';
}
}

关于BeanPropertyRowMapper的自动绑定,再讨论一下:

  • SQL中的user会被自动绑定到JavaBean的user
  • SQL中的user_name会被自动绑定到JavaBean的userName
  • SQL中的列名不区分大小写。
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/10810
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板