JDBC,你真的知道怎么用吗?

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操作数据源大致需要以下几个步骤:

  1. 与数据源建立连接;
  2. 执行SQL语句;
  3. 检索SQL执行结果;
  4. 关闭连接。

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接口比较重要的扩展,用于支撑企业级应用。这两个接口分别为:

  1. ConnectionPoolDataSource:支持缓存复用Connection对象,能够提升应用性能和伸缩性;
  2. XADataSource:返回的Connection对象能够支持分布式事务

1.2 执行SQL语句

Connection是JDBC对数据源连接的抽象,一旦建立了连接,使用JDBC API的应用程序就可以对目标数据源执行查询和更新操作。

JDBC API提供了访问SQL:2003规范中常用的实现特性,由于不同的JDBC厂商对这些特性的支持程度各不相同,JDBC API中提供了一个DatabaseMetadata接口,应用程序可以使用DatabaseMetadata的实例来确定目前使用的数据源是否支持某一特性。

获取到JDBC中的Connection对象之后,可以通过Connection对象设置事务属性,并且可以通过Connection接口中提供的方法创建StatementPreparedStatement或者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.sqljavax.sql两个包构成。

2.1 java.sql包

java.sql包中包含着JDBC最核心的API,下面是java.sql包中所有接口、枚举和类:

java.sql包中的接口、枚举和类
java.sql.SQLException
java.sql.Connection
java.sql.ResultSet
java.sql.PreparedStatement
java.sql.DriverManager
java.sql.Statement
java.sql.Timestamp
java.sql.Date
java.sql.ResultSetMetaData
java.sql.Types
java.sql.DatabaseMetaData
java.sql.Time
java.sql.Blob
java.sql.CallableStatement
java.sql.Driver
java.sql…Clob
java.sql.Array
java.sql.SQLFeatureNotSupportedException
java.sql.SQLWarning
java.sql.Savepoint
java.sql.SQLXML
java.sql.NClob
java.sql.Struct
java.sql.Ref
java.sql.SQLClientInfoException
java.sql.RowId
java.sql.DriverPropertyInfo
java.sql.ParameterMetaData
java.sql.BatchUpdateException
java.sql.RowIdLifetime
java.sql.SQLIntegrityConstraintViolationException
java.sql.SQLSyntaxErrorException
java.sql.SQLDataException
java.sql.SQLTimeoutException
java.sql.SQLNonTransientConnectionException
java.sql.SQLNonTransientException
java.sql.Wrapper
java.sql.SQLTransactionRollbackException
java.sql.SQLTransientException
java.sql.SQLPermission
java.sql.JDBCType
java.sql.SQLRecoverableException
java.sql.SQLTransientConnectionException
java.sql.SQLInvalidAuthorizationSpecException
java.sql.DataTruncation
java.sql.SQLOutput
java.sql.SQLData
java.sql.SQLType
java.sql.SQLInput
java.sql.ClientInfoStatus
java.sql.DriverAction
java.sql.ShardingKey

如表所示,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版本的可选包提供的,主要包括下面几个类和接口:

Java javax.sql 包,常用类列表
javax.sql.DataSource
javax.sql.ConnectionPoolDataSource
javax.sql.CommonDataSource
javax.sql.PooledConnection
javax.sql.ConnectionEvent
javax.sql.ConnectionEventListener
javax.sql.StatementEvent
javax.sql.StatementEventListener
javax.sql.XAConnection
javax.sql.XADataSource
javax.sql.RowSet
javax.sql.RowSetEvent
javax.sql.RowSetInternal
javax.sql.RowSetListener
javax.sql.RowSetMetaData
javax.sql.RowSetReader
javax.sql.RowSetWriter

JDBC 1.0中使用DriverManager类来产生一个与数据源连接的Connection对象。

JDBC 2.0提供的DataSource接口是一个更好的连接数据源的方式:
(1)应用程序不需要像使用DriverManager一样对加载的数据库驱动程序信息进行硬编码;
(2)使用DataSource接口的优势还体现在连接池分布式事务上。连接池通过对连接的复用,适用于任务繁忙、负担繁重的企业级应用。

javax.sql.DataSourcejava.sql.Connection之间的关系如图所示:
在这里插入图片描述


javax.sql包下还提供了一个PooledConnection接口。PooledConnectionConnection的不同之处在于,它提供了连接池管理的句柄。一个PooledConnection表示与数据源建立的物理连接,该连接在应用程序使用完后可以回收而不用关闭它,从而减少了与数据源建立连接的次数。

应用程序开发人员一般不直接使用PooledConnection接口,而是通过一个管理连接池的中间层基础设施使用。当应用程序调用DataSource对象的getConnection()方法时,它返回一个Connection对象。但是当我们使用数据库连接池时(例如Druid),该Connection对象实际上是到PooledConnection对象的句柄,这是一个物理连接。连接池管理器维护所有的PooledConnection 对象资源。如果池中存在可用的PooledConnection对象,则连接池管理器返回作为到该物理连接的句柄的Connection对象。如果不存在可用的PooledConnection对象,则连接池管理器调用ConnectionPoolDataSource对象的getConnection()方法创建新的物理连接。

连接池实现模块可以调用PooledConnection对象的addConnectionEventListener()将自己注册成为一个PooledConnection对象的监听者,当数据库连接需要重用或关闭的时候会产生一个ConnectionEvent对象,它表示一个连接事件,连接池实现模块将会得到通知。

javax.sql.PooledConnectionjava.sql.Connection之间的关系如图:

在这里插入图片描述


javax.sql包还提供了一个RowSet接口,该接口继承自java.sql包下的ResultSet接口。RowSet用于为数据源和应用程序在内存中建立一个映射,通俗来讲,RowSet就相当于数据库表数据在应用程序内存中的映射。RowSet默认是一个可滚动、可更新、可序列化的结果集,而且它作为一个JavaBean组件,可以方便地在网络间传输,用于两端的数据同步。

RowSet对象根据是否关闭数据源连接分为:

  1. 连接的RowSetRowSet对象可以建立一个与数据源的连接并在其整个生命周期中维持该连接;
  2. 非连接的RowSetRowSet对象还可以建立一个与数据源的连接,从其获取数据,然后关闭它。非连接Rowset可以在断开连接时更改其数据,然后将这些更改写回底层数据源,不过它必须重新建立连接才能完成此操作。相较于java.sql.ResultSet而言,RowSet的离线操作能够有效地利用计算机越来越充足的内存减轻数据库服务器的负担。

javax.sql.RowSetjava.sql.ResultSet之间的关系如图:
在这里插入图片描述

3 Connection详解

一个Connection对象表示通过JDBC驱动与数据源建立的连接,这里的数据源可以是关系型数据库管理系统(DBMS)、文件系统或者其他通过JDBC驱动访问的数据。使用JDBC API的应用程序可能需要维护多个Connection对象,一个Connection对象可能访问多个数据源,也可能访问单个数据源。

从JDBC驱动的角度来看,Connection对象表示客户端会话,可以通过两种方式获取JDBC中的Connection对象:

  1. 通过JDBC API中提供的DriverManager类获取;
  2. 通过DataSource接口的实现类获取。目前使用比较广泛的数据库连接池C3P0DBCPDruid等都是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.ServiceLoaderServiceLoader类提供了一个静态的load()方法,用于加载指定接口的所有实现类。调用该方法后,classpathMETA-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类的静态代码块会在调用DriverManagergetConnection()方法之前调用。

静态代码块中调用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种类型的参数:INOUTINOUT。可以将参数指定为序数参数或命名参数,必须为ININOUT参数的每个参数占位符设置一个值,必须为OUTINOUT参数中的每个参数占位符调用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_1PARAM_5)需要设置值。

对于IN参数的设置,调用CallableStatement接口中提供的setXXX()方法即可;但是对于OUTINOUT参数,在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种不同的类型:

  1. TYPE_FORWARD_ONLY:不可滚动,游标只能向前移动,即只能使用ResultSet接口的next()方法,而不能使用previous()方法,否则会产生错误;
  2. TYPE_SCROLL_INSENSITIVE:可滚动的,它的游标可以相对于当前位置向前或向后移动,也可以移动到绝对位置。当ResultSet没有关闭时,ResultSet的修改对数据库不敏感,也就是说对ResultSet对象的修改不会影响对应的数据库中的记录;
  3. TYPE_SCROLL_SENSITIVE:可滚动的,它的游标可以相对于当前位置向前或向后移动,也可以移动到绝对位置。当ResultSet没有关闭时,对ResultSet对象的修改会直接影响数据库中的记录。

默认情况下,ResultSet的类型为TYPE_FORWARD_ONLYDatabaseMetaData接口中提供了一个supportsResultSetType()方法,用于判断数据库驱动是否支持某种类型的ResultSet对象,如果支持,则返回true,否则返回false

5.2 ResultSet并行性

ResultSet对象的并行性决定了它支持更新的级别,目前JDBC中支持两个级别:

  1. CONCUR_READ_ONLY:只能从ResulSet对象中读取数据,但是不能更新ResultSet对象中的数据;
  2. CONCUR_UPDATABLE:既可以从ResulSet对象中读取数据,又能更新ResultSet中的数据。

ResultSet对象默认并行性为CONCUR_READ_ONLYDatabaseMetaData接口中提供了一个supportsResultSetConcurrency()方法,用于判断JDBC驱动是否支持某一级别的并行性,如果支持就返回true,否则返回false

5.3 ResultSet可保持性

调用Connection对象的commit()方法能够关闭当前事务中创建的ResultSet对象。然而,在某些情况下,这可能不是我们期望的行为。ResultSet对象的holdability属性使得应用程序能够在Connection对象的commit()方法调用后控制ResultSet对象是否关闭。

下面两个常量指定ResultSet对象的可保持性:

  1. HOLD_CURSORS_OVER_COMMIT:当调用Connection对象的commit()方法时,不关闭当前事务创建的ResultSet对象;
  2. 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):相对于当前位置移动游标。如果参数rows0,则游标不会移动;如果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_UPDATABLEResultSet对象可以使用ResultSet接口中提供的方法对其进行更新,包括更新行、删除行,在JDBC驱动支持的情况下,还可以插入新的行。

更新ResultSet对象中的某一行是一个两阶段的过程:

  1. 为某一行的每个字段设置新的值,此时底层数据库数据不会更新;
  2. 更新修改到具体的行,此时会把ResultSet对象的修改同步到数据库。

如果在并行性级别为ResultSet.CONCUR_READ_ONLYResultSet对象上调用更新方法,将会抛出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字段,接着调用ResultSetupdateRow()方法将ResultSet对象的修改同步到数据库。

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对象中插入行需要以下几步:

  1. 移动游标到待插入的位置。
  2. 调用ResultSet接口提供的updateXXX()方法为每一个字段设置值。
  3. 插入行到当前ResultSet对象中。

下面是一个例子:

 // 将数据更新同步到数据库
 resultSet.updateRow();
 
 // 光标移动到插入行
 resultSet.moveToInsertRow();
 
 // 设置要插入的数据
 resultSet.updateString(1, "jidi");
 resultSet.updateString(2, "123@163.com");
 
 // 插入行
 resultSet.insertRow();
 
 // 移动到当前行
 resultSet.moveToCurrentRow();

5.6 关闭ResultSet对象

ResultSet对象在下面两种情况下会显式地关闭:

  1. 调用ResultSet对象的close()方法。
  2. 创建ResultSet对象的Statement或者Connection对象被显式地关闭。

在下面两种情况下ResultSet对象会被隐式地关闭:

  1. 相关联的Statement对象重复执行时。
  2. 可保持性为CLOSE_CURSORS_AT_COMMITResultSet对象在当前事务提交后会被关闭。

6 DatabaseMetaData接口

DatabaseMetaData接口是由JDBC驱动程序实现的,用于提供底层数据源相关的信息。该接口主要用于为应用程序或工具确定如何与底层数据源交互。
DatabaseMetaData接口中包含超过150个方法,根据这些方法的类型可以分为以下几类:

  1. 获取数据源信息。
  2. 确定数据源是否支持某一特性或功能。
  3. 获取数据源的限制。
  4. 确定数据源包含哪些SQL对象以及这些对象的属性。
  5. 获取数据源对事务的支持。

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源码深度解析》江荣波

阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丑基地

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值