JDBC详解
1、什么是JDBC,有什么作用,优缺点是什么?
** JDBC**(Java Database Connectivity)是一个Java API,它提供了一种标准的方法,允许Java程序连接到数据库并执行SQL语句。JDBC为不同类型的数据库提供了统一的访问方式,包括关系型数据库如MySQL、Oracle、SQL Server等。
作用:
- 数据库连接:JDBC允许Java应用程序连接到数据库。
- 执行SQL语句:通过JDBC,可以执行SQL查询、更新、删除和插入操作。
- 结果处理:JDBC提供了处理数据库查询结果的机制。
- 事务管理:JDBC支持事务管理,包括提交和回滚事务。
- 数据库无关性:JDBC提供了一种数据库无关的访问方式,这意味着使用JDBC编写的代码可以运行在任何支持JDBC的数据库上,而不需要修改代码。
优点:
- 平台无关性:JDBC使得Java程序可以在多种数据库平台上运行,而不需要关心数据库的具体实现。
- 简化数据库访问:提供了一种简单、统一的方法来访问数据库。
- 预编译SQL语句:JDBC支持预编译SQL语句,这可以提高性能并防止SQL注入攻击。
- 批量更新:JDBC允许批量更新数据库,这可以提高数据操作的效率。
- 可扩展性:JDBC API可以被扩展以支持新的数据库和SQL数据类型。
缺点:
- 性能问题:对于大量数据的处理,JDBC可能不如一些特定的数据库访问技术快。
- 复杂性:对于简单的数据库操作,JDBC可能显得过于复杂,需要编写更多的代码。
- 资源消耗:JDBC连接是重量级的,如果不正确管理,可能会导致资源泄露。
- 缺乏事务控制:虽然JDBC支持事务,但是它的事务控制不如一些集成了事务管理的ORM框架(如Hibernate)强大。
- 错误处理:JDBC的错误处理可能比较复杂,因为它使用了大量的异常类。
总的来说,JDBC是一个强大的工具,用于在Java应用程序中访问数据库,但它也有其局限性,特别是在处理复杂的事务和大量数据时。在实际应用中,开发者可能会结合使用JDBC和其他技术(如连接池、ORM框架等)来优化性能和简化开发。
2、JDBC架构设计
JDBC(Java Database Connectivity)的架构主要分为两种模型:两级架构(Two-tier Architecture)和三级架构(Three-tier Architecture)。
2.1、两级架构(Two-tier Architecture)
在两级架构中,Java应用程序直接与数据源通信。这种模型要求使用能够与特定数据源通信的JDBC驱动程序。用户的命令被传递到数据库或其他数据源,并且这些语句的结果被发送回用户。数据源可以位于通过网络连接的另一台机器上,这被称为客户端/服务器配置,用户机器作为客户端,而托管数据源的机器作为服务器。
2.2、三级架构(Three-tier Architecture)
在三级架构中,命令被发送到"中间层"服务,然后这些命令被发送到数据源。数据源处理命令并将结果发送回中间层,然后中间层再将结果发送给用户。中间层使得MIS(管理信息系统)主管能够控制对企业数据的访问以及可以对企业数据进行的更新类型。此外,它简化了应用程序的部署,并在许多情况下可以提供性能优势。
2.3、JDBC架构的组成部分
JDBC架构的主要组成部分包括:
- 应用程序(Application):Java小程序或Servlet,与数据源通信。
- JDBC API:允许Java程序执行SQL语句并从数据库检索结果。一些重要的接口包括Driver接口、ResultSet接口、RowSet接口、PreparedStatement接口和Connection接口,以及DriverManager类、Types类、Blob类和Clob类等。
- DriverManager:在JDBC架构中扮演重要角色,使用数据库特定的驱动程序有效地将企业应用程序连接到数据库。
- JDBC驱动程序(JDBC drivers):通过JDBC与数据源通信,需要一个能够与相应数据源智能通信的JDBC驱动程序。
3、事务
3.1 什么是事务
事务是一组数据库操作的集合,命令组要么都执行成功,要么都失败,保证数据库数据的完整性。
3.2 事务的四大特性(ACID)
原子性(Atomicity):原子性要求事务中的命令组要么都执行成功,要么都执行失败,确保数据库数据的完整性。如果命令组中的命令发生异常,即事务发生回滚操作,将数据撤回至事务开始前的状态。
一致性(Consistency):持久性要求事务的系统从一个持久性状态转换至另一个持久性状态,保证数据库的完整性约束。
隔离性(Isolation):隔离性要求事务之间是独立的,互不影响。即一个事务的操作不影响其他的事务执行。隔离性可配置的隔离级别来实现,包含读未提交,读已提交,可重复度,串行化等四个隔离级别,随着隔离级别的提升,事务之间的影响会减少,但数据库性能也会随之降低。
持久性(Durability):持久性要求事务一旦提交,对数据库的更改是永久的。就算应用系统异常/崩溃,数据也不会丢失。
4、JDBC核心组件介绍
4.1 JDBC API
这是一组接口和类,它们定义了Java程序如何与数据库进行交互。JDBC API允许Java程序执行SQL语句并检索结果。它包括java.sql
包中的接口和类,如Connection
、Statement
、PreparedStatement
、CallableStatement
和ResultSet
。
4.2 加载驱动(DriverManager)
DriverManager
类是JDBC的核心,负责管理JDBC驱动程序,并根据请求的数据库类型加载相应的驱动程序。它使用getConnection
方法来建立与数据库的连接。
4.3 JDBC Driver
JDBC驱动程序是Java程序与特定数据库之间的桥梁。它们将Java程序发出的JDBC调用转换为特定数据库能够理解的协议。有四种类型的JDBC驱动程序:Type 1(JDBC-ODBC桥接器)、Type 2(部分本地客户端)、Type 3(全本地客户端)和Type 4(本地协议驱动程序)。
4.4 连接(Connection)
Connection
接口代表与数据库的连接。通过DriverManager
获取Connection
对象后,可以进行数据库操作,如执行SQL语句。
4.3 语句处理器(Statement,PreparedStatement,CallableStatement)
4.3.1 Statement
Statement
接口用于执行静态SQL语句并返回它所产生的结果。它提供了executeQuery
、executeUpdate
和execute
方法来执行查询、更新和执行SQL语句。
4.3.2 PreparedStatement
PreparedStatement
接口是Statement
接口的子接口,它允许预编译SQL语句,提高执行效率,并防止SQL注入攻击。它使用参数化查询,可以提高性能并增强安全性。
4.3.3 CallableStatement
CallableStatement
接口用于调用数据库中的存储过程。它继承自PreparedStatement
接口,并提供了额外的方法来设置和检索存储过程的参数
4.4 结果(ResultSet)
ResultSet
接口代表查询数据库后返回的数据集。它提供了一种方式来遍历查询结果集中的数据。
4.5 SQL异常(SQLException)
SQLException
类是处理数据库访问中发生的异常的类。当数据库访问出现问题时,JDBC驱动程序会抛出SQLException
。
4.6 类型常量(Types)
Types
类是一个包含SQL类型常量的类,这些常量用于JDBC API与数据库交互时指定数据类型。
5、JDBC简单使用
5.1 常量配置
// 定义数据库驱动,数据库地址,用户名,密码, 生产环境这里可以通过配置的方式实现(properties,yaml)
String driver = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/jdbc_test";
String dbUserName = "root";
String password = "root";
5.2 加载驱动
// 加载驱动
try {
// driver = com.mysql.cj.jdbc.Driver
Class.forName(driver);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
5.3 获取连接
根据驱动管理器根据数据库地址,用户名,密码来获取连接。
// 使用驱动管理器来获取连接
Connection connection = null;
try {
//String url = "jdbc:mysql://127.0.0.1:3306/jdbc_test?characterEncoding=utf8&useSSL=false";
//String dbUserName = "root";
//String password = "root";
connection = DriverManager.getConnection(url, dbUserName, password);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
5.4 创建语句处理器对象
5.4.1 Statement对象(不建议使用,会造成SQL注入问题)
Statement statement = null;
ResultSet resultSet = null;
try {
// 创建Statement对象
statement = connection.createStatement();
// 执行Sql。注意:sql中是完整的SQL语句,不存在所谓的占位符
String sql = "select id, user_name, age, gender from d_user where user_name like '张%'";
resultSet = statement.executeQuery(sql);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
5.4.2 PreparStatement预编译对象(不存在SQL注入问题)
// 定义语句
String sql = "select id, user_name, age, gender from d_user where user_name like concat(?,'%') and age >= ?";
// 采用预编译的执行器
// 注意:采用预编译处理器,SQL的参数需要置换成 ? 来进行占位,这样能保证不出现SQL注入问题
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 设置语句参数。注意:预编译处理对象在设置占位值时,下标第一个是 1 开始的。
preparedStatement.setString(1, "张");
preparedStatement.setInt(2, 18);
// 执行并获取结果
ResultSet resultSet = preparedStatement.executeQuery();
5.5 ResultSet结果处理
// 遍历结果
while (resultSet.next()) {
// 获取结果并进行封装
int id = resultSet.getInt("id");
String userName = resultSet.getString("user_name");
Integer age = resultSet.getInt("age");
String gender = resultSet.getString("gender");
// 放入结果集合
resultList.add(User.build(id, userName, age, gender));
}
5.6 关闭资源
// 关闭资源
if (resultSet != null) {
resultSet.close();
}
if (preparedStatement != null) {
preparedStatement.close();
}
if (connection != null) {
connection.close();
}
5.7 完整实例
/**
* 开始书写JDBC简单的demo
* 备注:所有的异常都统一抛出去了,也可使用try()catch()来捕获
*
* @param args
*/
public static void main(String[] args) throws Exception {
// 定义数据库驱动,数据库地址,用户名,密码, 生产环境这里可以通过配置的方式实现
String driver = "com.mysql.cj.jdbc.Driver";
String url = "jdbc:mysql://127.0.0.1:3306/jdbc_test?characterEncoding=utf8&useSSL=false";
String dbUserName = "root";
String password = "root";
// 定义语句
String sql = "select id, user_name, age, gender from d_user where user_name like concat(?,'%') and age >= ?";
// 定义结果
List<User> resultList = new ArrayList<User>();
// 加载驱动
Class.forName(driver);
// 使用驱动管理器来获取连接
Connection connection = DriverManager.getConnection(url, dbUserName, password);
// 采用预编译的执行器
// 注意:采用预编译处理器,SQL的参数需要置换成 ? 来进行占位,这样能保证不出现SQL注入问题
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 设置语句参数。注意:预编译处理对象在设置占位值时,下标第一个是 1 开始的。
preparedStatement.setString(1, "张");
preparedStatement.setInt(2, 18);
// 执行并获取结果
ResultSet resultSet = preparedStatement.executeQuery();
// 遍历结果
while (resultSet.next()) {
// 获取结果并进行封装
int id = resultSet.getInt("id");
String userName = resultSet.getString("user_name");
Integer age = resultSet.getInt("age");
String gender = resultSet.getString("gender");
// 放入结果集合
resultList.add(User.build(id, userName, age, gender));
}
// 关闭资源
if (resultSet != null) {
resultSet.close();
}
if (preparedStatement != null) {
preparedStatement.close();
}
if (connection != null) {
connection.close();
}
}
结果展示:
mysql:
程序:
6、JDBC源码介绍
6.1 Connection
JDBC获取数据库连接是根据DriverManage管理器来获取的,但连接获取时需根据:地址+用户名+密码配合来获取。
// 使用驱动管理器来获取连接
Connection connection = null;
try {
connection = DriverManager.getConnection(url, dbUserName, password);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
DriverManager.getConnection:
public static Connection getConnection(String url, String user, String password) throws SQLException {
// 实例化Properties对象
java.util.Properties info = new java.util.Properties();
// 用户名 与 密码 不为空,放入properties中
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
// 真正获取连接的地方
return (getConnection(url, info, Reflection.getCallerClass()));
}
真正获取连接前的方法getConnection(url, info, classClass):
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
// 获取类加载器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
// 加锁,避免高并发出现资源抢占情况
// 这一步是获取类加载器,也是一种上一步类加载器重试补偿机制
synchronized(DriverManager.class) {
// 为空,获取当前线程的类加载器
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
// 数据库访问地址不允许为空
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
// SQL异常类
SQLException reason = null;
// 遍历所有驱动器,来获取连接
for(DriverInfo aDriver : registeredDrivers) {
// 检测驱动器是否允许通过。其实内部就是将驱动器再次加载,然后判断与当前加载器是否一致来判定的,后续代码在讲解
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
// 这里是真正开启连接的地方
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// 成功获取连接,返回给客户端
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
}
// 判定是否发生异常,发生异常直接返回null
if (reason != null) {
throw reason;
}
// 没有获取连接,同时也没有发生异常,那就主动抛出异常
throw new SQLException("No suitable driver found for "+ url, "08001");
}
检测驱动器isDriverAllowed()
/**
* 检测驱动器class与根据当前类加载器加载的驱动类的class,是否一致,保证是同一个驱动器
*
* @param driver 驱动器
* @param classLoader 类加载器
* @return 返回比对结果
*/
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
// 驱动器为空,则直接返回false
if (driver != null) {
Class<?> aClass = null;
try {
// 再次加载驱动器,并进行初始化
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
// 传入的驱动器class 与 再次加载的驱动器class 是否一致
result = (aClass == driver.getClass()) ? true : false;
}
return result;
}
最终获取Connection的方法
/**
* 获取数据库连接
*
* @param url 数据库访问地址
* @param info 用户名+密码的配置类
* @return 返回连接
* @throws SQLException 异常
*/
public Connection connect(String url, Properties info) throws SQLException {
try {
try {
// 检测地址
if (!ConnectionUrl.acceptsUrl(url)) {
return null;
} else {
// 获取连接URL,里面涉及到并发包的读写锁,以及缓存
// 当前地址如果在缓存中可以获取连接URL,直接返回。这一步是读锁
// 如果不存在,则需要释放读锁,加写锁,同时获取连接URL放入缓存中
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
// 根据连接类型来获取连接实例
switch (conStr.getType()) {
// 单一连接
case SINGLE_CONNECTION:
return ConnectionImpl.getInstance(conStr.getMainHost());
// 故障转移连接
case FAILOVER_CONNECTION:
case FAILOVER_DNS_SRV_CONNECTION:
// 代理的方式
return FailoverConnectionProxy.createProxyInstance(conStr);
// 负载均衡连接
case LOADBALANCE_CONNECTION:
case LOADBALANCE_DNS_SRV_CONNECTION:
return LoadBalancedConnectionProxy.createProxyInstance(conStr);
// 复制连接
case REPLICATION_CONNECTION:
case REPLICATION_DNS_SRV_CONNECTION:
return ReplicationConnectionProxy.createProxyInstance(conStr);
default:
return null;
}
}
} catch (UnsupportedConnectionStringException var5) {
return null;
} catch (CJException var6) {
throw (UnableToConnectException) ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("NonRegisteringDriver.17", new Object[]{var6.toString()}), var6);
}
} catch (CJException var7) {
throw SQLExceptionsMapping.translateException(var7);
}
}
7、总结
JDBC是实现应用程序访问数据库的一个API,其底层就是各个服务商需要提供的具体实现,让应用程序能够访问数据库的过程。在使用期间,建议采用预编译处理,防止SQL注入(这是一个非常严重的生产事故),如果想使用存储过程,则需要使用CallableStatement处理器,但是生产环境一般也是不建议使用的,本文章中也没有详细讲解,如果有使用的,可以参考官方文档。