Java(9):9.0 [过旧] Java JDBC-操作Mysql数据库

第1章:JDBC概述

读前提醒:由于JDBC几乎没有人去直接使用,而是使用封装好的Mybatis或是其他的ORM框架,这个笔记也是和网络编程一样,早期写的,个人建议直接学习Mybatis即可,如果不想落下这一章或者对Sql还是不太熟悉的可以继续阅读这章节的笔记

1.1 数据的持久化

  • 持久化(persistence):把数据保存到可掉电式存储设备中以供之后使用。大多数情况下,特别是企业级应用,数据持久化意味着将内存中的数据保存到硬盘上加以”固化”,而持久化的实现过程大多通过各种关系数据库来完成

  • 持久化的主要应用是将``内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中。

1566741430592

1.2 Java中的数据存储技术

  • 在Java中,数据库存取技术可分为如下几类:

    • JDBC直接访问数据库

    • JDO (Java Data Object )技术

    • 第三方O/R工具,如Hibernate, Mybatis 等

  • JDBC是java访问数据库的基石,JDO、Hibernate、MyBatis等只是更好的封装了JDBC。

1.3 JDBC介绍

  • JDBC(Java Database Connectivity)是一个独立于特定数据库管理系统、通用的SQL数据库存取和操作的公共接口(一组API),定义了用来访问数据库的标准Java类库,(java.sql,javax.sql)使用这些类库可以以一种标准的方法、方便地访问数据库资源。
  • JDBC为访问不同的数据库提供了一种统一的途径,为开发者屏蔽了一些细节问题。
  • JDBC的目标是使Java程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,这样就使得程序员无需对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程。
  • 如果没有JDBC,那么Java程序访问数据库时是这样的:

1555575760234


  • 有了JDBC,Java程序访问数据库时是这样的:

1555575981203


  • 总结如下:

1566741692804

1.4 JDBC体系结构

  • JDBC接口(API)包括两个层次:
    • 面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果)。
    • 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用。

JDBC是sun公司提供一套用于数据库操作的接口,java程序员只需要面向这套接口编程即可。

不同的数据库厂商,需要针对这套接口,提供不同实现。不同的实现的集合,即为不同数据库的驱动。 ————面向接口编程

1.5 JDBC程序编写步骤

1565969323908

补充:ODBC(Open Database Connectivity,开放式数据库连接),是微软在Windows平台下推出的。使用者在程序中只需要调用ODBC API,由 ODBC 驱动程序将调用转换成为对特定的数据库的调用请求。

第2章:获取数据库连接

2.1 要素一:Driver接口实现类

2.1.1 Driver接口介绍

  • java.sql.Driver 接口是所有 JDBC 驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供不同的实现。

  • 在程序中不需要直接去访问实现了 Driver 接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现。

    • Oracle的驱动:oracle.jdbc.driver.OracleDriver
    • mySql的驱动: com.mysql.jdbc.Driver

1555576157618

1555576170074

  • 将上述jar包拷贝到Java工程的一个目录中,习惯上新建一个lib文件夹。

1566134718955

注意:如果是Dynamic Web Project(动态的web项目)话,则是把驱动jar放到WebContent(有的开发工具叫WebRoot)目录中的WEB-INF目录中的lib目录下即可

1566135290460

2.1.2 加载与注册JDBC驱动

  • 加载驱动:加载 JDBC 驱动需调用 Class 类的静态方法 forName(),向其传递要加载的 JDBC 驱动的类名

    • Class.forName(“com.mysql.jdbc.Driver”);
  • 注册驱动:DriverManager 类是驱动程序管理器类,负责管理驱动程序

    • 使用DriverManager.registerDriver(com.mysql.jdbc.Driver)来注册驱动

    • 通常不用显式调用 DriverManager 类的 registerDriver() 方法来注册驱动程序类的实例,因为 Driver 接口的驱动程序类包含了静态代码块,在这个静态代码块中,会调用 DriverManager.registerDriver()方法来注册自身的一个实例。下图是MySQL的Driver实现类的源码:

      1566136831283

2.2 要素二:URL

  • JDBC URL 用于标识一个被注册的驱动程序,驱动程序管理器通过这个 URL 选择正确的驱动程序,从而建立到数据库的连接。

  • JDBC URL的标准由三部分组成,各部分间用冒号分隔。

    • jdbc:子协议:子名称
    • 协议:JDBC URL中的协议总是jdbc
    • 子协议:子协议用于标识一个数据库驱动程序
    • 子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库提供足够的信息。包含主机名(对应服务端的ip地址),端口号,数据库名
  • 举例:

    1555576477107

  • 几种常用数据库的 JDBC URL

    • MySQL的连接URL编写方式:

      • jdbc:mysql://localhost:3306/test
      • jdbc:mysql://localhost:3306/test**?useUnicode=true&characterEncoding=utf8**(如果JDBC程序与服务器端的字符集不一致,会导致乱码,那么可以通过参数指定服务器端的字符集)
      • jdbc:mysql://localhost:3306/test?user=root&password=123456
  • Oracle 9i的连接URL编写方式:

    • jdbc:oracle:thin:@主机名称:oracle服务端口号:数据库名称
      • jdbc:oracle:thin:@localhost:1521:test
  • SQLServer的连接URL编写方式:

    • jdbc:sqlserver://主机名称:sqlserver服务端口号:DatabaseName=数据库名称

    • jdbc:sqlserver://localhost:1433:DatabaseName=test

2.3 要素三:用户名和密码

  • user,password可以用“属性名=属性值”方式告诉数据库
  • 可以调用 DriverManager 类的 getConnection() 方法建立到数据库的连接

2.4 数据库连接方式举例

2.4.1 连接方式一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package chapter01;
import org.junit.Test;

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

public class Practice {
@Test
public void testConnection() throws SQLException {
Driver driver = new com.mysql.jdbc.Driver();
String url = "jdbc:mysql://localhost:3306/test";
Properties info = new Properties();
info.setProperty("user","root");
info.setProperty("password","root");

Connection conn = driver.connect(url,info);
System.out.println(conn);
}
}

说明:上述代码中显式出现了第三方数据库的API

2.4.2 连接方式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testConnection2() throws Exception {
//1.实例化Driver对象
String className = "com.mysql.jdbc.Driver";
Class aClass = Class.forName(className);
Driver driver = (Driver) aClass.newInstance();

// 2.提供url,指明具体操作的数据
String url = "jdbc:mysql://localhost:3306/test";

//3.提供Properties的对象,指明用户名和密码
Properties info = new Properties();
info.setProperty("user","root");
info.setProperty("password","root");

//4.调用driver的connect(),获取连接
Connection conn = driver.connect(url, info);
System.out.println(conn); //输出连接对象
}

说明:相较于方式一,这里使用反射实例化Driver,不在代码中体现第三方数据库的API。体现了面向接口编程思想。

2.4.3 连接方式三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testConnection3() throws Exception {
//1.数据库连接的4个基本要素:
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "root";
String driverName = "com.mysql.jdbc.Driver";

//2.实例化Driver
Class aClass = Class.forName(driverName);
Driver driver = (Driver) aClass.newInstance();

//3.注册驱动
DriverManager.registerDriver(driver);

// 4.获取连接
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn); //输出连接对象

}

说明:使用DriverManager实现数据库的连接。体会获取连接必要的4个基本要素。

2.4.4 连接方式四

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testConnection4() throws Exception {
//1.数据库连接的4个基本要素:
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "root";
String driverName = "com.mysql.jdbc.Driver";

Class.forName(driverName);
Connection conn = DriverManager.getConnection(url, user, password);
System.out.println(conn); //输出连接对象
}

说明:不必显式的注册驱动了。因为在DriverManager的源码中已经存在静态代码块,实现了驱动的注册。

2.4.5 连接方式五(最终版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void testConnection5() throws Exception {
//1.加载配置文件
InputStream is = Practice.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);

//2.读取配置信息
String url = pros.getProperty("url");
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String driverClass = pros.getProperty("driverClass");

// 3.加载驱动
Class.forName(driverClass);

//4.获取连接
Connection conn = DriverManager.getConnection(url,user,password);
System.out.println(conn);

}

其中,配置文件声明在工程的src目录的resource文件夹下:【jdbc.properties】

1
2
3
4
user=root
password=abc123
url=jdbc:mysql://localhost:3306/test
driverClass=com.mysql.jdbc.Driver

说明:使用配置文件的方式保存配置信息,在代码中加载配置文件

使用配置文件的好处:

①实现了代码和数据的分离,如果需要修改配置信息,直接在配置文件中修改,不需要深入代码
②如果修改了配置信息,省去重新编译的过程。

第3章:使用PreparedStatement实现CRUD操作

3.1 操作和访问数据库

  • 数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。

  • 在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式:

    • Statement:用于执行静态 SQL 语句并返回它所生成结果的对象。
    • PrepatedStatement:SQL 语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句。
    • CallableStatement:用于执行 SQL 存储过程

    1566573842140

3.2 使用Statement操作数据表的弊端

  • 通过调用 Connection 对象的 createStatement() 方法创建该对象。该对象用于执行静态的 SQL 语句,并且返回执行结果。

  • Statement 接口中定义了下列方法用于执行 SQL 语句:

    1
    2
    int excuteUpdate(String sql):执行更新操作INSERTUPDATEDELETE
    ResultSet executeQuery(String sql):执行查询操作SELECT
  • 但是使用Statement操作数据表存在弊端:

    • 问题一:存在拼串操作,繁琐
    • 问题二:存在SQL注入问题
  • SQL 注入是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的 SQL 语句段或命令(如:SELECT user, password FROM user_table WHERE user=‘a’ OR 1 = ’ AND password = ’ OR ‘1’ = ‘1’) ,从而利用系统的 SQL 引擎完成恶意行为的做法。

  • 对于 Java 而言,要防范 SQL 注入,只要用 PreparedStatement(从Statement扩展而来) 取代 Statement 就可以了。

  • 代码演示:

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
98
99
100
101
102
103
104
105
106
107
108
109
110
public class StatementTest {

// 使用Statement的弊端:需要拼写sql语句,并且存在SQL注入的问题
@Test
public void testLogin() {
Scanner scan = new Scanner(System.in);

System.out.print("用户名:");
String userName = scan.nextLine();
System.out.print("密 码:");
String password = scan.nextLine();

// SELECT user,password FROM user_table WHERE USER = '1' or ' AND PASSWORD = '='1' or '1' = '1';
String sql = "SELECT user,password FROM user_table WHERE USER = '" + userName + "' AND PASSWORD = '" + password
+ "'";
User user = get(sql, User.class);
if (user != null) {
System.out.println("登陆成功!");
} else {
System.out.println("用户名或密码错误!");
}
}

// 使用Statement实现对数据表的查询操作
public <T> T get(String sql, Class<T> clazz) {
T t = null;

Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
// 1.加载配置文件
InputStream is = StatementTest.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);

// 2.读取配置信息
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String url = pros.getProperty("url");
String driverClass = pros.getProperty("driverClass");

// 3.加载驱动
Class.forName(driverClass);

// 4.获取连接
conn = DriverManager.getConnection(url, user, password);

st = conn.createStatement();

rs = st.executeQuery(sql);

// 获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();

// 获取结果集的列数
int columnCount = rsmd.getColumnCount();

if (rs.next()) {

t = clazz.newInstance();

for (int i = 0; i < columnCount; i++) {
// //1. 获取列的名称
// String columnName = rsmd.getColumnName(i+1);

// 1. 获取列的别名
String columnName = rsmd.getColumnLabel(i + 1);

// 2. 根据列名获取对应数据表中的数据
Object columnVal = rs.getObject(columnName);

// 3. 将数据表中得到的数据,封装进对象
Field field = clazz.getDeclaredField(columnName);
field.setAccessible(true);
field.set(t, columnVal);
}
return t;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (st != null) {
try {
st.close();
} catch (SQLException e) {
e.printStackTrace();
}
}

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

return null;
}
}

综上:

1566569819744

3.3 PreparedStatement的使用


3.3.1 PreparedStatement 介绍

  1. 获取方式
    通过 Connection 对象的 preparedStatement(String sql) 方法获取 PreparedStatement 对象。

  2. 本质特性

    • Statement 接口的子接口
    • 表示预编译过的 SQL 语句,执行效率更高
  3. 参数设置

    • SQL 语句中的参数用 ? 占位符表示
    • 通过 setXxx(int parameterIndex, Xxx value) 方法设置参数:
      • parameterIndex: 参数索引(从 1 开始
      • value: 参数值(类型需与 SQL 字段匹配)

3.3.2 PreparedStatement vs Statement

对比维度PreparedStatementStatement
可读性支持参数化查询,SQL 与代码分离,可维护性更强SQL 拼接字符串,可读性差
性能1. 预编译机制,DBServer 会缓存编译后的执行代码
2. 重复调用时直接替换参数,无需重新编译
每次执行需重新编译 SQL,性能较低
安全性自动防止 SQL 注入攻击直接拼接参数,存在 SQL 注入风险
性能优化原理
  • 预编译机制:DBServer 对预编译语句的编译结果(语法检查、语义检查、二进制命令)进行缓存,后续调用时直接复用。
  • Statement 缺点:即使 SQL 结构相同,因数据内容变化会被视为不同语句,无法利用缓存,每次需重新编译。

3.3.3 Java 与 SQL 数据类型转换表

Java 类型SQL 类型
booleanBIT
byteTINYINT
shortSMALLINT
intINTEGER
longBIGINT
StringCHAR, VARCHAR, LONGVARCHAR
byte[]BINARY, VARBINARY
java.sql.DateDATE
java.sql.TimeTIME
java.sql.TimestampTIMESTAMP

关键总结

  1. 优先使用 PreparedStatement:提高性能、安全性和可维护性。
  2. 参数索引从 1 开始:调用 setXxx() 时需注意参数位置。
  3. 类型严格匹配:Java 类型与 SQL 类型的对应关系需准确。

3.3.4 使用PreparedStatement实现增、删、改操作

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
package chapter01;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;


public class Practice {
public static void main(String[] args) throws Exception {
testInsert();
// testDelete(2);
testUpdate(22);

}

public static void testInsert() throws Exception {
String sql = "insert into customers(name,email,birth) values(?,?,?)";
try (Connection conn = connectToDatabase();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1,"Prorise");
ps.setString(2,"3381292732@qq.com");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2005-01-01");
ps.setDate(3, new java.sql.Date(date.getTime()));

int rows = ps.executeUpdate();
if (rows > 0) {
System.out.println("数据库成功插入 " + rows + " 条数据!");
} else {
System.out.println("数据库插入数据失败!");
}
}
}


public static void testDelete(int id) throws Exception {
String sql = "delete from customers where id=?";
try (Connection conn = connectToDatabase();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, id);
int rows = ps.executeUpdate();
if (rows > 0) {
System.out.println("数据库成功删除 " + rows + " 条数据!");
} else {
System.out.println("未找到要删除的数据!");
}
}
}


public static void testUpdate(int id) throws Exception {
String sql = "update customers set name=?,email=?,birth=? where id=?";
try (Connection conn = connectToDatabase();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1,"Jane");
ps.setString(2,"3381292732@qq.com");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2005-01-01");
ps.setDate(3, new java.sql.Date(date.getTime()));
ps.setInt(4, id);

int rows = ps.executeUpdate();
if (rows > 0) {
System.out.println("数据库成功修改 " + rows + " 条数据!");
} else {
System.out.println("未找到要修改的数据!");
}
}
}

public static Connection connectToDatabase() throws IOException, ClassNotFoundException, SQLException {
// 1.加载配置文件
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties pros = new Properties();
pros.load(is);

//2.读取配置信息
String url = pros.getProperty("url");
String user = pros.getProperty("user");
String password = pros.getProperty("password");
String driverClass = pros.getProperty("driverClass");

// 3.加载驱动
Class.forName(driverClass);

//4.获取连接
Connection conn = DriverManager.getConnection(url,user,password);
return conn;
}
}

3.3.5 使用PreparedStatement实现查询操作

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
package chapter01;

import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
import java.lang.reflect.Field;

public class Practice {
public static void main(String[] args) throws Exception {
// main方法内容
testQuery1(2);
queryForCustomers("select * from customers where id = ?", 2);
}


public static void queryForCustomers(String sql,Object...args) throws Exception {
Connection conn = connectToDatabase();
PreparedStatement ps = conn.prepareStatement(sql);
// 填充占位符
for (int i = 0; i < args.length ; i++) {
ps.setObject(i + 1, args[i]);
}
ResultSet rs = ps.executeQuery();
ResultSetMetaData rsmd = rs.getMetaData(); // 获取结果集元数据
int columnCount = rsmd.getColumnCount();// 获取列数
if (rs.next()) {
Customer customer = new Customer();
for (int i = 1; i <= columnCount; i++) {
Object rsValue = rs.getObject(i + 1);
// 获取每一列的列名
String columnName = rsmd.getColumnName(i + 1);
if (columnName.equals("photo")) return; // 跳过photo列

// 通过反射设置属性值
Field field = Customer.class.getDeclaredField(columnName);
field.setAccessible(true);
field.set(customer, rsValue);
System.out.println(columnName + " = " + rsValue);
}
}
// 记得关闭资源
rs.close();
ps.close();
closeConnection(conn);
}

public static void testQuery1(int id) throws Exception {
/**
* testQuery1:查询客户信息的示例
* @param [id]
* @return void
* @throws Exception
* @since 2025年-02月-05日
*/

Connection conn = connectToDatabase();
String sql = "select * from customers where id = ?";
PreparedStatement ps = conn.prepareStatement(sql);

ps.setInt(1, id);
ResultSet resultSet = ps.executeQuery();

// 只调用一次next()
if (resultSet.next()) {
int uid = resultSet.getInt(1);
String name = resultSet.getString(2);
String email = resultSet.getString(3);
Date birth = resultSet.getDate(4);

Customer customer = new Customer(id, name, email, birth);
System.out.println(customer.toString());
} else {
System.out.println("未找到ID为 " + id + " 的客户");
}

// 关闭资源
resultSet.close();
ps.close();
closeConnection(conn);
}

static class Customer { // 改为static class
private int id;
private String name;
private String email;
private Date birth;

public Customer(int id, String name, String email, Date birth) {
this.id = id;
this.name = name;
this.email = email;
this.birth = birth;
}

public Customer() {}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public Date getBirth() {
return birth;
}

public void setBirth(Date birth) {
this.birth = birth;
}

@Override
public String toString() {
return "Customer{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", birth=" + birth +
'}';
}
}

public static Connection connectToDatabase() throws Exception {
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties props = new Properties();
props.load(is);

String url = props.getProperty("url");
String driverClass = props.getProperty("driverClass");
String user = props.getProperty("user");
String password = props.getProperty("password");

Class.forName(driverClass);

Connection conn = DriverManager.getConnection(url, user, password);
if (conn == null) {
throw new SQLException("Could not connect to database");
} else {
System.out.println("连接成功");
}
return conn;
}

public static void closeConnection(Connection conn) throws Exception {
if (conn != null) {
conn.close();
}
}
}

说明:使用PreparedStatement实现的查询操作可以替换Statement实现的查询操作,解决Statement拼串和SQL注入问题。

3.4 ResultSet与ResultSetMetaData

3.4.1 ResultSet

  1. 获取方式
    通过 PreparedStatementexecuteQuery() 方法执行查询,返回一个 ResultSet 对象。
  2. 本质特性
    • 以逻辑表格的形式封装查询结果集。
    • 由数据库厂商提供实现。
    • 初始时,指针指向第一条记录的前面。
  3. 游标操作
    • 通过 next() 方法移动游标:
      • 检测下一行是否有效,若有效返回 true 并下移指针。
      • 类似于 IteratorhasNext()next() 结合。
    • 通过 getXxx(int index)getXxx(String columnName) 获取列值:
      • 例如:getInt(1)getString("name")
      • 注意:JDBC API 中的索引从 1 开始。
  4. 常用方法
    • boolean next():移动游标到下一行。
    • getString(int index) / getString(String columnName):获取字符串类型值。
    • 其他类似方法:getInt()getDouble() 等。

3.4.2 ResultSetMetaData

  1. 获取方式
    通过 ResultSetgetMetaData() 方法获取 ResultSetMetaData 对象。
  2. 核心功能
    • 获取结果集的列信息(列名、别名、类型等)。
    • 常用方法:
      • getColumnName(int column):获取指定列的名称。
      • getColumnLabel(int column):获取指定列的别名。
      • getColumnCount():返回结果集中的列数。
      • getColumnTypeName(int column):获取指定列的数据库类型名称。
      • getColumnDisplaySize(int column):获取指定列的最大显示宽度(字符数)。
      • isNullable(int column):判断指定列是否允许 NULL 值。
      • isAutoIncrement(int column):判断指定列是否自增。

问题1:得到结果集后, 如何知道该结果集中有哪些列 ? 列名是什么?

 需要使用一个描述 ResultSet 的对象, 即 ResultSetMetaData

问题2:关于ResultSetMetaData

  1. 如何获取 ResultSetMetaData: 调用 ResultSet 的 getMetaData() 方法即可
  2. 获取 ResultSet 中有多少列:调用 ResultSetMetaData 的 getColumnCount() 方法
  3. 获取 ResultSet 每一列的列的别名是什么:调用 ResultSetMetaData 的getColumnLabel() 方法

1555579816884

3.5 资源的释放

  • 释放ResultSet, Statement,Connection。
  • 数据库连接(Connection)是非常稀有的资源,用完后必须马上释放,如果Connection不能及时正确的关闭将导致系统宕机。Connection的使用原则是尽量晚创建,尽量早的释放。
  • 可以在finally中关闭,保证及时其他代码出现异常,资源也一定能被关闭。

3.6 JDBC API小结

  • 两种思想

    • 面向接口编程的思想

    • ORM思想(object relational mapping)

      • 一个数据表对应一个java类
      • 表中的一条记录对应java类的一个对象
      • 表中的一个字段对应java类的一个属性

    sql是需要结合列名和表的属性名来写。注意起别名。

  • 两种技术

    • JDBC结果集的元数据:ResultSetMetaData
      • 获取列数:getColumnCount()
      • 获取列的别名:getColumnLabel()
    • 通过反射,创建指定类的对象,获取指定的属性并赋值

章节练习

练习题1:从控制台向数据库的表customers中插入一条数据,表结构如下:

1555580275036

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
package chapter01;


import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Date;
import java.text.SimpleDateFormat;

public class Practice {
public static void main(String[] args) throws Exception {
addToCustomers();

}

public static void addToCustomers() throws Exception {
Connection conn = JDBCUtils.getConnection();
String sql = "INSERT INTO customers (name,email,birth) VALUES (?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "Tom");
ps.setString(2, "tom@gmail.com");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
ps.setDate(3,new Date(sdf.parse("2005-01-02").getTime()));
int count = ps.executeUpdate();
if (count > 0) {
System.out.println("添加成功!");
}
JDBCUtils.closeResource(conn, ps);
}
}

练习题2:创立数据库表 examstudent,表结构如下:

1555580735377

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建考试表
public static void createExamTable() throws Exception {
Connection conn = JDBCUtils.getConnection();
String sql = "CREATE TABLE IF NOT EXISTS examStudent (id INT PRIMARY KEY," +
"FlowId INT(10)," +
"Type int(5)," +
"IDCard varchar(18)," +
"ExamCard varchar(15)," +
"StudentName varchar(20)," +
"Location varchar(20)," +
"Grade int(10))";
PreparedStatement ps = conn.prepareStatement(sql);
int count = ps.executeUpdate();
if (count == 0) System.out.println("创建考试表成功!");
JDBCUtils.closeResource(conn, ps);
}

向数据表中添加如下数据:

1555580763636

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 chapter01;


import java.sql.Connection;
import java.sql.PreparedStatement;

public class Practice {
public static void main(String[] args) throws Exception {
// 定义批量插入考试数据
Object[][] batchData = {
{1, 4, "412824195263214584", "200523164754000", "张峰", "郑州", 85},
{2, 4, "222224195263214584", "200523164754001", "孙朗", "大连", 56},
{3, 6, "342824195263214584", "200523164754002", "刘明", "沈阳", 72},
{4, 6, "100824195263214584", "200523164754003", "赵虎", "哈尔滨", 95},
{5, 4, "454524195263214584", "200523164754004", "杨丽", "北京", 64},
{6, 4, "854524195263214584", "200523164754005", "王小红", "太原", 60}
};
String sql = "INSERT INTO examstudent(FlowId,Type,IDCard,ExamCard,StudentName,Location,Grade) VALUES(?,?,?,?,?,?,?)";
batchInsertExamData(sql, batchData);
}

// 批量插入考试数据
public static void batchInsertExamData(String sql, Object[][] batchData) throws Exception {
Connection conn = JDBCUtils.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);

for (Object[] data : batchData) {
for (int i = 0; i < data.length; i++) {
ps.setObject(i + 1, data[i]);
}
ps.addBatch();
}

ps.executeBatch();
System.out.println("批量插入数据成功!");
JDBCUtils.closeResource(conn, ps);
}
}


第4章 操作BLOB类型字段

4.1 MySQL BLOB类型

  • MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据。

  • 插入BLOB类型的数据必须使用PreparedStatement,因为BLOB类型的数据无法使用字符串拼接写的。

  • MySQL的四种BLOB类型(除了在存储的最大信息量上不同外,他们是等同的)

1555581069798

  • 实际使用中根据需要存入的数据大小定义不同的BLOB类型。
  • 需要注意的是:如果存储的文件过大,数据库的性能会下降。
  • 如果在指定了相关的Blob类型以后,还报错:xxx too large,那么在mysql的安装目录下,找my.ini文件加上如下的配置参数: max_allowed_packet=16M。同时注意:修改了my.ini文件之后,需要重新启动mysql服务。

4.2 向数据表中插入大数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void testInsertBlob() throws Exception {
Connection conn = JDBCUtils.getConnection();
String sql = "insert into customers(name,email,birth,photo) values(?,?,?,?)";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "Tom");
ps.setString(2, "tom@gmail.com");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date birth = new Date(sdf.parse("2005-01-01").getTime());
ps.setDate(3, birth);

// 获取项目根目录
String projectPath = System.getProperty("user.dir");
FileInputStream fis = new FileInputStream(projectPath + "/src/chapter01/Tom.png");

ps.setBlob(4, fis);
int count = ps.executeUpdate();
if (count > 0) {
System.out.println("插入成功!");
}
fis.close();
JDBCUtils.closeResource(conn, ps);
}

4.3 修改数据表中的Blob类型字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 修改blob的数据
public static void testUpdateBlob() throws Exception {
Connection conn = JDBCUtils.getConnection();
String sql = "UPDATE customers SET photo = ? WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);

// 获取项目根目录
String projectPath = System.getProperty("user.dir");
FileInputStream fis = new FileInputStream(projectPath + "/src/chapter01/Tom_new.png");
ps.setBlob(1, fis);
ps.setInt(2, 27);

int count = ps.executeUpdate();
if (count > 0) {
System.out.println("更新成功!");
}
fis.close();
JDBCUtils.closeResource(conn, ps);
}

4.4 从数据表中读取大数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 读取blob数据
public static void testReadBlob() throws Exception {
Connection conn = JDBCUtils.getConnection();
String sql = "SELECT id,name,email,birth,photo FROM customers WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 27);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
Blob photo = rs.getBlob("photo");
InputStream is = photo.getBinaryStream();
String projectPath = System.getProperty("user.dir");
OutputStream os = new FileOutputStream(projectPath + "/src/chapter01/Tom_output.png");
byte[] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.close();
is.close();
JDBCUtils.closeResource(conn, ps, rs);
}
}

第5章 批量插入

5.1 批量执行SQL语句

当需要成批插入或者更新记录时,可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理。通常情况下比单独提交处理更有效率

JDBC的批量处理语句包括下面三个方法:

  • addBatch(String):添加需要批量处理的SQL语句或是参数;
  • executeBatch():执行批量处理语句;
  • clearBatch():清空缓存的数据

通常我们会遇到两种批量执行SQL语句的情况:

  • 多条SQL语句的批量处理;
  • 一个SQL语句的批量传参;
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 chapter01;


import java.sql.Connection;
import java.sql.PreparedStatement;

public class Practice {
public static void main(String[] args) throws Exception {
// 定义批量插入考试数据
Object[][] batchData = {
{1, 4, "412824195263214584", "200523164754000", "张峰", "郑州", 85},
{2, 4, "222224195263214584", "200523164754001", "孙朗", "大连", 56},
{3, 6, "342824195263214584", "200523164754002", "刘明", "沈阳", 72},
{4, 6, "100824195263214584", "200523164754003", "赵虎", "哈尔滨", 95},
{5, 4, "454524195263214584", "200523164754004", "杨丽", "北京", 64},
{6, 4, "854524195263214584", "200523164754005", "王小红", "太原", 60}
};
String sql = "INSERT INTO examstudent(FlowId,Type,IDCard,ExamCard,StudentName,Location,Grade) VALUES(?,?,?,?,?,?,?)";
batchInsertExamData(sql, batchData);
}

// 批量插入考试数据
public static void batchInsertExamData(String sql, Object[][] batchData) throws Exception {
Connection conn = JDBCUtils.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);

for (Object[] data : batchData) {
for (int i = 0; i < data.length; i++) {
ps.setObject(i + 1, data[i]);
}
ps.addBatch();
}

ps.executeBatch();
System.out.println("批量插入数据成功!");
JDBCUtils.closeResource(conn, ps);
}
}

5.2 高效的批量插入

举例:向数据表中插入20000条数据

  • 数据库中提供一个goods表。创建如下:
1
2
3
4
CREATE TABLE goods(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(20)
);

5.2.1 实现层次一:使用Statement

1
2
3
4
5
6
Connection conn = JDBCUtils.getConnection();
Statement st = conn.createStatement();
for(int i = 1;i <= 20000;i++){
String sql = "insert into goods(name) values('name_' + "+ i +")";
st.executeUpdate(sql);
}

5.2.2 实现层次二:使用PreparedStatement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
long start = System.currentTimeMillis();

Connection conn = JDBCUtils.getConnection();

String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);
for(int i = 1;i <= 20000;i++){
ps.setString(1, "name_" + i);
ps.executeUpdate();
}

long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//82340


JDBCUtils.closeResource(conn, ps);

5.2.3 实现层次三

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
/*
* 修改1: 使用 addBatch() / executeBatch() / clearBatch()
* 修改2:mysql服务器默认是关闭批处理的,我们需要通过一个参数,让mysql开启批处理的支持。
* ?rewriteBatchedStatements=true 写在配置文件的url后面
* 修改3:使用更新的mysql 驱动:mysql-connector-java-5.1.37-bin.jar
*
*/
@Test
public void testInsert1() throws Exception{
long start = System.currentTimeMillis();

Connection conn = JDBCUtils.getConnection();

String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);

for(int i = 1;i <= 1000000;i++){
ps.setString(1, "name_" + i);

//1.“攒”sql
ps.addBatch();
if(i % 500 == 0){
//2.执行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
}

long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//20000条:625 //1000000条:14733

JDBCUtils.closeResource(conn, ps);
}

5.2.4 实现层次四

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
/*
* 层次四:在层次三的基础上操作
* 使用Connection 的 setAutoCommit(false) / commit()
*/
@Test
public void testInsert2() throws Exception{
long start = System.currentTimeMillis();

Connection conn = JDBCUtils.getConnection();

//1.设置为不自动提交数据
conn.setAutoCommit(false);

String sql = "insert into goods(name)values(?)";
PreparedStatement ps = conn.prepareStatement(sql);

for(int i = 1;i <= 1000000;i++){
ps.setString(1, "name_" + i);

//1.“攒”sql
ps.addBatch();

if(i % 500 == 0){
//2.执行
ps.executeBatch();
//3.清空
ps.clearBatch();
}
}

//2.提交数据
conn.commit();

long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));//1000000条:4978

JDBCUtils.closeResource(conn, ps);
}

好的,下面是整理后的笔记内容,突出重点并添加相关的代码示例。


第6章:数据库事务

6.1 数据库事务介绍

  • 事务 是一组逻辑操作单元,确保数据从一种状态变换到另一种状态。

  • 事务处理:保证所有事务作为一个工作单元来执行,即使出现故障,事务也不能被中断。事务中的多个操作要么全部提交(commit),要么全部回滚(rollback)。

    • 提交(commit):当事务中的所有操作成功时,所有修改永久保存到数据库中。
    • 回滚(rollback):如果事务中的某个操作失败,所有修改会被撤销,回到事务开始前的状态。
  • 为确保数据一致性,事务中的操作必须被当做一个整体来执行,只有在所有操作成功时,数据的一致性才会得到保持。


6.2 JDBC事务处理

  • 自动提交与手动提交

    • 默认自动提交:当连接创建时,事务会自动提交,每执行一次 SQL 语句,都会自动提交。
    • 关闭数据库连接:如果连接关闭,事务会自动提交。
    • 手动提交事务:使用 setAutoCommit(false); 来关闭自动提交,事务的提交和回滚由程序控制。
  • JDBC事务管理步骤

    1. 取消自动提交:conn.setAutoCommit(false);
    2. 执行所有 SQL 语句。
    3. 如果没有异常,提交事务:conn.commit();
    4. 如果有异常,回滚事务:conn.rollback();

案例:用户AA向用户BB转账100

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 chapter01;

import java.sql.Connection;
import java.sql.PreparedStatement;

public class Practice {
public static void main(String[] args) throws Exception {
Connection conn = null;
try {
conn = JDBCUtils.getConnection();
// 1. 开启事务
conn.setAutoCommit(false);

// 3. 执行数据库操作
String sql1 = "UPDATE user_table SET balance = balance - 100 WHERE user = ?";
update(conn, sql1, "AA");

String sql2 = "UPDATE user_table SET balance = balance + 100 WHERE user = ?";
update(conn, sql2, "BB");

// 4. 若没有异常,则提交事务
conn.commit();

} catch (Exception e) {
e.printStackTrace();
// 5. 若有异常,则回滚事务
if (conn != null) {
conn.rollback();
}
} finally {
// 6. 关闭资源
JDBCUtils.closeResource(conn, null, null);
}
}

// 更新方法
public static void update(Connection conn, String sql, Object... args) throws Exception {
PreparedStatement ps = null;
try {
ps = conn.prepareStatement(sql);
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
// 执行SQL
ps.execute();
} finally {
// 确保 PreparedStatement 关闭
if (ps != null) {
JDBCUtils.closeResource(null, ps);
}
}
}
}


第7章:DAO及相关实现类

1. 什么是DAO?

  • DAO:封装数据访问操作的对象,提供统一接口进行 CRUD(创建、读取、更新、删除) 操作。
  • 作用:解耦业务逻辑和数据库操作,提高代码维护性。

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
src

├── com
│ └── Prorise
│ └── DAO
│ ├── dao // 数据访问层
│ │ ├── UserDao.java
│ │ └── UserDaoImpl.java
│ │
│ ├── service // 业务逻辑层
│ │ └── UserService.java
│ │
│ ├── model // 数据模型层(实体类)
│ │ └── User.java
│ │
│ ├── util // 工具类(如数据库连接工具)
│ │ └── JDBCUtils.java
│ │
│ └── test // 测试类
│ └── UserServiceTest.java

1. 数据模型层(User.java

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.Prorise.DAO.model;

public class User {
private int id;
private String name;
private int age;

// 构造函数
public User(String name, int age) {
this.name = name;
this.age = age;
}

// 带id的构造函数
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}

// Getters 和 Setters
public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}


2. DAO 接口(UserDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.Prorise.DAO.dao;

import com.Prorise.DAO.model.User;

import java.sql.SQLException;

public interface UserDao {
void initStudentTable() throws Exception; // 初始化学生表
void addUser(User user) throws SQLException; // 添加用户
User getUserById(int id) throws SQLException; // 获取用户
void updateUser(User user) throws SQLException; // 更新用户
void deleteUser(int id) throws SQLException; // 删除用户
}

3. DAO 实现类(UserDaoImpl.java

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
package com.Prorise.DAO.dao;
import com.Prorise.DAO.util.JDBCUtils;
import com.Prorise.DAO.model.User;
import java.sql.*;
public class UserDaoImpl implements UserDao {
private Connection conn;

public UserDaoImpl() {
try {
this.conn = JDBCUtils.getConnection();
} catch (Exception e) {
throw new RuntimeException("数据库连接初始化失败", e);
}
}

@Override
public void initStudentTable() throws SQLException {
String sql = "CREATE TABLE IF NOT EXISTS `users` (\n" +
" `id` int(11) NOT NULL AUTO_INCREMENT,\n" +
" `name` varchar(255) NOT NULL,\n" +
" `age` int(11) NOT NULL,\n" +
" PRIMARY KEY (`id`)\n" +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
int count = ps.executeUpdate();
if (count > 0) {
System.out.println("创建users表成功!");
}
}
}

@Override
public void addUser(User user) throws SQLException {
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
int count = ps.executeUpdate();
if (count > 0) {
System.out.println("添加用户成功!");
}
}
}

@Override
public User getUserById(int id) throws SQLException {
String sql = "SELECT * FROM users WHERE id = ?";
try(PreparedStatement ps = conn.prepareStatement(sql)){
ps.setInt(1, id);
try(ResultSet rs = ps.executeQuery()){
if(rs.next()){
return new User(rs.getInt("id"),rs.getString("name"),rs.getInt("age"));
}
}
}
return null;
}

@Override
public void updateUser(User user) throws SQLException {
String sql = "UPDATE users SET name = ?, age = ? where id = ?";
try(PreparedStatement ps = conn.prepareStatement(sql)){
ps.setString(1, user.getName());
ps.setInt(2, user.getAge());
ps.setInt(3, user.getId());
int count = ps.executeUpdate();
if (count > 0) {
System.out.println("更新用户成功!");
}
}
}

@Override
public void deleteUser(int id) throws SQLException {
String sql = "DELETE FROM users WHERE id = ?";
try(PreparedStatement ps = conn.prepareStatement(sql)){
ps.setInt(1, id);
int count = ps.executeUpdate();
if (count > 0) {
System.out.println("删除用户成功!");
}
}
}


}



4. 服务层(UserService.java

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.companyname.projectname.service;

import com.companyname.projectname.dao.UserDao;
import com.companyname.projectname.model.User;

import java.sql.Connection;

public class UserService {
private UserDao userDao;

public UserService(Connection conn) {
this.userDao = new UserDaoImpl(conn);
}

// 创建用户
public void createUser(String name, int age) {
User user = new User(name, age);
userDao.addUser(user);
}

// 获取用户
public User getUser(int id) {
return userDao.getUser(id);
}

// 更新用户
public void updateUser(int id, String name, int age) {
User user = new User(id, name, age);
userDao.updateUser(user);
}

// 删除用户
public void deleteUser(int id) {
userDao.deleteUser(id);
}
}

5. JDBC 工具类(JDBCUtils.java

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
package com.Prorise.DAO.util;

import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

public class JDBCUtils {
/**
* getConnection:获取数据库连接
* @param []
* @return java.sql.Connection
* @throws Exception
* @since 2025年-02月-05日
*/
public static Connection getConnection() throws Exception {
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties props = new Properties();
props.load(is);
String driver = props.getProperty("driverClass");
String url = props.getProperty("url");
String user = props.getProperty("user");
String password = props.getProperty("password");

Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
if (conn == null) {
System.out.println("连接数据库失败!");
return null;
}
return conn;
}

// 重载closeResource方法,支持两个参数
public static void closeResource(Connection conn, Statement ps) {
closeResource(conn, ps, null);
}

// 三个参数的closeResource方法
/**
* closeResource:关闭数据库资源,可提供两个或三个参数
* @param [conn, ps, rs]
* @return void
* @throws SQLException
* @since 2025年-02月-05日
*/
public static void closeResource(Connection conn, Statement ps, ResultSet rs) {

// 先关闭ResultSet
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 再关闭Statement
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 最后关闭Connection
if (conn != null) {
try {
conn.close();
System.out.println("连接关闭成功!");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}


6. 测试类(UserServiceTest.java

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
package com.Prorise.DAO.test;

import com.Prorise.DAO.dao.UserDaoImpl;
import com.Prorise.DAO.model.User;
import com.Prorise.DAO.service.UserService;
import java.sql.SQLException;
import java.util.Scanner;

public class UserServiceTest {
public static void main(String[] args) {
try {
// 先初始化创建users表
UserDaoImpl tempDao = new UserDaoImpl();
tempDao.initStudentTable();

// 使用UserService进行用户操作,构造函数中Connection参数未使用,可传null
UserService userService = new UserService(null);

Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请选择操作:\n1. 添加用户\n2. 查询用户\n3. 更新用户\n4. 删除用户\n5. 退出");
int choice = Integer.parseInt(scanner.nextLine());
switch (choice) {
case 1:
System.out.println("请输入用户名:");
String name = scanner.nextLine();
System.out.println("请输入年龄:");
int age = Integer.parseInt(scanner.nextLine());
userService.createUser(name, age);
System.out.println("添加用户成功!");
break;
case 2:
System.out.println("请输入用户ID:");
int id = Integer.parseInt(scanner.nextLine());
User queriedUser = userService.getUser(id);
if (queriedUser != null) {
System.out.println("查询到的用户:" + queriedUser);
} else {
System.out.println("没有找到该用户!");
}
break;
case 3:
System.out.println("请输入需要更新的用户ID:");
int updateId = Integer.parseInt(scanner.nextLine());
System.out.println("请输入新的用户名:");
String newName = scanner.nextLine();
System.out.println("请输入新的年龄:");
int newAge = Integer.parseInt(scanner.nextLine());
userService.updateUser(updateId, newName, newAge);
System.out.println("更新用户成功!");
break;
case 4:
System.out.println("请输入需要删除的用户ID:");
int deleteId = Integer.parseInt(scanner.nextLine());
userService.deleteUser(deleteId);
System.out.println("删除用户成功(如果用户存在)");
break;
case 5:
System.out.println("退出测试程序!");
scanner.close();
System.exit(0);
break;
default:
System.out.println("请输入有效的选项!");
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}



第8章:数据库连接池


8.1 为什么需要数据库连接池?

传统的 JDBC 数据库连接方式:

  1. 每次建立数据库连接(DriverManager 获取 Connection)。
  2. 执行 SQL 操作(增删改查)。
  3. 断开数据库连接(关闭 Connection)。

传统方式存在的问题

连接创建 & 关闭成本高:每次连接都需要验证用户名和密码,建立连接时间 0.05s~1s,影响性能。
无法复用数据库连接:每个请求都新建连接,导致数据库连接资源浪费。
连接数不受控制:当并发用户增多(几百/几千人在线),大量连接可能会导致服务器崩溃。
容易导致内存泄漏:如果异常导致连接未关闭,可能会耗尽数据库资源。


8.2 什么是数据库连接池?

数据库连接池(Connection Pool) 通过 “缓存数据库连接” 来优化数据库连接的管理,避免频繁创建和关闭连接的开销。

工作流程

  1. 应用程序启动时,数据库连接池创建一组连接(最小连接数)。
  2. 应用程序请求数据库连接,从连接池获取可用连接。
  3. 使用完数据库连接后,不直接关闭,而是归还给连接池,以便重复利用。
  4. 当请求连接数超过最大连接数时,新的请求会等待,避免连接过载。

8.3 数据库连接池的优点

减少资源消耗:避免频繁创建/销毁数据库连接,减少 CPU 和 IO 负担。
提高系统性能:数据库连接已经初始化,业务请求可以快速获取连接,减少响应时间。
统一管理连接:控制最大并发连接数,防止数据库因连接数过多崩溃。
避免连接泄漏:数据库连接池可以自动回收超时连接,防止资源泄漏。


8.4 常见的数据库连接池

连接池优点缺点适用场景
DBCP (Apache)速度快,Tomcat 自带存在 Bug,Hibernate3 已不再支持适用于 Tomcat 服务器
C3P0Hibernate 官方推荐,稳定性好速度比 DBCP 慢适用于 Hibernate 项目
Proxool支持连接池监控稳定性略低适用于对连接池状态监控要求高的项目
BoneCP速度快逐渐被 Druid 取代适用于高性能并发应用
Druid (阿里巴巴)集 DBCP、C3P0、Proxool 优势,支持 SQL 监控-推荐用于企业级项目

👉 推荐使用:Druid(阿里巴巴)连接池,性能和稳定性最佳。


8.3.1 C3P0数据库连接池

  • 获取连接方式一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   //使用C3P0数据库连接池的方式,获取数据库的连接:不推荐
@Test
public void getConnection1() throws Exception {
ComboPooledDataSource cpds = new ComboPooledDataSource();
cpds.setDriverClass("com.mysql.jdbc.Driver");
cpds.setJdbcUrl("jdbc:mysql://localhost:3306/test");
cpds.setUser("root");
cpds.setPassword("root");

// 设置数据库连接池的初始大小、最大连接数、最小连接数
cpds.setInitialPoolSize(10); // 设置初始连接数
cpds.setMaxPoolSize(100); // 设置最大连接数
cpds.setMinPoolSize(10); // 设置最小连接数

// 获取数据库连接
Connection conn = cpds.getConnection();

// 关闭连接池
cpds.close();
// 可选(销毁连接池)
// DataSources.destroy(cpds);
}
  • 获取连接方式二
1
2
3
4
5
6
7
8
9
//使用C3P0配置文件的方式,获取数据库的连接
@Test
public void testGetConnection2() throws Exception {
ComboPooledDataSource cpds = new ComboPooledDataSource("helloc3p0");
Connection conn = cpds.getConnection();
System.out.println(conn.toString());
conn.close();
cpds.close();
}

其中,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
<?xml version="1.0" encoding="UTF-8" ?>
<c3p0-config>
<named-config name="helloc3p0">
<!-- 获取连接的4个基本信息 -->
<property name="user">root</property>
<property name="password">root</property>
<property name="jdbcUrl">jdbc:mysql:///test</property>
<property name="driverClass">com.mysql.jdbc.Driver</property>

<!-- 涉及到数据库连接池的管理的相关属性的设置 -->
<!-- 若数据库中连接数不足时, 一次向数据库服务器申请多少个连接 -->
<property name="acquireIncrement">5</property>
<!-- 初始化数据库连接池时连接的数量 -->
<property name="initialPoolSize">5</property>
<!-- 数据库连接池中的最小的数据库连接数 -->
<property name="minPoolSize">5</property>
<!-- 数据库连接池中的最大的数据库连接数 -->
<property name="maxPoolSize">10</property>
<!-- C3P0 数据库连接池可以维护的 Statement 的个数 -->
<property name="maxStatements">20</property>
<!-- 每个连接同时可以使用的 Statement 对象的个数 -->
<property name="maxStatementsPerConnection">5</property>

</named-config>
</c3p0-config>

8.3.2 DBCP数据库连接池

  • DBCP 是 Apache 软件基金组织下的开源连接池实现,该连接池依赖该组织下的另一个开源系统:Common-pool。如需使用该连接池实现,应在系统中增加如下两个 jar 文件:
    • Commons-dbcp.jar:连接池的实现
    • Commons-pool.jar:连接池实现的依赖库
  • **Tomcat 的连接池正是采用该连接池来实现的。**该数据库连接池既可以与应用服务器整合使用,也可由应用程序独立使用。
  • 数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据源即可。
  • 当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但上面的代码并没有关闭数据库的物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。
  • 配置属性说明
属性默认值说明
initialSize0连接池启动时创建的初始化连接数量
maxActive8连接池中可同时连接的最大的连接数
maxIdle8连接池中最大的空闲的连接数,超过的空闲连接将被释放,如果设置为负数表示不限制
minIdle0连接池中最小的空闲的连接数,低于这个数量会被创建新的连接。该参数越接近maxIdle,性能越好,因为连接的创建和销毁,都是需要消耗资源的;但是不能太大。
maxWait无限制最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待
poolPreparedStatementsfalse开启池的Statement是否prepared
maxOpenPreparedStatements无限制开启池的prepared 后的同时最大连接数
minEvictableIdleTimeMillis连接池中连接,在时间段内一直空闲, 被逐出连接池的时间
removeAbandonedTimeout300超过时间限制,回收没有用(废弃)的连接
removeAbandonedfalse超过removeAbandonedTimeout时间后,是否进 行没用连接(废弃)的回收
  • 获取连接方式一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testGetConnectionByDBCP() throws Exception {
BasicDataSource source = new BasicDataSource();
source.setDriverClassName("com.mysql.jdbc.Driver");
source.setUrl("jdbc:mysql://localhost:3306/test");
source.setUsername("root");
source.setPassword("root");
source.setMaxActive(10); // 设置最大连接数
source.setMaxIdle(5); // 设置最大空闲连接数
source.setInitialSize(5); // 设置初始连接数

Connection conn = source.getConnection();
System.out.println("Connection: " + conn);
}
  • 获取连接方式二:
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 chapter01;

import java.io.FileInputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;

import org.apache.commons.dbcp.BasicDataSourceFactory;
import org.junit.*;

import javax.sql.DataSource;

public class Practice {
public static void main(String[] args) {

}

// 静态代码块,在类加载时初始化DataSource
private static DataSource dataSource;

static {
try {
String path = System.getProperty("user.dir");
InputStream is = new FileInputStream(path + "/resources/dbcp.properties");
Properties pros = new Properties();
pros.load(is);
dataSource = BasicDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

@Test
public void testGetConnectionByDBCP2() throws Exception {
Connection conn = dataSource.getConnection();
System.out.println(conn);
conn.close();
}
}

其中,src下的配置文件为:【dbcp.properties】

1
2
3
4
5
6
7
8
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true&useServerPrepStmts=false
username=root
password=root
initialSize=10
maxActive=100
maxIdle=10
maxWait=10000

8.3.3 Druid(德鲁伊)数据库连接池

Druid是阿里巴巴开源平台上一个数据库连接池实现,它结合了C3P0、DBCP、Proxool等DB池的优点,同时加入了日志监控,可以很好的监控DB池连接和SQL的执行情况,可以说是针对监控而生的DB连接池,可以说是目前最好的连接池之一。

1
2
3
4
5
6
7
8
9
@Test
public void getConnectionByDruid() throws Exception {
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
Properties prop = new Properties();
prop.load(is);
DataSource source = DruidDataSourceFactory.createDataSource(prop);
Connection conn = source.getConnection();
System.out.println(conn);
}

其中,src下的配置文件为:【druid.properties】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 数据库连接配置
druid.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=UTC
druid.username=root
druid.password=root
druid.driver-class-name=com.mysql.cj.jdbc.Driver

# 连接池配置
druid.initialSize=5
druid.maxActive=20
druid.minIdle=5
druid.maxWait=3000

# 连接检测
druid.validationQuery=SELECT 1
druid.testOnBorrow=false
druid.testOnReturn=false
druid.testWhileIdle=true
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000

# SQL 监控和日志
druid.filters=stat,wall,log4j

Druid 数据库连接池详细配置参数

参数名称默认值说明
name-连接池名称,多个数据源时用于区分,默认格式:DataSource- + System.identityHashCode(this)
url-数据库连接 URL,例如:
MySQL:jdbc:mysql://localhost:3306/dbname
Oracle:jdbc:oracle:thin:@localhost:1521:orcl
username-数据库用户名
password-数据库密码,建议使用 ConfigFilter 进行加密存储,详见 ConfigFilter
driverClassName-JDBC 驱动类名,Druid 会自动识别,但建议手动配置,例如:
MySQL:com.mysql.cj.jdbc.Driver
Oracle:oracle.jdbc.OracleDriver
initialSize0初始化时创建的物理连接数,第一次 getConnection()init() 触发创建
maxActive8最大连接数(连接池最大可用连接数量)
minIdle-最小连接数(空闲时保持的最小连接数量)
maxWait-获取连接的最大等待时间(毫秒),超时则抛出异常,默认无限等待
poolPreparedStatementsfalse是否缓存 PreparedStatement(PSCache),Oracle 建议开启,MySQL 建议关闭
maxOpenPreparedStatements-1最大缓存的 PreparedStatement 数量,大于 0 时,poolPreparedStatements 自动启用,建议 100
validationQuery-检测连接是否有效的 SQL 语句,如 SELECT 1(MySQL)或 SELECT 1 FROM DUAL(Oracle)
testOnBorrowtrue获取连接时检查有效性,执行 validationQuery,影响性能
testOnReturnfalse归还连接时检查有效性,执行 validationQuery,影响性能
testWhileIdlefalse空闲时检查连接是否有效timeBetweenEvictionRunsMillis 触发检测,建议设置为 true
timeBetweenEvictionRunsMillis-检测空闲连接的时间间隔(毫秒),影响 testWhileIdle 逻辑
numTestsPerEvictionRun-单次空闲连接回收检测的连接数(Druid 仅支持单个 EvictionRun)
minEvictableIdleTimeMillis-最小空闲连接存活时间(毫秒),低于该值的连接会被回收
connectionInitSqls-物理连接初始化时执行的 SQL 语句
exceptionSorter-数据库异常分类处理器,Druid 会自动根据 dbType 选择合适的 ExceptionSorter
filters-扩展插件配置,可用值:
stat(监控统计)
log4j(日志)
wall(SQL 防御)
proxyFilters-自定义过滤器,如果 filtersproxyFilters 都配置,则 组合 过滤,而非替换

推荐配置(示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 数据库连接配置
druid.url=jdbc:mysql://localhost:3306/mydb?serverTimezone=UTC
druid.username=root
druid.password=root
druid.driver-class-name=com.mysql.cj.jdbc.Driver

# 连接池配置
druid.initialSize=5
druid.maxActive=20
druid.minIdle=5
druid.maxWait=3000

# 连接检测
druid.validationQuery=SELECT 1
druid.testOnBorrow=false
druid.testOnReturn=false
druid.testWhileIdle=true
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000

# SQL 监控和日志
druid.filters=stat,wall,log4j