JDBC总结分析
JDBC
规定了 java 应用应该如何连接和操作数据库,它是规范,而非实现,具体的实现由不同的数据库厂商提供。对我们来说,JDBC 有效地将我们的代码和具体的数据库实现解耦合,这是非常有好处的,例如,当我的数据库从 mysql 切换到 oracle 时,几乎不需要调整代码。
几个重要的类
JDBC 的 API 中,重点关注下面几个类
- DriverManager 驱动管理器,用于管理驱动以及获取Connection对象
- Connection 与指定数据库的连接/会话,用于获取Statement对象、管理事务、获取数据库元数据等。下面的几个类都是在Connection的基础上工作的
- Statement 静态sql执行对象
- PreparedStatement 预编译sql执行对象。相比Statement,它可以有效避免 sql 注入,同一 sql 多次执行时性能更好
- ResultSet 用于封装查询结果集。包扩存储查询结果,遍历结果集
Statement和PreparedStatement的区别
1.Statement:
- Statement 接口用于执行静态 SQL 语句,并且每次执行 SQL 语句都会进行编译和解释,因此效率相对较低。
- 可以执行任何 SQL 语句,但在执行前需要手动拼接 SQL 字符串,这可能会导致 SQL 注入的风险。
- 适用于执行一次性的、不需要参数化的 SQL 语句。
2.PreparedStatement:
- PreparedStatement 接口继承自 Statement 接口,它预编译 SQL 语句,可以多次执行,因此效率更高。
- 在创建 PreparedStatement 对象时,SQL 语句中的参数以问号 (?) 形式表示,然后可以通过设置参数的方法动态地传入参数值,从而避免了手动拼接 SQL 字符串,提高了安全性。
- 适用于需要多次执行、需要参数化的 SQL 语句。
总的来说,PreparedStatement 比 Statement 更高效、更安全,特别是在需要执行多次相似 SQL 语句或者涉及用户输入的情况下。因此,通常推荐使用 PreparedStatement 来执行 SQL 查询和更新操作。
PreparedStatement 的多次执行
是指在执行同一个预编译的 SQL 语句时,可以多次改变参数值并执行,而不需要重新编译 SQL 语句。这种机制可以提高性能,因为 SQL语句只需要编译一次,然后可以多次执行,而不必每次执行都重新编译。
在创建 PreparedStatement 对象时,SQL 语句中的参数通常以问号 (?) 表示。然后,可以使用 setXxx() 系列方法(如 setInt(), setString(), setDate() 等)为每个参数设置具体的值。这些参数可以是动态的,每次执行时可以设置不同的值。
举个例子:
codeString sql = "INSERT INTO users (name, age) VALUES (?, ?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 第一次执行
preparedStatement.setString(1, "John");
preparedStatement.setInt(2, 30);
preparedStatement.executeUpdate();
// 第二次执行
preparedStatement.setString(1, "Alice");
preparedStatement.setInt(2, 25);
preparedStatement.executeUpdate();
SQL编译
在 JDBC 中,SQL 语句的编译是在数据库服务器端进行的,而不是在 Java 运行时环境中。当创建 PreparedStatement 对象时,传入的 SQL 语句会被发送到数据库服务器,并在数据库服务器上进行编译。
数据库服务器会将 SQL 语句编译成可执行的查询计划(execution plan),这个计划会告诉数据库系统如何执行这个 SQL 语句以获得结果。编译过程包括语法分析、语义分析、查询优化等步骤。编译后的查询计划通常会被缓存,以便在后续执行相同 SQL 语句时可以重用,提高性能。
当多次执行同一个 PreparedStatement 对象时,实际上只是在向数据库发送已经编译好的 SQL 语句,并传入不同的参数值。数据库服务器不会重新编译这个 SQL 语句,而是直接使用之前编译好的查询计划来执行。因此,PreparedStatement 可以多次执行同一个 SQL 语句而不会导致额外的编译开销。
为什么不需要Class.forName("com.mysql.cj.jdbc.Driver")也能注册驱动?
JDK6 之后,DriverManager增加了以下静态代码块,在这段静态代码块中,会通过查询系统参数(jdbc.drivers)和SPI机制两种方式去加载数据库驱动。
static {
loadInitialDrivers();
}
//这个方法通过两个方式加载所有数据库驱动:
//1. 查询系统参数jdbc.drivers获得数据驱动类名,多个以“:”分隔
//2. SPI机制
private static void loadInitialDrivers() {
// 通过系统参数jdbc.drivers读取数据库驱动的全路径名
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 使用SPI机制加载驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 读取META-INF/services/java.sql.Driver文件的类全路径名
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 实例化对象java.sql.Driver实现类
// 这个过程会自动注册
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
if (drivers == null || drivers.equals("")) {
return;
}
// 加载jdbc.drivers参数配置的实现类
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
// 这个过程会自动注册
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
SPI机制本质上提供了一种服务发现机制,通过配置文件的方式,实现服务的自动装载,有利于解耦和面向接口编程。具体实现过程为:在项目的META-INF/services文件夹下放入以接口全路径名命名的文件,并在文件中加入实现类的全限定名,接着就可以通过ServiceLoder动态地加载实现类。
在mysql 的驱动包就可以看到一个java.sql.Driver文件,里面就是mysql驱动的全路径名。
JDBC流程:
// 获取连接
Connection connection = DriverManager.getConnection(url , username , password);
//预编译sql执行对象
PreparedStatement preparedStatement = connection.prepareStatement("select * from user where id = ? and user_id > ? ");
//设置参数
preparedStatement.setQueryTimeout(60);
preparedStatement.setInt(1 , 1);
preparedStatement.setInt(2 , 1);
// 执行 SQL 查询,获取返回结果
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
System.out.println("id:" + resultSet.getInt(1) + " userId:" + resultSet.getString(2) );
}