引言
数据的存储
我们在开发 java 程序时,数据都是存储在内存中的,属于临时存储,当程序停止或重启时,内存中的数据就会丢失,我们为了解决数据的长期存储问题,有以下解决方案:
- 通过 IO流书记,将数据存储在本地磁盘中,这样就解决了持久化问题,但是数据没有结构和逻辑,不方便管理和维护。
- 通过关系型数据库(例如 MySQL),将数据按照特定的格式交由数据库管理系统维护,关系型数据库是通过库和表分隔不同的数据,表中的数据的存储方式是行和列,区分相同格式不同值的数据。
数据的操作
数据存储在数据库中,仅仅只是解决了我们数据存储的问题,当我们程序运行时,需要读取数据以及对数据做增删改的操作,那么我们如何通过 java 程序对数据库中的数据做增删改查呢?
答案就是今天的主角------ jdbc
jdbc
什么是 jdbc
JDBC(Java Database Connectivity)是Java编程语言中用于执行 SQL 语句的 API,它为数据库访问提供了一种标准的方法。通过使用 JDBC API,开发者可以以一种统一的方式与各种不同的数据库进行交互,而无需关心底层的数据库驱动细节。
以下是关于 JDBC 的一些关键点:
- 通用性:JDBC 允许 Java 应用程序连接到几乎任何 SQL 数据库,包括 MySQL、Oracle、PostgreSQL、Microsoft SQL Server 等。
- 数据库无关性:编写一次代码,就可以在不同类型的数据库上运行,只要相应地更换数据库驱动即可。
- 驱动程序:为了使 JDBC 能够与特定的数据库通信,需要有相应的数据库驱动程序。这些驱动程序实现了JDBC 接口,并负责处理与特定数据库的通信协议。
- JDBC URL:每个数据库都有一个唯一的 URL 格式,用于建立到数据库的连接。这个 URL 通常包含数据库类型、主机地址、端口号和数据库名称等信息。
- 连接管理 :JDBC 提供了
java.sql.Connection
接口来表示与数据库的连接。开发者可以通过DriverManager.getConnection()
方法获取连接。 - 执行 SQL 命令 :通过
Statement
、PreparedStatement
或CallableStatement
对象,可以发送 SQL 语句给数据库并处理返回的结果。 - 结果集处理 :执行查询后,会返回一个
ResultSet
对象,它包含了查询结果的数据。可以通过迭代ResultSet
来读取每一行数据。 - 事务管理 :JDBC 支持事务控制,包括提交(
commit
)和回滚(rollback
)操作,这使得可以在一组相关联的操作完成后作为一个整体来提交或撤销更改。 - 性能优化 :如使用预编译的 SQL 语句(
PreparedStatement
)可以提高性能,减少 SQL 注入风险;批量更新(Batch Updates
)可以一次性执行多个插入、更新或删除操作。 - 资源清理 :在完成数据库操作后,必须正确关闭所有打开的资源(如
Connection
、Statement
和ResultSet
),以防止内存泄漏。
随着 Java 的发展,JDBC 也不断演进,增加了新的特性和改进了性能。例如,JDBC 4.0引入了自动加载驱动程序的功能,简化了开发过程。此外,JDBC 还支持分布式事务、XA资源管理和更多高级特性,以满足企业级应用的需求。
jdbc 的核心组成
- 接口规范:
- 为了项目代码的可移植性,可维护性,SUN公司从最初就制定了 java 程序连接各种数据库的统一接口规范,这样的话,不管是连接哪一种 DBMS 软件,java 代码都可以保持一致性。
- 接口存储在
java.sql
和javax.sql
包下。
- 实现规范:
- 因为各个数据库厂商的 DBMS 软件各有不同,那么各自的内部如何通过 sql 实现增删改查等操作管理数据,只有这个数据库厂商自己清楚,因此把接口规范的内部实现由各个数据库厂商自己实现。
- 厂商将实现内容和过程封装成 jar 文件包,我们程序员只需要将 jar 包引入到项目中集成即可,就可以开发调用实现过程操作数据库了。
jdbc 实现的常用接口和类
- DriverManager :
DriverManager
类管理一组JDBC驱动程序,并选择适当的驱动程序来建立到给定数据库URL的连接。它还处理加载和注册JDBC驱动程序的任务。
- Driver接口 :
- 每个JDBC驱动程序必须实现
java.sql.Driver
接口。该接口定义了用于与数据库通信的方法。当DriverManager
尝试建立连接时,它会使用这些方法。
- 每个JDBC驱动程序必须实现
- Connection接口 :
Connection
对象代表与特定数据库的连接。通过这个对象可以创建Statement
、PreparedStatement
或CallableStatement
对象来执行SQL命令,并且可以管理事务。
- Statement接口 :
Statement
接口用于执行静态的SQL语句并返回结果。它是执行SQL语句的基础,包括简单的查询和更新操作。
- PreparedStatement接口 :
PreparedStatement
是Statement
的子接口,用于执行预编译的SQL语句。它允许设置参数化查询,这有助于防止SQL注入攻击,并可能提高性能。
- CallableStatement接口 :
CallableStatement
也是Statement
的子接口,专门用于调用数据库中的存储过程。它可以处理输入和输出参数。
- ResultSet接口 :
ResultSet
对象封装了执行SQL查询后得到的结果表。它提供了遍历表格数据的方法以及获取每一列数据值的方法。
- SQLException类 :
SQLException
是一种受检异常,用于报告数据库访问错误。它包含了有关数据库错误的信息,如错误代码和消息文本。
- DataSource接口(自JDBC 2.0引入) :
DataSource
提供了一种更灵活的获取数据库连接的方式,特别是在容器环境中。它不仅支持标准的用户名/密码认证,还可以支持分布式事务和其他高级特性。
- RowSet接口(自JDBC 2.0扩展包引入) :
RowSet
是一个特殊的ResultSet
,它增加了滚动和更新能力,并且可以在断开连接的情况下工作。它分为连接型(例如JdbcRowSet
)和非连接型(例如CachedRowSet
)两种类型。
这些组件一起工作,使得Java应用程序能够以一种标准化的方式连接到不同的数据库系统,执行SQL查询和更新,并处理返回的数据。随着JDBC规范的发展,新的功能被添加进来以支持更多的特性和改进性能。
jdbc 快速开始
搭建 jdbc
- 准备数据库。
- 官网下载数据库连接驱动 jar 包。
- 创建 java 项目,在项目下创建 lib 文件夹,将下载的驱动 jar 包复制到文件夹里。
- 选中 lib 文件夹右键 -> Add as Library,与项目集成。
- 编写代码。
代码实现
数据库
sql
CREATE DATABASE `myjdbc`;
USE `myjdbc`;
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '学生编号',
`name` varchar(10) NOT NULL COMMENT '学生姓名',
`age` int NOT NULL COMMENT '学生年龄',
`score` double(10,5) NOT NULL COMMENT '学生成绩',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `student` (`id`, `name`, `age`, `score`) VALUES (1, '张三', 18, 59.50000);
INSERT INTO `student` (`id`, `name`, `age`, `score`) VALUES (2, '李四', 3, 70.00000);
INSERT INTO `student` (`id`, `name`, `age`, `score`) VALUES (3, '王五', 66, 30.00000);
INSERT INTO `student` (`id`, `name`, `age`, `score`) VALUES (4, '赵六', 100, 22.33333);
INSERT INTO `student` (`id`, `name`, `age`, `score`) VALUES (5, '田七', 28, 30.00000);
编写 java 代码
步骤:
- 注册驱动。
- 获取连接对象。
- 获取执行 sql 语句的对象。
- 编写 sql 语句并执行。
- 处理结果。
- 释放资源。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class Demo01 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.获取执行sql语句的对象
Statement statement = connection.createStatement();
// 4.编写sql语句并执行
String sql = "select * from student";
ResultSet result = statement.executeQuery(sql);
// 5.处理结果
while (result.next()) {
int id = result.getInt("id");
String name = result.getString("name");
int age = result.getInt("age");
double score = result.getDouble("score");
System.out.println(id + "\t" + name + "\t" + age + "\t" + score);
}
// 6.释放资源
result.close();
statement.close();
connection.close();
}
}
结果:
txt
1 张三 18 59.5
2 李四 3 70.0
3 王五 66 30.0
4 赵六 100 22.33333
5 田七 28 30.0
核心 API 理解
注册驱动
java
Class.forName("com.mysql.cj.jdbc.Driver");
学习过反射的可以看出,这段代码是在加载 com.mysql.cj.jdbc
包下的 Driver
,我们进入这个类,可以看到有这样一段代码:
java
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
这个类在加载时会执行 DriverManager.registerDriver(new Driver());
,这就是注册驱动
- 在Java 中,当使用 JDBC (Java Database Connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得 JDBC API 能够多识别并与特定的数据库进行交互。
- 从 JDK6 开始,不再需要显示地调用
Class.forName()
来加载 JDBC 驱动程序,只要在类路径中集成了对应的 jar 文件,会自动在初始化时注册驱动程序。
DriverManager
DriverManager
类管理一组 JDBC 驱动程序,并选择适当的驱动程序来建立到给定数据库 URL 的连接。它负责加载和注册 JDBC 驱动程序,以及创建 Connection
对象。
getConnection(String url, String user, String password)
:尝试根据提供的数据库URL、用户名和密码建立连接。registerDriver(Driver driver)
和deregisterDriver(Driver driver)
:显式地注册或注销驱动程序
Connection
Connection
接口是 JDBC API 的重要接口,用于建立与数据库的通信通道,换而言之,Connection
对象不为空,则代表一次数据库连接。- 在建立连接时,需要指定数据库 url,用户名,密码参数。格式:
java
# jdbc:mysql://IP地址:端口号/数据库名称?参数键值对1&参数键值对2&...
jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
- 负责管理事务,提供了
commit
和rollback
方法,用于提交事务和回滚事务。 - 可以创建
Statement
对象,用于执行 sql 语句并与数据库进行交互。 - 在使用 jdbc 技术时,必须要先获取
Connection
对象,在使用完毕后,要释放资源,避免资源占用浪费及泄露。
常用方法
createStatement()
:创建一个Statement
对象用于发送SQL语句。prepareStatement(String sql)
:创建一个PreparedStatement
对象,它可以包含IN参数。prepareCall(String sql)
:创建一个CallableStatement
对象用于调用存储过程。setAutoCommit(boolean autoCommit)
:设置是否自动提交更改。commit()
和rollback()
:手动提交或回滚事务。close()
:关闭连接并释放资源。
Statement
Statement
接口用于执行 sql 语句并与数据库进行交互,通过Statement
对象,可以向数据库发送 sql 语句并获取执行结果。- 结果可以是一个或多个结果。
- 增删改:受影响行数单个结果。
- 查询:单行单列、多行多列、单行多列等结果。
- 但是
Statement
接口在执行 sql 语句时,会产生 sql 注入问题:- 因为它是将查询条件与 sql 语句直接拼在一起,不会验证,这时候黑客可以钻漏洞在查询条件里加上 sql 语句,让 sql 的查询条件始终为 true,例如查询条件为
where user = 'root'
,黑客在查询条件输入xxx' or '1'='1
,这样最后拼接的 sql 为where user = 'xxx' or '1'='1'
,这样结果也为 true。
- 因为它是将查询条件与 sql 语句直接拼在一起,不会验证,这时候黑客可以钻漏洞在查询条件里加上 sql 语句,让 sql 的查询条件始终为 true,例如查询条件为
常用方法
executeQuery(String sql)
:执行查询语句并返回结果集。executeUpdate(String sql)
:执行插入、更新或删除语句,并返回受影响的行数。execute(String sql)
:执行任意SQL语句,对于复杂的操作非常有用。addBatch(String sql)
和executeBatch()
:用于批量执行多个SQL语句。
PreparedStatement
PreparedStatement
是Statement
接口的子接口,用于执行预编译的 sql 查询,作用如下:- 预编译 sql 语句:在创建
PreparedStatement
时,就会预编译 sql 语句,也就是 sql 语句已经固定。 - 防止 sql 注入:
PreparedStatement
支持参数化查询,将数据作为参数传递到 sql 语句中,采用?
占位符的方式,将传入的参数用一对单引号包裹起来,无论传递什么都只作为值,可以有效防止传入关键字或值导致 sql 注入问题。 - 性能提升:
PreparedStatement
是预编译 sql 语句,同一 sql 语句多次执行的情况下,可以复用,不比每次重新编译和解析。
- 预编译 sql 语句:在创建
- 更加安全,效率更高
常用方法
setString(int parameterIndex, String x)
等方法:为SQL语句中的参数占位符设置值。executeQuery()
和executeUpdate()
:与Statement
类似,但针对预编译的SQL语句。
代码示例
java
// 3.获取执行sql语句的对象
PreparedStatement statement = connection.prepareStatement("select * from student where name = ?");
// 4.编写sql语句并执行
statement.setString(1, "张三");
ResultSet result = statement.executeQuery();
ResultSet
ResultSet
用于表示从数据库中执行 sql 语句所返回的结果集,它提供了一种用于遍历和访问查询结果的方式。- 遍历结果:
ResultSet
可以使用next()
方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为boolean
,true
代表有下一行结果,false
则代表没有。 - 获取单列结果:可以通过
getXxx()
的方法获取单列数据库,Xxx
代表数据类型,支持索引和列名进行获取。
常用方法
next()
:将游标移动到下一行。getXxx(int columnIndex)
或getXxx(String columnName)
:获取当前行中指定列的数据值,其中XXX
代表数据类型。beforeFirst()
、afterLast()
、absolute(int row)
等方法:控制游标的移动位置。
基于 PreparedStatement 实现 crud
查询单行单列
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class Demo02 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS count FROM student");
// 4.执行sql语句
ResultSet result = statement.executeQuery();
// 5.获取结果(如果明确只有一个结果,也要进行一次next()方法判断)
if (result.next()) {
// 根据列名获取,上面sql设置了别名,所以这里取count
int count = result.getInt("count");
System.out.println("总数为:" + count);
}
// 6.释放资源
result.close();
statement.close();
connection.close();
}
}
查询单行多列
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class Demo03 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("SELECT id,`name`,age,score FROM student WHERE id = ?");
// 4.补充占位符,执行sql语句
statement.setInt(1, 5);
ResultSet result = statement.executeQuery();
// 5.获取结果
while (result.next()) {
int id = result.getInt("id");
String name = result.getString("name");
int age = result.getInt("age");
double score = result.getDouble("score");
System.out.println(id + "\t" + name + "\t" + age + "\t" + score);
}
// 6.释放资源
result.close();
statement.close();
connection.close();
}
}
查询多行多列
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class Demo04 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("SELECT id,`name`,age,score FROM student WHERE age > ?");
// 4.补充占位符,执行sql语句
statement.setInt(1, 10);
ResultSet result = statement.executeQuery();
// 5.获取结果
while (result.next()) {
int id = result.getInt("id");
String name = result.getString("name");
int age = result.getInt("age");
double score = result.getDouble("score");
System.out.println(id + "\t" + name + "\t" + age + "\t" + score);
}
// 6.释放资源
result.close();
statement.close();
connection.close();
}
}
新增数据
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class Demo05 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("INSERT INTO student(`name`,age,score) VALUES (?,?,?)");
// 4.补充占位符,执行sql语句
statement.setString(1, "钱八");
statement.setInt(2, 9);
statement.setDouble(3, 99.9);
int result = statement.executeUpdate();
// 5.结果result是受影响的行数
if (result > 0) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}
// 6.释放资源
statement.close();
connection.close();
}
}
修改数据
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class Demo06 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("UPDATE student SET score = ? WHERE id = ?");
// 4.补充占位符,执行sql语句
statement.setDouble(1, 66.66);
statement.setInt(2, 6);
int result = statement.executeUpdate();
// 5.结果result是受影响的行数
if (result > 0) {
System.out.println("修改成功");
} else {
System.out.println("修改失败");
}
// 6.释放资源
statement.close();
connection.close();
}
}
删除数据
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class Demo07 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("DELETE FROM student WHERE id = ?");
// 4.补充占位符,执行sql语句
statement.setInt(1, 6);
int result = statement.executeUpdate();
// 5.结果result是受影响的行数
if (result > 0) {
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}
// 6.释放资源
statement.close();
connection.close();
}
}
实体类和 orm 思想
- 在使用 jdbc 操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了 java 中变成了一个一个的变量,不利于维护和管理,由于 java 是面向对象的,所以一个表应该对应的是一个类,一行数据就对应的是 java 中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,这个载体就是实体类。
- orm(Object Relational Mapping,对象关系映射)思想,对象到关系数据库的映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性。
- jdbc 的这种过程我们称其为手动 orm,后续会升级为 orm 框架,例如 Mybatis 等。
实体类代码示例
java
package pojo;
public class Student {
private int id;
private String name;
private int age;
private double score;
public Student() {
}
public Student(int id, String name, int age, double score) {
this.id = id;
this.name = name;
this.age = age;
this.score = score;
}
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;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", score=" + score +
'}';
}
}
我们通常会把所有的实体类放在同一个包下。
查询单个数据
java
import pojo.Student;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class Demo08 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("SELECT id,`name`,age,score FROM student WHERE id = ?");
// 4.补充占位符,执行sql语句
statement.setInt(1, 1);
ResultSet result = statement.executeQuery();
// 创建一个student对象
Student student = new Student();
// 5.获取结果
while (result.next()) {
int id = result.getInt("id");
String name = result.getString("name");
int age = result.getInt("age");
double score = result.getDouble("score");
// 将数据映射到对象上
student.setId(id);
student.setName(name);
student.setAge(age);
student.setScore(score);
System.out.println(student);
}
// 6.释放资源
result.close();
statement.close();
connection.close();
}
}
查询多个数据,使用集合封装
java
import pojo.Student;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class Demo09 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象
PreparedStatement statement = connection.prepareStatement("SELECT id,`name`,age,score FROM student");
// 4.补充占位符,执行sql语句
ResultSet result = statement.executeQuery();
// 创建一个student集合
List<Student> studentList = new ArrayList<>();
// 5.获取结果
while (result.next()) {
Student student = new Student();
int id = result.getInt("id");
String name = result.getString("name");
int age = result.getInt("age");
double score = result.getDouble("score");
// 将数据映射到对象上
student.setId(id);
student.setName(name);
student.setAge(age);
student.setScore(score);
studentList.add(student);
}
studentList.forEach(System.out::println);
// 6.释放资源
result.close();
statement.close();
connection.close();
}
}
主键回显
在数据中,执行新增操作时,主键列为自动增长,可以在表中直观的看到,但是在 java 程序中,我们执行完新增后,只能得到受影响行数,无法得知当前新增数据的主键值。在 java 程序中获取数据库中插入新数据后的主键值,并赋值给 java 对象,此操作为主键回显。
代码示例
只要在预编译 sql 时添加一个参数 Statement.RETURN_GENERATED_KEYS
即可。
java
import pojo.Student;
import java.sql.*;
public class Demo10 {
public static void main(String[] args) throws Exception {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 3.预编译sql语句获取对象,告知数据库,返回新增数据主键的值
String sql = "INSERT INTO student(`name`,age,score) VALUES (?,?,?)";
PreparedStatement statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
// 4.补充占位符,执行sql语句
Student student = new Student(0, "钱八", 9, 99.9);
statement.setString(1, student.getName());
statement.setInt(2, student.getAge());
statement.setDouble(3, student.getScore());
int result = statement.executeUpdate();
// 5.结果result是受影响的行数
if (result > 0) {
System.out.println("添加成功");
// 获取新增数据的主键值,返回到student对象的id属性
// 返回的主键值是一个单行单列的结果集
ResultSet resultSet = statement.getGeneratedKeys();
while (resultSet.next()) {
int id = resultSet.getInt(1);
student.setId(id);
}
System.out.println(student);
resultSet.close();
} else {
System.out.println("添加失败");
}
// 6.释放资源
statement.close();
connection.close();
}
}
批量操作
如果想要一次性插入多条数据,常用的方法就是使用循环,但是循环本质是执行多次插入操作,循环每进行一次就会执行一次 insert
插入,也就要与数据库交互一次,非常消耗时间。
批量操作的本质是对 sql 语句的拼接,将要执行的多条 insert ... value ()
语句拼接成 insert ... values (),(),.....
一条 sql 语句,这样可以减少与数据库的交互次数,节省时间。
循环插入代码
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class Demo11 {
public static void main(String[] args) throws Exception {
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 预编译sql语句
String sql = "INSERT INTO student(`name`,age,score) VALUES (?,?,?)";
PreparedStatement statement = connection.prepareStatement(sql);
// 获取开始执行,执行循环插入10000条数据
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
statement.setString(1, "赵六"+i);
statement.setInt(2, 55);
statement.setDouble(3, 60);
statement.executeUpdate();
}
// 获取结束时间,查看耗费时间
long end = System.currentTimeMillis();
System.out.println("耗费时间:"+(end-start));
// 释放资源
statement.close();
connection.close();
}
}
批量插入代码
想要执行批量插入,需要注意以下几点:
- 必须在连接数据库的 url 的
?
后面追加rewriteBatchedStatements=true
,允许批量操作。 - 新增 sql 语句必须使用 values,且语句最后不要追加
;
结束。 - 调用
addBatch()
方法,将 sql 语句进行批量添加操作。 - 统一执行批量操作,调用
executeBatch()
方法。
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class Demo12 {
public static void main(String[] args) throws Exception {
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true";
String username = "root";
String password = "123456";
Connection connection = DriverManager.getConnection(url, username, password);
// 预编译sql语句
String sql = "INSERT INTO student(`name`,age,score) VALUES (?,?,?)";
PreparedStatement statement = connection.prepareStatement(sql);
// 获取开始执行,执行循环插入10000条数据
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
statement.setString(1, "赵六"+i);
statement.setInt(2, 55);
statement.setDouble(3, 60);
statement.addBatch();
}
statement.executeBatch();
// 获取结束时间,查看耗费时间
long end = System.currentTimeMillis();
System.out.println("耗费时间:"+ (end-start));
// 释放资源
statement.close();
connection.close();
}
}
可以对比,批量插入的时间有了显著降低。
连接池
现有问题
- 每次操作数据库都要获取新连接,使用完毕后就 close 释放,频繁的创建和销毁造成资源浪费。
- 连接的数量无法把控,对服务器来说压力巨大。
连接池介绍
连接池本质上就是数据库连接对象的缓冲区,通过配置,由连接池负责创建连接,管理连接,释放连接等操作。
预先创建数据库连接放入连接池,用户在请求时,通过池直接获取连接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率。
当池中无连接可用,且未达到上限时,连接池会新建连接。
池中连接达到上限,用户请求会等待,可以设置超时时间。
常见连接池
Druid
Druid 是阿里巴巴开源的一个综合数据库连接池解决方案,除了连接池功能外,还提供了 SQL 解析、监控等功能。
- 特点 :
- 高性能,经过大规模生产环境验证。
- 内置监控面板,可以实时查看连接池状态和性能指标。
- 支持多种数据库类型。
- 提供了 SQL 注入防护机制。
- 可以动态调整连接池参数。
- 适用场景:特别适合于中国开发者,因为它是国内广泛使用的连接池之一,并且有中文文档和支持。
Hikari
HikariCP 是一个高性能、轻量级的连接池库,因其出色的性能而广受欢迎。它的设计目标是成为最快的 Java 连接池之一。
- 特点 :
- 极高的性能。
- 简单配置,易于使用。
- 支持自动加载驱动程序。
- 提供详细的监控和统计信息。
- 兼容多种数据库(如 MySQL、PostgreSQL 等)。
- 适用场景:适用于追求极致性能的应用,特别是在高并发环境下。
Apache DBCP (Commons DBCP)
Apache DBCP 是由 Apache Commons 项目提供的一个成熟的连接池解决方案。
- 特点 :
- 功能丰富,支持广泛的配置选项。
- 集成了与 Apache Tomcat 的紧密协作。
- 提供了两种不同的实现:BasicDataSource 和 PoolingDriver。
- 包含了对 JMX 的支持,便于管理和监控。
- 适用场景:适合那些希望使用稳定、功能齐全的连接池的应用程序,尤其是在 Web 应用中。
C3P0
C3P0 是另一个流行的开源连接池库,以其灵活性和可配置性著称。
- 特点 :
- 强大的配置选项,包括自动测试连接、预加载连接等功能。
- 支持多数据源配置。
- 内置了对 Hibernate 的支持。
- 提供了良好的文档和社区支持。
- 适用场景:适用于需要高度定制化配置的应用程序,特别是当您需要复杂的连接管理策略时。
在目前的开发中,Druid 和 Hikari 是使用最多的两个连接池。
Druid 连接池使用
使用步骤
- 引入 jar 包。
- 创建
DruidDataSource
连接池对象 - 设置连接池的配置信息(包含必要信息和非必要信息)
- 通过连接池获取连接对象
- 回收连接(不是释放连接,而是将连接归还给连接池,给其它线程进行复用)
连接池配置信息
基本连接属性(必要信息)
url
:数据库的 JDBC URL。username
:用于连接数据库的用户名。password
:用于连接数据库的密码。driverClassName
:JDBC驱动程序类名(通常可以通过URL自动检测)。
以上4种属性是创建连接池时必须配置的属性,下面几种是非必要配置的属性,根据实际需求进行相关配置即可。
连接池大小配置
initialSize
:初始化时创建的连接数。minIdle
:最小空闲连接数。maxActive
:最大活跃连接数,即同时可用的最大连接数。maxWait
:当没有可用连接时,等待获取连接的最大时间(毫秒),默认值为-1表示无限期等待。
连续回收策略
timeBetweenEvictionRunsMillis
:检测连接是否空闲的时间间隔(毫秒),用于回收空闲连接,默认是60秒。minEvictableIdleTimeMillis
:连接在池中最小生存时间(毫秒),超过这个时间如果空闲则被回收,默认是1800秒(30分钟)。validationQuery
:用来验证连接是否有效的SQL查询语句,例如SELECT 'x'
或SELECT 1
。testWhileIdle
:建议设置为true
,表示在空闲时测试连接的有效性。testOnBorrow
和testOnReturn
:分别表示在从池中借用连接前和归还连接后是否进行有效性测试,默认都是false
。
PreparedStatement 缓存
poolPreparedStatements
:是否开启PSCache,默认为false
。对于支持的数据库,如Oracle,可以显著提高性能。maxPoolPreparedStatementPerConnectionSize
:每个连接上最大的PSCache数量。
监控与统计
filters
:指定要启用的过滤器,多个过滤器用逗号分隔。常见的过滤器包括:stat
:用于收集和展示连接池的状态信息。log4j
或common-log
:用于记录SQL日志。wall
:提供简单的SQL防火墙功能,防止某些类型的SQL注入攻击。
事务相关配置
defaultAutoCommit
:设置默认的自动提交模式,默认为null
,意味着使用数据库的默认设置。defaultTransactionIsolation
:设置默认的事务隔离级别,默认为null
,同样表示使用数据库的默认设置。
其它配置
removeAbandoned
:是否移除长时间未关闭的物理连接,默认为false
。removeAbandonedTimeout
:移除长时间未关闭的物理连接之前等待的超时时间(秒),默认为60秒。logAbandoned
:是否记录移除长时间未关闭的物理连接事件,默认为false
。
代码示例
硬编码实现(不推荐)
硬编码:将连接池的配置信息与 java 程序耦合在一起。
java
import com.alibaba.druid.pool.DruidDataSource;
import java.sql.Connection;
public class Demo12 {
public static void main(String[] args) throws Exception {
// 1.创建 DruidDataSource 连接池对象
DruidDataSource dataSource = new DruidDataSource();
// 2.设置连接池的配置信息(包含必要信息和非必要信息)
// 2.1 设置必要信息
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("123456");
// 2.2 设置非必要信息
dataSource.setInitialSize(10);
dataSource.setMaxActive(20);
// 3.通过连接池获取连接对象
Connection connection = dataSource.getConnection();
// 实现crud等操作
System.out.println(connection);
// 4.回收连接(不是释放连接,而是将连接归还给连接池,给其它线程进行复用)
connection.close();
}
}
软编码方式(推荐)
软编码:是指在项目目录下创建 resources
文件夹,标识该文件夹为资源目录,创建 db.properties
配置文件,将连接信息定义在该文件中。
步骤:
- 创建
Properties
集合,用于存储外部配置文件的 key 和 values 值。 - 读取外部配置文件,获取输入流,加载到
Properties
集合里。 - 基于
Properties
集合构建DruidDataSource
连接池。 - 通过连接池获取连接对象。
- 回收连接。
代码实现:
db.properties
代码:
properties
# 必要信息
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/myjdbc?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
username=root
password=root
# 非必要信息
initialSize=10
maxActive=20
main
代码:
java
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
public class Demo13 {
public static void main(String[] args) throws Exception {
// 1.创建Properties集合,用于存储外部配置文件的key和values值。
Properties properties = new Properties();
// 2.读取外部配置文件,获取输入流,加载到Properties集合里。
InputStream inputStream = Demo13.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(inputStream);
// 3.基于Properties集合构建DruidDataSource连接池。
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 4.通过连接池获取连接对象。
Connection connection = dataSource.getConnection();
// 实现crud等操作
System.out.println(connection);
// 5.回收连接。
connection.close();
}
}
Hikari 连接池使用
连接池配置信息
基本连接属性
jdbcUrl
:JDBC URL,指定要连接的数据库。username
:用于连接数据库的用户名。password
:用于连接数据库的密码。driverClassName
(可选):JDBC驱动程序类名,通常不需要设置,因为HikariCP会自动检测。
连接池大小配置
minimumIdle
:最小空闲连接数,默认为idleTimeout
和maximumPoolSize
中的较小者。maximumPoolSize
:最大活跃连接数,默认是10。idleTimeout
:空闲连接被关闭之前等待的时间(毫秒),默认是10分钟(600,000毫秒)。connectionTimeout
:获取连接的最大等待时间(毫秒),默认是30秒(30,000毫秒)。maxLifetime
:连接的最大生命周期(毫秒),超过这个时间将被关闭并替换,默认是30分钟(1800,000毫秒)。
连接测试
connectionTestQuery
:用于验证连接是否有效的SQL查询语句。对于大多数现代数据库驱动,此属性可以省略,因为HikariCP使用了更高效的"connection init SQL"方法。validationTimeout
:验证连接有效性的超时时间(毫秒),默认是5秒(5000毫秒)。
初始化与清理
initSQL
:在每个新连接创建时执行的SQL语句,可用于设置会话级别的参数或模式。poolName
:为连接池指定一个名称,便于监控和调试。
泄露检测
leakDetectionThreshold
:当连接从池中借出的时间超过给定的毫秒数时,记录警告日志。默认是0,表示禁用。
其他配置
autoCommit
:设置连接的自动提交模式,默认为true
。transactionIsolation
:设置事务隔离级别,默认为null
,即使用数据库的默认隔离级别。dataSourceProperties
:传递给数据源的其他属性,例如读取副本地址等。
代码实现(使用软编码方式)
步骤:
- 创建
Properties
集合,用于存储外部配置文件的 key 和 values 值。 - 读取外部配置文件,获取输入流,加载到
Properties
集合里。 - 创建
HikariConfig
连接池配置对象,将Properties
集合加载到HikariConfig
配置对象中。 - 基于
HikariConfig
连接池配置对象,构建HikariDataSource
连接池。 - 获取连接。
- 回收连接。
代码如下:
db.properties
:
properties
# 必要信息
driverClassName=com.mysql.cj.jdbc.Driver
jdbcUrl=jdbc:mysql://127.0.0.1:3306/myjdbc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username=root
password=123456
# 非必要信息
minimumIdle=10
maximumPoolSize=20
main
:
java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;
public class Demo14 {
public static void main(String[] args) throws Exception {
// 1.创建Properties集合,用于存储外部配置文件的key和values值。
Properties properties = new Properties();
// 2.读取外部配置文件,获取输入流,加载到Properties集合里。
InputStream inputStream = Demo14.class.getClassLoader().getResourceAsStream("db.properties");
properties.load(inputStream);
// 3.创建HikariConfig连接池配置对象,将Properties集合加载到HikariConfig配置对象中。
HikariConfig hikariConfig = new HikariConfig(properties);
// 4.基于HikariConfig连接池配置对象,构建HikariDataSource连接池。
HikariDataSource dataSource = new HikariDataSource(hikariConfig);
// 5.获取连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
// 6.回收连接。
connection.close();
}
}