目录
1 JDBC API 简介
JDBC(Java Data Base Connectivity),java语言提供的访问关系型数据库的接口。在Java编写的应用中,使用JDBC API可以执行SQL语句、检索SQL执行结果以及将数据更改写回到底层数据源。JDBC API也可以用于分布式、异构的环境中与多个数据源交互。
JDBC API基于X/Open SQL CLI,是ODBC的基础。JDBC提供了一种自然的、易于使用的Java语言与数据库交互的接口。
使用JDBC操作数据源大致需要以下几个步骤:
- 与数据源建立连接;
- 执行SQL语句;
- 检索SQL执行结果;
- 关闭连接。
1.1 建立数据源连接
JDBC API定义了Connection
接口,用来表示与底层数据源的连接。JDBC应用程序可以使用以下方式获取Connection
对象:
(1)DriverManager
:在JDBC 1.0规范中就已经存在、完全由JDBC API实现的驱动管理类。当应用程序第一次尝试通过URL
连接数据源时,DriverManager
会自动加载CLASSPATH
下所有的JDBC驱动。DriverManager
类提供了一系列重载的getConnection()
方法,用来获取Connection
对象。
Connection connection = DriverManager.getConnection("url","user","password");
(2)DataSource
:在JDBC 2.0规范可选包中引入的API。它提供了更多底层数据源相关的细节,而且对应用来说,不需要关注JDBC驱动的实现。一个DataSource
对象的属性被设置后,它就代表一个特定的数据源。当DataSource
实例的getConnection()
方法被调用后,DataSource
实例就会返回一个与数据源建立连接的Connection
对象。但是,JDBC API中只提供了DataSource
接口,没有提供DataSource
的具体实现,DataSource
具体的实现由JDBC驱动程序提供。
// 创建DataSource实例,使用的阿里巴巴的Druid连接池
DataSource dataSource = new DruidDataSource();
// 获得连接
Connection connection = dataSource.getConnection();
此外,JDBC API中定义了两个DataSource
接口比较重要的扩展,用于支撑企业级应用。这两个接口分别为:
ConnectionPoolDataSource
:支持缓存和复用Connection
对象,能够提升应用性能和伸缩性;XADataSource
:返回的Connection
对象能够支持分布式事务。
1.2 执行SQL语句
Connection
是JDBC对数据源连接的抽象,一旦建立了连接,使用JDBC API的应用程序就可以对目标数据源执行查询和更新操作。
JDBC API提供了访问SQL:2003规范中常用的实现特性,由于不同的JDBC厂商对这些特性的支持程度各不相同,JDBC API中提供了一个DatabaseMetadata
接口,应用程序可以使用DatabaseMetadata
的实例来确定目前使用的数据源是否支持某一特性。
获取到JDBC中的Connection
对象之后,可以通过Connection
对象设置事务属性,并且可以通过Connection
接口中提供的方法创建Statement
、PreparedStatement
或者CallableStatement
对象,然后通过执行这些对象提供的方法,完成对数据库的操作。
1.3 检索SQL执行结果
SQL语句执行完毕后,需要获取执行的结果。JDBC API中提供了ResultSet
接口,该接口的实现类封装SQL查询的结果,我们可以对ResultSet
对象进行遍历,然后通过ResultSet
提供的一系列getXXX()
方法获取查询结果集。
1.4 关闭连接
数据库连接是一种非常紧俏的资源,在使用完毕之后,一定要关闭,免得资源浪费,影响应用程序性能。
1.5 使用JDBC操作数据库
下面使用一个具体的例子来演示使用 jdbc 操作数据库。
@Test
public void operateDataBaseByJDBC() throws SQLException {
// 1.获得连接(druidDataSource会自己注册到DriverManager)
DataSource druidDataSource = new DruidDataSource();
((DruidDataSource) druidDataSource).setUsername("root");
((DruidDataSource) druidDataSource).setUrl("jdbc:mysql://localhost:3306/mlxg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC");
((DruidDataSource) druidDataSource).setPassword("123456");
Connection connection = druidDataSource.getConnection();
// 2.创建SQL
PreparedStatement preparedStatement = connection.prepareStatement("select * from sys_user where id = 1");
// 3.执行SQL
ResultSet resultSet = preparedStatement.executeQuery();
// 4.处理结果
while (resultSet.next()){
// ResultSet对象的getMetaData()方法获取结果集元数据信息
System.out.println("结果集元数据信息:" + resultSet.getMetaData().toString());
System.out.println("用户名:" + resultSet.getString(2));
}
// 5.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
上面代码执行结果:
D:\development\jdk-8u171\bin\java.exe...
十二月 16, 2020 10:20:50 下午 com.alibaba.druid.pool.DruidDataSource info
信息: {dataSource-1} inited
结果集元数据信息:com.mysql.cj.jdbc.result.ResultSetMetaData@100fc185 - Field level information:
com.mysql.cj.result.Field@643b1d11[dbName=mlxg,tableName=sys_user,originalTableName=sys_user,columnName=id,originalColumnName=id,mysqlType=8(FIELD_TYPE_BIGINT),sqlType=-5,flags= AUTO_INCREMENT PRIMARY_KEY, charsetIndex=63, charsetName=ISO-8859-1]
com.mysql.cj.result.Field@2ef5e5e3[dbName=mlxg,tableName=sys_user,originalTableName=sys_user,columnName=username,originalColumnName=username,mysqlType=253(FIELD_TYPE_VARCHAR),sqlType=12,flags=, charsetIndex=33, charsetName=UTF-8]
com.mysql.cj.result.Field@36d4b5c[dbName=mlxg,tableName=sys_user,originalTableName=sys_user,columnName=email,originalColumnName=email,mysqlType=253(FIELD_TYPE_VARCHAR),sqlType=12,flags=, charsetIndex=33, charsetName=UTF-8]
com.mysql.cj.result.Field@6d00a15d[dbName=mlxg,tableName=sys_user,originalTableName=sys_user,columnName=password,originalColumnName=password,mysqlType=253(FIELD_TYPE_VARCHAR),sqlType=12,flags=, charsetIndex=33, charsetName=UTF-8]
com.mysql.cj.result.Field@51efea79[dbName=mlxg,tableName=sys_user,originalTableName=sys_user,columnName=phoneNum,originalColumnName=phoneNum,mysqlType=253(FIELD_TYPE_VARCHAR),sqlType=12,flags=, charsetIndex=33, charsetName=UTF-8]
用户名:zhangsan
Process finished with exit code 0
2 JDBC API中的类与接口
JDBC API由java.sql
和javax.sql
两个包构成。
2.1 java.sql包
java.sql
包中包含着JDBC最核心的API,下面是java.sql
包中所有接口、枚举和类:
如表所示,java.sql
包中的内容大致可以分为数据类型接口、枚举类、驱动相关类和接口、异常类。
作为Java开发人员需要掌握的API,主要包括下面几个接口:
java.sql.Wrapper |
java.sql.Connection |
java.sql.DatabaseMetaData |
java.sql.Statement |
java.sql.PreparedStatement |
java.sql.ParameterMetaData |
java.sql.ResultSet |
java.sql.ResultSetMetaData |
以上接口都继承了java.sql.Wrapper
接口。许多JDBC驱动程序提供超越传统JDBC的扩展,为了符合JDBC API规范,驱动厂商可能会在原始类型的基础上进行包装,Wrapper
接口为使用JDBC的应用程序提供访问原始类型的功能,从而使用JDBC驱动中一些非标准的特性。
java.sql.Wrapper
接口提供了两个方法,具体如下:
<T> T unwrap(java.lang.Class<T> iface) throws java.sql.SQLException;
boolean isWrapperFor(java.lang.Class<?> iface) throws java.sql.SQLException;
其中,unwrap()
方法用于返回未经过包装的JDBC驱动原始类型实例,可以通过该实例调用JDBC驱动中提供的非标准的方法。
isWrapperFor()
方法用于判断当前实例是否是JDBC驱动中某一类型的包装类型。
下面是unwrap()
方法和isWrapperFor()
方法的一个使用案例:
// 创建标准的Statement对象
Statement statement = connection.createStatement();
// 利用反射获得驱动的类
Class clazz = Class.forName("com.mysql.jdbc.Driver");
if(statement.isWrapperFor(clazz)){// 如果当前实例statement是jdbc驱动的包装类型
// 返回未经过包装的JDBC驱动原始类型的实例
MySqlStatement mySqlStatement = (MySqlStatement) statement.unwrap(clazz);
// 调用JDBC驱动原始类型实例自己的方法
mySqlStatement.hasBeforeComment();
}
java.sql
包核心类之间的关系如下图:
2.2 javax.sql包
javax.sql
包中的类和接口最早是由JDBC 2.0版本的可选包提供的,主要包括下面几个类和接口:
JDBC 1.0中使用DriverManager
类来产生一个与数据源连接的Connection
对象。
JDBC 2.0提供的DataSource
接口是一个更好的连接数据源的方式:
(1)应用程序不需要像使用DriverManager
一样对加载的数据库驱动程序信息进行硬编码;
(2)使用DataSource
接口的优势还体现在连接池和分布式事务上。连接池通过对连接的复用,适用于任务繁忙、负担繁重的企业级应用。
javax.sql.DataSource
与java.sql.Connection
之间的关系如图所示:
javax.sql
包下还提供了一个PooledConnection
接口。PooledConnection
和Connection
的不同之处在于,它提供了连接池管理的句柄。一个PooledConnection
表示与数据源建立的物理连接,该连接在应用程序使用完后可以回收而不用关闭它,从而减少了与数据源建立连接的次数。
应用程序开发人员一般不直接使用PooledConnection
接口,而是通过一个管理连接池的中间层基础设施使用。当应用程序调用DataSource
对象的getConnection()
方法时,它返回一个Connection
对象。但是当我们使用数据库连接池时(例如Druid),该Connection
对象实际上是到PooledConnection
对象的句柄,这是一个物理连接。连接池管理器维护所有的PooledConnection
对象资源。如果池中存在可用的PooledConnection
对象,则连接池管理器返回作为到该物理连接的句柄的Connection
对象。如果不存在可用的PooledConnection
对象,则连接池管理器调用ConnectionPoolDataSource
对象的getConnection()
方法创建新的物理连接。
连接池实现模块可以调用PooledConnection
对象的addConnectionEventListener()
将自己注册成为一个PooledConnection
对象的监听者,当数据库连接需要重用或关闭的时候会产生一个ConnectionEvent
对象,它表示一个连接事件,连接池实现模块将会得到通知。
javax.sql.PooledConnection
与java.sql.Connection
之间的关系如图:
javax.sql
包还提供了一个RowSet
接口,该接口继承自java.sql
包下的ResultSet
接口。RowSet
用于为数据源和应用程序在内存中建立一个映射,通俗来讲,RowSet
就相当于数据库表数据在应用程序内存中的映射。RowSet
默认是一个可滚动、可更新、可序列化的结果集,而且它作为一个JavaBean组件,可以方便地在网络间传输,用于两端的数据同步。
RowSet
对象根据是否关闭数据源连接分为:
- 连接的
RowSet
:RowSet
对象可以建立一个与数据源的连接并在其整个生命周期中维持该连接; - 非连接的
RowSet
:RowSet
对象还可以建立一个与数据源的连接,从其获取数据,然后关闭它。非连接Rowset
可以在断开连接时更改其数据,然后将这些更改写回底层数据源,不过它必须重新建立连接才能完成此操作。相较于java.sql.ResultSet
而言,RowSet
的离线操作能够有效地利用计算机越来越充足的内存减轻数据库服务器的负担。
javax.sql.RowSet
与java.sql.ResultSet
之间的关系如图:
3 Connection详解
一个Connection
对象表示通过JDBC驱动与数据源建立的连接,这里的数据源可以是关系型数据库管理系统(DBMS)、文件系统或者其他通过JDBC驱动访问的数据。使用JDBC API的应用程序可能需要维护多个Connection
对象,一个Connection
对象可能访问多个数据源,也可能访问单个数据源。
从JDBC驱动的角度来看,Connection
对象表示客户端会话,可以通过两种方式获取JDBC中的Connection
对象:
- 通过JDBC API中提供的
DriverManager
类获取; - 通过
DataSource
接口的实现类获取。目前使用比较广泛的数据库连接池C3P0
、DBCP
、Druid
等都是javax.sql.DataSource
接口的具体实现。
3.1 JDBC驱动类型
JDBC驱动程序有很多可能的实现,这些驱动实现类型主要包括以下几种:
-
JDBC-ODBC Bridge Driver
:SUN发布的JDBC-ODBC的桥接驱动,利用现成的ODBC架构将JDBC调用转换为ODBC调用,避免了JDBC无驱动可用的窘境。但是,由于桥接的限制,并非所有功能都能直接转换并正常调用,而多层调用转换对性能也有一定的影响。
-
Native API Driver
:直接调用数据库提供的原生链接库或客户端,因为没有中间过程,访问速度通常表现良好。但是驱动程序与数据库和平台绑定无法达到JDBC跨平台的基本目的。
-
JDBC-Net Driver
:将JDBC调用转换为独立于数据库的协议,然后通过特定的中间组件或服务器转换为数据库通信协议,主要目的是获得更好的架构灵活性,微软的ADO.NET
是这种架构的典型。
-
Native Protocol Driver
:最常见的驱动程序类型,开发中使用的驱动包基本都属于此类,通常由数据库厂商直接提供,例如mysql-connector-java
,驱动程序把JDBC调用转换为数据库特定的网络通信协议。使用网络通信,驱动程序可以纯Java实现,支持跨平台部署,性能也较好。
3.2 java.sql.Driver接口
所有的JDBC驱动都必须实现Driver
接口,而且驱动实现类需要向DriverManager
注册自己的一个实例,例如:
public Class MyJdbcDriver implements Driver{
static{
// 静态代码块中向DriverManager注册驱动实现类实例,类一加载就会调用
DriverManager.registerDriver(new MyJdbcDriver());
}
}
DriverManager
类与注册的驱动程序进行交互时会调用Driver
接口中提供的方法。Driver
接口中提供了一个acceptsURL()
方法,DriverManager
类可以通过Driver
实现类的acceptsURL()
来判断一个给定的URL
是否能与数据库成功建立连接。当使用DriverManager
与数据库成功建立连接时,会调用Driver
接口中提供的connect()
方法,返回一个Connection
对象。
3.3 java SPI机制
在JDBC 4.0版本之前,使用DriverManager
获取Connection
对象之前都需要通过代码显式地加载驱动实现类,例如:
// 显式加载驱动
Class.forName("com.mysql.jdbc.Driver");
JDBC 4.0之后的版本不再需要显式地加载驱动实现类。这得益于Java中的SPI
机制。
SPI
(Service Provider Interface)是JDK内置的一种服务提供发现机制。SPI
是一种动态替换发现的机制。比如有一个接口,想在运行时动态地给它添加实现,只需要添加一个实现,SPI
机制在程序运行时就会发现该实现类,整体流程如图:
当服务的提供者提供了一种接口的实现之后,需要在classpath
下的META-INF/services
目录中创建一个以服务接口命名的文件,文件内容就是这个接口具体的实现类。当其他的程序需要这个服务的时候,就可以查找这个JAR
包中META-INF/services
目录的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名加载服务实现类,然后就可以使用该服务了。
java中用于查找服务实现的工具类是java.util.ServiceLoader
。ServiceLoader
类提供了一个静态的load()
方法,用于加载指定接口的所有实现类。调用该方法后,classpath
下META-INF/services
目录的文件中指定的所有实现类都会被加载。例如:
// 1.定义一个接口
package com.jidi.test.jdbc.service;
public interface SysUserService {
void sayHello();
}
// 2.定义接口实现类
package com.jidi.test.jdbc.service.impl;
import com.jidi.test.jdbc.service.SysUserService;
public class SysUserServieImpl implements SysUserService {
@Override
public void sayHello() {
System.out.println("你好!");
}
}
// 3.在META-INF/service目录下面创建文件com.jidi.test.jdbc.service.SysUserService,该文件内容为接口实现类的全限定名:com.jidi.test.jdbc.service.impl.SysUserServieImpl
// 4.测试
@Test
public void testSpi(){
// 使用ServiceLoader加载接口实现类
ServiceLoader<SysUserService> sysUserServices = ServiceLoader.load(SysUserService.class);
// 调用接口实现类方法
sysUserServices.forEach((sysUserService) -> sysUserService.sayHello());
}
符合JDBC 4.0以上版本的JDBC驱动都会在META-INF/services
目录的java.sql.Driver
文件中指定驱动实现类的完全限定名。DriverManager
类中定义了静态初始化代码块,代码如下:
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
如上所示,DriverManager
类的静态代码块会在调用DriverManager
的getConnection()
方法之前调用。
静态代码块中调用loadInitialDrivers()
方法加载驱动实现类,该方法的关键代码如下:
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
如上所示,在loadInitialDrivers()
方法中,通过JDK内置的ServiceLoader
机制加载java.sql.Driver
接口的实现类,然后对所有实现类进行遍历,这样就完成了驱动类的加载。驱动实现类会将驱动实现类的实例注册到DriverManager
中,这样就取代了通过调用Class.forName()
方法加载驱动的过程。
3.4 java.sql.DriverAction接口
Driver
实现类在被加载时会调用DriverManager
类的registerDriver()
方法注册驱动。也可以在应用程序中显式地调用DriverManager
类的deregisterDriver()
方法来解除注册。JDBC驱动可以通过实现DriverAction
接口来监听DriverManager
类的deregisterDriver()
方法的调用。
3.5 java.sql.DriverManager类
DriverManager
类通过Driver
接口为JDBC客户端管理一组可用的驱动实现,当客户端通过DriverManager
类和数据库建立连接时,DriverManager
类会根据getConnection()
方法参数中的URL
找到对应的驱动实现类,然后使用具体的驱动实现连接到对应的数据库。
DriverManager
类提供了两个关键的静态方法:
registerDriver()
:将驱动的实现类注册到DriverManager
类中,这个方法会在驱动加载时隐式地调用,而且通常在每个驱动实现类的静态初始化代码块中调用;getConnection()
:提供给JDBC客户端调用的,可以接收一个URL
作为参数,DriverManager
类会对所有注册驱动进行遍历,调用Driver
实现的connect()
方法找到能够识别URL
的驱动实现后,会与数据库建立连接,然后返回Connection
对象。
3.6 关闭Connection对象
当使用完Connection
对象后,需要显式地关闭该对象。
Connection
接口中提供了以下方法:
java.sql.Connection.close()
:当应用程序使用完Connection
对象后,应该显式地调用close()
方法。调用该方法后,由该Connection
对象创建的所有Statement
对象都会被关闭。java.sql.Connection.isClosed()
:用于判断应用中是否调用了close()
方法关闭该Connection
对象。java.sql.Connection.isValid()
:用于判断连接是否有效,如果连接依然有效,则返回true
,否则返回false
。
4 Statement详解
Statement
接口定义了执行SQL语句的方法,但是这些方法不支持参数输入。PreparedStatement
接口是Statement
接口的子接口,增加了设置SQL参数的方法。
CallableStatement
接口继承自PreparedStatement
接口,又增加了调用存储过程和检索存储过程调用结果的方法。
4.1 java.sql.Statement接口
Statement
是JDBC API操作数据库的核心接口,具体的实现由JDBC驱动来完成。
Statement
对象的创建,需要调用Connection
对象的createStatement()
方法,例如:
Statement statement = connection.createStatement();
Connection
接口中还提供了几个重载的createStatement()
方法,用于通过Statement
对象指定ResultSet
的属性,例如:
// 创建一个statement对象,指定该对象创建的ResultSet对象是不可滚动且只读,修改提交时ResultSet会关闭
Statement statement = connection.createStatement(
ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY,
ResultSet.CLOSE_CURSORS_AT_COMMIT);
Statement
的主要作用是与数据库进行交互,该接口中定义了一些数据库操作以及检索SQL执行结果相关的方法,具体调用哪个方法取决于SQL语句的类型。
如果使用Statement
执行一条查询语句,并返回一个结果集,则可以调用executeQuery()
方法;如果SQL语句是一个返回更新数量的DML语句,则需要调用executeUpdate()
方法,该方法有几个重载的方法:
-
int executeUpdate(String sql)
:执行一个UPDATE、INSERT或者DELETE语句,返回更新数量; -
int executeUpdate(String sql, int autoGeneratedKeys)
:执行一个UPDATE、INSERT或者DELETE语句。当SQL语句是INSERT语句时,autoGeneratedKeys
参数用于指定自动生成的键是否能够被检索,取值为Statement.RETURN_GENERATED_KEYS
(INSERT语句自动生成的键能够被检索)或Statement.NO_GENERATED_KEYS
(INSERT语句自动生成的键不能够被检索); -
int executeUpdate(String sql, int[] columnIndexes)
:执行一个UPDATE、INSERT或者DELETE语句,通过columnIndexes
参数告诉驱动程序哪些列中自动生成的键可以用于检索。如果SQL语句不是INSERT语句,columnIndexes
参数将会被忽略; -
int executeUpdate(String sql, String[]columnNames)
:columnNames
参数是一个数组,通过字段名的方式指定哪些字段中自动生成的键能够被检索。如果SQL语句不是INSERT语句,columnNames
参数就会被忽略。
在执行数据库操作之前,若不确定SQL语句的类型,则可以调用excute()
方法。该方法也有几个重载的方法:
boolean execute(String sql)
:执行一个SQL语句,通过返回值判断SQL类型,当返回值为true
时,说明SQL语句为SELECT
语句,可以通过Statement
接口中的getResultSet()
方法获取查询结果集;否则为UPDATE、INSERT或者DELETE语句,可以通过Statement
接口中的getUpdateCount()
方法获取影响的行数;boolean execute(String sql, int autoGeneratedKeys)
:通过autoGeneratedKeys
参数(只对INSERT语句有效)指定INSERT语句自动生成的键是否能够被检索;boolean execute(String sql, int[]columnIndexes)
:通过columnIndexes
参数告诉驱动程序哪些列中自动生成的键可以用于检索。如果SQL语句不是INSERT语句,则columnIndexes
参数将会被忽略;boolean execute(String sql, String[]columnNames)
:columnNames
参数是一个数组,通过字段名的方式指定哪些字段中自动生成的键能够被检索。如果SQL语句不是INSERT语句,则columnNames
参数会被忽略。
Statement
接口中还提供了用于批量执行SQL语句的方法,分别为:
void addBatch(String sql)
:把一条SQL语句添加到批量执行的SQL列表中;void clearBatch()
:清空批量执行的SQL列表:int[]executeBatch()
:批量地执行SQL列表中的语句。
4.2 java.sql.PreparedStatement接口
PreparedStatement
接口继承自Statement
接口,在Statement
接口的基础上增加了参数占位符功能。PreparedStatement
的实例表示可以被预编译的SQL语句,执行一次后,后续多次执行时效率会比较高。
使用PreparedStatement
实例执行SQL语句时,可以使用?
作为参数占位符,然后使用PreparedStatement
接口中提供的方法为占位符设置参数值。
PreparedStatement
对象的创建与Statement
类似,只需要调用Connection
对象的prepareStatement()
方法,但是prepareStatement()
方法需要提供一个SQL语句作为参数,例如:
PreparedStatement preparedStatement = connection.prepareStatement(
"select * from sys_user where id = ?");
prepareStatement()
也可以通过重载的方法指定ResultSet
的特性,例如:
PreparedStatement preparedStatement = connection.prepareStatement(
"select * from sys_user where id = ?",// sql语句
ResultSet.TYPE_SCROLL_SENSITIVE, // 可滑动
ResultSet.CONCUR_UPDATABLE,// 可读写
ResultSet.HOLD_CURSORS_OVER_COMMIT);// 修改提交时不会关闭
PreparedStatement
接口中定义了一系列的Setter
方法,用于为SQL语句中的占位符赋值,这些Setter
方法名称遵循set<Type>
格式,其中Type
为数据类型。这些Setter
方法一般都有两个参数,第一个参数为int
类型,表示参数占位符的位置(从1开始);第二个参数为占位符指定的值。例如:
// 创建PreparedStatement对象
PreparedStatement preparedStatement = connection.prepareStatement(
"select * from sys_user where id = ? and userName = ?");
// 通过setter方法给sql语句中的占位符?设置值
preparedStatement.setInt(1, 1);
preparedStatement.setString(2, "jidi");
在使用PreparedStatement
对象执行SQL语句之前必须为每个参数占位符设置对应的值,否则调用executeQuery()
、executeUpdate()
或execute()
等方法时会抛出SQLException
异常。
PreparedStatement
对象设置的参数在执行后不能被重置,需要显式地调用clearParameters()
方法清除先前设置的值,再为参数重新设置值即可。
JDBC API中提供了一个ParameterMetaData
接口,用于描述PreparedStatement
对象的参数信息。PreparedStatement
接口中提供了一个getParameterMetaData()
方法,用于获取ParameterMetaData
实例。例如:
// 1.获得连接(druidDataSource会自己注册到DriverManager)
DataSource druidDataSource = new DruidDataSource();
((DruidDataSource) druidDataSource).setUsername("root");
((DruidDataSource) druidDataSource).setUrl("jdbc:mysql://localhost:3306/mlxg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&generateSimpleParameterMetadata=true");
((DruidDataSource) druidDataSource).setPassword("123456");
Connection connection = druidDataSource.getConnection();
// 2.创建SQL
PreparedStatement preparedStatement = connection.prepareStatement("select * from sys_user where username = ?");
// 3.通过setter方法给sql语句中的占位符?设置值
preparedStatement.setString(1,"lisi");
// 4.获取ParameterMetaData对象
ParameterMetaData parameterMetaData = preparedStatement.getParameterMetaData();
// 5.根据ParameterMetaData对象获取PreparedStatement对象参数信息
for (int i = 1; i <= parameterMetaData.getParameterCount(); i++){
String parameterClassName = parameterMetaData.getParameterClassName(i);
String parameterTypeName = parameterMetaData.getParameterTypeName(i);
System.out.println("第" + i + "个参数," + " ClassName:" + parameterClassName + " TypeName:"+ parameterTypeName);
}
// 6.释放资源
preparedStatement.close();
connection.close();
注意:在使用getParameterMetaData()
获取ParameterMetaData
对象的时候报错:SQLException:Parameter metadata not available for the given statement
,是由于驱动设置默认不支持使用这个对象,只需要在数据库连接URL加上一个参数generateSimpleParameterMetadata=true
即可解决。
4.3 java.sql.CallableStatement接口
CallableStatement
接口继承自PreparedStatement
接口,在PreparedStatement
的基础上增加了调用存储过程并检索调用结果的功能。
CallableStatement
对象是通过调用Connection
对象的prepareCall()
方法创建的,例如:
CallableStatement callableStatement = connection.prepareCall("{? = call validate(?, ?)}");
CallableStatement
对象可以使用3种类型的参数:IN
、OUT
和INOUT
。可以将参数指定为序数参数或命名参数,必须为IN
或INOUT
参数的每个参数占位符设置一个值,必须为OUT
或INOUT
参数中的每个参数占位符调用registerOutParameter()
方法。
存储过程参数的数量、类型和属性可以使用DatabaseMetaData
接口提供的getProcedureColumns()
方法获取。
命名参数可以用来指定特定的参数,这在存储过程有多个给定默认值的参数时特别有用,命名参数可以用于为那些没有默认值的参数设置值,参数名称可以通过DatabaseMetaData
对象的getProcedureColumns()
方法返回的COLUMN_NAME
字段获取。例如:
CallableStatement callableStatement = connection.prepareCall("{CALL PROC(?, ?)}");
// 通过命名参数给存储过程的参数设置
callableStatement.setString("PARAM_1", "Prie");
callableStatement.setFloat("PARAM_5", 123.2f);
在上面例子中,CALL PROC
存储过程可以接收10个参数,但是只有第1个和第5个参数(PARAM_1
和PARAM_5
)需要设置值。
对于IN参数的设置,调用CallableStatement
接口中提供的setXXX()
方法即可;但是对于OUT
和INOUT
参数,在CallableStatement
执行之前,必须为每个参数调用CallableStatement
接口中提供的registerOutParameter()
方法,例如:
// 创建CallableStatement 对象
CallableStatement callableStatement = connection.prepareCall("{CALL GET_NAME_OR_NUMBER(?, ?)}");
// 调用registerOutParameter方法
callableStatement.registerOutParameter(1, Types.VARCHAR);
callableStatement.registerOutParameter(2, Types.FLOAT);
// 执行SQL
callableStatement.execute();
// 获取OUT参数值
String name = callableStatement.getString(1);
float number = callableStatement.getFloat(2);
5 ReusltSet详解
ResultSet
接口提供了检索和操作SQL执行结果相关的方法。
5.1 ResultSet类型
ResultSet
对象的类型主要体现在两个方面:
- 游标可操作的方式;
ResultSet
对象的修改对数据库的影响,又叫敏感性。
ResultSet
有3种不同的类型:
TYPE_FORWARD_ONLY
:不可滚动,游标只能向前移动,即只能使用ResultSet
接口的next()
方法,而不能使用previous()
方法,否则会产生错误;TYPE_SCROLL_INSENSITIVE
:可滚动的,它的游标可以相对于当前位置向前或向后移动,也可以移动到绝对位置。当ResultSet
没有关闭时,ResultSet
的修改对数据库不敏感,也就是说对ResultSet
对象的修改不会影响对应的数据库中的记录;TYPE_SCROLL_SENSITIVE
:可滚动的,它的游标可以相对于当前位置向前或向后移动,也可以移动到绝对位置。当ResultSet
没有关闭时,对ResultSet
对象的修改会直接影响数据库中的记录。
默认情况下,ResultSet
的类型为TYPE_FORWARD_ONLY
。DatabaseMetaData
接口中提供了一个supportsResultSetType()
方法,用于判断数据库驱动是否支持某种类型的ResultSet
对象,如果支持,则返回true
,否则返回false
。
5.2 ResultSet并行性
ResultSet
对象的并行性决定了它支持更新的级别,目前JDBC中支持两个级别:
CONCUR_READ_ONLY
:只能从ResulSet
对象中读取数据,但是不能更新ResultSet
对象中的数据;CONCUR_UPDATABLE
:既可以从ResulSet
对象中读取数据,又能更新ResultSet
中的数据。
ResultSet
对象默认并行性为CONCUR_READ_ONLY
。DatabaseMetaData
接口中提供了一个supportsResultSetConcurrency()
方法,用于判断JDBC驱动是否支持某一级别的并行性,如果支持就返回true
,否则返回false
。
5.3 ResultSet可保持性
调用Connection
对象的commit()
方法能够关闭当前事务中创建的ResultSet
对象。然而,在某些情况下,这可能不是我们期望的行为。ResultSet
对象的holdability
属性使得应用程序能够在Connection
对象的commit()
方法调用后控制ResultSet
对象是否关闭。
下面两个常量指定ResultSet
对象的可保持性:
HOLD_CURSORS_OVER_COMMIT
:当调用Connection
对象的commit()
方法时,不关闭当前事务创建的ResultSet
对象;CLOSE_CURSORS_AT_COMMIT
:当前事务创建的ResultSet
对象在事务提交后会被关闭。
应用程序可以调用ResultSet
对象的getHoldability()
方法获取ResultSet
的可保持性。
ResultSet
对象的默认可保持性取决于具体的驱动实现,DatabaseMetaData
接口中提供了getResultSetHoldability()
方法用于获取JDBC驱动的默认可保持性。
5.4 ResultSet游标移动
ResultSet
对象中维护了一个游标,游标指向当前数据行。当ResultSet
对象第一次创建时,游标指向数据的第一行。
ResultSet
接口中提供了一系列的方法,用于操作ResultSet
对象中的游标:
next()
:游标向前移动一行,如果游标定位到下一行,则返回true
;如果游标位于最后一行之后,则返回false
。previous()
:游标向后移动一行,如果游标定位到上一行,则返回true
;如果游标位于第一行之前,则返回false
。first()
:游标移动到第一行,如果游标定位到第一行,则返回true
;如果ResultSet
对象中一行数据都没有,则返回false
。last()
:移动游标到最后一行,如果游标定位到最后一行,则返回true
;如果ResultSet
不包含任何数据行,则返回false
。beforeFirst()
:移动游标到ResultSet
对象的第一行之前,如果ResultSet
对象不包含任何数据行,则该方法不生效。afterLast()
:游标位置移动到ResultSet
对象最后一行之后,如果ResultSet
对象中不包含任何行,则该方法不生效。relative(int rows)
:相对于当前位置移动游标。如果参数rows
为0
,则游标不会移动;如果rows
为正数,则游标向前移动指定的行数,如果rows
大于当前位置到最后一行的偏移量,则游标移动到最后一行之后;如果rows为负数,则游标向后移动,如果rows
大于当前位置到第一行的偏移量,则游标移动到第一行之前的位置。当游标正确定位到某一行时,该方法返回true
,否则返回false
。absolute(int row)
:游标定位到ResultSet
对象中的第row
行。如果row
为正数,则游标移动到ResultSet
对象的第row行
,如果参数row
大于ResultSet
对象中的最大行数,则游标移动到最后一行之后;如果参数row
为负数,游标从行尾开始移动,如果row
的绝对值大于最大行,则游标移动到第一行之前。
注意:当ResultSet
对象的类型为TYPE_FORWARD_ONLY
时,游标只能向前移动,调用其他方法操作游标向后移动时将会抛出SQLException
。
5.5 修改ResultSet对象
并行性为CONCUR_UPDATABLE
的ResultSet
对象可以使用ResultSet
接口中提供的方法对其进行更新,包括更新行、删除行,在JDBC驱动支持的情况下,还可以插入新的行。
更新ResultSet
对象中的某一行是一个两阶段的过程:
- 为某一行的每个字段设置新的值,此时底层数据库数据不会更新;
- 更新修改到具体的行,此时会把
ResultSet
对象的修改同步到数据库。
如果在并行性级别为ResultSet.CONCUR_READ_ONLY
的ResultSet
对象上调用更新方法,将会抛出SQLException
异常。
ResultSet
对象的updateRow()
方法用于将所有列的修改应用到当前行,并清除先前更新方法所做更改的记录。例如:
@Test
public void operateDataBaseByJDBC() throws SQLException {
// 1.获得连接(druidDataSource会自己注册到DriverManager)
DataSource druidDataSource = new DruidDataSource();
((DruidDataSource) druidDataSource).setUsername("root");
((DruidDataSource) druidDataSource).setUrl("jdbc:mysql://localhost:3306/mlxg?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&generateSimpleParameterMetadata=true");
((DruidDataSource) druidDataSource).setPassword("123456");
Connection connection = druidDataSource.getConnection();
// 2.创建SQL
PreparedStatement preparedStatement = connection.prepareStatement(
"select * from sys_user where id != ?",
ResultSet.TYPE_SCROLL_SENSITIVE,
ResultSet.CONCUR_UPDATABLE, // 可更新的
ResultSet.HOLD_CURSORS_OVER_COMMIT);
preparedStatement.setInt(1, 7);
// 3.执行SQL
ResultSet resultSet = preparedStatement.executeQuery();
// 4.处理结果
while (resultSet.next()){
// 更新每一行的数据
resultSet.updateString(2, "jidi12312");
// 将数据更新同步到数据库
resultSet.updateRow();
}
// 5.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
如上所示,执行查询SQL生成ResultSet
对象后,调用next()
方法将游标定位到第一行,然后调用updateString()
方法更新每一行的username
字段,接着调用ResultSet
的updateRow()
方法将ResultSe
t对象的修改同步到数据库。
ResultSet
对象可以调用rowUpdated()
方法来判断是否调用了updateRow()
方法确认更新。
ResultSet
对象中的行可以调用deleteRow()
方法进行删除,例如:
// 定位到要删除的行
resultSet.absolute(5);
// 删除行
resultSet.deleteRow();
如上所示,调用ResultSet
对象的absolute()
方法将游标定位到第5行,然后调用deleteRow()
方法会删除数据库中对应的行。
当前行被删除后,如果ResultSet
对象能够检测到行被删除,那么ResultSet
对象的rowDeleted()
方法会返回true
,否则返回false
。
在JDBC驱动支持的情况下,可以调用ResultSet
接口提供的方法insertRow()
向ResultSet
对象中插入行。如果JDBC驱动不支持就会抛出SQLFeatureNotSupportedException
异常。
ResultSet
对象中插入行需要以下几步:
- 移动游标到待插入的位置。
- 调用
ResultSet
接口提供的updateXXX()
方法为每一个字段设置值。 - 插入行到当前ResultSet对象中。
下面是一个例子:
// 将数据更新同步到数据库
resultSet.updateRow();
// 光标移动到插入行
resultSet.moveToInsertRow();
// 设置要插入的数据
resultSet.updateString(1, "jidi");
resultSet.updateString(2, "123@163.com");
// 插入行
resultSet.insertRow();
// 移动到当前行
resultSet.moveToCurrentRow();
5.6 关闭ResultSet对象
ResultSet
对象在下面两种情况下会显式地关闭:
- 调用
ResultSet
对象的close()
方法。 - 创建
ResultSet
对象的Statement
或者Connection
对象被显式地关闭。
在下面两种情况下ResultSet
对象会被隐式地关闭:
- 相关联的
Statement
对象重复执行时。 - 可保持性为
CLOSE_CURSORS_AT_COMMIT
的ResultSet
对象在当前事务提交后会被关闭。
6 DatabaseMetaData接口
DatabaseMetaData
接口是由JDBC驱动程序实现的,用于提供底层数据源相关的信息。该接口主要用于为应用程序或工具确定如何与底层数据源交互。
DatabaseMetaData
接口中包含超过150个方法,根据这些方法的类型可以分为以下几类:
- 获取数据源信息。
- 确定数据源是否支持某一特性或功能。
- 获取数据源的限制。
- 确定数据源包含哪些SQL对象以及这些对象的属性。
- 获取数据源对事务的支持。
Connection
对象中提供了一个getMetadata()
方法,用于创建DatabaseMetaData
对象。
// 创建DatabaseMetaData对象
DatabaseMetaData metaData = connection.getMetaData();
7 JDBC事务
事务用于提供数据完整性、正确的应用程序语义和并发访问的数据一致性。所有遵循JDBC规范的驱动程序都需要提供事务支持。
JDBC API中的事务管理符合SQL:2003规范,主要包含下面几个概念:
- 自动提交模式
- 事务隔离级别
- 保存点
7.1 事务边界与自动提交
Connection
对象的autoCommit
属性决定什么时候结束一个事务。启用自动提交后,会在每个SQL语句执行完毕后自动提交事务。
当Connection
对象创建时,默认情况下,事务自动提交是开启的。Connection
接口中提供了一个setAutoCommit()
方法,可以禁用事务自动提交。此时,需要显式地调用Connection
接口提供commit()
方法提交事务,或者调用rollback()
方法回滚事务。禁用事务自动提交适用于需要将多个SQL语句作为一个事务提交或者事务由应用服务器管理。
7.2 事务隔离级别
事务隔离级别用于指定事务中对数据的操作对其他事务的“可见性”。不同的事务隔离级别能够解决并发访问数据带来的不同的并发问题,而且会直接影响并发访问效率。数据并发访问可能会出现以下几种问题:
- 脏读:事务中允许读取未提交的数据。例如,A事务修改了一条数据,但是未提交修改,此时A事务对数据的修改对其他事务是可见的,B事务中能够读取A事务未提交的修改。一旦A事务回滚,B事务中读取的就是不正确的数据。
- 不可重复读:这种情况发生在如下场景:A事务中读取一行数据,B事务中修改了该行数据,A事务中再次读取该行数据将得到不同的结果。
- 幻读:这种情况发生在如下场景:A事务中通过WHERE条件读取若干行,B事务中插入了符合条件的若干条数据,A事务中通过相同的条件再次读取数据时将会读取到B事务中插入的数据。
JDBC遵循SQL:2003规范,定义的事务隔离级别如下:
TRANSACTION_NONE
:表示驱动不支持事务,这意味着它是不兼容JDBC规范的驱动程序。TRANSACTION_READ_UNCOMMITTED
:允许事务读取未提交更改的数据,这意味着可能会出现脏读、不可重复读、幻读等现象。TRANSACTION_READ_COMMITTED
:表示在事务中进行的任何数据更改,在提交之前对其他事务是不可见的。这样可以防止脏读,但是不能解决不可重复读和幻读的问题。TRANSACTION_REPEATABLE_READ
:该事务隔离级别能够解决脏读和不可重复读问题,但是不能解决幻读问题。TRANSACTION_SERIALIZABLE
:该事务隔离级别下,所有事务串行执行,能够有效解决脏读、不可重复读和幻读问题,但是并发效率较低。
Connection
对象的默认事务级别由JDBC驱动程序指定。通常它是底层数据源支持的默认事务隔离级别。Connection
接口中提供了一个setTransactionIsolation()
方法,允许JDBC客户端设置Connection
对象的事务隔离级别。新设置的事务隔离级别会在之后的会话中生效。
7.3 事务中的保存点
保存点通过在事务中标记一个中间的点来对事务进行更细粒度的控制,一旦设置保存点,事务就可以回滚到保存点,而不影响保存点之前的操作。DatabaseMetaData
接口提供了supportsSavepoints()
方法,用于判断JDBC驱动是否支持保存点。
Connection
接口中提供了setSavepoint()
方法用于在当前事务中设置保存点,如果setSavepoint()
方法在事务外调用,则调用该方法后会在setSavepoint()
方法调用处开启一个新的事务。setSavepoint()
方法的返回值是一个Savepoint
对象,该对象可作为Connection
对象rollback()
方法的参数,用于回滚到对应的保存点。例如:
// 设置事务提交方式为手动提交
conn.setAutoCommit(false);
// 要执行的SQL语句
String sql1 = "delete from sys_user where id='8'";
String sql2 = "delete from sys_user where id='9'";
//2、获得sql语句执行对象
Statement stmt = conn.createStatement();
// 执行sql1
stmt.executeUpdate(sql1);
// 创建保存点
Savepoint savepoint1 = conn.setSavepoint("savepoint1");
// 执行sql2
stmt.executeUpdate(sql2);
// 回滚到保存点
conn.rollback(savepoint1);
// 提交事务
conn.commit();
如上所示,执行完第一条SQL语句后,创建了保存点,然后再执行了第二条SQL语句后,回滚到创建的保存点,然后提交事务,结果是数据库中的记录只会删除一条(id=8的记录)。
保存点创建后,可以被手动释放。Connection
对象中提供了一个releaseSavepoint()
方法,接收一个Savepoint
对象作为参数,用于释放当前事务中的保存点。该方法调用后,此保存点之后的保存点都会被释放。一旦保存点被释放,试图回滚到被释放的保存点时就将会抛出SQLException
异常。
事务中创建的所有保存点在事务提交或完成回滚之后会自动释放,事务回滚到某一保存点后,该保存点之后创建的保存点会自动释放。
8 参考文献
Java™教程-JDBC基础
Java SPI(Service Provider Interface)简介
JDBC详解
《MyBatis3源码深度解析》江荣波