Java JDBC
Java JDBC
JDBC是访问关系型数据库的Java API。JDBC给程序员提供访问和操纵众多关系型数据库的一个统一接口。使用JDBC API,程序员可以用Java以一种用户友好的接口执行SQL语句、获取结果以及显示数据,并且操纵数据。JDBC API还可用于分布式、异构环境中的多种数据源之间实现交互。
JDBC API是一组规范(Java接口和类的集合),用于编写和访问操作关系数据库的Java程序。JDBC驱动程序起一个接口的作用,它使JDBC与具体数据库之间的通信灵活方便(实现了API的规范),由具体的数据库供应商提供。下图显示了Java程序、JDBC API、JDBC驱动程序和关系数据库之间的关系。

使用JDBC开发数据库应用程序
准备MySQL
CREATE DATABASE IF NOT EXISTS cuit_scss;
USE cuit_scss;
CREATE TABLE course (
id INT PRIMARY KEY auto_increment COMMENT '课程标识id',
title VARCHAR (20) NOT NULL COMMENT '课程名称',
credits TINYINT UNSIGNED COMMENT '学分',
intro VARCHAR (200) COMMENT '课程简介'
) COMMENT = '课程信息表';
INSERT INTO course (title, credits, intro)
VALUES
('Linux IN Action', 4, '生产环境操作系统Linux实战'),
('MySQL 从入门到精通', 6, '关系型数据库MySQL的使用'),
('线性代数与空间解析几何', 6, '院长亲自授课,需要抢座');
CREATE TABLE student (
ssn CHAR (11) PRIMARY KEY COMMENT '学号',
sname VARCHAR (20) NOT NULL COMMENT '姓名',
gender ENUM ('M', 'F') COMMENT '学生性别:男(M)或女(F)',
phone VARCHAR (15) COMMENT '联系电话',
birth_date DATE COMMENT '学生出生日期',
address VARCHAR (50) COMMENT '联系地址'
) COMMENT = '学生信息表';
INSERT INTO student (ssn, sname, gender, phone, birth_date, address)
VALUES
('20250901001', 'Peppa', 'F', '95533', '2006-10-20', 'xx省xx市xx街道'),
('20250901002', 'Emily', 'F', '95588', '2006-5-7', 'yy省yy市yy街道'),
('20250901003', 'Pedro', 'M', '95599', '2004-6-2', 'zz省zz市zz街道');
CREATE TABLE score (
id INT PRIMARY KEY auto_increment COMMENT '成绩标识id',
ssn CHAR (11) COMMENT '学号',
cid INT COMMENT '课程编号',
sc_datte DATE COMMENT '选课日期',
score FLOAT (5, 2) COMMENT '成绩',
CONSTRAINT ForeignKey_student_score_ssn FOREIGN KEY (ssn) REFERENCES student (ssn) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT ForeignKey_course_score_cid FOREIGN KEY (cid) REFERENCES course (id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT Unique_key_ssn_cid UNIQUE (ssn, cid)
) COMMENT = '成绩信息表';
-- 添加检查约束
ALTER TABLE score ADD CONSTRAINT CK_score_score CHECK (score >= 0 && score <= 100);
核心接口
使用Java开发任何数据库应用程序都需要4个主要接口:Driver、Connection、Statement和ResultSet。这些接口定义了使用SQL访问数据库的一般架构,由JDBC API定义,JDBC驱动程序开发商提供实现。
- Driver接口:用于加载合适的驱动程序。
- Connection接口:用于连接到数据库。
- Statement接口:用于创建和执行SQL语句。
- ResultSet接口:用于处理SQL语句返回的结果。
下图展示了各接口之间的关系:

开发JDBC程序的典型步骤
1.加载驱动程序
在连接到数据库之前,需要加载合适的驱动程序。
Class.forName("com.mysql.cj.jdbc.Driver");
驱动程序是实现java.sql.Driver接口的具体类。Java 6支持驱动程序自动加载,但并非所有驱动程序都具备该特性,为安全起见,建议显式加载。
2.建立连接
使用DriverManager类的静态方法getConnection(url, username, password)连接数据库。
- url:数据库在Internet上的唯一标识符。
- username:数据库用户名。
- password:数据库密码。
不同数据库的url模式如下:
| 数据库 | url模式 |
|---|---|
| Mysql | jdbc:mysql://hostname:port/dbname |
| Oracle | jdbc:oracle:thin:@hostname:port:oracleDBSID |
3.创建语句
Connection对象如同程序与数据库的连接缆道,Statement对象则像缆车,用于传输SQL语句并返回结果。
Statement stat = conn.createStatement();
4.执行语句
execute(sql):可执行任意类型SQL语句,复杂且极少使用。executeUpdate(sql):执行DML语句(insert、update、delete),返回影响行数。executeQuery(sql):执行DQL语句(select),返回查询结果ResultSet。
5.处理结果ResultSet
ResultSet维护SQL查询结果,当前行初始位置为null。可通过next方法移动到下一行,通过getter方法获取当前行数据。
6.回收数据库资源
关闭连接并释放相关资源,可使用try-with-resource语法,但不建议。
项目依赖配置(Gradle)
在build.gradle中添加如下内容:
dependencies {
// mysql连接驱动的依赖
implementation 'mysql:mysql-connector-java:8.0.33'
}

示例代码
package com.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* @author Jing61
*/
public class JDBCTest {
public static void main(String[] args) {
Connection connection = null;
try {
// 1. 加载驱动(对jdbc规范的实现,由具体的数据库厂商提供)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取连接
String user = "root";
String password = "root";
String url = "jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8";
connection = DriverManager.getConnection(url, user, password);
// 3. 创建语句
var sql = "INSERT INTO course(title, credits, intro) VALUES ('Spring', 10, 'Spring企业级一站式商业解决方案')";
var statement = connection.createStatement();
// 4. 执行语句
int count = statement.executeUpdate(sql);
// 5. 处理结果
if (count > 0) {
System.out.println("添加成功");
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 6. 释放资源
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
SQL注入问题
问题演示
创建用户表并插入测试数据:
CREATE TABLE `user`(
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(12) NOT NULL UNIQUE,
`password` VARCHAR(36) NOT NULL
);
INSERT INTO `user` VALUES(DEFAULT,'peppa','peppa'),
(DEFAULT,'suzy','suzy'),(DEFAULT,'emily','emily');
存在SQL注入漏洞的代码:
package com.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author Jing61
*/
public class SQLInject {
public static void main(String[] args) throws RuntimeException {
findUserByUsernameAndPassword("peppa", "123");
findUserByUsernameAndPassword("peppa", "1' or true or '1");
}
public static void findUserByUsernameAndPassword(String username, String password) {
Connection connection = null;
try {
//1. 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取连接
String url = "jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8";
connection = DriverManager.getConnection(url, "root", "root");
// 3. 创建语句
var sql = "SELECT id, username, password FROM user where username='" + username + "' and password='" + password + "'";
var statement = connection.createStatement();
// 4. 执行语句 (DDL ==> ResultSet)
ResultSet rs = statement.executeQuery(sql);
// 5. 处理结果
while(rs.next()) {
System.out.println(rs.getString("id") + " " + rs.getString("username") + " " + rs.getString("password"));
}
rs.close();
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 6. 释放资源
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
当密码输入1' or true or '1时,查询字符串变为SELECT id, username, password FROM user where username='peppa' and password='1' or true or '1',会返回所有用户数据,存在安全漏洞。
解决方案:使用PreparedStatement
PreparedStatement接口继承自Statement,用于执行预编译的SQL语句,可避免SQL注入。
优化后的代码:
package com.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author Jing61
*/
public class SQLInject {
public static void main(String[] args) throws RuntimeException {
findUserByUsernameAndPassword("peppa", "peppa");
findUserByUsernameAndPassword("peppa", "1' or true or '1");
}
public static void findUserByUsernameAndPassword(String username, String password) {
Connection connection = null;
try {
//1. 加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取连接
String url = "jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8";
connection = DriverManager.getConnection(url, "root", "root");
// 3. 创建语句
var sql = "SELECT id, username, password FROM user where username=? and password=?";
var ptst = connection.prepareStatement(sql);
// 4. 执行语句 (DDL ==> ResultSet)
ptst.setString(1, username);
ptst.setString(2, password);
ResultSet rs = ptst.executeQuery();
// 5. 处理结果
while(rs.next()) {
System.out.println(rs.getString("id") + " " + rs.getString("username") + " " + rs.getString("password"));
}
rs.close();
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 6. 释放资源
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
PreparedStatement的优势:
- 预编译SQL语句,性能更好。
- 无需拼接SQL语句,编程更简单。
- 可防止SQL注入,安全性更好。
规范和封装JDBC代码
目前存在的问题
- 驱动重复注册,应在系统启动时注册一次。
- 数据库连接属性硬编码,耦合度高。
- 获取连接、释放资源等公共操作未抽取,代码冗余。
解决方案:JDBC助手类
配置文件(jdbc.properties)
在resources目录下创建jdbc.properties文件:

# mysql的连接驱动类名
driverClassName = com.mysql.cj.jdbc.Driver
# mysql的连接地址
url = jdbc:mysql://localhost:3306/cuit_scss?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=utf-8
# mysql的连接用户名
username = root
# mysql的连接密码
password = root
这样的好处是,当需要改变连接条件时,无需改变源代码,修改配置文件即可。
JDBCHelper工具类(单例模式)
package com.jdbc;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
/**
* JDBCHelper jdbc工具类
* 解决:
* 1.系统启动时注册驱动,只注册一次
* 2.可配置数据库连接,完成解耦
* 3.抽取出公共的方法 获取连接、释放资源等
* 单例模式:该类在系统中只存在唯一的实例
* 1.构造方法私有化,外部不允许创建对象
* 饥汉模式:在类加载的时候创建对象,并保存在静态成员变量中
* 2.获取单例对象
* 懒汉模式:在需要对象的时候创建对象,保存在静态成员变量中
* @author Jing61
*/
public class JDBCHelper {
private static String driver = null;
private static String url = null;
private static String username = null;
private static String password = null;
// private static JDBCHelper instance = new JDBCHelper(); // 饥汉模式
private static volatile JDBCHelper instance = null; // volatile保证修改对线程可见 懒汉模式
private JDBCHelper() {}
static {
try {
// 从资源文件中加载数据库相关连接属性
Properties properties = new Properties();
// 获取属性值,使用类加载器加载类路径下的jdbc.properties文件
InputStream input = JDBCHelper.class.getClassLoader().getResourceAsStream("jdbc.properties");
properties.load(input);
driver = properties.getProperty("driverClassName");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");
System.out.println("初始化信息:\ndriver:" + driver + "\nurl:" + url + "\nusername:" + username + "\npassword:" + password);
// 1. 加载驱动
Class.forName(driver);
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}
// 2. 获取连接
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
// 6. 释放资源
public void free(Connection connection) {
try {
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 饥汉模式
// public static JDBCHelper getInstance() {
// return instance;
// }
// 懒汉模式(双重检查锁)
public static JDBCHelper getInstance() {
if(instance == null) {
synchronized (JDBCHelper.class) {
// 双重检查,解决多线程并发访问时创建多个实例的问题
if(instance == null) instance = new JDBCHelper();
}
}
return instance;
}
}
测试增删改查(CourseTest)
package com.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
public class CourseTest {
public static boolean insert(String title, int credits, String intro) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
var sql = "INSERT INTO course(title, credits, intro) VALUES (?, ?, ?)";
var ptst = conn.prepareStatement(sql);
ptst.setString(1, title);
ptst.setInt(2, credits);
ptst.setString(3, intro);
return ptst.executeUpdate() == 1;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
public static boolean update(int id, String title, int credits, String intro) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
var sql = "UPDATE course SET title = ?, credits = ?, intro = ? WHERE id = ?";
var ptst = conn.prepareStatement(sql);
ptst.setString(1, title);
ptst.setInt(2, credits);
ptst.setString(3, intro);
ptst.setInt(4, id);
return ptst.executeUpdate() >= 1;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
public static boolean remove(int id) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
var sql = "DELETE FROM course WHERE id = ?";
var ptst = conn.prepareStatement(sql);
ptst.setInt(1, id);
return ptst.executeUpdate() >= 1;
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
public static void findById(int id) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
var sql = "SELECT * FROM course WHERE id = ?";
var ptst = conn.prepareStatement(sql);
ptst.setInt(1, id);
var rs = ptst.executeQuery();
System.out.println("id\ttitle\tcredits\tintro");
while(rs.next()) {
System.out.println(rs.getInt("id") + "\t" + rs.getString("title") + "\t" + rs.getInt("credits") + "\t" + rs.getString("intro"));
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
public static void findAll() {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
var sql = "SELECT * FROM course";
var ptst = conn.prepareStatement(sql);
var rs = ptst.executeQuery();
System.out.println("id\ttitle\tcredits\tintro");
while(rs.next()) {
System.out.println(rs.getInt("id") + "\t" + rs.getString("title") + "\t" + rs.getInt("credits") + "\t" + rs.getString("intro"));
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
public static void main(String[] args) {
// insert("MyBatis", 2, "MyBatis是一个持久性框架");
update(5, "MyBatis", 3, "MyBatis是一个持久性框架");
remove(8);
findById(5);
findAll();
}
}
CallableStatement(执行存储过程)
创建存储过程
-- 创建存储过程:根据学生学号获取该学生的平均成绩
DELIMITER $$
CREATE PROCEDURE compute_average(IN p_ssn CHAR(11), OUT p_score FLOAT(5, 2))
BEGIN
SELECT AVG(score) INTO p_score FROM score WHERE ssn = p_ssn;
END;
//
DELIMITER ;
调用存储过程的代码
package com.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
/**
* @author Jing61
*/
public class JDBCCallableStatementTest {
public static void main(String[] args) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
// 准备调用存储过程的SQL语句(转义语法)
var st = conn.prepareCall("{call compute_average(?, ?)}");
// 设置IN参数
st.setString(1, "20250901001");
// 注册OUT参数的类型
st.registerOutParameter(2, Types.FLOAT);
// 执行存储过程
st.execute();
// 获取OUT参数的值
System.out.println(st.getFloat(2));
} catch (SQLException e) {
System.out.println(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
}
CallableStatement继承自PreparedStatement,提供了注册OUT参数和获取OUT参数值的方法。
管理结果集
ResultSet的滚动和更新配置
ResultSet默认只能向后滚动且只读,若需支持向前滚动、指定行跳转或更新,需在创建PreparedStatement时指定参数:
| 参数类型 | 可选值 | 说明 |
|---|---|---|
| resultSetType | ResultSet.TYPE_FORWARD_ONLY | 结果集不能滚动(默认值) |
| ResultSet.TYPE_SCROLL_INSENSITIVE | 结果集可以滚动,对数据库变化不敏感 | |
| ResultSet.TYPE_SCROLL_SENSITIVE | 结果集可以滚动,对数据库变化敏感 | |
| resultSetConcurrency | ResultSet.CONCUR_READ_ONLY | 只读,结果集不能更新(默认值) |
| ResultSet.CONCUR_UPDATABLE | 结果集可用于更新数据库 |
示例代码
package com.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author Jing61
*/
public class ResultSetTest {
public static void main(String[] args) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
// var ptst = conn.prepareStatement("SELECT id, title, credits, intro FROM course");
// ResultSet rs = ptst.executeQuery();
// while(rs.next()) { // 向后滚动访问查询结果
// System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
// }
// while (rs.previous()) {// 向前滚动访问查询结果,默认不允许
// System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
// }
// rs.absolute(3); // 移动到指定行,默认不允许
/*
* 若需要移动到指定行或者向前滚动或者更新,则需要设置允许
* ResultSet
* prepareStatement(String sql, int resultSetType,int resultSetConcurrency)
* resultSetType:
* ResultSet.TYPE_FORWARD_ONLY:结果集不能滚动(默认值)
* ResultSet.TYPE_SCROLL_INSENSITIVE:结果集可以滚动,但是对数据库变化不敏感,即不能变化更新
* ResultSet.TYPE_SCROLL_SENSITIVE:结果集可以滚动,对数据库变化敏感
* resultSetConcurrency
* ResultSet.CONCUR_READ_ONLY:只读,结果集不能更新(默认值)
* ResultSet.CONCUR_UPDATABLE:结果集可以用于更新数据库
*/
var ptst = conn.prepareStatement("SELECT id, title, credits, intro FROM course", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = ptst.executeQuery();
while (rs.next()) { // 向后滚动访问查询结果
System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
}
while (rs.previous()) {// 向前滚动访问查询结果,现在允许
System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
}
rs.absolute(3); // 移动到指定行,现在运行
System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
// 更新第四条记录的课程学分为5
if(rs.absolute(4)) {
rs.updateInt("credits", 5);
rs.updateRow(); // 更新结果集
System.out.println(rs.getInt("id") + " " + rs.getString("title") + " " + rs.getInt("credits") + " " + rs.getString("intro"));
}
} catch (SQLException e) {
System.out.println(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
}
处理大数据类型数据
利用Clob处理大文本数据,Blob处理长二进制数据(实际开发中,文件通常存储在文件服务系统,数据表仅存储访问路径,此处为演示)。
新增字段
ALTER TABLE student ADD COLUMN resume LONGTEXT COMMENT '大数据文本';
ALTER TABLE student ADD COLUMN avatar LONGBLOB COMMENT '二进制数据,头像';
示例代码(写入和读取大数据)
package com.jdbc;
import java.io.*;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @author Jing61
*/
public class WriteBigDate {
public static void main(String[] args) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
// 待写入的文件
File file1 = new File("E:/CUIT/Java/Demo/cuit/homework.txt"); // 大文本文件
File file2 = new File("D:/qqDownload/d320fe824a20ae0a1af3df8e936d9fa9.png"); // 二进制文件(头像)
// 写入大数据
String sql = "UPDATE student SET resume=?, avatar=? WHERE ssn=?";
var ptst = conn.prepareStatement(sql);
// 设置大文本数据(Clob)(字符输入流)
ptst.setClob(1, new FileReader(file1), file1.length());
// 设置二进制数据(Blob)(字节输入流)
ptst.setBlob(2, new FileInputStream(file2), file2.length());
ptst.setString(3, "20250901001");
ptst.executeUpdate();
// 读取大数据
String sql2 = "SELECT resume, avatar FROM student WHERE ssn=?";
var ptst2 = conn.prepareStatement(sql2);
ptst2.setString(1, "20250901001");
var rs = ptst2.executeQuery();
while(rs.next()) {
System.out.println("20250901001的简历和头像");
var resume = rs.getClob("resume");
var avatar = rs.getBlob("avatar");
// 写出大文本到文件
resume.getCharacterStream().transferTo(new FileWriter("E:/CUIT/Java/Demo/cuit/test.txt"));
// 写出二进制数据到文件
avatar.getBinaryStream().transferTo(new FileOutputStream("E:/CUIT/Java/Demo/cuit/test.png"));
}
} catch (SQLException | IOException e) {
throw new RuntimeException(e);
}
}
}
获取元数据
数据库元数据
用于获取数据库的基本信息(如URL、版本、驱动等)。
package com.jdbc;
import java.sql.SQLException;
/**
* @author Jing61
*/
public class DatabaseMetaDemo {
public static void main(String[] args) {
try(var conn = JDBCHelper.getInstance().getConnection()){
var dbMeta = conn.getMetaData();
System.out.println("数据库连接url:" + dbMeta.getURL());
System.out.println("数据库用户名:" + dbMeta.getDatabaseProductName());
System.out.println("数据库主版本:" + dbMeta.getDatabaseMajorVersion());
System.out.println("数据库次版本:" + dbMeta.getDatabaseMinorVersion());
System.out.println("数据库驱动:" + dbMeta.getDriverName());
System.out.println("数据库驱动版本:" + dbMeta.getDriverVersion());
System.out.println("数据库供应商:" + dbMeta.getDatabaseProductName());
System.out.println("=======================结果集数据库表=================");
var rsTables = dbMeta.getTables(null, null, null, new String[] {"TABLE"});
System.out.println("user tables:");
while(rsTables.next())
System.out.println(rsTables.getString("TABLE_NAME"));
}catch (SQLException e) {
e.printStackTrace();
}
}
}
结果集元数据
用于获取查询结果集的结构信息(如列名、数据类型等)。
package com.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @author Jing61
*/
public class ResultSetMetaDataDemo {
public static void main(String[] args) {
Connection conn = null;
try {
conn = JDBCHelper.getInstance().getConnection();
var sql = "SELECT id, title, credits, intro FROM course";
var ptst = conn.prepareStatement(sql);
var rs = ptst.executeQuery();
var meta = rs.getMetaData(); // 获取结果集元数据
for (int i = 1; i <= meta.getColumnCount(); i++) {
/*
* getColumnLabel(int column) 获取列的别名
* getColumnTypeName(int column) 获取列的数据类型
* getColumnName(int column) 获取列名
*/
System.out.println(meta.getColumnLabel(i) + "\t" + meta.getColumnTypeName(i));
}
while(rs.next()) {
for (int i = 1; i <= meta.getColumnCount(); i++) {
// 若不知道列的数据类型,则使用getObject方法
System.out.print(rs.getObject(i) + "\t");
}
System.out.println();
}
} catch (SQLException e) {
System.out.println(e.getMessage());
} finally {
JDBCHelper.getInstance().free(conn);
}
}
}
事务处理
事务的ACID特性
- 原子性(Atomicity):事务是一个整体的工作单元,事务对数据库所做的操作要么全部执行,要么全部取消。如果某条语句执行失败,则所有语句全部回滚。
- 一致性(Consistency):事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。如果事务成功,则所有数据将变为一个新的状态;如果事务失败,则所有数据将处于开始之前的状态。
- 隔离性(Isolation):事务与事务之间是相互隔离的。由事务所作的修改必须与其他事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。
- 持久性(Durability):事务提交后,对数据库数据的影响是持久性(永久的)。
JDBC事务控制
JDBC默认自动提交事务,每条SQL语句作为一个独立事务。可通过setAutoCommit(false)关闭自动提交,手动控制事务。
增强JDBCHelper(添加事务管理)
package com.jdbc;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
/**
* JDBCHelper jdbc工具类
* 解决:
* 1.系统启动时注册驱动,只注册一次
* 2.可配置数据库连接,完成解耦,
* 3.抽取出公共的方法 获取连接、释放资源等
* 单例模式:该类在系统中只存在唯一的实例
* 1.构造方法私有化,外部不允许创建对象
* 饥汉模式:在类加载的时候创建对象,并保存在静态成员变量中
* 2.获取单例对象
* 满汉模式:在需要对象的时候创建对象,保存在静态成员变量中
*
* @author Jing61
*/
public class JDBCHelper {
private static String driver = null;
private static String url = null;
private static String username = null;
private static String password = null;
// private static JDBCHelper instance = new JDBCHelper(); // 饥汉模式
private static volatile JDBCHelper instance = null; // volatile保证修改对线程可见 满汉模式
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
private JDBCHelper() {
}
static {
try {
// 从资源文件中加载数据库相关连接属性
Properties properties = new Properties();
// 获取属性值,使用类加载器位于类路径下的jdbc.properties文件
InputStream input = JDBCHelper.class.getClassLoader().getResourceAsStream("jdbc.properties");
properties.load(input);
driver = properties.getProperty("driverClassName");
url = properties.getProperty("url");
username = properties.getProperty("username");
password = properties.getProperty("password");
System.out.println("初始化信息:\ndriver:" + driver + "\nurl:" + url + "\nusername:" + username + "\npassword:" + password);
// 1. 加载驱动
Class.forName(driver);
} catch (ClassNotFoundException | IOException e) {
throw new RuntimeException(e);
}
}
// 2. 获取连接
public Connection getConnection() throws SQLException {
var connection = threadLocal.get();
if (connection == null) {
connection = DriverManager.getConnection(url, username, password);
// 保存连接对象
threadLocal.set(connection);
}
return connection;
}
// 6. 释放资源
public void free(Connection connection) {
try {
if (connection != null) connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
// 饥汉模式
// public static JDBCHelper getInstance() {
// return instance;
// }
// 满汉模式
public static JDBCHelper getInstance() {
if (instance == null) {
synchronized (JDBCHelper.class) {
//双重检查,解决多线程并发访问时,多个线程同时进入if判断,创建多个实例
if (instance == null) instance = new JDBCHelper();
}
}
return instance;
}
/**
* 开启事务
*/
public void transaction() {
try {
Connection connection = getConnection();
connection.setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 提交事务
*/
public void commit() {
try {
Connection connection = getConnection();
connection.commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 回滚事务
*/
public void rollback() {
try {
Connection connection = getConnection();
connection.rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 用于事务控制中释放资源
*/
public void free() {
var connection = threadLocal.get();
try {
if (connection != null) {
connection.close();
// 移除连接对象
threadLocal.remove();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
事务测试代码
package com.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;
/**
* @author Jing61
*/
public class TransactionalDemo {
public static void main(String[] args) {
Connection conn = null;
Savepoint sp = null;
try {
conn = JDBCHelper.getInstance().getConnection();
// 开启事务就是默认的自动提交关闭
// var sql = "UPDATE course SET credits = credits + 1 WHERE id = ?";
// var ptst = conn.prepareStatement(sql);
// ptst.setInt(1, 1);
// ptst.executeUpdate(); // 成功
// sql = "UPDATE course SET credits = 1 - credits WHERE id = ?"; // 规定了学分大于等于0
// ptst = conn.prepareStatement(sql);
// ptst.setInt(1, 1);
// ptst.executeUpdate(); // fail(不是事务的情况下抛出异常,事务的情况下则回滚)
// 不利用事务,上面的语句就会出现部分成功,部分不成功
// 利用事务
// 开启事务
JDBCHelper.getInstance().transaction();
var sql = "UPDATE course SET credits = credits + 1 WHERE id = ?";
var ptst = conn.prepareStatement(sql);
ptst.setInt(1, 1);
ptst.executeUpdate();
sp = conn.setSavepoint();
sql = "UPDATE course SET credits = credits - 1 WHERE id = ?";
ptst = conn.prepareStatement(sql);
ptst.setInt(1, 1);
ptst.executeUpdate();
} catch (SQLException e) {
JDBCHelper.getInstance().rollback(); // 回滚
throw new RuntimeException(e);
} finally {
JDBCHelper.getInstance().commit(); // 提交
JDBCHelper.getInstance().free();
}
}
}
批量更新
多条SQL语句作为一批操作同时提交,提升执行效率。
示例代码
package com.jdbc;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Random;
/**
* @author Jing61
*/
public class BatchUpdatesDemo {
public static void main(String[] args) {
Connection conn = null;
Random random = new Random();
try {
conn = JDBCHelper.getInstance().getConnection();
var sql = "INSERT INTO course(title, credits, intro) VALUES (?, ?, ?)";
var ptst = conn.prepareStatement(sql);
// 批量添加5200条数据,每1000条提交一次
for (int i = 1; i <= 5200; i++) {
ptst.setString(1, "课程_" + i);
ptst.setInt(2, random.nextInt(5)); // 学分随机0-4
ptst.setString(3, "课程" + i + "的简介");
ptst.addBatch(); // 添加到批量操作中
// 每1000条执行一次批量更新
if (i % 1000 == 0) {
ptst.executeLargeBatch();
}
}
// 执行剩余的批量操作
ptst.executeLargeBatch();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
JDBCHelper.getInstance().free(conn);
}
}
}
使用连接池
数据库连接的建立和关闭耗费资源,连接池可复用连接,提升系统性能。JDBC 2.0规范引入javax.sql.DataSource接口,常用实现有Druid、C3P0、Hikari等。
依赖配置(Gradle)
dependencies {
// mysql连接驱动依赖
implementation 'mysql:mysql-connector-java:8.0.33'
// 数据库连接池(Druid)
implementation 'com.alibaba:druid:1.2.27'
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}

连接池工具类(JDBCUtil)
package com.jdbc;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import javax.sql.DataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
/**
* jdbc工具类
* 采用数据源(jdbc扩展规范)来获取连接
* 传统方式:DriverManager.getConnection(url,username,password);使用完毕后进行释放
* 创建数据库连接是非常消耗系统资源,频繁创建及释放连接,对系统性能影响很大
* 连接池参数
* 初始化连接数
* 最大连接数
* 最大等待时间
* 最大空闲数
* 最小空闲数
*
* 选用产品:
* dbcp2、c3p0、druid、boneCP、hikari .......
*
*/
@SuppressWarnings("static-access")
public class JDBCUtil {
//数据源
private static DataSource dataSource;
private static JDBCUtil instance;
static {
try { //初始化数据源
var prop = new Properties();
prop.load(JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties"));
//通过工厂创建数据源,只要properties中的property的名称为DruidDataSourceFactory里的属性名相同,会自动将对应的属性值设置给对应的属性
var factory = new DruidDataSourceFactory();
dataSource = factory.createDataSource(prop);
} catch (Exception e) {
e.printStackTrace();
}
}
private JDBCUtil() {
}
/**
* 通过数据源获取连接对象
* @return 数据库连接对象
* @throws SQLException
*/
public Connection getConnetion() throws SQLException {
return dataSource.getConnection();
}
/**
* 释放jdbc相关资源
* @param rs 结果集
* @param stat 语句对象
* @param conn 连接对象
*/
public void free(ResultSet rs, Statement stat, Connection conn) {
try {
if(rs != null)
rs.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if(stat != null)
stat.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
if(conn != null)
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
public static JDBCUtil getInstance() {
if(instance == null) {
synchronized (JDBCUtil.class) {
if(instance == null)
instance = new JDBCUtil();
}
}
return instance;
}
}
DAO模式
DAO(Data Access Object)是数据访问对象,为数据库操作提供抽象接口,隔离数据访问逻辑与业务逻辑。
DAO模式组成
- 数据对象(Data Object):封装数据表数据的JavaBean(如Course)。
- 数据访问接口(Data Accessor):定义数据操作方法(如CourseDao)。
- 接口实现类:实现数据访问接口,具体执行数据库操作(如CourseDaoImpl)。
示例实现
数据对象(Course.java)
package com.jdbc.course;
/**
* 课程数据对象(JavaBean)
* @author Jing61
*/
public class Course {
private int id;
private String title;
private int credits;
private String intro;
public Course() {
}
public Course(int id, String title, int credits, String intro) {
this.id = id;
this.title = title;
this.credits = credits;
this.intro = intro;
}
// getter和setter方法
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getCredits() {
return credits;
}
public void setCredits(int credits) {
this.credits = credits;
}
public String getIntro() {
return intro;
}
public void setIntro(String intro) {
this.intro = intro;
}
@Override
public String toString() {
return "Course{" +
"id=" + id +
", title='" + title + '\'' +
", credits=" + credits +
", intro='" + intro + '\'' +
'}';
}
}
数据访问接口(CourseDao.java)
package com.jdbc.course;
/**
* 课程数据访问接口
* @author Jing61
*/
public interface CourseDao {
/**
* 根据id查询课程信息
* @param id 课程id
* @return 课程信息
*/
Course getCourseById(int id);
/**
* 更新课程信息
* @param course 课程信息
*/
void updateCourse(Course course);
/**
* 删除课程信息
* @param id 课程id
*/
void deleteCourse(int id);
/**
* 添加课程信息
* @param course 课程信息
*/
void insertCourse(Course course);
}
接口实现类(CourseDaoImpl.java)
package com.jdbc.course;
import com.jdbc.JDBCHelper;
import java.sql.Connection;
import java.sql.SQLException;
/**
* 课程数据访问接口实现类
* @author Jing61
*/
public class CourseDaoImpl implements CourseDao{
private JDBCHelper jdbcHelper = JDBCHelper.getInstance();
@Override
public Course getCourseById(int id) {
Course course = null;
Connection conn = null;
try {
conn = jdbcHelper.getConnection();
var sql = "SELECT id, title, credits, intro FROM course WHERE id = ?";
var ptst = conn.prepareStatement(sql);
ptst.setInt(1, id);
var rs = ptst.executeQuery();
if (rs.next()) {
course = new Course();
course.setId(rs.getInt("id"));
course.setTitle(rs.getString("title"));
course.setCredits(rs.getInt("credits"));
course.setIntro(rs.getString("intro"));
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
jdbcHelper.free(conn);
}
return course;
}
@Override
public void updateCourse(Course course) {
Connection conn = null;
try {
conn = jdbcHelper.getConnection();
var sql = "UPDATE course SET title = ?, credits = ?, intro = ? WHERE id = ?";
var ptst = conn.prepareStatement(sql);
ptst.setString(1, course.getTitle());
ptst.setInt(2, course.getCredits());
ptst.setString(3, course.getIntro());
ptst.setInt(4, course.getId());
ptst.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
jdbcHelper.free(conn);
}
}
@Override
public void deleteCourse(int id) {
Connection conn = null;
try {
conn = jdbcHelper.getConnection();
var sql = "DELETE FROM course WHERE id = ?";
var ptst = conn.prepareStatement(sql);
ptst.setInt(1, id);
ptst.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
jdbcHelper.free(conn);
}
}
@Override
public void insertCourse(Course course) {
Connection conn = null;
try {
conn = jdbcHelper.getConnection();
var sql = "INSERT INTO course(title, credits, intro) VALUES (?, ?, ?)";
var ptst = conn.prepareStatement(sql);
ptst.setString(1, course.getTitle());
ptst.setInt(2, course.getCredits());
ptst.setString(3, course.getIntro());
ptst.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
jdbcHelper.free(conn);
}
}
}
RowSet
RowSet接口继承自ResultSet,默认支持滚动、更新、序列化,部分实现(如CachedRowSet)支持离线操作,无需保持数据库连接。
常用RowSet子接口
JdbcRowSet:需保持数据库连接。CachedRowSet:离线结果集,数据缓存到内存。FilteredRowSet:支持数据过滤。JoinRowSet:支持多结果集关联。WebRowSet:支持XML数据交互。
离线结果集
在使用ResultSet的时代,程序查询得到ResultSet之后必须立即读取或处理它对应的记录,否则一旦Connection关闭,再取通过ResultSet读取记录就会引发异常。在这种模式下,JDBC编程十分痛苦,假设应用程序分为数据访问层和视图显式层,当应用程序在数据访问层查询得到ResultSet后,对ResultSet的处理有如下常见操作:使用迭代访问ResultSet里的记录,并将这些记录转成JavaBean,再将多个JavaBean封装到一个List集合,处理完成后就可以关闭Connection等资源,然后再将JavaBean集合传到视图显示层。直接将ResultSet传到视图显示层——这要求当视图显示层显示数据时,底层Connection必须一直处于打开状态,否则ResultSet无法读取数据,这种方式比较安全,但编程要求较高,对各种类型转换处理也比较繁琐。另一种方式则需要Connection一直处于打开状态,这样不仅不安全,而且对程序性能也有很大影响。通过使用离线RowSet可以十分"优雅"地处理上面的问题,离线RowSet会直接将底层数据读入内存中,封装成RowSet对象,而RowSet对象可以完全当成JavaBean来使用。因此不仅安全而且编程十分简单。CachedRowSet是所有离线RowSet的父接口。
示例代码(CachedRowSet)
package com.jdbc;
import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.RowSetFactory;
import javax.sql.rowset.RowSetProvider;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Logger;
/**
* 离线RowSet示例(CachedRowSet)
* @author Jing61
*/
public class RowSetDemo {
private JDBCHelper helper = JDBCHelper.getInstance();
/**
* 执行查询并返回离线RowSet
* @param sql 查询语句
* @param params 参数
* @return CachedRowSet 离线结果集
*/
public CachedRowSet executeQuery(String sql, Object...params){
Connection conn = null;
PreparedStatement ptst = null;
ResultSet rs = null;
try{
conn = helper.getConnection();
ptst = conn.prepareStatement(sql);
// 设置SQL参数
for (var i = 0; params != null && i < params.length; i++) {
ptst.setObject(i + 1, params[i]);
}
rs = ptst.executeQuery();
// 创建RowSetFactory
RowSetFactory factory = RowSetProvider.newFactory();
// 创建CachedRowSet实例
CachedRowSet crs = factory.createCachedRowSet();
// 用ResultSet填充CachedRowSet(数据缓存到内存)
crs.populate(rs);
return crs;
}catch (SQLException e){
throw new RuntimeException(e);
}finally {
// 关闭数据库连接(CachedRowSet已离线,不影响后续操作)
try {
if (rs != null) rs.close();
if (ptst != null) ptst.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 测试
public static void main(String[] args) {
RowSetDemo demo = new RowSetDemo();
var sql = "SELECT id, title, credits, intro FROM course WHERE credits > ?";
CachedRowSet crs = demo.executeQuery(sql, 4);
try {
// 离线状态下操作结果集(无需数据库连接)
System.out.println("离线结果集数据:");
while (crs.next()) {
System.out.println(crs.getInt("id") + "\t" + crs.getString("title") + "\t" + crs.getInt("credits"));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}

浙公网安备 33010602011771号