📢📢📢本篇博文主要学习廖雪峰博文JDBC编程,博文地址:https://www.liaoxuefeng.com/wiki/1252599548343744/1255943820274272。
JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。
例如,我们在Java代码中如果要访问MySQL,那么必须编写代码操作JDBC接口。注意到JDBC接口是Java标准库自带的,所以可以直接编译。而具体的JDBC驱动是由数据库厂商提供的,例如,MySQL的JDBC驱动由Oracle提供。因此,访问某个具体的数据库,我们只需要引入该厂商提供的JDBC驱动,就可以通过JDBC接口来访问,这样保证了Java程序编写的是一套数据库访问代码,却可以访问各种不同的数据库,因为他们都提供了标准的JDBC驱动:

从代码来看,Java标准库自带的JDBC接口其实就是定义了一组接口,而某个具体的JDBC驱动其实就是实现了这些接口的类:

实际上,一个MySQL的JDBC的驱动就是一个jar包,它本身也是纯Java编写的。我们自己编写的代码只需要引用Java标准库提供的java.sql包下面的相关接口,由此再间接地通过MySQL驱动的jar包通过网络访问MySQL服务器,所有复杂的网络通讯都被封装到JDBC驱动中,因此,Java程序本身只需要引入一个MySQL驱动的jar包就可以正常访问MySQL服务器:

创建数据库learjdbc并在该数据库下创建students:
在DOS环境下运行( 默认你已经安装MySQL数据库 )
-- 创建数据库learjdbc:
DROP DATABASE IF EXISTS learnjdbc;
CREATE DATABASE learnjdbc;
-- 创建表students:
USE learnjdbc;
CREATE TABLE students (
id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(50) NOT NULL,
gender TINYINT(1) NOT NULL,
grade INT NOT NULL,
score INT NOT NULL,
PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;
-- 插入初始数据:
INSERT INTO students (name, gender, grade, score) VALUES ('小明', 1, 1, 88);
INSERT INTO students (name, gender, grade, score) VALUES ('小红', 1, 1, 95);
INSERT INTO students (name, gender, grade, score) VALUES ('小军', 0, 1, 93);
INSERT INTO students (name, gender, grade, score) VALUES ('小白', 0, 1, 100);
INSERT INTO students (name, gender, grade, score) VALUES ('小牛', 1, 2, 96);
INSERT INTO students (name, gender, grade, score) VALUES ('小兵', 1, 2, 99);
INSERT INTO students (name, gender, grade, score) VALUES ('小强', 0, 2, 86);
INSERT INTO students (name, gender, grade, score) VALUES ('小乔', 0, 2, 79);
INSERT INTO students (name, gender, grade, score) VALUES ('小青', 1, 3, 85);
INSERT INTO students (name, gender, grade, score) VALUES ('小王', 1, 3, 90);
INSERT INTO students (name, gender, grade, score) VALUES ('小林', 0, 3, 91);
INSERT INTO students (name, gender, grade, score) VALUES ('小贝', 0, 3, 77);
运行效果:

准备相应驱动的jar包:
你可以参照这篇博文:怎么在官网上下载java连接mysql的驱动jar包?
注意到这里添加依赖的scope是runtime,因为编译Java程序并不需要MySQL的这个jar包,只有在运行期才需要使用。如果把runtime改成compile,虽然也能正常编译,但是在IDE里写程序的时候,会多出来一大堆类似com.mysql.jdbc.Connection这样的类,非常容易与Java标准库的JDBC接口混淆,所以坚决不要设置为compile。
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
<scope>runtimescope>
dependency>
项目中只需要在pom.xml中导入依赖即可:

数据库的操作过程如下:

备注:获取数据的参数一般从配置文件中获取。
关闭数据库避免遗忘,建议使用try()进行自动关闭:
实例代码:

数据库的查询操作过程如下:
实例代码:

运行效果:

相关细节:
Statment和ResultSet都是需要关闭的资源,因此嵌套使用try (resource)确保及时关闭:

rs.next()用于判断是否有下一行记录,如果有,将自动把当前行移动到下一行(一开始获得ResultSet时当前行不是第一行)
类似于MySQL里的光标,并且ResultSet获取列时,索引从1开始而不是0;

如果获取索引0会报如下异常:

索引对应数据库中对应列的顺序:
数据库里列的顺序如下:

查看一下ReslutSet的fields变量( 顺序和数据库中的顺序保持一致 ):

Java和数据库中的数据类型的映射关系,如下表所示:
| SQL数据类型 | Java数据类型 |
|---|---|
| BIT, BOOL | boolean |
| INTEGER | int |
| BIGINT | long |
| REAL | float |
| FLOAT, DOUBLE | double |
| CHAR, VARCHAR | String |
| DECIMAL | BigDecimal |
| DATE | java.sql.Date, LocalDate |
| TIME | java.sql.Time, LocalTime |
除了通过索引获取数据库对应列的值外,还可以通过列名获取(
推荐)

运行效果:

SQL注入:
当我们进行条件查询的时候,如果直接通过拼接的方法拼接SQL语句可能出现SQL注入的危险,如下:
实例例子 (通过用户输入相应用户的名字查询数据库的数据 ):

运行效果:

虽然上面的方式成功执行了我们的需求,但是这种方式非常危险,如果用户在传入的参数里面加入了相应SQL语句的话就可能执行一些非常危险的操作:

运行效果:
上面的操作违背了我们本来的意图,我们只是想让用户通过相应的姓名查询该姓名的信息,但是这里查询了数据库中所有用户信息,在一些登入业务场景下,这是非常危险的一件事情。

使用Java对数据库进行操作时,必须使用PreparedStatement,严禁任何通过参数拼字符串的代码,如下:
实例代码:

运行效果:

再次传入相关SQL语句试试:
实例代码:

运行效果:

我们看看通过PreparedStatement后的SQL语句:

插入操作是INSERT,即插入一条新记录。通过JDBC进行插入,本质上也是用PreparedStatement执行一条SQL语句,不过最后执行的不是executeQuery(),而是executeUpdate()。示例代码如下:

运行效果:

如果数据库的表设置了自增主键,那么在执行INSERT语句时,并不需要指定主键,数据库会自动分配主键。对于使用自增主键的程序,有个额外的步骤,就是如何获取插入后的自增主键的值。要获取自增主键,不能先插入,再查询。因为两条SQL执行期间可能有别的程序也插入了同一个表。获取自增主键的正确写法是在创建PreparedStatement的时候,指定一个RETURN_GENERATED_KEYS标志位,表示JDBC驱动必须返回插入的自增主键。示例代码如下:

运行效果:

在students表中id为自增主键:

更新操作是UPDATE语句,它可以一次更新若干列的记录。更新操作和插入操作在JDBC代码的层面上实际上没有区别,除了SQL语句不同:

执行前:

执行后:

删除操作是DELETE语句,它可以一次删除若干列。和更新一样,除了SQL语句不同外,JDBC代码都是相同的:

执行前:

执行后:

数据库事务(Transaction)是由若干个SQL语句构成的一个操作序列,有点类似于Java的synchronized同步。数据库系统保证在一个事务中的所有SQL要么全部执行成功,要么全部不执行,即数据库事务具有ACID特性:
数据库事务可以并发执行,而数据库系统从效率考虑,对事务定义了不同的隔离级别。SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致的情况:
| Isolation Level | 脏读(Dirty Read) | 不可重复读(Non Repeatable Read) | 幻读(Phantom Read) |
|---|---|---|---|
| Read Uncommitted | Yes | Yes | Yes |
| Read Committed | - | Yes | Yes |
| Repeatable Read | - | - | Yes |
| Serializable | - | - | - |
备注:如果用户没有修改事务的隔离级别的话,MySQL的默认隔离级别是REPEATABLE_READ,修改事务隔离级别使用conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);进行修改conn为数据库的连接对象。
实例:
用户A和B账户余额都为300元,用户A向用户B转账300,然后用户B向用户A转账600元,实例代码如下:
新增数据表:
-- 创建accounts 表
USE learnjdbc;
CREATE TABLE accounts (
id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(50) NOT NULL,
balance INT ,
PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;
-- 插入数据
INSERT INTO accounts (name,balance) VALUES ('A',300);
INSERT INTO accounts (name,balance) VALUES ('B',300);
实例代码:
@Test
public void transation() throws SQLException {
// 用户A向用户B转账300
String fromA = "UPDATE accounts SET balance = balance - 300 WHERE name = ? AND balance >= 300";
String toB = "UPDATE accounts SET balance = balance + 300 WHERE name = ?";
boolean a1 = updateUser(fromA, new Object[]{"A"});
boolean a2 = updateUser(toB, new Object[]{"B"});
System.out.println("a1执行情况:"+a1+" a2执行情况:"+a2);
// 当前余额情况
ShowInfo();
// 用户B向用户A转账300元
String fromB = "UPDATE accounts SET balance = balance - 300 WHERE name = ? AND balance >= 300";
String toA = "UPDATE accounts SET balance = balance + 300 WHERE name = ?";
boolean b1 = updateUser(fromB, new Object[]{"B"});
boolean b2 = updateUser(toA, new Object[]{"A"});
System.out.println("b1执行情况:"+b1+" b2执行情况:"+b2);
ShowInfo();
}
public boolean updateUser(String sql,Object[] parameters) throws SQLException {
try( Connection conn = getMySQLConnect();) {
try (PreparedStatement ps = conn.prepareStatement(sql);) {
for (int i = 0; i < parameters.length; i++) {
ps.setObject(i+1,parameters[i]);
}
int n = ps.executeUpdate(); // 返回更新的行数
System.out.println("执行了"+n+"条Update语句。");
return n > 0 ? true : false;
}
}
}
public void ShowInfo() throws SQLException {
// 【1】获取数据库的连接
try( Connection conn = getMySQLConnect();) {
// 【2】 获取Statement对象,用于执行查询
String sql = "select * from accounts ";
try (Statement statement = conn.createStatement();) {
// 【3】 执行Statement对象提供的executeQuery("SELECT * FROM students")并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集
try (ResultSet resultSet = statement.executeQuery(sql);) {
// 【4】反复调用ResultSet的next()方法并读取每一行结果【相当于MySQL里面的光标】
while (resultSet.next()) {
int id = resultSet.getInt(1);
String name = resultSet.getString(2);
Long balance = resultSet.getLong(3);
System.out.println("id=" + id + " name=" + name + " 余额=" + balance);
}
}
}
}
}
运行效果:

有的时候可能在某一步出现问题,但是数据库里的数据还是发生了变化,如下:

运行效果:

数据库里的数据还是发生了变化( 数据库中本金少了300元! ):

造成这个的原因是因为默认情况下,我们获取到Connection连接后,总是处于“自动提交”模式,也就是每执行一条SQL都是作为事务自动执行的,你可以通过conn.setAutoCommit(false);关闭自动提交模式,当你传入true的时候表示开启,下面我们用事务的方式完成上面的操作,实例代码如下:
数据恢复:
使用 update set balance = 300 from accounts where id = 1; 语句使A和B账号的余额都为300元。

实例代码:
@Test
public void transation() throws SQLException {
Connection mySQLConnect = null;
try {
// 获取数据的连接
mySQLConnect = getMySQLConnect();
// 【1】关闭自动提交
mySQLConnect.setAutoCommit(false);
// 用户A向用户B转账300
String fromA = "UPDATE accounts SET balance = balance - 300 WHERE name = ? AND balance >= 300";
String toB = "UPDATE accounts SET balance = balance + 300 name = ?";
boolean a1 = updateUser(mySQLConnect,fromA, new Object[]{"A"});
boolean a2 = updateUser(mySQLConnect,toB, new Object[]{"B"});
System.out.println("a1执行情况:"+a1+" a2执行情况:"+a2);
// 当前余额情况
ShowInfo(mySQLConnect);
// 用户B向用户A转账300元
String fromB = "UPDATE accounts SET balance = balance - 300 WHERE name = ? AND balance >= 300";
String toA = "UPDATE accounts SET balance = balance + 300 WHERE name = ?";
boolean b1 = updateUser(mySQLConnect,fromB, new Object[]{"B"});
boolean b2 = updateUser(mySQLConnect,toA, new Object[]{"A"});
System.out.println("b1执行情况:"+b1+" b2执行情况:"+b2);
ShowInfo(mySQLConnect);
// 【2】提交事务
mySQLConnect.commit();
} catch (Exception e) {
// 【3】回滚事务
if (mySQLConnect != null) {
mySQLConnect.rollback();
}
e.printStackTrace();
}
finally {
if (mySQLConnect != null) {
// 【4】开启自动提交
mySQLConnect.setAutoCommit(true);
// 关闭连接
mySQLConnect.close();
}
}
}
private Connection getMySQLConnect() throws SQLException {
// 【1】获取连接数据库的参数
String JDBC_URL = "jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8";
String JDBC_USER = "root";
String JDBC_PASSWORD = "root";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
return conn;
}
public boolean updateUser(Connection connection,String sql,Object[] parameters) throws SQLException {
Connection conn = connection;
try (PreparedStatement ps = conn.prepareStatement(sql);) {
for (int i = 0; i < parameters.length; i++) {
ps.setObject(i+1,parameters[i]);
}
int n = ps.executeUpdate(); // 返回更新的行数
System.out.println("执行了"+n+"条Update语句。");
return n > 0 ? true : false;
}
}
public void ShowInfo(Connection connection) throws SQLException {
// 【1】获取数据库的连接
Connection conn = connection;
// 【2】 获取Statement对象,用于执行查询
String sql = "select * from accounts ";
try (Statement statement = conn.createStatement();) {
// 【3】 执行Statement对象提供的executeQuery("SELECT * FROM students")并传入SQL语句,执行查询并获得返回的结果集,使用ResultSet来引用这个结果集
try (ResultSet resultSet = statement.executeQuery(sql);) {
// 【4】反复调用ResultSet的next()方法并读取每一行结果【相当于MySQL里面的光标】
while (resultSet.next()) {
int id = resultSet.getInt(1);
String name = resultSet.getString(2);
Long balance = resultSet.getLong(3);
System.out.println("id=" + id + " name=" + name + " 余额=" + balance);
}
}
}
}
运行效果:

观察数据库里的数据( 虽然发生了异常,但是数据库里的数据没有发生变化 ):

在前面的例子中(账号转钱例子),执行的4条修改语句中,不难发现存在二组SQL语句相同,只是参数不相同,对于上面的写法我们可以进行优化,减少执行updateUser()的次数,如下:
修改updateUser()方法里的代码如下:

修改transation()方法里的代码如下:

运行效果:

通过一个循环来执行每个PreparedStatement虽然可行,但是性能很低。SQL数据库对SQL语句相同,但只有参数不同的若干语句可以作为batch执行,即批量执行,这种操作有特别优化,速度远远快于循环执行每个SQL。在JDBC代码中,我们可以利用SQL数据库的这一特性,把同一个SQL但参数不同的若干次操作合并为一个batch执行,实例代码如下:

运行效果:

上面的例子可能不是很恰当,下面给出一个批量插入用户的例子代码:
插入数据前:

实例代码:
@Test
public void addUserByBatch() throws SQLException {
// 批量插入数据
List<student> list = new ArrayList<>();
Random random = new Random();
for (int i = 14; i <= 24; i++) {
student temStu = new student("User"+i,i %2 ==0?1:0,i %2 ==0?1:2,random.nextInt(100));
list.add(temStu);
}
// 批量插入数据
String sql = "INSERT INTO students (name, gender, grade, score) VALUES (?,?,?,?)";
insertUserByBatch(list,sql);
}
private void insertUserByBatch(List<student> list,String sql) throws SQLException {
// SQL连接
try (Connection connection = getMySQLConnect();)
{
// 关闭自动提交
connection.setAutoCommit(false);
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (student stu : list) {
statement.setObject(1, stu.getName());
statement.setObject(2, stu.getGender());
statement.setObject(3, stu.getGrade());
statement.setObject(4, stu.getScore());
statement.addBatch();
}
// 执行批量操作
int[] ints = statement.executeBatch();
for (int i = 0; i < ints.length; i++) {
System.out.println("执行了"+ints[i]+"条INSERT语句。");
}
// 提交事务
connection.commit();
} catch (Exception e) {
// 回滚事务
connection.rollback();
}
finally {
connection.setAutoCommit(true);
}
}
}
class student{
private String name = "";
private int gender = -1;
private long grade = -1;
private long score = -1;
public student(String name, int gender, long grade, long score) {
this.name = name;
this.gender = gender;
this.grade = grade;
this.score = score;
}
@Override
public String toString() {
return "student{" +
", name='" + name + '\'' +
", gender=" + gender +
", grade=" + grade +
", score=" + score +
'}';
}
public String getName() {
return name;
}
public int getGender() {
return gender;
}
public long getGrade() {
return grade;
}
public long getScore() {
return score;
}
public void setName(String name) {
this.name = name;
}
public void setGender(int gender) {
this.gender = gender;
}
public void setGrade(long grade) {
this.grade = grade;
}
public void setScore(long score) {
this.score = score;
}
}
运行效果:

执行后:

在执行JDBC的增删改查的操作时,如果每一次操作都来一次打开连接,操作,关闭连接,那么创建和销毁JDBC连接的开销就太大了。为了避免频繁地创建和销毁JDBC连接,我们可以通过连接池(Connection Pool)复用已经创建好的连接。JDBC连接池有一个标准的接口javax.sql.DataSource,注意这个类位于Java标准库中,但仅仅是接口。要使用JDBC连接池,我们必须选择一个JDBC连接池的实现。常用的JDBC连接池有:
| 连接池 | 最新发布版本 | 最新发布时间 |
|---|---|---|
| HikariCP | 5.0.1 | 2022年10月1日 |
| C3P0 | 0.9.5.5 | 2019年12月12日 |
| BoneCP | 0.8.x | 2013年10月23日 |
| Druid | 1.2.x | 2022年9月11日 |
| DBCP | 2.9.x | 2021年8月4日 |
| tomcat-jdbc | 10.1.x | 2022年7月20日 |
主流连接池各项功能对比如下:

备注:来源于这篇博文:https://blog.csdn.net/wyouwd1/article/details/122525759
目前使用最广泛的是HikariCP。我们以HikariCP为例,要使用JDBC连接池,先添加HikariCP的依赖如下:
依赖:
<dependency>
<groupId>com.zaxxergroupId>
<artifactId>HikariCPartifactId>
<version>2.7.1version>
dependency>
在pom.xml中导入依赖:

紧接着,我们需要创建一个DataSource实例,在创建这个实例之前我们需要创建一个HikariConfig 的实例,该实例为设置数据库连接池的配置信息,常常设置参数有维护的最小、最大活动连接数,指定一个连接在空闲一段时间后自动关闭等,,整个实例代码如下:
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8");
config.setUsername("root");
config.setPassword("root");
config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒
config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
DataSource ds = new HikariDataSource(config);
注意创建DataSource也是一个非常昂贵的操作,所以通常DataSource实例总是作为一个全局变量存储,并贯穿整个应用程序的生命周期。
有了连接池以后,我们如何使用它呢?和前面的代码类似,只是获取Connection时,把DriverManage.getConnection()改为ds.getConnection():
// 获取Connect对象
try( Connection connection = ds.getConnection();) {
}
完整实例代码如下:
@Test
public void ConnectPool() throws SQLException {
// 【1】 创建DataSource实例
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/learnjdbc?useSSL=false&characterEncoding=utf8");
config.setUsername("root");
config.setPassword("root");
config.addDataSourceProperty("connectionTimeout", "1000"); // 连接超时:1秒
config.addDataSourceProperty("idleTimeout", "60000"); // 空闲超时:60秒
config.addDataSourceProperty("maximumPoolSize", "10"); // 最大连接数:10
DataSource ds = new HikariDataSource(config);
// 获取Connect对象
try( Connection connection = ds.getConnection();) {
// 获取Statement对象
try (Statement statement = connection.createStatement()) {
String sql = "SELECT * FROM students";
try (ResultSet rs = statement.executeQuery(sql)){
while (rs.next()) {
long id = rs.getLong("id");
long grade = rs.getLong("grade");
String name = rs.getString("name");
String gender = rs.getString("gender");
long score = rs.getInt("score");
System.out.println("id="+id+" name="+name+" gender="+gender+" grade="+grade+" score="+score);
}
}
}
}
}
运行效果:
一开始,连接池内部并没有连接,所以,第一次调用ds.getConnection(),会迫使连接池内部先创建一个Connection,再返回给客户端使用。当我们调用conn.close()方法时(在try(resource){…}结束处),不是真正“关闭”连接,而是释放到连接池中,以便下次获取连接时能直接返回。因此,连接池内部维护了若干个Connection实例,如果调用ds.getConnection(),就选择一个空闲连接,并标记它为“正在使用”然后返回,如果对Connection调用close(),那么就把连接再次标记为“空闲”从而等待下次调用。这样一来,我们就通过连接池维护了少量连接,但可以频繁地执行大量的SQL语句。
